@tianhai/pi-workflow-kit 0.18.1 → 1.1.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/README.md +79 -62
- package/agents/pwk-hazard-reviewer.md +24 -0
- package/agents/pwk-smell-reviewer.md +17 -0
- package/agents/pwk-spec-reviewer.md +14 -0
- package/agents/pwk-tracing-reviewer.md +14 -0
- package/docs/developer-usage-guide.md +39 -44
- package/docs/lessons.md +2 -7
- package/docs/oversight-model.md +20 -14
- package/docs/workflow-phases.md +64 -30
- package/extensions/workflow-guard.ts +194 -92
- package/package.json +20 -4
- package/skills/pwk-brainstorming/SKILL.md +30 -27
- package/skills/pwk-code-review/SKILL.md +46 -0
- package/skills/pwk-diagnose/SKILL.md +5 -2
- package/skills/pwk-executing-tasks/SKILL.md +92 -328
- package/skills/pwk-finalizing/SKILL.md +23 -106
- package/skills/pwk-status/SKILL.md +28 -0
- package/skills/pwk-writing-plans/SKILL.md +49 -328
- package/docs/plans/2026-06-03-karpathy-guidelines-ab-comparison.md +0 -166
- package/docs/plans/completed/2026-04-11-checkpoint-review-gates-design.md +0 -50
- package/docs/plans/completed/2026-04-11-checkpoint-review-gates-implementation.md +0 -98
- package/docs/plans/completed/2026-04-11-finalizing-merge-options-design.md +0 -33
- package/docs/plans/completed/2026-04-11-finalizing-merge-options-implementation.md +0 -75
- package/docs/plans/completed/2026-04-11-workspace-setup-design.md +0 -28
- package/docs/plans/completed/2026-04-11-workspace-setup-implementation.md +0 -57
- package/docs/plans/completed/2026-04-15-bash-guard-design.md +0 -39
- package/docs/plans/completed/2026-04-15-bash-guard-implementation.md +0 -229
- package/docs/plans/completed/2026-04-21-workflow-guard-safe-commands-design.md +0 -172
- package/docs/plans/completed/2026-04-21-workflow-guard-safe-commands-implementation.md +0 -168
- package/docs/plans/completed/2026-04-22-go-readonly-safe-commands-implementation.md +0 -54
- package/docs/plans/completed/2026-04-28-executing-tasks-redesign-design.md +0 -171
- package/docs/plans/completed/2026-04-28-executing-tasks-redesign-implementation.md +0 -208
- package/docs/plans/completed/2026-04-28-executing-tasks-redesign-progress.md +0 -14
- package/docs/plans/completed/2026-05-01-incorporate-mattpocock-skills-design.md +0 -154
- package/docs/plans/completed/2026-05-01-incorporate-mattpocock-skills-implementation.md +0 -315
- package/docs/plans/completed/2026-05-01-incorporate-mattpocock-skills-progress.md +0 -15
- package/docs/plans/completed/2026-05-08-checkpoint-gates-design.md +0 -235
- package/docs/plans/completed/2026-05-08-checkpoint-gates-implementation.md +0 -83
- package/docs/plans/completed/2026-05-08-checkpoint-gates-progress.md +0 -11
- package/docs/plans/completed/2026-05-08-lessons-learned-design.md +0 -76
- package/docs/plans/completed/2026-05-08-lessons-learned-implementation.md +0 -219
- package/docs/plans/completed/2026-05-08-lessons-learned-progress.md +0 -15
- package/docs/plans/completed/2026-05-08-migrate-earendil-works-design.md +0 -39
- package/docs/plans/completed/2026-05-08-migrate-earendil-works-implementation.md +0 -45
- package/docs/plans/completed/2026-05-08-migrate-earendil-works-progress.md +0 -10
- package/docs/plans/completed/2026-05-08-worktree-handoff-design.md +0 -118
- package/docs/plans/completed/2026-05-08-worktree-handoff-implementation.md +0 -140
- package/docs/plans/completed/2026-05-08-worktree-handoff-progress.md +0 -10
- package/docs/plans/completed/2026-05-20-generic-lessons-design.md +0 -70
- package/docs/plans/completed/2026-05-20-generic-lessons-implementation.md +0 -114
- package/docs/plans/completed/2026-05-20-generic-lessons-progress.md +0 -11
- package/docs/plans/completed/2026-05-22-agentic-agile-enhancements-design.md +0 -77
- package/docs/plans/completed/2026-05-22-agentic-agile-enhancements-implementation.md +0 -473
- package/docs/plans/completed/2026-05-25-design-review-split-implementation.md +0 -622
- package/docs/plans/completed/2026-05-25-design-review-split-progress.md +0 -16
- package/docs/plans/completed/2026-05-25-pr5-improvements-implementation.md +0 -273
- package/docs/plans/completed/2026-05-25-pr5-improvements-progress.md +0 -17
- package/docs/plans/completed/2026-06-03-add-verify-skill-design.md +0 -51
- package/docs/plans/completed/2026-06-03-add-verify-skill-implementation.md +0 -111
- package/docs/plans/completed/2026-06-03-add-verify-skill-progress.md +0 -11
- package/docs/plans/completed/2026-06-03-verify-skill-design.md +0 -176
- package/docs/plans/completed/2026-06-09-code-review-fixes-implementation.md +0 -74
- package/docs/plans/completed/2026-06-09-code-review-fixes-progress.md +0 -14
- package/docs/plans/completed/2026-06-09-incremental-workflow-and-rename-design.md +0 -186
- package/docs/plans/completed/2026-06-09-incremental-workflow-and-rename-implementation.md +0 -675
- package/docs/plans/completed/2026-06-09-incremental-workflow-and-rename-progress.md +0 -18
- package/docs/plans/completed/2026-06-09-incremental-workflow-and-rename-verification-report.md +0 -81
- package/docs/plans/completed/2026-06-09-verification-fixes-implementation.md +0 -69
- package/docs/plans/completed/2026-06-09-verification-fixes-progress.md +0 -14
- package/skills/pwk-design-review/SKILL.md +0 -115
- package/skills/pwk-verify/SKILL.md +0 -170
package/docs/workflow-phases.md
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
# Workflow Phases
|
|
2
2
|
|
|
3
|
-
`pi-workflow-kit` has
|
|
3
|
+
`pi-workflow-kit` has 5 pipeline skills plus 2 utility skills. You invoke each one explicitly with `/skill:`.
|
|
4
4
|
|
|
5
5
|
```
|
|
6
|
-
brainstorm →
|
|
6
|
+
brainstorm → writing-plans → executing-tasks → finalizing
|
|
7
|
+
(per requirement: tests → ⏸ checkpoint → implement → ⏸ checkpoint → code-review)
|
|
7
8
|
```
|
|
8
9
|
|
|
9
|
-
For
|
|
10
|
-
```
|
|
11
|
-
brainstorm (name features) → plan next feature → [design-review?] → execute feature → [verify?] → loop...
|
|
12
|
-
```
|
|
10
|
+
A design doc is one PR; a requirement is one testable slice within it. For multi-design work (a large issue split into several design docs — each its own PR), run the pipeline once per design doc.
|
|
13
11
|
|
|
14
12
|
## brainstorm
|
|
15
13
|
|
|
@@ -17,50 +15,80 @@ brainstorm (name features) → plan next feature → [design-review?] → execut
|
|
|
17
15
|
/skill:pwk-brainstorming
|
|
18
16
|
```
|
|
19
17
|
|
|
20
|
-
- Explore requirements and shape the design
|
|
21
|
-
-
|
|
22
|
-
-
|
|
18
|
+
- Explore requirements and shape the design.
|
|
19
|
+
- Produce `docs/plans/YYYY-MM-DD-<topic>-design.md` — descriptive, opening with a `## Requirements` list, ending with a `## Feature acceptance` section (end-to-end scenarios that prove the requirements compose into the PRD's behavior — the feature's definition-of-done).
|
|
20
|
+
- May split a large issue into multiple design docs (human-approved).
|
|
21
|
+
- ADRs go to `docs/adr/` (permanent, never archived).
|
|
23
22
|
|
|
24
23
|
Write boundary: only `docs/plans/` is writable. Source files are hard-blocked.
|
|
25
24
|
|
|
26
|
-
##
|
|
25
|
+
## writing-plans
|
|
27
26
|
|
|
28
27
|
```
|
|
29
28
|
/skill:pwk-writing-plans
|
|
30
29
|
```
|
|
31
30
|
|
|
32
|
-
-
|
|
33
|
-
-
|
|
34
|
-
-
|
|
35
|
-
-
|
|
31
|
+
- Creates the feature branch first (`git checkout -b <topic>`), so design + plan docs live on the branch, not `main`.
|
|
32
|
+
- Reads the design doc's `## Requirements`; for each, derives **acceptance criteria + integration-test cases** (a behavioral spec, no implementation code), lists requirements in build order (dependencies positioned earlier), and challenges the design when `## Production-risk areas` is present.
|
|
33
|
+
- Derives a **`## Feature acceptance` section** in the plan from the design's Feature acceptance — an end-to-end integration test that exercises the requirements together (distinct from per-requirement tests). If the design has none, stops and asks the human to brainstorm one.
|
|
34
|
+
- Produce `docs/plans/YYYY-MM-DD-<topic>-implementation.md`.
|
|
36
35
|
|
|
37
|
-
Write boundary: only `docs/plans/` is writable.
|
|
36
|
+
Write boundary: only `docs/plans/` is writable.
|
|
38
37
|
|
|
39
|
-
##
|
|
38
|
+
## executing-tasks
|
|
40
39
|
|
|
41
40
|
```
|
|
42
41
|
/skill:pwk-executing-tasks
|
|
43
42
|
```
|
|
44
43
|
|
|
45
|
-
-
|
|
46
|
-
-
|
|
47
|
-
-
|
|
48
|
-
-
|
|
44
|
+
- Per requirement: write the integration tests (red) → **⏸ checkpoint: tests** → implement to green (full autonomy — the executor chooses structure/signatures/internals) → **⏸ checkpoint: complete** → commit → **per-requirement review** (four parallel reviewers via the `subagent` tool; falls back to inline `/skill:pwk-code-review` when `pi-subagents` is absent — see [code-review](#code-review)).
|
|
45
|
+
- Two **mandatory** human checkpoints per requirement — unless the plan tags a requirement lighter (see [Proportionality](#proportionality)).
|
|
46
|
+
- **Composition check after each commit** — if a requirement touched shared code, the executor runs the full suite now and fixes cross-requirement regressions immediately, rather than discovering them only at the integration gate.
|
|
47
|
+
- Progress tracked in `docs/plans/*-progress.md`.
|
|
48
|
+
- After all requirements: **integration gate** — run the full suite, **run the feature-acceptance test** (the end-to-end check from the plan's `## Feature acceptance` section), and confirm the requirements compose into the feature before `/skill:pwk-finalizing`.
|
|
49
49
|
|
|
50
50
|
No write restrictions. All tools available.
|
|
51
51
|
|
|
52
|
-
##
|
|
52
|
+
## Proportionality
|
|
53
|
+
|
|
54
|
+
The defaults preserve the 1.0.0 behavior (two checkpoints + parallel review per requirement). At plan time the human can tag each requirement lighter to right-size the workflow:
|
|
55
|
+
|
|
56
|
+
- **Checkpoints** — `full` (both stops, default) | `spec` (tests stop only — cheap spec-correctness gate, implementation covered by review) | `none` (no stops, trivial only). Test-first is preserved either way: even `none` writes tests first (red) and implements to green; only the human *stops* are optional. `spec` requires at least `inline` review (never combine with `skip`).
|
|
57
|
+
- **Review** — `parallel` (four fresh-context reviewers, default) | `inline` (single `pwk-code-review` pass) | `skip` (trivial diffs with no behavioral surface only).
|
|
58
|
+
|
|
59
|
+
A trivial fix can also skip the multi-turn brainstorm dialogue via the brainstorming trivial fast-path (compress to one turn, minimal design doc) — the guard still enforces read-only. Tags default conservatively, so nothing changes unless the human opts in.
|
|
60
|
+
|
|
61
|
+
## code-review
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
/skill:pwk-code-review
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The **inline reviewer**: code tracing, spec alignment (vs acceptance criteria), code smells (applies fixes), production hazard check. Unlocked — may modify code to fix smells.
|
|
68
|
+
|
|
69
|
+
**Not a phase you drive manually.** During `pwk-executing-tasks`, per-requirement review runs **four specialized reviewers in parallel** via the `subagent` tool (spec, tracing, smell, hazard — each fresh-context, read-only reporters); this skill is the **fallback** when [`pi-subagents`](https://pi.dev/packages/pi-subagents) is not installed. You can also invoke `/skill:pwk-code-review` standalone for an ad-hoc review of any diff.
|
|
70
|
+
|
|
71
|
+
No write restrictions.
|
|
72
|
+
|
|
73
|
+
## finalizing
|
|
53
74
|
|
|
54
75
|
```
|
|
55
76
|
/skill:pwk-finalizing
|
|
56
77
|
```
|
|
57
78
|
|
|
58
|
-
-
|
|
59
|
-
-
|
|
60
|
-
-
|
|
61
|
-
- Clean up worktree if one was used
|
|
79
|
+
- **Pre-check: run the full test suite** — don't ship a red suite (resume spans sessions; don't trust the last execute session).
|
|
80
|
+
- Delete consumed plan docs (per-`<topic>`) — code + tests are the source of truth; ADRs stay at `docs/adr/`.
|
|
81
|
+
- Curate `docs/lessons.md`, update README/CHANGELOG, create PR or merge.
|
|
62
82
|
|
|
63
|
-
No write restrictions.
|
|
83
|
+
No write restrictions.
|
|
84
|
+
|
|
85
|
+
## status
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
/skill:pwk-status
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Read-only overview of all active pipeline topics (phase + progress) when several designs are in flight. Not a pipeline phase — and it **does not exit the gated phase** (`pwk-status` is read-only; it runs fine under the brainstorm/plan write block, so the boundary stays up).
|
|
64
92
|
|
|
65
93
|
## diagnose
|
|
66
94
|
|
|
@@ -68,8 +96,14 @@ No write restrictions. All tools available.
|
|
|
68
96
|
/skill:pwk-diagnose
|
|
69
97
|
```
|
|
70
98
|
|
|
71
|
-
Not a pipeline phase. A utility skill invoked on demand when debugging is needed.
|
|
99
|
+
Not a pipeline phase. A utility skill invoked on demand when debugging is needed. Invoking it **exits the gated phase** — diagnosis needs to write failing tests and `[DEBUG-…]` instrumentation. To stay read-only mid-brainstorm, use `/skill:pwk-status` instead, or re-lock with `/pwk-guard on`.
|
|
100
|
+
|
|
101
|
+
No write restrictions.
|
|
102
|
+
|
|
103
|
+
## Manual override
|
|
104
|
+
|
|
105
|
+
`/pwk-guard on|off|auto` overrides the guard regardless of phase: `on` forces a read-only lock, `off` disables the guard entirely, `auto` (default) returns to skill-driven phases. Subcommands autocomplete. Use it as an escape hatch when the guard blocks something you genuinely need; phase transitions otherwise happen only via `/skill:` commands.
|
|
106
|
+
|
|
107
|
+
## Continuity across sessions
|
|
72
108
|
|
|
73
|
-
|
|
74
|
-
- Reproduce, hypothesise, instrument, fix, cleanup
|
|
75
|
-
- No write restrictions (used during execute/finalize, or outside the pipeline)
|
|
109
|
+
A new session resumes by invoking the skill for the phase to continue. The skill globs `docs/plans/` for its artifact (progress file / plan doc), resumes the single match, or asks if several. Each resumption skill reports what it found on entry — no registry file needed; the `<topic>` slug in the filenames is the identity.
|
|
@@ -4,14 +4,15 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
4
4
|
/**
|
|
5
5
|
* Workflow Guard extension.
|
|
6
6
|
*
|
|
7
|
-
* Blocks write/edit outside docs/plans/ and
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* Blocks write/edit outside docs/plans/ and destructive bash during brainstorm and plan phases.
|
|
8
|
+
* Bash uses a simple common-blacklist (DESTRUCTIVE_PATTERNS) — a command is allowed unless it matches
|
|
9
|
+
* a destructive pattern. A short phase reminder is appended after the user's message each turn via
|
|
10
|
+
* before_agent_start. You control phases explicitly via /skill: commands — no auto-detection, no prompts.
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
|
-
type Phase = "brainstorm" | "plan" |
|
|
13
|
+
type Phase = "brainstorm" | "plan" | null;
|
|
13
14
|
|
|
14
|
-
// Destructive commands blocked in brainstorm/plan
|
|
15
|
+
// Destructive commands blocked in brainstorm/plan phases (simple common blacklist)
|
|
15
16
|
const DESTRUCTIVE_PATTERNS = [
|
|
16
17
|
/\brm\b/i,
|
|
17
18
|
/\brmdir\b/i,
|
|
@@ -27,15 +28,22 @@ const DESTRUCTIVE_PATTERNS = [
|
|
|
27
28
|
/\btruncate\b/i,
|
|
28
29
|
/\bdd\b/i,
|
|
29
30
|
/\bshred\b/i,
|
|
30
|
-
/(^|[^<])>(?!>)/,
|
|
31
|
-
/>>/,
|
|
32
31
|
/\bnpm\s+(install|uninstall|update|ci|link|publish)/i,
|
|
33
32
|
/\byarn\s+(add|remove|install|publish)/i,
|
|
34
33
|
/\bpnpm\s+(add|remove|install|publish)/i,
|
|
35
34
|
/\bpip\s+(install|uninstall)/i,
|
|
36
35
|
/\bapt(-get)?\s+(install|remove|purge|update|upgrade)/i,
|
|
37
36
|
/\bbrew\s+(install|uninstall|upgrade)/i,
|
|
38
|
-
|
|
37
|
+
// git add/commit/apply merge files and are blocked below. Plain `git branch`/`checkout`/`switch`
|
|
38
|
+
// only create or move between branches (no source-file changes), so they are intentionally allowed
|
|
39
|
+
// during gated phases — pwk-writing-plans creates the feature branch before authoring the plan.
|
|
40
|
+
/\bgit\s+(add|commit|push|pull|merge|rebase|reset|branch\s+-[dD]|stash(?!\s+list)|cherry-pick|revert|tag(?!\s+(-l|--list))|init|clone|apply)/i,
|
|
41
|
+
// Edit-via-bash vectors: in-place editors, patch appliers, find-delete (bypass the write/edit tool block)
|
|
42
|
+
/\bsed\b.*\s-i\b/i,
|
|
43
|
+
/\bperl\b.*\s-[a-z]*i\b/i,
|
|
44
|
+
/\bawk\b.*-i\s+inplace\b/i,
|
|
45
|
+
/^\s*patch\b/i, // command-position only — avoids FP on "patch" in paths/searches (grep/cat/cd)
|
|
46
|
+
/\bfind\b.*\s-delete\b/i,
|
|
39
47
|
/\bsudo\b/i,
|
|
40
48
|
/\bsu\b/i,
|
|
41
49
|
/\bkill\b/i,
|
|
@@ -48,73 +56,9 @@ const DESTRUCTIVE_PATTERNS = [
|
|
|
48
56
|
/^\s*(vim?|nano|emacs|code|subl)\b/i,
|
|
49
57
|
];
|
|
50
58
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
/^\s*tail\b/,
|
|
55
|
-
/^\s*less\b/,
|
|
56
|
-
/^\s*more\b/,
|
|
57
|
-
/^\s*grep\b/,
|
|
58
|
-
/^\s*find\b/,
|
|
59
|
-
/^\s*ls\b/,
|
|
60
|
-
/^\s*pwd\b/,
|
|
61
|
-
/^\s*echo\b/,
|
|
62
|
-
/^\s*printf\b/,
|
|
63
|
-
/^\s*wc\b/,
|
|
64
|
-
/^\s*sort\b/,
|
|
65
|
-
/^\s*uniq\b/,
|
|
66
|
-
/^\s*diff\b/,
|
|
67
|
-
/^\s*file\b/,
|
|
68
|
-
/^\s*stat\b/,
|
|
69
|
-
/^\s*du\b/,
|
|
70
|
-
/^\s*df\b/,
|
|
71
|
-
/^\s*tree\b/,
|
|
72
|
-
/^\s*which\b/,
|
|
73
|
-
/^\s*whereis\b/,
|
|
74
|
-
/^\s*type\b/,
|
|
75
|
-
/^\s*env\b/,
|
|
76
|
-
/^\s*printenv\b/,
|
|
77
|
-
/^\s*uname\b/,
|
|
78
|
-
/^\s*whoami\b/,
|
|
79
|
-
/^\s*id\b/,
|
|
80
|
-
/^\s*date\b/,
|
|
81
|
-
/^\s*cal\b/,
|
|
82
|
-
/^\s*uptime\b/,
|
|
83
|
-
/^\s*ps\b/,
|
|
84
|
-
/^\s*top\b/,
|
|
85
|
-
/^\s*htop\b/,
|
|
86
|
-
/^\s*free\b/,
|
|
87
|
-
/^\s*git\s+(status|log|diff|show|branch|remote|config\s+--get)/i,
|
|
88
|
-
/^\s*git\s+ls-/i,
|
|
89
|
-
/^\s*npm\s+(list|ls|view|info|search|outdated|audit)/i,
|
|
90
|
-
/^\s*yarn\s+(list|info|why|audit)/i,
|
|
91
|
-
/^\s*node\s+--version/i,
|
|
92
|
-
/^\s*python\s+--version/i,
|
|
93
|
-
/^\s*curl\s/i,
|
|
94
|
-
/^\s*wget\s+-O\s*-/i,
|
|
95
|
-
/^\s*jq\b/,
|
|
96
|
-
/^\s*sed\s+-n/i,
|
|
97
|
-
/^\s*awk\b/,
|
|
98
|
-
/^\s*rg\b/,
|
|
99
|
-
/^\s*fd\b/,
|
|
100
|
-
/^\s*bat\b/,
|
|
101
|
-
/^\s*eza\b/,
|
|
102
|
-
/^\s*cd\b/,
|
|
103
|
-
/^\s*gh\s+pr\s+(view|list|diff|checks|status)\b/i,
|
|
104
|
-
/^\s*gh\s+issue\s+(view|list)\b/i,
|
|
105
|
-
/^\s*gh\s+repo\s+(view|fork|list)\b/i,
|
|
106
|
-
/^\s*gh\s+release\s+(view|list|download)\b/i,
|
|
107
|
-
/^\s*gh\s+run\s+(view|list)\b/i,
|
|
108
|
-
/^\s*git\s+blame\b/,
|
|
109
|
-
/^\s*git\s+shortlog\b/,
|
|
110
|
-
/^\s*git\s+stash\s+list\b/i,
|
|
111
|
-
/^\s*git\s+tag\s+(-l|--list)\b/i,
|
|
112
|
-
/^\s*git\s+describe\b/,
|
|
113
|
-
/^\s*go\s+doc\b/,
|
|
114
|
-
/^\s*go\s+list\b/,
|
|
115
|
-
/^\s*go\s+version\b/,
|
|
116
|
-
/^\s*go\s+env\b/,
|
|
117
|
-
];
|
|
59
|
+
// Redirect operators — tested on a quote-stripped command so '>' inside quoted
|
|
60
|
+
// arguments (e.g. grep 'x > y') doesn't false-positive.
|
|
61
|
+
const REDIRECT_PATTERNS = [/(^|[^<])>(?!>)/, />>/];
|
|
118
62
|
|
|
119
63
|
/** Split a compound command into individual sub-commands.
|
|
120
64
|
* Splits on &&, ||, and ; operators, ignoring leading whitespace.
|
|
@@ -134,24 +78,71 @@ function stripHarmlessRedirects(cmd: string): string {
|
|
|
134
78
|
return cmd.replace(/\s*2\s*>\s*(\/dev\/null|&1)\b/g, "");
|
|
135
79
|
}
|
|
136
80
|
|
|
81
|
+
/** Blank out single- and double-quoted substrings so operators (>,
|
|
82
|
+
* >>, &&) inside quoted arguments don't trigger destructive-pattern
|
|
83
|
+
* false-positives. Replaces quoted content and quote chars with spaces,
|
|
84
|
+
* preserving length so operators outside quotes still match.
|
|
85
|
+
* Advisory only — not a full shell parser. */
|
|
86
|
+
function stripQuoted(cmd: string): string {
|
|
87
|
+
let out = "";
|
|
88
|
+
let i = 0;
|
|
89
|
+
let inQuote = false;
|
|
90
|
+
while (i < cmd.length) {
|
|
91
|
+
const ch = cmd[i];
|
|
92
|
+
const code = cmd.charCodeAt(i);
|
|
93
|
+
if (inQuote) {
|
|
94
|
+
if (code === 0x5c) {
|
|
95
|
+
out += " ";
|
|
96
|
+
i += 2;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
out += " ";
|
|
100
|
+
if (code === 0x22 || code === 0x27) {
|
|
101
|
+
inQuote = false;
|
|
102
|
+
}
|
|
103
|
+
i++;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (code === 0x22 || code === 0x27) {
|
|
107
|
+
inQuote = true;
|
|
108
|
+
out += " ";
|
|
109
|
+
i++;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
out += ch;
|
|
113
|
+
i++;
|
|
114
|
+
}
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
|
|
137
118
|
export function isSafeCommand(command: string): boolean {
|
|
138
|
-
|
|
139
|
-
return parts.every((part) => {
|
|
119
|
+
return splitCompoundCommand(command).every((part) => {
|
|
140
120
|
const cleaned = stripHarmlessRedirects(part);
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
return !isDestructive && isSafe;
|
|
121
|
+
if (REDIRECT_PATTERNS.some((p) => p.test(stripQuoted(cleaned)))) return false;
|
|
122
|
+
return !DESTRUCTIVE_PATTERNS.some((p) => p.test(cleaned));
|
|
144
123
|
});
|
|
145
124
|
}
|
|
146
125
|
|
|
147
126
|
const SKILL_TO_PHASE: Record<string, Phase> = {
|
|
148
127
|
"pwk-brainstorming": "brainstorm",
|
|
149
128
|
"pwk-writing-plans": "plan",
|
|
150
|
-
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/** Skills whose invocation exits a gated phase (used by the input handler; exported for tests/
|
|
132
|
+
* skill-lint). Deliberately excludes pwk-status (read-only by design; stays gated). */
|
|
133
|
+
export const UNLOCK_SKILLS = ["pwk-executing-tasks", "pwk-finalizing", "pwk-code-review", "pwk-diagnose"] as const;
|
|
134
|
+
|
|
135
|
+
/** Phase-aware reminder appended after the user's message each turn while a gated phase is active.
|
|
136
|
+
* Returned as a message (not a system-prompt change) so it sits at the tail of the request and
|
|
137
|
+
* never invalidates the cached prefix. */
|
|
138
|
+
const PHASE_REMINDERS: Record<Exclude<Phase, null>, string> = {
|
|
139
|
+
brainstorm:
|
|
140
|
+
"[pi-workflow-kit] BRAINSTORM phase: read-only. No source edits; writes only under docs/plans/. No mutations.",
|
|
141
|
+
plan: "[pi-workflow-kit] PLAN phase: read-only. No source edits; writes only under docs/plans/. No mutations.",
|
|
151
142
|
};
|
|
152
143
|
|
|
153
144
|
/** Determine if a write/edit to filePath should be blocked during the given phase.
|
|
154
|
-
* Only writes under docs/plans/ are allowed during brainstorm
|
|
145
|
+
* Only writes under docs/plans/ are allowed during brainstorm and plan phases.
|
|
155
146
|
*/
|
|
156
147
|
export function shouldBlockFilePath(filePath: string, cwd: string): boolean {
|
|
157
148
|
const absolute = resolve(cwd, filePath);
|
|
@@ -164,10 +155,84 @@ export function getCurrentPhase(): Phase {
|
|
|
164
155
|
}
|
|
165
156
|
|
|
166
157
|
let phase: Phase = null;
|
|
158
|
+
// True on the turn a gated phase is entered; consumed once by before_agent_start so the reminder
|
|
159
|
+
// is shown only on the first turn of the phase (and re-armed on any later phase change into it).
|
|
160
|
+
let pendingPhaseReminder = false;
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Manual guard override set by `/pwk-guard`.
|
|
164
|
+
* - null → "auto": enforcement follows the skill-driven phase (default).
|
|
165
|
+
* - "on" → force a read-only lock (enforce regardless of phase).
|
|
166
|
+
* - "off" → disable the guard entirely (escape hatch).
|
|
167
|
+
* Skill transitions still update `phase` while an override is active, so returning
|
|
168
|
+
* to `auto` recovers the correct state; enforcement itself ignores `phase`.
|
|
169
|
+
*/
|
|
170
|
+
let guardOverride: "on" | "off" | null = null;
|
|
171
|
+
|
|
172
|
+
/** Is the guard actively enforcing read-only right now? */
|
|
173
|
+
function enforceActive(): boolean {
|
|
174
|
+
if (guardOverride === "off") return false;
|
|
175
|
+
if (guardOverride === "on") return true;
|
|
176
|
+
return phase !== null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Label for the current enforcement context, used in block reasons and reminders. */
|
|
180
|
+
function enforceLabel(): string {
|
|
181
|
+
return guardOverride === "on" ? "GUARD ON" : phase ? phase.toUpperCase() : "";
|
|
182
|
+
}
|
|
167
183
|
|
|
168
184
|
export default function (pi: ExtensionAPI) {
|
|
169
185
|
pi.on("session_start", () => {
|
|
170
186
|
phase = null;
|
|
187
|
+
pendingPhaseReminder = false;
|
|
188
|
+
guardOverride = null;
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// --- Manual override (escape hatch) -----------------------------------
|
|
192
|
+
// Phases are driven by `/skill:` commands; `/pwk-guard` lets the user pin the
|
|
193
|
+
// guard regardless of phase. `/pwk-guard auto` returns control to skill transitions.
|
|
194
|
+
pi.registerCommand("pwk-guard", {
|
|
195
|
+
description:
|
|
196
|
+
"Manual guard override: /pwk-guard on (force read-only lock) | off (disable guard) | auto (skill-driven phases, default).",
|
|
197
|
+
getArgumentCompletions: (prefix) => {
|
|
198
|
+
const p = (prefix ?? "").trim().toLowerCase();
|
|
199
|
+
const opts = [
|
|
200
|
+
{ value: "on", label: "on", description: "Force read-only lock (ignore skill phases)" },
|
|
201
|
+
{ value: "off", label: "off", description: "Disable the guard entirely" },
|
|
202
|
+
{ value: "auto", label: "auto", description: "Follow skill-driven phases (default)" },
|
|
203
|
+
];
|
|
204
|
+
const matched = opts.filter((o) => o.value.startsWith(p));
|
|
205
|
+
return matched.length ? matched : opts;
|
|
206
|
+
},
|
|
207
|
+
handler: async (args, ctx) => {
|
|
208
|
+
const arg = (args ?? "").trim().toLowerCase();
|
|
209
|
+
if (arg === "auto") {
|
|
210
|
+
guardOverride = null;
|
|
211
|
+
if (phase) pendingPhaseReminder = true; // re-announce the active gated phase, if any
|
|
212
|
+
ctx.ui.notify("Guard AUTO — enforcement follows skill-driven phases.", "info");
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (arg !== "on" && arg !== "off") {
|
|
216
|
+
ctx.ui.notify("Usage: /pwk-guard on | off | auto", "info");
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
guardOverride = arg;
|
|
220
|
+
pendingPhaseReminder = false; // override announces itself via the message below
|
|
221
|
+
pi.sendMessage({
|
|
222
|
+
customType: "pwk-guard:override",
|
|
223
|
+
content:
|
|
224
|
+
arg === "on"
|
|
225
|
+
? "[pi-workflow-kit] GUARD ON (manual override): read-only lock active — no source edits; writes only under docs/plans/. No mutations. Use /pwk-guard auto to resume skill-driven phases."
|
|
226
|
+
: "[pi-workflow-kit] GUARD OFF (manual override): enforcement disabled — writes and bash are unrestricted. Use /pwk-guard auto to resume skill-driven phases.",
|
|
227
|
+
display: false,
|
|
228
|
+
});
|
|
229
|
+
ctx.ui.notify(
|
|
230
|
+
arg === "on"
|
|
231
|
+
? "Guard ON — read-only lock active (skill phases ignored)."
|
|
232
|
+
: "Guard OFF — enforcement disabled (skill phases ignored).",
|
|
233
|
+
arg === "on" ? "warning" : "info",
|
|
234
|
+
);
|
|
235
|
+
},
|
|
171
236
|
});
|
|
172
237
|
|
|
173
238
|
pi.on("input", (event) => {
|
|
@@ -176,27 +241,65 @@ export default function (pi: ExtensionAPI) {
|
|
|
176
241
|
if (match) {
|
|
177
242
|
const skill = match[1];
|
|
178
243
|
if (skill in SKILL_TO_PHASE) {
|
|
179
|
-
|
|
244
|
+
const nextPhase = SKILL_TO_PHASE[skill];
|
|
245
|
+
if (phase !== nextPhase) {
|
|
246
|
+
phase = nextPhase;
|
|
247
|
+
// The reminder is a phase-entry cue; only relevant when the guard is auto-driven.
|
|
248
|
+
if (guardOverride === null) pendingPhaseReminder = true;
|
|
249
|
+
}
|
|
180
250
|
return;
|
|
181
251
|
}
|
|
182
252
|
}
|
|
183
|
-
|
|
253
|
+
// Phase transitions happen only via skills — no message keyword unlocks the plan phase.
|
|
254
|
+
// Run /skill:pwk-executing-tasks (or another write-needing skill) to leave a gated phase.
|
|
255
|
+
//
|
|
256
|
+
// Unlock list rationale: execute/finalize/code-review/diagnose all need to write source
|
|
257
|
+
// (implement, edit review fixes, add [DEBUG-] instrumentation), so they exit the gate.
|
|
258
|
+
// pwk-status is NOT here on purpose: it is read-only orientation, so it stays inside the
|
|
259
|
+
// gated phase and never drops the write boundary the user is relying on.
|
|
260
|
+
// (Orientation never needs write access; see skills/pwk-status.)
|
|
261
|
+
if (UNLOCK_SKILLS.some((s) => text.startsWith(`/skill:${s}`))) {
|
|
184
262
|
phase = null;
|
|
185
263
|
}
|
|
186
264
|
});
|
|
187
265
|
|
|
266
|
+
// Show the phase reminder exactly once: on the first turn of a gated phase (before_agent_start
|
|
267
|
+
// fires before the LLM is called). It is NOT repeated every turn. Re-armed on any phase change.
|
|
268
|
+
// Returned as a tail-appended custom message so it never touches the cached system-prompt prefix.
|
|
269
|
+
//
|
|
270
|
+
// Note: a soft reminder on every *permitted* bash call is not possible — tool_call can only
|
|
271
|
+
// return { block, reason }, not a message. So at bash time the reminder surfaces only when a
|
|
272
|
+
// destructive command is actually blocked (the reactive `reason` in the tool_call handler).
|
|
273
|
+
pi.on("before_agent_start", async () => {
|
|
274
|
+
if (!phase || !pendingPhaseReminder) return {};
|
|
275
|
+
pendingPhaseReminder = false;
|
|
276
|
+
// Override states announce themselves via the /pwk-guard handler; this reminder
|
|
277
|
+
// is the auto-mode phase-entry cue only.
|
|
278
|
+
if (guardOverride !== null) return {};
|
|
279
|
+
return {
|
|
280
|
+
message: {
|
|
281
|
+
customType: "pwk-phase-reminder",
|
|
282
|
+
content: PHASE_REMINDERS[phase],
|
|
283
|
+
display: false,
|
|
284
|
+
},
|
|
285
|
+
};
|
|
286
|
+
});
|
|
287
|
+
|
|
188
288
|
pi.on("tool_call", (event, ctx) => {
|
|
189
|
-
if (!
|
|
289
|
+
if (!enforceActive()) return;
|
|
290
|
+
const label = enforceLabel();
|
|
291
|
+
const manual = guardOverride === "on";
|
|
292
|
+
const scope = manual ? "manual read-only lock" : `${label.toLowerCase()} phase`;
|
|
190
293
|
|
|
191
294
|
if (event.toolName === "bash") {
|
|
192
295
|
const command = (event.input as { command?: string }).command ?? "";
|
|
193
296
|
if (!isSafeCommand(command)) {
|
|
194
297
|
if (ctx.hasUI) {
|
|
195
|
-
ctx.ui.notify(`Blocked bash command
|
|
298
|
+
ctx.ui.notify(`Blocked bash command (${scope}): ${command}`, "warning");
|
|
196
299
|
}
|
|
197
300
|
return {
|
|
198
301
|
block: true,
|
|
199
|
-
reason: `⚠️ ${
|
|
302
|
+
reason: `⚠️ ${label}: read-only — no source writes or destructive bash. Only read-only commands are permitted.\nBlocked command: ${command}`,
|
|
200
303
|
};
|
|
201
304
|
}
|
|
202
305
|
return;
|
|
@@ -210,15 +313,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
210
313
|
if (!shouldBlockFilePath(filePath, ctx.cwd)) return;
|
|
211
314
|
|
|
212
315
|
if (ctx.hasUI) {
|
|
213
|
-
ctx.ui.notify(
|
|
214
|
-
`Blocked ${event.toolName} to ${filePath} during ${phase} phase. Only docs/plans/ is writable.`,
|
|
215
|
-
"warning",
|
|
216
|
-
);
|
|
316
|
+
ctx.ui.notify(`Blocked ${event.toolName} to ${filePath} (${scope}). Only docs/plans/ is writable.`, "warning");
|
|
217
317
|
}
|
|
218
318
|
|
|
219
319
|
return {
|
|
220
320
|
block: true,
|
|
221
|
-
reason: `⚠️ ${
|
|
321
|
+
reason: `⚠️ ${label}: Cannot ${event.toolName} to ${filePath}. Only docs/plans/ is writable${
|
|
322
|
+
manual ? " under the manual read-only lock" : " during brainstorming and planning"
|
|
323
|
+
}.`,
|
|
222
324
|
};
|
|
223
325
|
});
|
|
224
326
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tianhai/pi-workflow-kit",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Enforce structured brainstorm→plan→execute→finalize workflow with TDD discipline in AI coding agents",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
"scripts": {
|
|
17
17
|
"test": "vitest run",
|
|
18
18
|
"lint": "biome check .",
|
|
19
|
-
"
|
|
19
|
+
"skill-lint": "node tests/skill-lint.mjs",
|
|
20
|
+
"check": "biome check . && vitest run && node tests/skill-lint.mjs"
|
|
20
21
|
},
|
|
21
22
|
"license": "MIT",
|
|
22
23
|
"author": "yinloo-ola",
|
|
@@ -27,7 +28,11 @@
|
|
|
27
28
|
"files": [
|
|
28
29
|
"extensions/",
|
|
29
30
|
"skills/",
|
|
30
|
-
"
|
|
31
|
+
"agents/",
|
|
32
|
+
"docs/developer-usage-guide.md",
|
|
33
|
+
"docs/workflow-phases.md",
|
|
34
|
+
"docs/oversight-model.md",
|
|
35
|
+
"docs/lessons.md",
|
|
31
36
|
"LICENSE",
|
|
32
37
|
"README.md"
|
|
33
38
|
],
|
|
@@ -39,9 +44,20 @@
|
|
|
39
44
|
"skills"
|
|
40
45
|
]
|
|
41
46
|
},
|
|
47
|
+
"pi-subagents": {
|
|
48
|
+
"agents": [
|
|
49
|
+
"./agents"
|
|
50
|
+
]
|
|
51
|
+
},
|
|
42
52
|
"peerDependencies": {
|
|
43
53
|
"@earendil-works/pi-coding-agent": "*",
|
|
44
|
-
"@sinclair/typebox": "*"
|
|
54
|
+
"@sinclair/typebox": "*",
|
|
55
|
+
"pi-subagents": "*"
|
|
56
|
+
},
|
|
57
|
+
"peerDependenciesMeta": {
|
|
58
|
+
"pi-subagents": {
|
|
59
|
+
"optional": true
|
|
60
|
+
}
|
|
45
61
|
},
|
|
46
62
|
"devDependencies": {
|
|
47
63
|
"@biomejs/biome": "^2.3.15",
|