@tianhai/pi-workflow-kit 2.2.0 → 2.5.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 +14 -15
- package/agents/pwk-spec-reviewer.md +1 -1
- package/agents/pwk-tracing-reviewer.md +1 -1
- package/docs/developer-usage-guide.md +11 -17
- package/docs/oversight-model.md +12 -10
- package/docs/provider-delegation-contract.md +1 -1
- package/docs/workflow-phases.md +23 -14
- package/extensions/workflow-guard.ts +4 -10
- package/package.json +1 -1
- package/skills/pwk-brainstorming/SKILL.md +37 -79
- package/skills/pwk-brainstorming/references/design-format.md +83 -0
- package/skills/pwk-brainstorming/references/umbrella.md +26 -0
- package/skills/pwk-brainstorming/scripts/validate-design.py +194 -0
- package/skills/pwk-code-review/SKILL.md +25 -17
- package/skills/pwk-executing-tasks/SKILL.md +88 -126
- package/skills/pwk-executing-tasks/references/progress-format.md +81 -0
- package/skills/pwk-executing-tasks/references/resume.md +15 -0
- package/skills/pwk-executing-tasks/references/review.md +42 -0
- package/skills/pwk-executing-tasks/scripts/build-review-packet.py +318 -0
- package/skills/pwk-finalizing/SKILL.md +27 -29
- package/skills/pwk-status/SKILL.md +9 -12
- package/skills/pwk-walkthrough/SKILL.md +2 -2
- package/skills/pwk-diagnose/SKILL.md +0 -59
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Build review evidence from explicit, immutable Git bounds. Python 3.9+, stdlib only."""
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import re
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
import tempfile
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PacketError(Exception):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Document:
|
|
18
|
+
"""Keep original lines for evidence; inspect headings only outside fenced examples."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, path):
|
|
21
|
+
with path.open(encoding='utf-8', newline='') as source:
|
|
22
|
+
self.lines = source.readlines()
|
|
23
|
+
self.headings = []
|
|
24
|
+
self.visible = set()
|
|
25
|
+
fence = None
|
|
26
|
+
for index, line in enumerate(self.lines):
|
|
27
|
+
marker = re.match(r'^ {0,3}(`{3,}|~{3,})(.*)$', line.rstrip('\r\n'))
|
|
28
|
+
if fence:
|
|
29
|
+
if marker and marker[1][0] == fence[0] and len(marker[1]) >= len(fence) and not marker[2].strip():
|
|
30
|
+
fence = None
|
|
31
|
+
continue
|
|
32
|
+
if marker:
|
|
33
|
+
fence = marker[1]
|
|
34
|
+
continue
|
|
35
|
+
self.visible.add(index)
|
|
36
|
+
heading = re.match(r'^ {0,3}(#{1,6})\s+(.+?)\s*#*\s*$', line.rstrip('\r\n'))
|
|
37
|
+
if heading:
|
|
38
|
+
self.headings.append((index, len(heading[1]), heading[2]))
|
|
39
|
+
if fence:
|
|
40
|
+
raise PacketError(f'{path.name}: unclosed code fence')
|
|
41
|
+
|
|
42
|
+
def section(self, title, level=2, required=False):
|
|
43
|
+
matches = [h for h in self.headings if h[1:] == (level, title)]
|
|
44
|
+
if len(matches) > 1 or (required and not matches):
|
|
45
|
+
raise PacketError(f'expected one {title} section; found {len(matches)}')
|
|
46
|
+
if not matches:
|
|
47
|
+
return None
|
|
48
|
+
start = matches[0][0]
|
|
49
|
+
end = next((i for i, depth, _ in self.headings if i > start and depth <= level), len(self.lines))
|
|
50
|
+
return start, end
|
|
51
|
+
|
|
52
|
+
def text(self, start, end):
|
|
53
|
+
return ''.join(self.lines[start:end])
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def git(project, *args):
|
|
57
|
+
result = subprocess.run(
|
|
58
|
+
['git', '-C', str(project), *args], stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
|
59
|
+
env={**os.environ, 'GIT_OPTIONAL_LOCKS': '0', 'GIT_TERMINAL_PROMPT': '0'},
|
|
60
|
+
)
|
|
61
|
+
if result.returncode:
|
|
62
|
+
invocation = ' '.join(['git', *args])
|
|
63
|
+
raise PacketError(f'{invocation} failed: {result.stderr.decode("utf-8", errors="replace").strip()}')
|
|
64
|
+
return result.stdout.decode('utf-8', errors='replace')
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def resolve_input(project, value):
|
|
68
|
+
path = Path(value)
|
|
69
|
+
return (path if path.is_absolute() else project / path).resolve(strict=True)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def paths(args):
|
|
73
|
+
project = Path(args.project).resolve(strict=True)
|
|
74
|
+
if Path(git(project, 'rev-parse', '--show-toplevel').strip()).resolve() != project:
|
|
75
|
+
raise PacketError('--project must be the Git worktree root')
|
|
76
|
+
for value in (args.design, args.progress):
|
|
77
|
+
supplied = Path(value)
|
|
78
|
+
supplied = Path(os.path.abspath(supplied if supplied.is_absolute() else project / supplied))
|
|
79
|
+
lexical_project = Path(os.path.abspath(args.project))
|
|
80
|
+
try:
|
|
81
|
+
relative_input = supplied.relative_to(lexical_project)
|
|
82
|
+
except ValueError:
|
|
83
|
+
relative_input = supplied.relative_to(project)
|
|
84
|
+
current = project
|
|
85
|
+
for part in relative_input.parts:
|
|
86
|
+
current = current / part
|
|
87
|
+
if current.is_symlink():
|
|
88
|
+
raise PacketError('design/progress paths must not traverse symlinks')
|
|
89
|
+
design = resolve_input(project, args.design)
|
|
90
|
+
progress = resolve_input(project, args.progress)
|
|
91
|
+
relative = design.relative_to(project / 'docs' / 'plans')
|
|
92
|
+
if len(relative.parts) != 2 or relative.parts[0] == 'completed' or not design.name.endswith('-design.md'):
|
|
93
|
+
raise PacketError('design must be docs/plans/<topic>/<leaf>-design.md in an active topic')
|
|
94
|
+
leaf = design.name[:-len('-design.md')]
|
|
95
|
+
if not leaf or progress != design.with_name(f'{leaf}-progress.md'):
|
|
96
|
+
raise PacketError('progress must be the matching <leaf>-progress.md beside the design')
|
|
97
|
+
# Resolving inputs must not route a packet write through symlinked topic directories.
|
|
98
|
+
lexical_topic = project / 'docs' / 'plans' / relative.parts[0]
|
|
99
|
+
for directory in (project / 'docs', project / 'docs' / 'plans', lexical_topic):
|
|
100
|
+
if directory.is_symlink():
|
|
101
|
+
raise PacketError('symlink topic directories are not writable packet destinations')
|
|
102
|
+
suffix = '' if args.scope == 'feature' else f'-r{args.requirement}'
|
|
103
|
+
destination = design.with_name(f'{leaf}-review-packet{suffix}.md')
|
|
104
|
+
validate_destination(destination)
|
|
105
|
+
return project, design, progress, destination
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def validate_destination(path):
|
|
109
|
+
if path.is_symlink() or (path.exists() and not path.is_file()):
|
|
110
|
+
raise PacketError('packet destination must be a regular file, not a symlink or directory')
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def requirements(document):
|
|
114
|
+
region = document.section('Requirements', required=True)
|
|
115
|
+
headings = []
|
|
116
|
+
for index, depth, title in document.headings:
|
|
117
|
+
match = re.match(r'^R([1-9][0-9]*):\s*\S', title)
|
|
118
|
+
if depth == 3 and re.match(r'^R[0-9]', title) and not match:
|
|
119
|
+
raise PacketError('malformed requirement heading')
|
|
120
|
+
if depth == 3 and match:
|
|
121
|
+
if not region[0] < index < region[1]:
|
|
122
|
+
raise PacketError('requirement heading outside Requirements section')
|
|
123
|
+
headings.append((int(match[1]), index))
|
|
124
|
+
numbers = [number for number, _ in headings]
|
|
125
|
+
if not numbers or len(set(numbers)) != len(numbers):
|
|
126
|
+
raise PacketError('missing or duplicate requirement headings')
|
|
127
|
+
return headings, region
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def progress_rows(document, project, design):
|
|
131
|
+
first_heading = next((i for i, depth, _ in document.headings if depth == 2), len(document.lines))
|
|
132
|
+
pointers = [document.lines[i].strip()[8:] for i in document.visible
|
|
133
|
+
if i < first_heading and document.lines[i].startswith('Design: ')]
|
|
134
|
+
if len(pointers) != 1 or resolve_input(project, pointers[0]) != design:
|
|
135
|
+
raise PacketError('progress Design header must identify the selected design exactly once')
|
|
136
|
+
start, end = document.section('Requirements', required=True)
|
|
137
|
+
lines = [document.lines[i].strip() for i in range(start + 1, end)
|
|
138
|
+
if i in document.visible and document.lines[i].strip()]
|
|
139
|
+
if len(lines) < 3:
|
|
140
|
+
raise PacketError('missing Requirements table')
|
|
141
|
+
|
|
142
|
+
def cells(line):
|
|
143
|
+
if not line.startswith('|') or not line.endswith('|'):
|
|
144
|
+
raise PacketError('malformed Requirements table row')
|
|
145
|
+
return [cell.strip() for cell in re.split(r'(?<!\\)\|', line)[1:-1]]
|
|
146
|
+
|
|
147
|
+
header = cells(lines[0])
|
|
148
|
+
if header != ['#', 'Done', 'Requirement', 'Per-req ceremony', 'Commit']:
|
|
149
|
+
raise PacketError('Requirements table must have #, Done, Requirement, Per-req ceremony, Commit columns')
|
|
150
|
+
separator = cells(lines[1])
|
|
151
|
+
if len(separator) != 5 or not all(re.fullmatch(r':?-+:?', cell) for cell in separator):
|
|
152
|
+
raise PacketError('malformed Requirements table separator')
|
|
153
|
+
rows = {}
|
|
154
|
+
for line in lines[2:]:
|
|
155
|
+
row = cells(line)
|
|
156
|
+
if len(row) != 5 or not re.fullmatch(r'[1-9][0-9]*', row[0]):
|
|
157
|
+
raise PacketError('malformed Requirements table row')
|
|
158
|
+
number = int(row[0])
|
|
159
|
+
if number in rows:
|
|
160
|
+
raise PacketError(f'duplicate R{number} progress row')
|
|
161
|
+
if row[1] not in ('', '⬜', '🔄', '✅', '❌', '⏭'):
|
|
162
|
+
raise PacketError(f'R{number}: invalid Done cell')
|
|
163
|
+
rows[number] = row
|
|
164
|
+
return rows
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def bounds(project, rows, headings, args):
|
|
168
|
+
numbers = [number for number, _ in headings]
|
|
169
|
+
position = numbers.index(args.requirement) if args.scope == 'requirement' else None
|
|
170
|
+
relevant = numbers if position is None else numbers[max(0, position - 1):position + 1]
|
|
171
|
+
if set(rows) - set(numbers):
|
|
172
|
+
raise PacketError('progress contains requirements absent from the design')
|
|
173
|
+
recorded = {}
|
|
174
|
+
previous = None
|
|
175
|
+
current_head = git(project, 'rev-parse', '--verify', 'HEAD^{commit}').strip()
|
|
176
|
+
for number in relevant:
|
|
177
|
+
if number not in rows:
|
|
178
|
+
raise PacketError(f'R{number}: missing progress row')
|
|
179
|
+
row = rows[number]
|
|
180
|
+
value = row[4]
|
|
181
|
+
if value in ('', '—', '-'):
|
|
182
|
+
if row[1] not in ('❌', '⏭'):
|
|
183
|
+
raise PacketError(f'R{number}: missing Commit entry')
|
|
184
|
+
recorded[number] = []
|
|
185
|
+
continue
|
|
186
|
+
# An ordered whitespace-separated list supports review-fix commits without losing
|
|
187
|
+
# the first feature commit. Other formats are ambiguous, never mined for hashes.
|
|
188
|
+
entries = value.split()
|
|
189
|
+
if not all(re.fullmatch(r'[0-9a-fA-F]{7,64}', entry) for entry in entries):
|
|
190
|
+
raise PacketError(f'R{number}: Commit must contain only ordered hexadecimal commit IDs')
|
|
191
|
+
commits = []
|
|
192
|
+
for entry in entries:
|
|
193
|
+
commit = git(project, 'rev-parse', '--verify', f'{entry}^{{commit}}').strip()
|
|
194
|
+
if previous:
|
|
195
|
+
if previous == commit:
|
|
196
|
+
raise PacketError('duplicate or overlapping recorded commits')
|
|
197
|
+
git(project, 'merge-base', '--is-ancestor', previous, commit)
|
|
198
|
+
git(project, 'merge-base', '--is-ancestor', commit, current_head)
|
|
199
|
+
commits.append(commit)
|
|
200
|
+
previous = commit
|
|
201
|
+
recorded[number] = commits
|
|
202
|
+
if args.scope == 'feature':
|
|
203
|
+
first = next((commits[0] for commits in recorded.values() if commits), None)
|
|
204
|
+
if not first:
|
|
205
|
+
raise PacketError('no recorded commits; cannot infer a feature base')
|
|
206
|
+
return git(project, 'rev-parse', '--verify', f'{first}^').strip(), current_head
|
|
207
|
+
selected = recorded[args.requirement]
|
|
208
|
+
if not selected:
|
|
209
|
+
raise PacketError('selected requirement has no recorded bound')
|
|
210
|
+
if position == 0:
|
|
211
|
+
base = git(project, 'rev-parse', '--verify', f'{selected[0]}^').strip()
|
|
212
|
+
else:
|
|
213
|
+
prior = recorded[numbers[position - 1]]
|
|
214
|
+
if not prior:
|
|
215
|
+
raise PacketError('previous requirement has no recorded bound; cannot guess a range')
|
|
216
|
+
base = prior[-1]
|
|
217
|
+
return base, selected[-1]
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def evidence(document, headings, region, args):
|
|
221
|
+
feature = document.section('Feature acceptance')
|
|
222
|
+
if feature and feature[0] < region[1]:
|
|
223
|
+
raise PacketError('Feature acceptance must follow Requirements')
|
|
224
|
+
if args.scope == 'feature':
|
|
225
|
+
start = headings[0][1]
|
|
226
|
+
end = feature[0] if feature else len(document.lines)
|
|
227
|
+
else:
|
|
228
|
+
position = next(i for i, (number, _) in enumerate(headings) if number == args.requirement)
|
|
229
|
+
start = headings[position][1]
|
|
230
|
+
end = headings[position + 1][1] if position + 1 < len(headings) else region[1]
|
|
231
|
+
criteria = document.text(start, end)
|
|
232
|
+
acceptance = ''
|
|
233
|
+
if feature:
|
|
234
|
+
stop = next((i for i, depth, title in document.headings
|
|
235
|
+
if feature[0] < i < feature[1] and depth == 3 and title.startswith('Feature review:')), feature[1])
|
|
236
|
+
acceptance = document.text(feature[0], stop)
|
|
237
|
+
notes = []
|
|
238
|
+
for index, depth, title in document.headings:
|
|
239
|
+
if depth == 3 and title == 'Production-risk notes' and start <= index < end:
|
|
240
|
+
stop = next((i for i, d, t in document.headings if i > index and
|
|
241
|
+
(d <= 2 or (d == 3 and re.match(r'^R[0-9]+:', t)))), len(document.lines))
|
|
242
|
+
notes.append(document.text(index, min(stop, end)))
|
|
243
|
+
risk = document.section('Production-risk areas')
|
|
244
|
+
if risk:
|
|
245
|
+
notes.append(document.text(*risk))
|
|
246
|
+
return criteria, acceptance, ''.join(notes)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def build(args):
|
|
250
|
+
project, design_path, progress_path, destination = paths(args)
|
|
251
|
+
design = Document(design_path)
|
|
252
|
+
headings, region = requirements(design)
|
|
253
|
+
if args.scope == 'requirement' and args.requirement not in [n for n, _ in headings]:
|
|
254
|
+
raise PacketError('selected requirement is absent from the design')
|
|
255
|
+
rows = progress_rows(Document(progress_path), project, design_path)
|
|
256
|
+
base, head = bounds(project, rows, headings, args)
|
|
257
|
+
criteria, acceptance, notes = evidence(design, headings, region, args)
|
|
258
|
+
span = f'{base}..{head}'
|
|
259
|
+
pathspec = ['--', '.', ':(glob,exclude)**/*-review-packet*.md']
|
|
260
|
+
sections = [
|
|
261
|
+
('Commits', git(project, 'log', '--oneline', '--no-decorate', span)),
|
|
262
|
+
('Changed files', git(project, 'diff', '--no-ext-diff', '--no-textconv', '--stat', span, *pathspec)),
|
|
263
|
+
('Acceptance criteria (verbatim from the design doc)', criteria),
|
|
264
|
+
('Feature acceptance (verbatim)', acceptance),
|
|
265
|
+
('Production-risk notes (verbatim, if any)', notes),
|
|
266
|
+
('Diff', git(project, 'diff', '--no-ext-diff', '--no-textconv', '--no-color', span, *pathspec)),
|
|
267
|
+
]
|
|
268
|
+
label = 'feature review' if args.scope == 'feature' else f'requirement R{args.requirement} review'
|
|
269
|
+
packet = f'# Review packet: {design_path.parent.name} — {label}\n\nBase: {base}\nHead: {head}\n\n'
|
|
270
|
+
for title, content in sections:
|
|
271
|
+
packet += f'## {title}\n\n{content}\n'
|
|
272
|
+
# All parsing and Git reads finish before a temporary file exists. A failure in
|
|
273
|
+
# assembly leaves an earlier complete packet intact. Replacement is same-directory.
|
|
274
|
+
temporary = None
|
|
275
|
+
try:
|
|
276
|
+
validate_destination(destination)
|
|
277
|
+
with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8', newline='', dir=destination.parent,
|
|
278
|
+
prefix=f'.{destination.name}.', delete=False) as output:
|
|
279
|
+
temporary = Path(output.name)
|
|
280
|
+
output.write(packet)
|
|
281
|
+
output.flush()
|
|
282
|
+
os.fsync(output.fileno())
|
|
283
|
+
validate_destination(destination)
|
|
284
|
+
os.replace(temporary, destination)
|
|
285
|
+
temporary = None
|
|
286
|
+
finally:
|
|
287
|
+
if temporary is not None:
|
|
288
|
+
temporary.unlink()
|
|
289
|
+
print(f'Wrote {destination}\nRange: {base}..{head}')
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def main():
|
|
293
|
+
parser = argparse.ArgumentParser(description=__doc__, epilog=(
|
|
294
|
+
'--project resolves from the caller cwd and must be a Git worktree root. '
|
|
295
|
+
'--design and --progress are absolute or relative to --project, not the skill or cwd. '
|
|
296
|
+
'Output is <leaf>-review-packet[-rN].md beside the matched docs in docs/plans/<topic>/. '
|
|
297
|
+
'Commit cells contain ordered whitespace-separated hexadecimal commit IDs, not revision expressions.'
|
|
298
|
+
))
|
|
299
|
+
parser.add_argument('--project', required=True)
|
|
300
|
+
parser.add_argument('--design', required=True)
|
|
301
|
+
parser.add_argument('--progress', required=True)
|
|
302
|
+
parser.add_argument('--scope', choices=['feature', 'requirement'], required=True)
|
|
303
|
+
parser.add_argument('--requirement', type=int)
|
|
304
|
+
args = parser.parse_args()
|
|
305
|
+
if (args.scope == 'requirement') != (args.requirement is not None):
|
|
306
|
+
parser.error('--requirement N is required only for --scope requirement')
|
|
307
|
+
try:
|
|
308
|
+
if sys.version_info < (3, 9):
|
|
309
|
+
raise PacketError('Python 3.9 or newer is required; install it explicitly before retrying')
|
|
310
|
+
build(args)
|
|
311
|
+
except (PacketError, OSError, ValueError) as error:
|
|
312
|
+
print(f'error: {error}', file=sys.stderr)
|
|
313
|
+
return 1
|
|
314
|
+
return 0
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
if __name__ == '__main__':
|
|
318
|
+
sys.exit(main())
|
|
@@ -9,58 +9,56 @@ Ship the completed work.
|
|
|
9
9
|
|
|
10
10
|
## Pre-finalization checks
|
|
11
11
|
|
|
12
|
-
1. **
|
|
13
|
-
2. **
|
|
14
|
-
3. Read **every** relevant progress file — for
|
|
15
|
-
- **Any `❌ failed`** (in any
|
|
12
|
+
1. **Verify the repo root** — run `pwd` (or your shell's equivalent) and `git rev-parse --show-toplevel`; mismatch → report both paths and stop; never `cd` (a worktree root counts).
|
|
13
|
+
2. **Run the FULL test suite** — every test must pass, and only a green suite ships. Resume spans sessions; re-run the suite yourself rather than trust the previous session's ending state. Anything failing → send the user back to `/skill:pwk-executing-tasks`.
|
|
14
|
+
3. Read **every** relevant progress file — for a multi-leaf topic that's each leaf's progress file — discovered by listing `docs/plans` recursively, excluding docs/plans/completed/, for `*-progress.md` (topic docs live in `docs/plans/<date>-<topic>/` folders — archived work is not in flight). Use whatever recurses in your harness; one example: `find docs/plans -name '<suffix>' -not -path '*/completed/*'`. More than one topic in flight? Ask which one this finalize ships — finalizing touches only its own topic, and another topic still being designed or executed never blocks this run. Then match, in the chosen topic's file(s), the `Feature phase:` line (e.g. `grep -m1 '^Feature phase:' <file>`) and read only the matching ❌/⏭ verdict rows in the Requirements table (e.g. `sed -n '/^## Requirements/,/^## Execution summary/p' <file> | grep '^| [0-9].*❌'` / `sed -n '/^## Requirements/,/^## Execution summary/p' <file> | grep '^| [0-9].*⏭'` — scoped to Requirements-table rows because the Execution summary rows share the same row shape, with reasons); the sections after the tables carry nothing the gate needs. Apply the gates below to the chosen topic's file(s) and no others:
|
|
15
|
+
- **Any `❌ failed`** (in any leaf of the topic) → **block**. Present counts and reasons; continue only when the user sends the task back to executing-tasks, or explicitly types `--force-failed` to acknowledge shipping with incomplete requirements.
|
|
16
16
|
- **Only `⏭ skipped`** → warn and confirm ("Requirement N was skipped. Continue, or go back?").
|
|
17
|
-
- **`Feature phase` must be `done`** in every progress file — any other value (`e2e-written`, `
|
|
17
|
+
- **`Feature phase` must be `done`** in every progress file of the topic — any other value (`e2e-written`, `implementing (k/N)`, `reviewing`, `ship-paused`) means that topic is still in flight: its ship checkpoint has not been approved. Send the user back to `/skill:pwk-executing-tasks` instead of finalizing.
|
|
18
|
+
4. **Cross-check the roster — fail closed on unstarted work.** Find the topic folder locally: the progress file's `Design:` ref names the design doc, and that directory is the topic folder. An `overview.md` **beside the design doc** (same folder — never a repo-wide `docs/plans/**/overview.md`, or a sibling topic's overview would route this finalize) means a multi-leaf topic. Read that overview's `## Parts (build order)` roster and require one `<leaf>-progress.md` per leaf. A roster leaf with no progress file is **unstarted** — **block**, naming the leaf, and send the user back to `/skill:pwk-executing-tasks` to build it; never dispose an unstarted design unread. (A leaf with a progress file that is not `done` is already stopped by the gate above.)
|
|
18
19
|
|
|
19
20
|
## Process
|
|
20
21
|
|
|
21
22
|
1. **Derive the topic set** —
|
|
22
|
-
- **
|
|
23
|
-
- **
|
|
23
|
+
- **Multi-leaf topic** (an `overview.md` sits beside the design doc, inside the same `docs/plans/<date>-<topic>/` folder — never a repo-wide `docs/plans/**/overview.md`, and always excluding docs/plans/completed/, so an archived or sibling topic never routes): read its roster; the set is the topic folder itself, disposed as one unit.
|
|
24
|
+
- **Single-leaf topic**: progress file → `Design:` ref → that design doc's own folder → `<topic>`, which **is the folder slug** — the topic directory's name, and the same value `pwk-executing-tasks` uses for the branch and worktree (never a leaf filename). One folder.
|
|
24
25
|
|
|
25
26
|
Ambiguous with several designs in flight? Ask.
|
|
26
|
-
2. **Run the learning sweep — before any disposal command.** The planning docs are about to be destroyed; extract the durable knowledge first, while its container still exists. Read
|
|
27
|
+
2. **Run the learning sweep — before any disposal command.** The planning docs are about to be destroyed; extract the durable knowledge first, while its container still exists. Read each design doc's At-a-glance key-decision bullets **and its `Approaches considered` section** (the full forks with their reasoning — every leaf's, for an umbrella), each progress file's `Deviated?` entries (including any deviation decision-records), and each Code digest's `[ALERT]` entries. (A trivial doc has no `## At a glance` and no `Approaches considered` — a trivial slice's sweep is expected to come up honest-empty.) Then:
|
|
27
28
|
- **Offer an ADR** (in `docs/adr/`) for each item that passes the three gates — **hard to reverse**, **surprising without context**, **a real trade-off** — informed by how the decision actually played out during execution, not just how it looked at design time.
|
|
28
29
|
- **Append generic rules** to `docs/lessons.md` (strip domain specifics).
|
|
29
30
|
- Both outputs are **honest-empty**: most features qualify for nothing — say so and move on; never manufacture. When an item passes the gates but the recorded material is too thin to draft a credible ADR, **ask the human rather than fabricating** context — the human was there.
|
|
30
31
|
- The sweep is file-based by necessity: executing and finalizing usually run in fresh sessions with no memory of the brainstorm conversation — everything the sweep needs must already be on disk.
|
|
31
|
-
3. **Dispose of consumed plan docs — ask archive or delete** — for **every topic** in the set, dispose its
|
|
32
|
+
3. **Dispose of consumed plan docs — ask archive or delete** — for **every topic** in the set, dispose its topic folder `docs/plans/<date>-<topic>/` **as one unit**: every leaf's `-design.md`, `-progress.md`, and `-review-packet*.md`, plus `overview.md` for an umbrella. The digest sections (`## At a glance`, `## Execution summary`) live inside their host docs and ride the folder — no separate disposal. The folder path is taken verbatim from discovery, and the `????-??-??-` dated shape is the topic's identity — a bare `*<topic>*` pattern would over-match unrelated topics. Present both options and let the human choose:
|
|
32
33
|
|
|
33
34
|
- **Delete (default)** — code + tests are the source of truth; removing the scaffold prevents stale plan docs from misleading future sessions:
|
|
34
35
|
|
|
35
36
|
```bash
|
|
36
|
-
#
|
|
37
|
-
|
|
38
|
-
#
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
37
|
+
# the topic folder goes as one unit (every leaf, plus overview.md if it has one).
|
|
38
|
+
# The folder path is taken verbatim from discovery — never typed or
|
|
39
|
+
# reconstructed (rm -rf has no glob guard):
|
|
40
|
+
rm -rf docs/plans/<date>-<topic>/
|
|
41
|
+
git add -A docs/plans/<date>-<topic>/ && git commit -m "chore: delete planning docs for <topic>"
|
|
42
|
+
# the add is scoped to the topic pathspec, so another in-flight topic's
|
|
43
|
+
# uncommitted drafts never ride the disposal commit.
|
|
43
44
|
```
|
|
44
45
|
|
|
45
46
|
- **Archive** — keep the planning history for future readers (e.g. a complex design worth preserving) by moving the artifacts into `docs/plans/completed/`:
|
|
46
47
|
|
|
47
48
|
```bash
|
|
48
49
|
mkdir -p docs/plans/completed
|
|
49
|
-
#
|
|
50
|
-
mv docs/plans/????-??-??-<topic>-design.md docs/plans/completed/ 2>/dev/null || true
|
|
51
|
-
mv docs/plans/????-??-??-<topic>-implementation.md docs/plans/completed/ 2>/dev/null || true
|
|
52
|
-
mv docs/plans/????-??-??-<topic>-progress.md docs/plans/completed/ 2>/dev/null || true
|
|
53
|
-
mv docs/plans/????-??-??-<topic>-review-packet*.md docs/plans/completed/ 2>/dev/null || true
|
|
54
|
-
# umbrella only — the whole folder goes as one unit (overview + every part).
|
|
50
|
+
# the topic folder goes as one unit (every leaf, plus overview.md if it has one).
|
|
55
51
|
# No error suppression on the folder move: verify the archive landed before
|
|
56
52
|
# committing, or a silently failed move would commit the deletion and destroy
|
|
57
53
|
# the history the human chose to keep:
|
|
58
|
-
mv docs/plans/<date>-<
|
|
59
|
-
ls docs/plans/completed/<date>-<
|
|
60
|
-
|
|
54
|
+
mv docs/plans/<date>-<topic>/ docs/plans/completed/
|
|
55
|
+
ls docs/plans/completed/<date>-<topic>/ >/dev/null
|
|
56
|
+
# both sides of the move, and nothing else — another in-flight topic's
|
|
57
|
+
# uncommitted drafts never ride the archive commit:
|
|
58
|
+
git add -A docs/plans/<date>-<topic>/ docs/plans/completed/ && git commit -m "chore: archive planning docs for <topic>"
|
|
61
59
|
```
|
|
62
60
|
|
|
63
|
-
|
|
61
|
+
Verify with `ls docs/plans/` before and after. Both paths commit the disposal so the shipped branch is clean. Neither path touches `docs/adr/`, `docs/lessons.md`, `CHANGELOG.md`, or `README.md` — those are permanent.
|
|
64
62
|
4. **Curate lessons (Agile Scrum Master hat)** — if `docs/lessons.md` exists: add missed lessons, generalize domain-specific rules into generic patterns, de-duplicate, categorize, retire stale rules. None exists but lessons were learned? Create it. (The learning sweep above feeds this; curation then shapes the whole file.)
|
|
65
63
|
5. **Update documentation** — if the API or surface changed: `README.md`, `CHANGELOG.md`, any inline docs. Bump the package version (major for breaking changes).
|
|
66
64
|
6. **Choose a merge strategy** — ask the human:
|
|
@@ -70,13 +68,13 @@ Ship the completed work.
|
|
|
70
68
|
3. **Squash & merge** — squash onto parent, push, delete branch.
|
|
71
69
|
4. **Merge commit** — `--no-ff` merge, push parent, delete branch.
|
|
72
70
|
|
|
73
|
-
For 2–4, confirm the detected parent branch before proceeding.
|
|
74
|
-
7. **Clean up** —
|
|
71
|
+
For 2–4, confirm the detected parent branch before proceeding. When a worktree was used, the parent-side commands (merge onto parent, push parent, branch deletion) run from the main working tree — the topic worktree holds the feature-branch checkout, so the parent cannot be checked out there (`git -C ../<repo> …` drives the main side without moving the session; the root check's never-`cd` rule still holds) — and step 7's worktree removal comes **first**: a branch checked out in a worktree cannot be deleted.
|
|
72
|
+
7. **Clean up** — if a worktree was used, verify presence first (`git worktree list`): when the topic's worktree exists, remove it (`git worktree remove ../<repo>-<topic>`, where `<topic>` is the folder slug — the same one `pwk-executing-tasks` created the branch under, never a leaf slug). The `../` path is written from the main working tree, and the removal runs from there — `git -C ../<repo> worktree remove ../<repo>-<topic>` when the session stands inside the worktree, which git refuses to remove under itself. When absent, cleanup is a silent no-op — never a failure.
|
|
75
73
|
|
|
76
74
|
## Principles
|
|
77
75
|
|
|
78
76
|
- The learning sweep runs before disposal and touches only `docs/adr/` and `docs/lessons.md` — the two permanent stores.
|
|
79
77
|
|
|
80
|
-
- Dispose of the active work's artifacts only (archive or delete, the human's choice) —
|
|
78
|
+
- Dispose of the active work's artifacts only (archive or delete, the human's choice) — the unit is the topic folder `docs/plans/<date>-<topic>/`, one folder whether the topic has a single leaf or an umbrella's many. Unrelated topics stay in `docs/plans/`.
|
|
81
79
|
- ADRs are permanent institutional memory — they stay out of archive/rotation forever.
|
|
82
80
|
- Bump the package version if this is a published change (major for breaking changes).
|
|
@@ -9,21 +9,18 @@ Report on in-flight pipelines in this working tree (a worktree has its own `docs
|
|
|
9
9
|
|
|
10
10
|
## Process
|
|
11
11
|
|
|
12
|
-
0. **Verify the root** — run `pwd` (or your shell's equivalent) and `git rev-parse --show-toplevel`;
|
|
13
|
-
1. **Discover** — List `docs/plans` recursively, excluding docs/plans/completed/,
|
|
14
|
-
2. **State per topic/part — extract, never ingest.** For each progress file take the `Feature phase:` line by matching it (e.g. `grep -m1 '^Feature phase:' <file>` — wherever the template puts it); the body (execution summary, review reports, code digest) carries nothing status needs, and neither do design docs. Map the line to the displayed state:
|
|
15
|
-
- `done` → **`done`** — terminal, never shown as in-flight;
|
|
12
|
+
0. **Verify the root** — run `pwd` (or your shell's equivalent) and `git rev-parse --show-toplevel`; mismatch → report both paths and stop; never `cd` (a worktree root counts).
|
|
13
|
+
1. **Discover** — List `docs/plans` recursively, excluding docs/plans/completed/, for `*-design.md`, `*-progress.md`, and `overview.md` (topic docs live in `docs/plans/<date>-<topic>/` folders — archived work is not in flight) — this working tree only. Use whatever recurses in your harness; one example: `find docs/plans -name '<suffix>' -not -path '*/completed/*'`.
|
|
14
|
+
2. **State per topic/part — extract, never ingest.** For each progress file take the `Feature phase:` line by matching it (e.g. `grep -m1 '^Feature phase:' <file>` — wherever the template puts it); the body (execution summary, review reports, code digest) carries nothing status needs, and neither do design docs. A header reading `Setup: pending` renders the topic as **`awaiting setup`** — the setup checkpoint was never approved (the slot is only ever written when the design doc has a `## Setup` section, so the header alone decides — no design-doc read) — ahead of any phase-derived state below. Map the line to the displayed state:
|
|
15
|
+
- `done` → **`done`** — terminal, never shown as in-flight; every row is resolved (`✅` passed, `❌` failed, `⏭` skipped — resolved, not necessarily passed). Append the tally as `N/N` when wanted — the Requirements-table row count, scoped to that table because the Execution summary rows share the same `| <n> |` row shape and a bare grep would double it (e.g. `sed -n '/^## Requirements/,/^## Execution summary/p' <file> | grep -c '^| [0-9]'`) — and when that same scoped range finds any — `sed -n '/^## Requirements/,/^## Execution summary/p' <file> | grep -c '^| [0-9].*❌'` / `sed -n '/^## Requirements/,/^## Execution summary/p' <file> | grep -c '^| [0-9].*⏭'` (the same scoping, so Execution-summary rows — which share the row shape — and prose mentions of the glyphs do not inflate the count) — render the counts, e.g. `done (1 ❌ · 1 ⏭)`, noting finalizing will require `--force-failed` while `❌` rows stand
|
|
16
16
|
- `implementing (k/N)` → `execute k/N` (the tally rides on the line itself — no extra read)
|
|
17
|
-
- `e2e-written` → `execute 0/N` (the E2E is written and the run continues into implementation — never shown as a paused state)
|
|
18
|
-
- `
|
|
19
|
-
- `
|
|
20
|
-
- `ship-paused` → `ship-paused`
|
|
17
|
+
- `e2e-written` → `execute 0/N` (the E2E is written and the run continues into implementation — never shown as a paused state; N comes from the same Requirements-table-scoped row count as `done` — so no tally renders without a defined source)
|
|
18
|
+
- `reviewing` → `review`
|
|
19
|
+
- `ship-paused` → `ship-paused` — next: `/skill:pwk-executing-tasks` (approve the ship checkpoint). Never hint finalizing: only `done` routes there.
|
|
21
20
|
- no progress file, only `*-design.md` → `design` — next: `/skill:pwk-executing-tasks`
|
|
22
21
|
- roster-only (named in the overview, no artifacts) → `not started`
|
|
23
22
|
- no parseable `Feature phase` line → `execute` (with tally if parseable).
|
|
24
|
-
|
|
25
|
-
A legacy 1.x topic (`*-implementation.md` stem-matched) uses the same phase-line inference on its progress file.
|
|
26
|
-
3. **Group by umbrella** — for each umbrella `overview.md` (read it — it is status-free and tiny), take its **parts** roster and roll the parts up by state (the overview carries no status): done parts, in-flight parts (design, execute, review, ship-paused), not-started parts. Print one roll-up line — `<umbrella> (umbrella): n done · n in-flight · n not-started` — and when every part is `done` append `— all parts done, ready for /skill:pwk-finalizing`. A `done` standalone topic gets the same hint. Once the umbrella finalizes, its folder — overview included — is disposed, so it no longer appears here. Topics not part of an overview print flat.
|
|
23
|
+
3. **Group by umbrella** — for each `overview.md`, read its **`## Parts (build order)`** roster (the heading and the `N. <part> — <scope>` item shape are lint-pinned) and roll the parts up by state (the overview carries no status): done parts, in-flight parts (design, execute, review, ship-paused, awaiting setup), not-started parts. A part sitting at the feature-E2E notice (`e2e-written`) rolls up as execute — the notice is a routing moment, not a bucket of its own. Print one roll-up line — `<topic> (umbrella): n done · n in-flight · n not-started` — and when every part is `done` append `— all parts done, ready for /skill:pwk-finalizing`. A `done` single-leaf topic gets the same hint. Once the topic finalizes, its folder — overview included — is disposed, so it no longer appears here. Topics without an overview print flat.
|
|
27
24
|
4. Print a compact table, grouped under any umbrellas, e.g.:
|
|
28
25
|
|
|
29
26
|
```text
|
|
@@ -32,7 +29,7 @@ Report on in-flight pipelines in this working tree (a worktree has its own `docs
|
|
|
32
29
|
payments-ui execute 1/2
|
|
33
30
|
payments-review review —
|
|
34
31
|
payments-webhooks not started
|
|
35
|
-
auth ship-paused — ready for /skill:pwk-
|
|
32
|
+
auth ship-paused — ready for /skill:pwk-executing-tasks
|
|
36
33
|
```
|
|
37
34
|
|
|
38
35
|
If nothing, suggest `/skill:pwk-brainstorming`.
|
|
@@ -13,7 +13,7 @@ Five sections, always: Summary / How it works / Key flows / Gotchas & invariants
|
|
|
13
13
|
|
|
14
14
|
## Contract
|
|
15
15
|
|
|
16
|
-
- **On demand only.** Someone asks
|
|
16
|
+
- **On demand only.** Someone asks — or the ship checkpoint offers it when the code digest's Flow outgrows its cap (see `pwk-executing-tasks`); it never runs unprompted.
|
|
17
17
|
- **Generated from code, not memory**: the branch diff (`<merge-base>...HEAD` — an unshipped branch is allowed, stamped at HEAD) plus the code as it exists now. Surviving artifacts (ADRs in `docs/adr/`, `docs/lessons.md`, an archived plan under `docs/plans/completed/`) are optional inputs — planning docs are usually gone by the time anyone asks, by design.
|
|
18
18
|
- **Output**: `docs/walkthroughs/<topic>.md`, **stamped with the commit range** it describes.
|
|
19
19
|
- **Regeneration overwrites wholesale.** The file is a generated cache of an explanation, never hand-edited; a re-run replaces it end to end. It is **never disposed** — finalize's globs do not touch `docs/walkthroughs/`.
|
|
@@ -28,7 +28,7 @@ Five sections, always: Summary / How it works / Key flows / Gotchas & invariants
|
|
|
28
28
|
```markdown
|
|
29
29
|
# Walkthrough: <topic>
|
|
30
30
|
|
|
31
|
-
Generated from <merge-base
|
|
31
|
+
Generated from <merge-base>...<HEAD> — file references are valid at that range.
|
|
32
32
|
|
|
33
33
|
## Summary
|
|
34
34
|
2–4 sentences: what this feature does and what changed.
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: pwk-diagnose
|
|
3
|
-
description: "Disciplined debugging loop for hard bugs and performance regressions. Use when a test fails unexpectedly, a bug is found during execution, or something is broken. Use this skill whenever the user reports a bug, says 'this doesn't work', 'something's wrong', 'help me debug', or when tests fail for unclear reasons. Works at any point in the workflow — brainstorm, execute, or standalone."
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Diagnose
|
|
7
|
-
|
|
8
|
-
A 6-phase debugging discipline. Phase 1 is the skill — spend disproportionate effort here.
|
|
9
|
-
|
|
10
|
-
Invoking `/skill:pwk-diagnose` **exits the gated design phase** (the workflow guard unlocks) — diagnosis needs to write failing tests and `[DEBUG-…]` instrumentation. If you only wanted read-only investigation, use `/skill:pwk-status` (stays gated) or reinstate the lock with `/pwk-guard on`.
|
|
11
|
-
|
|
12
|
-
## Phase 1 — Build a feedback loop
|
|
13
|
-
|
|
14
|
-
Create a fast, deterministic, agent-runnable pass/fail signal for the bug before doing anything else. Try in this order: failing test, curl script, CLI invocation, headless browser script.
|
|
15
|
-
|
|
16
|
-
Other strategies when the basics don't work:
|
|
17
|
-
- **Bisection** — bug appeared between two known states? Automate "boot at state X, check, repeat" to bisect
|
|
18
|
-
- **Replay** — save a real network request or event log to disk, replay it through the code path in isolation
|
|
19
|
-
|
|
20
|
-
The loop must produce the failure mode the **user** described — not a nearby but different failure. Iterate on the loop itself: can you make it faster? Sharper? More deterministic?
|
|
21
|
-
|
|
22
|
-
If you genuinely cannot build a loop, stop and say so. List what you tried. Ask for access to a reproducing environment or a captured artifact.
|
|
23
|
-
|
|
24
|
-
Hold at Phase 1 until you have a loop you believe in. Everything downstream — hypotheses, instrumentation, the fix — depends on that loop actually reproducing the user's symptom.
|
|
25
|
-
|
|
26
|
-
## Phase 2 — Reproduce
|
|
27
|
-
|
|
28
|
-
Run the loop. Confirm:
|
|
29
|
-
- The failure matches the user's reported symptom
|
|
30
|
-
- The failure is reproducible across multiple runs
|
|
31
|
-
- You've captured the exact symptom (error message, wrong output, slow timing)
|
|
32
|
-
|
|
33
|
-
Then **minimize the repro** — strip it down to the smallest input, shortest path, or fewest steps that still triggers the bug. A minimized repro dramatically narrows the hypothesis space.
|
|
34
|
-
|
|
35
|
-
## Phase 3 — Hypothesise
|
|
36
|
-
|
|
37
|
-
Generate 3-5 ranked hypotheses. Each must be falsifiable:
|
|
38
|
-
|
|
39
|
-
> "If `<X>` is the cause, then `<changing Y>` will make the bug disappear / `<changing Z>` will make it worse."
|
|
40
|
-
|
|
41
|
-
Show the ranked list to the user before testing. They often have domain knowledge that re-ranks instantly.
|
|
42
|
-
|
|
43
|
-
## Phase 4 — Instrument
|
|
44
|
-
|
|
45
|
-
Each probe must map to a specific hypothesis. Change one variable at a time. Tag every debug log with a unique prefix (e.g. `[DEBUG-a4f2]`) for easy cleanup later. Prefer a debugger breakpoint over logs when available.
|
|
46
|
-
|
|
47
|
-
## Phase 5 — Fix + regression test
|
|
48
|
-
|
|
49
|
-
Write the regression test **before** the fix — but only if there's a correct seam (one that exercises the real bug pattern at the call site). If no correct seam exists, note it — the codebase architecture is preventing the bug from being locked down.
|
|
50
|
-
|
|
51
|
-
## Phase 6 — Cleanup
|
|
52
|
-
|
|
53
|
-
Required before declaring done:
|
|
54
|
-
- Original repro no longer triggers
|
|
55
|
-
- Regression test passes (or absence of seam is documented)
|
|
56
|
-
- All `[DEBUG-...]` instrumentation removed
|
|
57
|
-
- Ask: what would have prevented this bug?
|
|
58
|
-
- If the answer is a repeatable pattern, append a **generic** rule to `docs/lessons.md` (strip domain specifics) so future sessions catch it early.
|
|
59
|
-
- If the bug was caused by an architectural problem (no good test seam, tangled callers, hidden coupling), suggest writing an ADR to `docs/adr/` capturing that insight
|