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.
Files changed (34) hide show
  1. package/README.md +16 -15
  2. package/bin/pincer.js +52 -12
  3. package/package.json +2 -2
  4. package/template/.agents/skills/pincer-code/SKILL.md +19 -16
  5. package/template/.agents/skills/pincer-evaluate/SKILL.md +28 -9
  6. package/template/.agents/skills/pincer-narrow/SKILL.md +27 -21
  7. package/template/.agents/skills/pincer-plan/SKILL.md +28 -20
  8. package/template/.agents/skills/pincer-release/SKILL.md +14 -9
  9. package/template/.agents/skills/pincer-status/SKILL.md +1 -1
  10. package/template/.claude/commands/pincer-code.md +18 -15
  11. package/template/.claude/commands/pincer-evaluate.md +28 -9
  12. package/template/.claude/commands/pincer-narrow.md +25 -19
  13. package/template/.claude/commands/pincer-plan.md +26 -18
  14. package/template/.claude/commands/pincer-release.md +13 -8
  15. package/template/.claude/commands/pincer-status.md +1 -1
  16. package/template/.claude/hooks/block-dangerous.sh +7 -18
  17. package/template/.claude/hooks/hook-policy.cjs +258 -0
  18. package/template/.claude/hooks/ticket-guard.sh +6 -63
  19. package/template/.claude/references/prd-template.md +3 -3
  20. package/template/.claude/references/ticket-template.md +22 -4
  21. package/template/.codex/README.md +8 -4
  22. package/template/.github/prompts/pincer-code.prompt.md +18 -15
  23. package/template/.github/prompts/pincer-evaluate.prompt.md +28 -9
  24. package/template/.github/prompts/pincer-narrow.prompt.md +25 -19
  25. package/template/.github/prompts/pincer-plan.prompt.md +26 -18
  26. package/template/.github/prompts/pincer-release.prompt.md +13 -8
  27. package/template/.github/prompts/pincer-status.prompt.md +1 -1
  28. package/template/AGENTS.md +2 -3
  29. package/template/docs/dry-run-checklist.md +19 -15
  30. package/template/docs/release-checklist.md +34 -0
  31. package/template/scripts/pincer-status.sh +64 -23
  32. package/template/scripts/pincer-ticket-lib.sh +270 -0
  33. package/template/scripts/pincer-ticket.sh +57 -46
  34. 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
- verification_cmds() { # the fenced block under "## Verification"
60
- awk '
61
- /^## Verification/ { inv = 1; next }
62
- inv && /^## / { exit }
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
- verify_hash() { verification_cmds "$1" | sha256 | cut -c1-12; }
68
-
69
- unticked() { # unticked boxes inside "## Acceptance Criteria"
70
- awk '
71
- /^## Acceptance Criteria/ { ina = 1; next }
72
- ina && /^## / { exit }
73
- ina && /^- \[ \]/ { print }' "$1"
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) echo "$id already in progress (started $(fm_get "$f" started))"; return 0 ;;
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 (receipt left unchanged)" ;;
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
- set +e; bash -eo pipefail -c "$cmds"; rc=$?; set -e
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
- [ "$st" = done ] || fm_set "$f" verified "$(now) $(verify_hash "$f")"
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
- [ "$st" = done ] && { echo "$id already done"; return 0; }
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. Commit it with the code: git add -A && git commit -m \"$id: ${slug//-/ }\""
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/the text that follows the \`\$$name\` mention in the user's message (ask for it if there is none)/g" \
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