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
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:
@@ -15,6 +15,7 @@ from scribe.config import Config
|
||||
from scribe.services import plugin_context as plugin_ctx_svc
|
||||
from scribe.services import repo_bindings as repo_bindings_svc
|
||||
from scribe.services import report_check as report_check_svc
|
||||
from scribe.services import shape_check as shape_check_svc
|
||||
from scribe.services import task_claims as task_claims_svc
|
||||
from scribe.services.settings import get_admin_setting, set_setting
|
||||
|
||||
@@ -359,6 +360,71 @@ async def report_check():
|
||||
return jsonify(body)
|
||||
|
||||
|
||||
@plugin_bp.get("/shape-check")
|
||||
@login_required
|
||||
async def shape_check():
|
||||
"""The end-of-turn question (milestone 439): what did this turn write that
|
||||
nobody has judged?
|
||||
|
||||
The client's write hooks keep a ledger of the definitions each turn wrote;
|
||||
its Stop hook sends them here. The server decides which carry no judgment
|
||||
— against the ledger and the project's recorded snippets — records the
|
||||
outcome, and for a block returns the `reason` the agent is sent back with.
|
||||
A hook blocks only on a reason it received, so only on a block that was
|
||||
recorded, and never on the stop that follows one (`phase=after`).
|
||||
|
||||
A GET like every plugin endpoint: a read-scoped key runs the plugin, and
|
||||
this records telemetry the way /report-check does. It changes no ledger
|
||||
row — the agent's own classify_shapes call does that.
|
||||
|
||||
Query:
|
||||
written (str) — newline-separated `path<TAB>kind<TAB>name` lines,
|
||||
repo-relative, kind css|sym|file.
|
||||
phase (str) — check | after. Anything else is a 400.
|
||||
repo (opt) — working repo remote, resolved like the other arms.
|
||||
project_id (opt) — explicit scope override.
|
||||
"""
|
||||
phase = (request.args.get("phase") or "check").strip()
|
||||
if phase not in shape_check_svc.PHASES:
|
||||
return jsonify({"error": f"phase must be one of {list(shape_check_svc.PHASES)}"}), 400
|
||||
written = shape_check_svc.parse_written(request.args.get("written") or "")
|
||||
project_id, repo, _unbound = await _project_scope()
|
||||
body: dict = {"status": "ok", "unjudged": []}
|
||||
if not project_id or not written:
|
||||
return jsonify(body)
|
||||
from scribe.services import access
|
||||
from scribe.services import coverage as coverage_svc
|
||||
from scribe.services import shape_ledger as shape_ledger_svc
|
||||
|
||||
if not await access.can_read_project(g.user.id, project_id):
|
||||
return jsonify(body)
|
||||
repo_key = repo_bindings_svc.normalize_repo_key(repo) if repo else ""
|
||||
unjudged = await shape_ledger_svc.unjudged_shapes(project_id, written, repo_key=repo_key)
|
||||
# A snippet recorded AT a shape is the strongest verdict there is — the
|
||||
# next refresh stamps that row canonical. Until then the ledger cannot
|
||||
# know, so the recorded locations answer for it: an agent who just
|
||||
# recorded what it built is not asked again.
|
||||
if unjudged:
|
||||
recorded = {
|
||||
(path, symbol)
|
||||
for _sid, path, symbol in await coverage_svc._recorded_locations(g.user.id, project_id)
|
||||
}
|
||||
unjudged = [u for u in unjudged if (u["path"], u["symbol"]) not in recorded
|
||||
and not (u["kind"] == "file" and (u["path"], "") in recorded)]
|
||||
outcome = shape_check_svc.outcome_for(phase, unjudged)
|
||||
await shape_check_svc.record_shape_check(
|
||||
g.user.id, outcome, written=len(written), unjudged=len(unjudged),
|
||||
project_id=project_id,
|
||||
)
|
||||
body["outcome"] = outcome
|
||||
body["unjudged"] = unjudged
|
||||
if outcome == "blocked":
|
||||
body["reason"] = shape_check_svc.block_reason(
|
||||
unjudged, project_id=project_id, repo=repo_key or repo,
|
||||
)
|
||||
return jsonify(body)
|
||||
|
||||
|
||||
@plugin_bp.get("/claim-session")
|
||||
@login_required
|
||||
async def claim_session():
|
||||
|
||||
Reference in New Issue
Block a user