pincer-workflow 0.2.2 → 0.3.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 +16 -15
- package/bin/pincer.js +52 -12
- package/package.json +2 -2
- package/template/.agents/skills/pincer-code/SKILL.md +19 -16
- package/template/.agents/skills/pincer-evaluate/SKILL.md +28 -9
- package/template/.agents/skills/pincer-narrow/SKILL.md +27 -21
- package/template/.agents/skills/pincer-plan/SKILL.md +28 -20
- package/template/.agents/skills/pincer-release/SKILL.md +14 -9
- package/template/.agents/skills/pincer-status/SKILL.md +1 -1
- package/template/.claude/commands/pincer-code.md +18 -15
- package/template/.claude/commands/pincer-evaluate.md +28 -9
- package/template/.claude/commands/pincer-narrow.md +25 -19
- package/template/.claude/commands/pincer-plan.md +26 -18
- package/template/.claude/commands/pincer-release.md +13 -8
- package/template/.claude/commands/pincer-status.md +1 -1
- package/template/.claude/hooks/block-dangerous.sh +7 -18
- package/template/.claude/hooks/hook-policy.cjs +258 -0
- package/template/.claude/hooks/ticket-guard.sh +6 -63
- package/template/.claude/references/prd-template.md +3 -3
- package/template/.claude/references/ticket-template.md +22 -4
- package/template/.codex/README.md +8 -4
- package/template/.github/prompts/pincer-code.prompt.md +18 -15
- package/template/.github/prompts/pincer-evaluate.prompt.md +28 -9
- package/template/.github/prompts/pincer-narrow.prompt.md +25 -19
- package/template/.github/prompts/pincer-plan.prompt.md +26 -18
- package/template/.github/prompts/pincer-release.prompt.md +13 -8
- package/template/.github/prompts/pincer-status.prompt.md +1 -1
- package/template/AGENTS.md +2 -3
- package/template/docs/dry-run-checklist.md +19 -15
- package/template/docs/release-checklist.md +34 -0
- package/template/scripts/pincer-status.sh +64 -23
- package/template/scripts/pincer-ticket-lib.sh +270 -0
- package/template/scripts/pincer-ticket.sh +57 -46
- package/template/scripts/sync-prompts.sh +6 -1
|
@@ -11,38 +11,17 @@
|
|
|
11
11
|
#
|
|
12
12
|
# The receipt is `verified: <UTC time> <12-hex hash of the Verification block>`.
|
|
13
13
|
# Change the check after it passed and `done` refuses until it passes again.
|
|
14
|
+
# Every attempt revokes its predecessor; done runs a fresh final check.
|
|
14
15
|
set -euo pipefail
|
|
15
16
|
|
|
17
|
+
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/pincer-ticket-lib.sh"
|
|
18
|
+
|
|
16
19
|
ROOT=${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}
|
|
17
20
|
cd "$ROOT"
|
|
18
21
|
|
|
19
22
|
die() { printf 'pincer-ticket: %b\n' "$*" >&2; exit 1; }
|
|
20
23
|
now() { date -u +%Y-%m-%dT%H:%M:%SZ; }
|
|
21
|
-
sha256() { if command -v sha256sum >/dev/null; then sha256sum; else shasum -a 256; fi; }
|
|
22
|
-
|
|
23
|
-
normalize() { # T-3 / t-03 / 3 -> T-03
|
|
24
|
-
local n=${1#T-}; n=${n#t-}
|
|
25
|
-
[[ $n =~ ^[0-9]+$ ]] || die "not a ticket id: $1"
|
|
26
|
-
printf 'T-%02d' "$((10#$n))"
|
|
27
|
-
}
|
|
28
24
|
|
|
29
|
-
ticket_file() { # id -> tickets/T-NN-*.md (exactly one)
|
|
30
|
-
local id; id=$(normalize "$1")
|
|
31
|
-
local matches; matches=$(ls "tickets/$id"-*.md 2>/dev/null || true)
|
|
32
|
-
[ -n "$matches" ] || die "no ticket file tickets/$id-*.md"
|
|
33
|
-
[ "$(printf '%s\n' "$matches" | wc -l)" -eq 1 ] || die "several files match tickets/$id-*.md"
|
|
34
|
-
printf '%s' "$matches"
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
fm_get() { # file key -> value with any inline comment stripped; empty if absent
|
|
38
|
-
awk -v k="$2" '
|
|
39
|
-
NR == 1 && $0 != "---" { exit }
|
|
40
|
-
NR > 1 && $0 == "---" { exit }
|
|
41
|
-
NR > 1 && index($0, k ":") == 1 {
|
|
42
|
-
v = substr($0, length(k) + 2); sub(/#.*/, "", v)
|
|
43
|
-
gsub(/^[ \t]+|[ \t]+$/, "", v); print v; exit
|
|
44
|
-
}' "$1"
|
|
45
|
-
}
|
|
46
25
|
|
|
47
26
|
fm_set() { # file key value — replace inside the frontmatter (keeping an inline comment) or add before the closing ---
|
|
48
27
|
local tmp; tmp=$(mktemp)
|
|
@@ -56,79 +35,111 @@ fm_set() { # file key value — replace inside the frontmatter (keeping an inlin
|
|
|
56
35
|
{ print }' "$1" > "$tmp" && mv "$tmp" "$1"
|
|
57
36
|
}
|
|
58
37
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
inv && /^```/ { if (inb) exit; inb = 1; next }
|
|
64
|
-
inb { print }' "$1"
|
|
38
|
+
fm_unset() {
|
|
39
|
+
local tmp; tmp=$(mktemp)
|
|
40
|
+
awk -v k="$2" 'NR > 1 && $0 == "---" { closed = 1 }
|
|
41
|
+
!closed && index($0, k ":") == 1 { next } { print }' "$1" > "$tmp" && mv "$tmp" "$1"
|
|
65
42
|
}
|
|
66
43
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
44
|
+
cmd_bind() {
|
|
45
|
+
local f ref existing
|
|
46
|
+
f=$(ticket_file "$1"); ref=$2
|
|
47
|
+
validate_prd "$ref" || exit 1
|
|
48
|
+
existing=$(fm_get "$f" prd)
|
|
49
|
+
[ -z "$existing" ] || [ "$existing" = "$ref" ] || die "$1 already references $existing; refusing to rebind it to $ref"
|
|
50
|
+
[ "$existing" = "$ref" ] || fm_set "$f" prd "$ref"
|
|
51
|
+
echo "$1 bound to PRD $ref"
|
|
74
52
|
}
|
|
75
53
|
|
|
76
54
|
cmd_start() {
|
|
77
|
-
local f id st dep df
|
|
55
|
+
local f id st dep df ref dep_ref readiness
|
|
78
56
|
f=$(ticket_file "$1"); id=$(normalize "$1"); st=$(fm_get "$f" status)
|
|
57
|
+
ref=$(usable_ticket_prd "$f") || exit 1
|
|
79
58
|
case "$st" in
|
|
80
|
-
in_progress)
|
|
59
|
+
in_progress)
|
|
60
|
+
[ -n "$(fm_get "$f" prd)" ] || fm_set "$f" prd "$ref"
|
|
61
|
+
echo "$id already in progress (started $(fm_get "$f" started))"; return 0 ;;
|
|
81
62
|
done) die "$id is already done" ;;
|
|
82
63
|
esac
|
|
83
64
|
for dep in $(fm_get "$f" depends_on | grep -oE 'T-[0-9]+' || true); do
|
|
84
65
|
df=$(ticket_file "$dep")
|
|
85
66
|
[ "$(fm_get "$df" status)" = done ] || die "$id depends on $dep, which is '$(fm_get "$df" status)' — finish $dep first (or fix depends_on in $f)"
|
|
67
|
+
dep_ref=$(usable_ticket_prd "$df") || exit 1
|
|
68
|
+
[ "$dep_ref" = "$ref" ] || die "$id references $ref but dependency $dep references $dep_ref"
|
|
69
|
+
if ! readiness=$(ticket_readiness "$df"); then die "$id depends on $dep: $readiness"; fi
|
|
86
70
|
done
|
|
71
|
+
[ -n "$(fm_get "$f" prd)" ] || fm_set "$f" prd "$ref"
|
|
87
72
|
fm_set "$f" status in_progress
|
|
88
73
|
[ -n "$(fm_get "$f" started)" ] || fm_set "$f" started "$(now)"
|
|
89
74
|
echo "▶ $id started $(fm_get "$f" started) — $f"
|
|
90
75
|
}
|
|
91
76
|
|
|
92
77
|
cmd_verify() {
|
|
93
|
-
local f id st cmds rc
|
|
78
|
+
local f id st cmds rc hash child
|
|
94
79
|
f=$(ticket_file "$1"); id=$(normalize "$1"); st=$(fm_get "$f" status)
|
|
80
|
+
usable_ticket_prd "$f" >/dev/null || exit 1
|
|
95
81
|
case "$st" in
|
|
96
82
|
open) cmd_start "$id" ;;
|
|
97
|
-
done) echo "$id is done — re-running its check
|
|
83
|
+
done) echo "$id is done — re-running its check and updating the latest outcome" ;;
|
|
98
84
|
esac
|
|
99
85
|
cmds=$(verification_cmds "$f")
|
|
86
|
+
fm_unset "$f" verified
|
|
87
|
+
hash=$(verify_hash "$f")
|
|
88
|
+
fm_set "$f" last_check "$(now) running $hash"
|
|
100
89
|
printf '%s\n' "$cmds" | grep -vE '^[[:space:]]*(#|$)' >/dev/null || die "no runnable command in the Verification block of $f"
|
|
101
90
|
echo "── $id verification ──"
|
|
102
91
|
printf '%s\n' "$cmds" | sed 's/^/ $ /'
|
|
103
|
-
|
|
92
|
+
bash -eo pipefail -c "$cmds" & child=$!
|
|
93
|
+
trap 'kill "$child" 2>/dev/null || true; fm_set "$f" last_check "$(now) interrupted $hash"; exit 130' INT TERM
|
|
94
|
+
set +e; wait "$child"; rc=$?; set -e
|
|
95
|
+
trap - INT TERM
|
|
104
96
|
if [ "$rc" -ne 0 ]; then
|
|
97
|
+
fm_set "$f" last_check "$(now) failed $hash"
|
|
105
98
|
echo "✗ $id verification FAILED (exit $rc) — no receipt written. Fix, then re-run." >&2
|
|
106
99
|
exit "$rc"
|
|
107
100
|
fi
|
|
108
|
-
[ "$
|
|
101
|
+
if [ "$hash" != "$(verify_hash "$f")" ]; then
|
|
102
|
+
fm_set "$f" last_check "$(now) failed $hash"
|
|
103
|
+
die "Verification block changed during execution — re-run verify"
|
|
104
|
+
fi
|
|
105
|
+
if ! validate_ticket "$f" || ! usable_ticket_prd "$f" >/dev/null; then
|
|
106
|
+
fm_set "$f" last_check "$(now) failed $hash"
|
|
107
|
+
die "ticket became invalid during verification — fix it and re-run verify"
|
|
108
|
+
fi
|
|
109
|
+
fm_set "$f" last_check "$(now) passed $hash"
|
|
110
|
+
fm_set "$f" verified "$(now) $hash"
|
|
109
111
|
echo "✓ $id verified — receipt: $(fm_get "$f" verified)"
|
|
110
112
|
}
|
|
111
113
|
|
|
112
114
|
cmd_done() {
|
|
113
115
|
local f id st rec cur u slug
|
|
114
116
|
f=$(ticket_file "$1"); id=$(normalize "$1"); st=$(fm_get "$f" status)
|
|
115
|
-
|
|
116
|
-
[ "$st" = in_progress ] || die "$id is '$st' — run '$0 verify $id' first"
|
|
117
|
+
usable_ticket_prd "$f" >/dev/null || exit 1
|
|
118
|
+
[ "$st" = in_progress ] || [ "$st" = done ] || die "$id is '$st' — run '$0 verify $id' first"
|
|
117
119
|
rec=$(fm_get "$f" verified)
|
|
118
120
|
[ -n "$rec" ] || die "no verification receipt on $id — run '$0 verify $id' and get a green check first"
|
|
119
121
|
cur=$(verify_hash "$f")
|
|
120
122
|
[ "${rec##* }" = "$cur" ] || die "receipt hash ${rec##* } does not match the current Verification block ($cur): the check changed after it passed — run '$0 verify $id' again"
|
|
121
123
|
u=$(unticked "$f")
|
|
122
124
|
[ -z "$u" ] || die "unticked acceptance criteria on $id:\n$u\nTick each verified criterion; a criterion that was cut is a scope change to record in the PRD, not a box to skip."
|
|
125
|
+
cmd_verify "$id"
|
|
126
|
+
u=$(unticked "$f")
|
|
127
|
+
[ -z "$u" ] || die "unticked acceptance criteria on $id after verification:\n$u"
|
|
128
|
+
[ "$st" = done ] && { echo "$id already done — current check passed"; return 0; }
|
|
123
129
|
fm_set "$f" status done
|
|
124
130
|
fm_set "$f" finished "$(now)"
|
|
125
131
|
slug=$(basename "$f" .md); slug=${slug#T-[0-9][0-9]-}
|
|
126
|
-
echo "✓ $id done.
|
|
132
|
+
echo "✓ $id done. Inspect staged work, stage only this ticket's paths, review git diff --cached, then commit: $id: ${slug//-/ }"
|
|
127
133
|
}
|
|
128
134
|
|
|
135
|
+
case "${1:-}" in
|
|
136
|
+
start|verify|done|bind) validate_ticket_set || exit 1 ;;
|
|
137
|
+
esac
|
|
138
|
+
|
|
129
139
|
case "${1:-}" in
|
|
130
140
|
start) [ $# -eq 2 ] || die "usage: $0 start T-NN"; cmd_start "$2" ;;
|
|
131
141
|
verify) [ $# -eq 2 ] || die "usage: $0 verify T-NN"; cmd_verify "$2" ;;
|
|
132
142
|
done) [ $# -eq 2 ] || die "usage: $0 done T-NN"; cmd_done "$2" ;;
|
|
143
|
+
bind) [ $# -eq 3 ] || die "usage: $0 bind T-NN .prd/prd-vN.md"; cmd_bind "$2" "$3" ;;
|
|
133
144
|
*) sed -n '2,13p' "$0" | sed 's/^# \{0,1\}//'; exit 1 ;;
|
|
134
145
|
esac
|
|
@@ -18,6 +18,11 @@ count=0
|
|
|
18
18
|
for src in .claude/commands/pincer-*.md; do
|
|
19
19
|
name=$(basename "$src" .md)
|
|
20
20
|
desc=$(sed -n 's/^description: *"\{0,1\}\([^"]*\)"\{0,1\}$/\1/p' "$src" | head -1)
|
|
21
|
+
if grep -q '^argument-hint:.*optional' "$src"; then
|
|
22
|
+
argument_text="the text that follows the \`\$$name\` mention, if any (when omitted, use the playbook's documented default)"
|
|
23
|
+
else
|
|
24
|
+
argument_text="the text that follows the \`\$$name\` mention in the user's message (ask only if the required input is missing)"
|
|
25
|
+
fi
|
|
21
26
|
body=$(awk 'flag; /^---$/ { if (++c == 2) flag = 1 }' "$src")
|
|
22
27
|
|
|
23
28
|
# Codex: one skill directory per playbook. Skills have no $ARGUMENTS
|
|
@@ -29,7 +34,7 @@ for src in .claude/commands/pincer-*.md; do
|
|
|
29
34
|
printf -- '---\nname: %s\ndescription: "%s"\n---\n' "$name" "$desc"
|
|
30
35
|
printf '<!-- Generated from %s by scripts/sync-prompts.sh — edit the source, not this file -->\n\n' "$src"
|
|
31
36
|
printf '%s\n' "$body" \
|
|
32
|
-
| sed "s/\$ARGUMENTS/
|
|
37
|
+
| sed "s/\$ARGUMENTS/$argument_text/g" \
|
|
33
38
|
| sed -e 's#^/pincer-\([a-z]*\)#$pincer-\1#' -e 's#\([^A-Za-z0-9_./]\)/pincer-\([a-z]*\)#\1$pincer-\2#g'
|
|
34
39
|
} > ".agents/skills/$name/SKILL.md"
|
|
35
40
|
|