pincer-workflow 0.4.1 → 0.6.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 +9 -7
- package/bin/pincer.js +58 -5
- package/package.json +3 -3
- package/template/.agents/skills/pincer-code/SKILL.md +117 -12
- package/template/.agents/skills/pincer-evaluate/SKILL.md +57 -10
- package/template/.agents/skills/pincer-narrow/SKILL.md +48 -8
- package/template/.agents/skills/pincer-plan/SKILL.md +12 -4
- package/template/.agents/skills/pincer-release/SKILL.md +32 -3
- package/template/.agents/skills/pincer-status/SKILL.md +25 -2
- package/template/.claude/commands/pincer-code.md +117 -12
- package/template/.claude/commands/pincer-evaluate.md +57 -10
- package/template/.claude/commands/pincer-narrow.md +48 -8
- package/template/.claude/commands/pincer-plan.md +12 -4
- package/template/.claude/commands/pincer-release.md +32 -3
- package/template/.claude/commands/pincer-status.md +25 -2
- package/template/.claude/hooks/hook-policy.cjs +24 -3
- package/template/.claude/references/prd-template.md +11 -4
- package/template/.claude/references/ticket-template.md +4 -0
- package/template/.codex/README.md +3 -2
- package/template/.github/prompts/pincer-code.prompt.md +117 -12
- package/template/.github/prompts/pincer-evaluate.prompt.md +57 -10
- package/template/.github/prompts/pincer-narrow.prompt.md +48 -8
- package/template/.github/prompts/pincer-plan.prompt.md +12 -4
- package/template/.github/prompts/pincer-release.prompt.md +32 -3
- package/template/.github/prompts/pincer-status.prompt.md +25 -2
- package/template/AGENTS.md +22 -0
- package/template/docs/dry-run-checklist.md +70 -6
- package/template/docs/release-checklist.md +5 -2
- package/template/docs/runtime-contracts.md +1683 -0
- package/template/scripts/pincer-evidence.cjs +13 -229
- package/template/scripts/pincer-runtime/adopt.cjs +132 -0
- package/template/scripts/pincer-runtime/agreement.cjs +240 -0
- package/template/scripts/pincer-runtime/authorization.cjs +167 -0
- package/template/scripts/pincer-runtime/changes.cjs +517 -0
- package/template/scripts/pincer-runtime/checks.cjs +48 -0
- package/template/scripts/pincer-runtime/coverage.cjs +361 -0
- package/template/scripts/pincer-runtime/dispositions.cjs +95 -0
- package/template/scripts/pincer-runtime/evidence.cjs +676 -0
- package/template/scripts/pincer-runtime/fsutil.cjs +37 -0
- package/template/scripts/pincer-runtime/gates.cjs +73 -0
- package/template/scripts/pincer-runtime/identity.cjs +163 -0
- package/template/scripts/pincer-runtime/impact.cjs +177 -0
- package/template/scripts/pincer-runtime/io.cjs +41 -0
- package/template/scripts/pincer-runtime/lifecycle.cjs +311 -0
- package/template/scripts/pincer-runtime/locator.cjs +158 -0
- package/template/scripts/pincer-runtime/migrate.cjs +204 -0
- package/template/scripts/pincer-runtime/parse.cjs +316 -0
- package/template/scripts/pincer-runtime/phases.cjs +245 -0
- package/template/scripts/pincer-runtime/readiness.cjs +97 -0
- package/template/scripts/pincer-runtime/requirements.cjs +255 -0
- package/template/scripts/pincer-runtime/resume.cjs +205 -0
- package/template/scripts/pincer-runtime/routing.cjs +54 -0
- package/template/scripts/pincer-runtime/runner.cjs +242 -0
- package/template/scripts/pincer-runtime/sanitize.cjs +63 -0
- package/template/scripts/pincer-runtime/source.cjs +129 -0
- package/template/scripts/pincer-runtime/state.cjs +314 -0
- package/template/scripts/pincer-runtime/status.cjs +514 -0
- package/template/scripts/pincer-runtime/transaction.cjs +200 -0
- package/template/scripts/pincer-runtime/transitions.cjs +134 -0
- package/template/scripts/pincer-runtime.cjs +661 -0
- package/template/scripts/pincer-status.sh +11 -162
- package/template/scripts/pincer-ticket.sh +19 -139
- package/template/scripts/pincer-ticket-lib.sh +0 -321
|
@@ -1,321 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# Shared, read-only parsing for the supported Pincer ticket format.
|
|
3
|
-
# Validators return nonzero with a diagnostic; callers decide how to report it.
|
|
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
|
-
|
|
25
|
-
normalize() { # CLI shorthand -> canonical ID; bound arithmetic before conversion.
|
|
26
|
-
local n=${1#T-}; n=${n#t-}
|
|
27
|
-
if ! [[ $n =~ ^[0-9]{1,6}$ ]] || [ "$((10#$n))" -eq 0 ]; then
|
|
28
|
-
printf 'pincer-ticket: not a ticket id (expected 1..999999): %s\n' "$1" >&2
|
|
29
|
-
return 1
|
|
30
|
-
fi
|
|
31
|
-
printf 'T-%02d' "$((10#$n))"
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
fm_get() {
|
|
35
|
-
awk -v k="$2" '
|
|
36
|
-
NR == 1 && $0 != "---" { exit }
|
|
37
|
-
NR > 1 && $0 == "---" { exit }
|
|
38
|
-
NR > 1 && index($0, k ":") == 1 {
|
|
39
|
-
v = substr($0, length(k) + 2); sub(/#.*/, "", v)
|
|
40
|
-
gsub(/^[ \t]+|[ \t]+$/, "", v); print v; exit
|
|
41
|
-
}' "$1"
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
verification_cmds() {
|
|
45
|
-
awk '
|
|
46
|
-
inb { if ($0 ~ /^[ \t]*```[ \t]*$/) exit; print; next }
|
|
47
|
-
otherfence { if ($0 ~ /^[ \t]*```/) otherfence = 0; next }
|
|
48
|
-
/^## Verification[ \t]*$/ { inv = 1; next }
|
|
49
|
-
inv && /^[ \t]*```bash[ \t]*$/ { inb = 1; next }
|
|
50
|
-
inv && /^## / { exit }
|
|
51
|
-
/^[ \t]*```/ { otherfence = 1 }
|
|
52
|
-
' "$1"
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
unticked() {
|
|
56
|
-
awk '
|
|
57
|
-
otherfence { if ($0 ~ /^[ \t]*```/) otherfence = 0; next }
|
|
58
|
-
/^[ \t]*```/ { otherfence = 1; next }
|
|
59
|
-
/^## Acceptance Criteria[ \t]*$/ { ina = 1; next }
|
|
60
|
-
ina && /^## / { exit }
|
|
61
|
-
ina && /^[ \t]*([-+*]|[0-9]+[.)])[ \t]+\[ \]/ { print }
|
|
62
|
-
' "$1"
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
validate_ticket() {
|
|
66
|
-
local file=$1 cmds
|
|
67
|
-
awk -v file="$file" '
|
|
68
|
-
function fail(msg) { print "pincer-ticket: " file ": " msg > "/dev/stderr"; bad = 1; exit 1 }
|
|
69
|
-
function trim(s) { gsub(/^[ \t]+|[ \t]+$/, "", s); return s }
|
|
70
|
-
function idok(s, n) {
|
|
71
|
-
if (s !~ /^T-[0-9][0-9]+$/) return 0
|
|
72
|
-
n = substr(s, 3)
|
|
73
|
-
return length(n) <= 6 && n + 0 > 0 && sprintf("T-%02d", n + 0) == s
|
|
74
|
-
}
|
|
75
|
-
NR == 1 { if ($0 != "---") fail("frontmatter must begin with ---"); next }
|
|
76
|
-
!closed {
|
|
77
|
-
if ($0 == "---") { closed = 1; next }
|
|
78
|
-
if ($0 ~ /^[ \t]*(#.*)?$/) next
|
|
79
|
-
if ($0 !~ /^[a-z_][a-z0-9_]*:[ \t]*/) fail("frontmatter requires unindented key: value fields")
|
|
80
|
-
key = $0; sub(/:.*/, "", key)
|
|
81
|
-
if (seen[key]++) fail("duplicate frontmatter key: " key)
|
|
82
|
-
value = substr($0, length(key) + 2); sub(/#.*/, "", value); values[key] = trim(value)
|
|
83
|
-
next
|
|
84
|
-
}
|
|
85
|
-
inverify {
|
|
86
|
-
if ($0 ~ /^[ \t]*```[ \t]*$/) { inverify = 0; next }
|
|
87
|
-
if ($0 ~ /^[ \t]*```/) fail("Verification requires one closed bash fence")
|
|
88
|
-
if ($0 !~ /^[ \t]*(#.*)?$/) runnable++
|
|
89
|
-
next
|
|
90
|
-
}
|
|
91
|
-
otherfence {
|
|
92
|
-
if ($0 ~ /^[ \t]*```/) otherfence = 0
|
|
93
|
-
next
|
|
94
|
-
}
|
|
95
|
-
/^## Acceptance Criteria[ \t]*$/ { if (accept++) fail("duplicate Acceptance Criteria section"); section = "accept"; next }
|
|
96
|
-
/^## Verification[ \t]*$/ { if (verify++) fail("duplicate Verification section"); section = "verify"; next }
|
|
97
|
-
/^## / { section = ""; next }
|
|
98
|
-
section == "accept" {
|
|
99
|
-
if ($0 ~ /^[ \t]*(```|~~~)/) fail("Acceptance Criteria must contain visible checkboxes, not fences")
|
|
100
|
-
if ($0 ~ /^[ \t]*([-+*]|[0-9]+[.)])[ \t]+/ || $0 ~ /^[ \t]*([-+*]|[0-9]+[.)])?[ \t]*\[/) {
|
|
101
|
-
if ($0 !~ /^[ \t]*([-+*]|[0-9]+[.)])[ \t]+\[[ xX]\][ \t]+[^ \t]/)
|
|
102
|
-
fail("malformed acceptance checkbox; use - [ ] text or - [x] text (indented/list marker variants supported)")
|
|
103
|
-
boxes++
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
/^[ \t]*~~~/ { fail("use backtick fences; tilde fences are unsupported") }
|
|
107
|
-
/^[ \t]*```/ {
|
|
108
|
-
if (section == "verify") {
|
|
109
|
-
if ($0 !~ /^[ \t]*```bash[ \t]*$/ || fences++) fail("Verification requires exactly one bash fence")
|
|
110
|
-
inverify = 1
|
|
111
|
-
} else otherfence = 1
|
|
112
|
-
}
|
|
113
|
-
END {
|
|
114
|
-
if (bad) exit 1
|
|
115
|
-
if (!closed) fail("frontmatter must close with ---")
|
|
116
|
-
if (!seen["ticket"] || !idok(values["ticket"])) fail("ticket must be a canonical ID between T-01 and T-999999")
|
|
117
|
-
base = file; sub(/^.*\//, "", base)
|
|
118
|
-
prefix = values["ticket"] "-"
|
|
119
|
-
if (index(base, prefix) != 1 || base !~ /.+\.md$/ || length(base) <= length(prefix) + 3) fail("ticket ID must match filename T-NN-slug.md")
|
|
120
|
-
if (values["status"] !~ /^(open|in_progress|done)$/) fail("status must be open, in_progress, or done")
|
|
121
|
-
if (values["size"] !~ /^(S|M|L)$/) fail("size must be S, M, or L")
|
|
122
|
-
timestamp = "^[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]T[0-9][0-9]:[0-9][0-9]:[0-9][0-9]Z$"
|
|
123
|
-
for (key in seen) {
|
|
124
|
-
if ((key == "started" || key == "finished") && values[key] !~ timestamp) fail(key " must be an ISO UTC timestamp")
|
|
125
|
-
if (key == "verified" || key == "last_check") {
|
|
126
|
-
parts = split(values[key], stamp, /[ \t]+/)
|
|
127
|
-
expected = key == "verified" ? 2 : 3
|
|
128
|
-
if (parts != expected || stamp[1] !~ timestamp || length(stamp[parts]) != 12 || stamp[parts] !~ /^[0-9a-f]+$/)
|
|
129
|
-
fail("malformed " key " timestamp/hash")
|
|
130
|
-
if (key == "last_check" && stamp[2] !~ /^(running|passed|failed|interrupted)$/) fail("invalid last_check outcome")
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
deps = values["depends_on"]
|
|
134
|
-
if (!seen["depends_on"] || deps !~ /^\[[ \t]*(T-[0-9]+([ \t]*,[ \t]*T-[0-9]+)*)?[ \t]*\]$/) fail("depends_on must be an inline list such as [T-01, T-02]")
|
|
135
|
-
sub(/^\[[ \t]*/, "", deps); sub(/[ \t]*\]$/, "", deps)
|
|
136
|
-
count = split(deps, entries, ",")
|
|
137
|
-
for (i = 1; i <= count; i++) {
|
|
138
|
-
dep = trim(entries[i]); if (dep == "") continue
|
|
139
|
-
if (!idok(dep)) fail("depends_on contains noncanonical ticket ID: " dep)
|
|
140
|
-
if (dep == values["ticket"]) fail("ticket cannot depend on itself")
|
|
141
|
-
if (depseen[dep]++) fail("duplicate depends_on ID: " dep)
|
|
142
|
-
}
|
|
143
|
-
if (!accept || !boxes) fail("Acceptance Criteria requires at least one nonempty checkbox")
|
|
144
|
-
if (!verify || fences != 1 || inverify || !runnable) fail("Verification requires exactly one closed runnable bash fence")
|
|
145
|
-
}
|
|
146
|
-
' "$file" || return 1
|
|
147
|
-
cmds=$(verification_cmds "$file") || return 1
|
|
148
|
-
if ! bash -n -c "$cmds"; then
|
|
149
|
-
printf 'pincer-ticket: %s: invalid bash syntax in Verification block\n' "$file" >&2
|
|
150
|
-
return 1
|
|
151
|
-
fi
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
validate_ticket_set() {
|
|
155
|
-
local file id ids=' '
|
|
156
|
-
for file in tickets/T-*.md; do
|
|
157
|
-
[ -e "$file" ] || continue
|
|
158
|
-
id=$(fm_get "$file" ticket)
|
|
159
|
-
if [ -n "$id" ]; then
|
|
160
|
-
case "$ids" in *" $id "*) printf 'pincer-ticket: duplicate ticket ID: %s\n' "$id" >&2; return 1 ;; esac
|
|
161
|
-
ids="$ids$id "
|
|
162
|
-
fi
|
|
163
|
-
done
|
|
164
|
-
for file in tickets/T-*.md; do
|
|
165
|
-
[ -e "$file" ] || continue
|
|
166
|
-
validate_ticket "$file" || return 1
|
|
167
|
-
done
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
ticket_file() {
|
|
171
|
-
local id file found=''
|
|
172
|
-
id=$(normalize "$1") || return 1
|
|
173
|
-
for file in "tickets/$id"-*.md; do
|
|
174
|
-
[ -e "$file" ] || continue
|
|
175
|
-
if [ -n "$found" ]; then printf 'pincer-ticket: several files match tickets/%s-*.md\n' "$id" >&2; return 1; fi
|
|
176
|
-
found=$file
|
|
177
|
-
done
|
|
178
|
-
if [ -z "$found" ]; then printf 'pincer-ticket: no ticket file tickets/%s-*.md\n' "$id" >&2; return 1; fi
|
|
179
|
-
printf '%s' "$found"
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
# PRDs and evaluation notes use the same deliberately small metadata format.
|
|
183
|
-
validate_metadata() {
|
|
184
|
-
awk -v file="$1" '
|
|
185
|
-
function fail(msg) { print "pincer: " file ": " msg > "/dev/stderr"; bad = 1; exit 1 }
|
|
186
|
-
NR == 1 { if ($0 != "---") fail("frontmatter must begin with ---"); next }
|
|
187
|
-
!closed {
|
|
188
|
-
if ($0 == "---") { closed = 1; next }
|
|
189
|
-
if ($0 ~ /^[ \t]*(#.*)?$/) next
|
|
190
|
-
if ($0 !~ /^[a-z_][a-z0-9_]*:[ \t]*/) fail("expected unindented key: value metadata")
|
|
191
|
-
key = $0; sub(/:.*/, "", key)
|
|
192
|
-
if (seen[key]++) fail("duplicate metadata key: " key)
|
|
193
|
-
}
|
|
194
|
-
END { if (bad) exit 1; if (!closed) fail("frontmatter must close with ---") }
|
|
195
|
-
' "$1"
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
validate_prd() {
|
|
199
|
-
local ref=$1 version
|
|
200
|
-
if ! [[ $ref =~ ^\.prd/prd-v([1-9][0-9]{0,8})\.md$ ]]; then
|
|
201
|
-
printf 'pincer: invalid PRD reference %s; use .prd/prd-vN.md\n' "$ref" >&2; return 1
|
|
202
|
-
fi
|
|
203
|
-
version=${BASH_REMATCH[1]}
|
|
204
|
-
[ -f "$ref" ] || { printf 'pincer: PRD does not exist: %s\n' "$ref" >&2; return 1; }
|
|
205
|
-
validate_metadata "$ref" || return 1
|
|
206
|
-
[ "$(fm_get "$ref" version)" = "$version" ] || { printf 'pincer: %s: version must match filename (%s)\n' "$ref" "$version" >&2; return 1; }
|
|
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}"
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
latest_prd() {
|
|
217
|
-
local ref latest='' highest=0 n
|
|
218
|
-
for ref in .prd/prd-v*.md; do
|
|
219
|
-
[ -e "$ref" ] || continue
|
|
220
|
-
if ! [[ $ref =~ ^\.prd/prd-v([1-9][0-9]{0,8})\.md$ ]]; then
|
|
221
|
-
printf 'pincer: invalid PRD filename: %s\n' "$ref" >&2; return 1
|
|
222
|
-
fi
|
|
223
|
-
n=${BASH_REMATCH[1]}
|
|
224
|
-
if [ "$n" -gt "$highest" ]; then highest=$n; latest=$ref; fi
|
|
225
|
-
done
|
|
226
|
-
[ -z "$latest" ] || validate_prd "$latest" || return 1
|
|
227
|
-
printf '%s' "$latest"
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
ticket_prd() { # resolve without writing; ambiguous legacy tickets require bind.
|
|
231
|
-
local file=$1 ref candidate count=0
|
|
232
|
-
ref=$(fm_get "$file" prd)
|
|
233
|
-
if [ -z "$ref" ]; then
|
|
234
|
-
for candidate in .prd/prd-v*.md; do
|
|
235
|
-
[ -e "$candidate" ] || continue
|
|
236
|
-
ref=$candidate; count=$((count + 1))
|
|
237
|
-
done
|
|
238
|
-
if [ "$count" -ne 1 ]; then
|
|
239
|
-
printf 'pincer: %s: missing or ambiguous PRD association; use pincer-ticket.sh bind %s .prd/prd-vN.md\n' "$file" "$(fm_get "$file" ticket)" >&2
|
|
240
|
-
return 1
|
|
241
|
-
fi
|
|
242
|
-
fi
|
|
243
|
-
validate_prd "$ref" || return 1
|
|
244
|
-
printf '%s' "$ref"
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
usable_ticket_prd() {
|
|
248
|
-
local ref
|
|
249
|
-
ref=$(ticket_prd "$1") || return 1
|
|
250
|
-
case "$(fm_get "$ref" status)" in
|
|
251
|
-
ticketed|built) printf '%s' "$ref" ;;
|
|
252
|
-
*) printf 'pincer: %s: PRD is draft; complete the authorized breakdown before starting (expected ticketed or built)\n' "$ref" >&2; return 1 ;;
|
|
253
|
-
esac
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
sha256() { if command -v sha256sum >/dev/null; then sha256sum; else shasum -a 256; fi; }
|
|
257
|
-
verify_hash() { verification_cmds "$1" | sha256 | cut -c1-12; }
|
|
258
|
-
|
|
259
|
-
ticket_readiness() { # explain why a done ticket needs attention, without mutation.
|
|
260
|
-
local file=$1 receipt attempt hash
|
|
261
|
-
receipt=$(fm_get "$file" verified); attempt=$(fm_get "$file" last_check)
|
|
262
|
-
hash=$(verify_hash "$file")
|
|
263
|
-
if [ -z "$attempt" ]; then
|
|
264
|
-
printf 'missing latest verification outcome — re-run verify'; return 1
|
|
265
|
-
fi
|
|
266
|
-
if ! [[ $attempt =~ ^[0-9T:Z-]+\ passed\ [a-f0-9]{12}$ ]] || [ "${attempt##* }" != "$hash" ]; then
|
|
267
|
-
printf 'latest verification: %s — re-run verify' "$attempt"; return 1
|
|
268
|
-
fi
|
|
269
|
-
if [ -z "$receipt" ]; then printf 'done without a verification receipt — re-run verify'; return 1; fi
|
|
270
|
-
if ! [[ $receipt =~ ^[0-9T:Z-]+\ [a-f0-9]{12}$ ]] || [ "${receipt##* }" != "$hash" ]; then
|
|
271
|
-
printf 'stale or malformed verification receipt — re-run verify'; return 1
|
|
272
|
-
fi
|
|
273
|
-
if [ -n "$(unticked "$file")" ]; then printf 'unticked acceptance criteria — complete and re-run verify'; return 1; fi
|
|
274
|
-
}
|
|
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.
|
|
281
|
-
notes_current() {
|
|
282
|
-
local prd=$1 candidate base changes manifest output listed file stored canonical offending
|
|
283
|
-
[ -f NOTES.md ] || { printf 'missing'; return 1; }
|
|
284
|
-
validate_metadata NOTES.md >/dev/null 2>&1 || { printf 'stale: invalid or missing evaluation metadata'; return 1; }
|
|
285
|
-
[ "$(fm_get NOTES.md prd)" = "$prd" ] || { printf 'stale: evaluation PRD does not match'; return 1; }
|
|
286
|
-
candidate=$(fm_get NOTES.md candidate); base=$(fm_get NOTES.md base)
|
|
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
|
|
289
|
-
fi
|
|
290
|
-
if ! git rev-parse --verify "$candidate^{commit}" >/dev/null 2>&1 ||
|
|
291
|
-
! git rev-parse --verify "$base^{commit}" >/dev/null 2>&1 ||
|
|
292
|
-
! git merge-base --is-ancestor "$base" "$candidate" 2>/dev/null ||
|
|
293
|
-
! git merge-base --is-ancestor "$candidate" HEAD 2>/dev/null; then
|
|
294
|
-
printf 'stale: evaluation commits or ancestry unavailable'; return 1
|
|
295
|
-
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
|
|
318
|
-
changes=$(git status --porcelain --untracked-files=all -- . ':(exclude)NOTES.md') || return 1
|
|
319
|
-
if [ -n "$changes" ]; then printf 'stale: working tree has changes outside NOTES.md'; return 1; fi
|
|
320
|
-
printf 'current (%s)' "$candidate"
|
|
321
|
-
}
|