pincer-workflow 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/README.md +6 -2
  2. package/bin/pincer.js +1 -1
  3. package/package.json +2 -2
  4. package/template/.agents/skills/pincer-code/SKILL.md +59 -4
  5. package/template/.agents/skills/pincer-evaluate/SKILL.md +73 -13
  6. package/template/.agents/skills/pincer-narrow/SKILL.md +46 -7
  7. package/template/.agents/skills/pincer-plan/SKILL.md +39 -2
  8. package/template/.agents/skills/pincer-release/SKILL.md +17 -3
  9. package/template/.agents/skills/pincer-status/SKILL.md +7 -3
  10. package/template/.claude/commands/pincer-code.md +59 -4
  11. package/template/.claude/commands/pincer-evaluate.md +73 -13
  12. package/template/.claude/commands/pincer-narrow.md +46 -7
  13. package/template/.claude/commands/pincer-plan.md +39 -2
  14. package/template/.claude/commands/pincer-release.md +17 -3
  15. package/template/.claude/commands/pincer-status.md +7 -3
  16. package/template/.claude/hooks/hook-policy.cjs +101 -8
  17. package/template/.claude/references/prd-template.md +38 -6
  18. package/template/.claude/references/ticket-template.md +15 -0
  19. package/template/.github/prompts/pincer-code.prompt.md +59 -4
  20. package/template/.github/prompts/pincer-evaluate.prompt.md +73 -13
  21. package/template/.github/prompts/pincer-narrow.prompt.md +46 -7
  22. package/template/.github/prompts/pincer-plan.prompt.md +39 -2
  23. package/template/.github/prompts/pincer-release.prompt.md +17 -3
  24. package/template/.github/prompts/pincer-status.prompt.md +7 -3
  25. package/template/AGENTS.md +9 -2
  26. package/template/docs/dry-run-checklist.md +132 -14
  27. package/template/docs/release-checklist.md +6 -5
  28. package/template/scripts/pincer-evidence.cjs +292 -0
  29. package/template/scripts/pincer-status.sh +21 -5
  30. package/template/scripts/pincer-ticket-lib.sh +56 -5
  31. package/template/scripts/pincer-ticket.sh +1 -1
@@ -2,6 +2,26 @@
2
2
  # Shared, read-only parsing for the supported Pincer ticket format.
3
3
  # Validators return nonzero with a diagnostic; callers decide how to report it.
4
4
 
5
+ PINCER_LIB_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
6
+ PINCER_EVIDENCE="$PINCER_LIB_DIR/pincer-evidence.cjs"
7
+
8
+ # Run the shared evidence validator; prints its combined output, returns its status.
9
+ # Usage: evidence_validate <manifest> <candidate> <base> <prd> [--files]
10
+ # Absent or malformed candidate/base/prd values are simply not checked, so a bad
11
+ # NOTES.md field is reported by notes_current, never as a manifest verdict.
12
+ evidence_validate() {
13
+ local manifest=$1 candidate=$2 base=$3 prd=$4; shift 4
14
+ command -v node >/dev/null 2>&1 || { printf 'evidence: %s: Node.js 18+ is required to validate evidence\n' "$manifest"; return 1; }
15
+ local -a args=(validate "$manifest")
16
+ if [[ $candidate =~ ^[a-f0-9]{40}$ ]]; then args+=(--candidate "$candidate"); fi
17
+ if [[ $base =~ ^[a-f0-9]{40}$ ]]; then args+=(--base "$base"); fi
18
+ if [[ $prd =~ ^\.prd/prd-v[1-9][0-9]{0,8}\.md$ ]]; then args+=(--prd "$prd"); fi
19
+ node "$PINCER_EVIDENCE" "${args[@]}" "$@" 2>&1
20
+ }
21
+ evidence_reason() { # first diagnostic line without the manifest prefix; usage errors are labelled
22
+ printf '%s\n' "$1" | head -1 | sed -e 's/^evidence: [^:]*: //' -e 's/^pincer-evidence: /validator usage: /'
23
+ }
24
+
5
25
  normalize() { # CLI shorthand -> canonical ID; bound arithmetic before conversion.
6
26
  local n=${1#T-}; n=${n#t-}
7
27
  if ! [[ $n =~ ^[0-9]{1,6}$ ]] || [ "$((10#$n))" -eq 0 ]; then
@@ -185,6 +205,12 @@ validate_prd() {
185
205
  validate_metadata "$ref" || return 1
186
206
  [ "$(fm_get "$ref" version)" = "$version" ] || { printf 'pincer: %s: version must match filename (%s)\n' "$ref" "$version" >&2; return 1; }
187
207
  case "$(fm_get "$ref" status)" in draft|ticketed|built) ;; *) printf 'pincer: %s: PRD status must be draft, ticketed, or built\n' "$ref" >&2; return 1 ;; esac
208
+ case "$(fm_get "$ref" profile)" in ''|small|standard) ;; *) printf 'pincer: %s: profile must be small or standard (omit for standard)\n' "$ref" >&2; return 1 ;; esac
209
+ }
210
+
211
+ prd_profile() { # effective planning profile; older PRDs without the field are standard
212
+ local profile; profile=$(fm_get "$1" profile)
213
+ printf '%s' "${profile:-standard}"
188
214
  }
