feat(shapes): the agent judges what it wrote, at the end of the turn (milestone 439 steps 1-3)
CI & Build / Python lint (push) Successful in 5s
CI & Build / Plugin hooks (push) Successful in 18s
CI & Build / TypeScript typecheck (push) Successful in 54s
CI & Build / integration (push) Successful in 52s
CI & Build / Python tests (push) Successful in 1m38s
CI & Build / Build & push image (push) Successful in 33s

Recording used to be decided by machinery — the only "record it" prompt
fired when a same-named copy already existed (#2664), so a first instance of
a reusable piece was never asked about, and judgment arrived only through
audits. Now the question is asked where the knowledge is: the end of the
turn that wrote the code, of the agent that wrote it.

- Write hooks keep `<sid>.written.ids` (path, kind, name) for every
  definition a write names; a new file adds a `file` line for its stem — a
  candidate in any language without a framework rule (scribe_written_append).
- Stop hook scribe_shape_check.sh sends the ledger to GET
  /api/plugin/shape-check and blocks once, in the server's words, when
  anything is unjudged. Same discipline as the report check: never twice,
  never without a recorded check, another hook's loop left alone; the ledger
  is kept when the instance cannot be reached.
- shape_ledger.unjudged_shapes: no row, unclassified, scoped and hook stamps
  are unjudged; an agent/audit/import verdict is not. A snippet recorded at
  the shape answers for it until the refresh stamps it canonical.
- services/shape_check owns the reason text and records every outcome in
  app_logs (passed / blocked / judged_after_block / left_after_block).
- classify_shapes(repo=…) judges a shape the ledger has not synced yet via a
  provisional row under a bound repo; the sync confirms it, or vanishes and
  revives it with the verdict intact. An unbound repo is refused.

Plugin version minted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-01 08:39:36 -04:00
co-authored by Claude Opus 5.5
parent eb5cc6d3a7
commit f1fbdf746a
17 changed files with 854 additions and 9 deletions
+25
View File
@@ -1158,6 +1158,31 @@ scribe_held_query() {
# the tests read THIS string rather than a copy of it.
SCRIBE_LEDGER_DIRS="scribe-priorart scribe-autoinject"
# THE WRITTEN-SHAPES LEDGER (milestone 439). Every definition a write names,
# appended as `path<TAB>kind<TAB>name`, for the Stop hook to ask about at the
# end of the turn (scribe_shape_check.sh). Its own file — one file, one
# question (lesson #4226) — under the prior-art directory, so the session
# clear above already sweeps it. Read `kind<TAB>name` lines on stdin (the
# scribe_defs format). A NEW file adds one `file` line named by its stem: a
# component, module or package file is a candidate in its own right, in any
# language, without a framework rule saying so.
# $1 session id (already made filename-safe) $2 repo-relative path
# $3 "new" when the file did not exist before this write
scribe_written_append() {
local sid="$1" rel="$2" fresh="${3:-}" dir stem
if [ -z "$sid" ] || [ -z "$rel" ]; then cat >/dev/null; return 0; fi
dir="${TMPDIR:-/tmp}/scribe-priorart"
mkdir -p "$dir" 2>/dev/null || true
{
if [ "$fresh" = "new" ]; then
stem=${rel##*/}; stem=${stem%.*}
[ -n "$stem" ] && printf '%s\tfile\t%s\n' "$rel" "$stem"
fi
awk -F'\t' -v p="$rel" 'NF >= 2 && ($1 == "sym" || $1 == "css") && $2 != "" { printf "%s\t%s\t%s\n", p, $1, $2 }'
} >> "$dir/${sid}.written.ids" 2>/dev/null || true
return 0
}
scribe_clear_session_ledgers() {
# SPARES `*.keep.ids`, which are evidence rather than exclusions — see
# `scribe_rules_append`. Everything else still goes: the convention is