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.
- package/README.md +6 -2
- package/bin/pincer.js +1 -1
- package/package.json +2 -2
- package/template/.agents/skills/pincer-code/SKILL.md +59 -4
- package/template/.agents/skills/pincer-evaluate/SKILL.md +73 -13
- package/template/.agents/skills/pincer-narrow/SKILL.md +46 -7
- package/template/.agents/skills/pincer-plan/SKILL.md +39 -2
- package/template/.agents/skills/pincer-release/SKILL.md +17 -3
- package/template/.agents/skills/pincer-status/SKILL.md +7 -3
- package/template/.claude/commands/pincer-code.md +59 -4
- package/template/.claude/commands/pincer-evaluate.md +73 -13
- package/template/.claude/commands/pincer-narrow.md +46 -7
- package/template/.claude/commands/pincer-plan.md +39 -2
- package/template/.claude/commands/pincer-release.md +17 -3
- package/template/.claude/commands/pincer-status.md +7 -3
- package/template/.claude/hooks/hook-policy.cjs +101 -8
- package/template/.claude/references/prd-template.md +38 -6
- package/template/.claude/references/ticket-template.md +15 -0
- package/template/.github/prompts/pincer-code.prompt.md +59 -4
- package/template/.github/prompts/pincer-evaluate.prompt.md +73 -13
- package/template/.github/prompts/pincer-narrow.prompt.md +46 -7
- package/template/.github/prompts/pincer-plan.prompt.md +39 -2
- package/template/.github/prompts/pincer-release.prompt.md +17 -3
- package/template/.github/prompts/pincer-status.prompt.md +7 -3
- package/template/AGENTS.md +9 -2
- package/template/docs/dry-run-checklist.md +132 -14
- package/template/docs/release-checklist.md +6 -5
- package/template/scripts/pincer-evidence.cjs +292 -0
- package/template/scripts/pincer-status.sh +21 -5
- package/template/scripts/pincer-ticket-lib.sh +56 -5
- 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 =~ ^
|
|
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
|
-
|
|
266
|
-
if [ -
|
|
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) —
|
|
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
|