189
215
 
190
216
  latest_prd() {
@@ -247,14 +273,19 @@ ticket_readiness() { # explain why a done ticket needs attention, without mutati
247
273
  if [ -n "$(unticked "$file")" ]; then printf 'unticked acceptance criteria — complete and re-run verify'; return 1; fi
248
274
  }
249
275
 
276
+ # An evaluation is current when NOTES.md names this PRD, a reviewed base and
277
+ # candidate that are ancestors of HEAD, and an evidence manifest that validates
278
+ # for that candidate; after the candidate, only NOTES.md and the files the
279
+ # manifest lists may have changed, all of them tracked, and the working tree is
280
+ # clean apart from NOTES.md. Legacy notes without a manifest never grant readiness.
250
281
  notes_current() {
251
- local prd=$1 candidate base changes
282
+ local prd=$1 candidate base changes manifest output listed file stored canonical offending
252
283
  [ -f NOTES.md ] || { printf 'missing'; return 1; }
253
284
  validate_metadata NOTES.md >/dev/null 2>&1 || { printf 'stale: invalid or missing evaluation metadata'; return 1; }
254
285
  [ "$(fm_get NOTES.md prd)" = "$prd" ] || { printf 'stale: evaluation PRD does not match'; return 1; }
255
286
  candidate=$(fm_get NOTES.md candidate); base=$(fm_get NOTES.md base)
256
- if ! [[ $candidate =~ ^([a-f0-9]{40}|[a-f0-9]{64})$ ]] || ! [[ $base =~ ^([a-f0-9]{40}|[a-f0-9]{64})$ ]]; then
257
- printf 'stale: candidate and base must be full commit IDs'; return 1
287
+ if ! [[ $candidate =~ ^[a-f0-9]{40}$ ]] || ! [[ $base =~ ^[a-f0-9]{40}$ ]]; then
288
+ printf 'stale: candidate and base must be full 40-hex commit IDs'; return 1
258
289
  fi
259
290
  if ! git rev-parse --verify "$candidate^{commit}" >/dev/null 2>&1 ||
260
291
  ! git rev-parse --verify "$base^{commit}" >/dev/null 2>&1 ||
@@ -262,8 +293,28 @@ notes_current() {
262
293
  ! git merge-base --is-ancestor "$candidate" HEAD 2>/dev/null; then
263
294
  printf 'stale: evaluation commits or ancestry unavailable'; return 1
264
295
  fi
265
- changes=$(git diff --name-only "$candidate" HEAD -- . ':(exclude)NOTES.md') || return 1
266
- if [ -n "$changes" ]; then printf 'stale: candidate changed after evaluation'; return 1; fi
296
+ manifest=$(fm_get NOTES.md evidence)
297
+ if [ -z "$manifest" ]; then
298
+ printf 'stale: legacy evaluation without evidence manifest — re-run /pincer-evaluate for evidence schema 1'; return 1
299
+ fi
300
+ if ! output=$(evidence_validate "$manifest" "$candidate" "$base" "$prd" --files); then
301
+ printf 'stale: evidence invalid: %s' "$(evidence_reason "$output")"; return 1
302
+ fi
303
+ listed=$(printf '%s\n' "$output" | tail -n +2)
304
+ # Every listed file must be tracked; keep git's own spelling of each path
305
+ # (unquoted, relative to this directory, NFC where git normalises) so the set
306
+ # compares byte-for-byte with the diff below.
307
+ canonical=""
308
+ while IFS= read -r file; do
309
+ [ -n "$file" ] || continue
310
+ stored=$(git -c core.quotePath=false ls-files --error-unmatch -- "$file" 2>/dev/null) || { printf 'stale: evidence not tracked: %s' "$file"; return 1; }
311
+ canonical="$canonical$stored
312
+ "
313
+ done <<< "$listed"
314
+ # Paths relative to the project root (which may be below the git toplevel).
315
+ changes=$(git -c core.quotePath=false diff --name-only --relative "$candidate" HEAD -- . ':(exclude)NOTES.md') || return 1
316
+ offending=$(printf '%s\n' "$changes" | grep -vxF -f <(printf '%s' "$canonical") | grep -v '^$' | head -1)
317
+ if [ -n "$offending" ]; then printf 'stale: candidate changed after evaluation: %s' "$offending"; return 1; fi
267
318
  changes=$(git status --porcelain --untracked-files=all -- . ':(exclude)NOTES.md') || return 1
268
319
  if [ -n "$changes" ]; then printf 'stale: working tree has changes outside NOTES.md'; return 1; fi
269
320
  printf 'current (%s)' "$candidate"
@@ -95,7 +95,7 @@ cmd_verify() {
95
95
  trap - INT TERM
96
96
  if [ "$rc" -ne 0 ]; then
97
97
  fm_set "$f" last_check "$(now) failed $hash"
98
- echo "✗ $id verification FAILED (exit $rc) — no receipt written. Fix, then re-run." >&2
98
+ echo "✗ $id verification FAILED (exit $rc) — failure recorded in last_check of $f; any prior successful receipt was revoked. Fix, then re-run verify." >&2
99
99
  exit "$rc"
100
100
  fi
101
101
  if [ "$hash" != "$(verify_hash "$f")" ]; then