diff --git a/.agents/skills/perform-task/references/pipeline.md b/.agents/skills/perform-task/references/pipeline.md index f2a2ac0d48..8b389bcdfa 100644 --- a/.agents/skills/perform-task/references/pipeline.md +++ b/.agents/skills/perform-task/references/pipeline.md @@ -744,9 +744,19 @@ Publish final AI state only after the Telegram commit and result are final: ```bash python3 SOURCE_ROOT/.agents/skills/process-inbox/scripts/workspace.py \ finish --source-root SOURCE_ROOT --task TASK_ID \ - --status approved|blocked + --status approved|blocked --model MODEL_SHORT_NAME ``` +`--model` is required and records which model finished the task, into the +`model` field of its `state.yaml`. Self-report the model you are actually +running as, as a lowercase short name — `claude-opus-5`, `claude-fable-5`, +`gpt-5.6-sol`, `glm-5.3`, `kimi-k3`, `grok-4.6`. Report the model running the +performer that reaches this boundary, not a leaf's model and not whichever model +happened to start the task: the field answers "who finished it", so a task +resumed by a different model after an interruption records the model that +actually completed it. Never guess or copy the value from another task; if you +cannot tell what you are, say so and stop rather than recording a wrong name. + The helper verifies a clean source checkout, local task refs, current `HEAD`, and the retained implementation's exact three-line commit message. It commits all task-scoped local artifacts and final state as `Approve ` or the diff --git a/.agents/skills/process-inbox/SKILL.md b/.agents/skills/process-inbox/SKILL.md index eb26d5865a..45525df0be 100644 --- a/.agents/skills/process-inbox/SKILL.md +++ b/.agents/skills/process-inbox/SKILL.md @@ -200,6 +200,10 @@ phase: null inbox_receipt: receipts/YYYY/MM/DD/.md ``` +Do not write a `model` field. It records which model finished the task, so only +`finish` writes it, at the canonical `Approve` or `Block` boundary; a task +carrying one before it is claimed is malformed. + Use a project slug instead of `null` when routed to a project. Use a YAML list of task identifiers for dependencies. Dependencies record code lineage as well as readiness: keep an approved source task in `depends_on` when the new task's diff --git a/.agents/skills/process-inbox/scripts/workspace.py b/.agents/skills/process-inbox/scripts/workspace.py index d638774b61..c7928f853d 100755 --- a/.agents/skills/process-inbox/scripts/workspace.py +++ b/.agents/skills/process-inbox/scripts/workspace.py @@ -22,6 +22,7 @@ SHA256_PATTERN = re.compile(r"[0-9a-f]{64}") VALID_STATUSES = {"todo", "in-progress", "approved", "blocked"} DEFAULT_TASK_TYPE = "implement" VALID_TASK_TYPES = {DEFAULT_TASK_TYPE, "verify", "minimal"} +MODEL_PATTERN = re.compile(r"[a-z0-9][a-z0-9.-]{0,39}") VALID_FINDINGS = {"confirmed", "deviation", "inconclusive"} CONSOLIDATION_PENDING = "work/consolidation-pending.md" CONSOLIDATION_COMPLETE = "work/consolidation-complete.md" @@ -43,6 +44,7 @@ STATE_FIELD_ORDER = [ "claim_order", "lease_until", "phase", + "model", "inbox_receipt", ] PORTABLE_GOLDEN = "test_TelegramForcePortable" @@ -383,6 +385,9 @@ def load_state(root, path): order = int(order) except ValueError as error: raise WorkspaceError(f"Invalid claim_order in {path}: {order!r}") from error + model = parse_scalar(values.get("model", "null")) + if model is not None and not MODEL_PATTERN.fullmatch(str(model)): + raise WorkspaceError(f"Invalid model name {model!r} in {path}") task_id = task_id_for_state(root, path) task_file = path.with_name("task.md") title = task_id.rsplit("/", 1)[-1] @@ -404,6 +409,7 @@ def load_state(root, path): "claim_order": order, "lease_until": parse_scalar(values.get("lease_until", "null")), "phase": parse_scalar(values.get("phase", "null")), + "model": model, "inbox_receipt": parse_scalar(values.get("inbox_receipt", "null")), "state_path": str(path), } @@ -2658,6 +2664,13 @@ def validate_verify_result(lines, result_path, approved): def command_finish(args): + model = args.model.strip() + if not MODEL_PATTERN.fullmatch(model): + raise WorkspaceError( + f"Invalid --model short name {args.model!r}. Report the model you are " + "actually running as, lowercase, for example claude-opus-5, " + "gpt-5.6-sol, glm-5.3." + ) config, slot = task_action_config(args, allow_project=True) ensure_clean(Path(config["source_root"]), "Telegram source checkout") kind = task_type(slot, args.task) @@ -2689,6 +2702,7 @@ def command_finish(args): "status": args.status, "phase": "complete" if args.status == "approved" else "blocked", "lease_until": None, + "model": model, }) verb = "Approve" if args.status == "approved" else "Block" paths = [task_relative_dir(args.task)] @@ -3707,6 +3721,7 @@ def parse_args(): add_common_arguments(finish) finish.add_argument("--task", required=True) finish.add_argument("--status", choices=("approved", "blocked"), required=True) + finish.add_argument("--model", required=True) finish.set_defaults(handler=command_finish) publish = subparsers.add_parser("publish") diff --git a/.claude/ai-workflow-adapter.md b/.claude/ai-workflow-adapter.md index 8c39e591a3..53af677e82 100644 --- a/.claude/ai-workflow-adapter.md +++ b/.claude/ai-workflow-adapter.md @@ -55,6 +55,16 @@ This file adapts harness mechanics and removes unnecessary text normalization. leaf rules (no delegation, no commits, progress and reply contracts); an adapter read there is wasted context. +## Model self-reporting + +`workspace.py finish` requires `--model` and records it in the task's +`state.yaml`. In Claude Code, report the Claude model actually running the +performer, lowercase, without a context-window or date suffix: +`claude-opus-5`, `claude-sonnet-5`, `claude-fable-5`, `claude-haiku-4-5`. A +model id such as `claude-opus-5[1m]` becomes `claude-opus-5`. Take the value +from the model you are running as, never from the task, the queue, or another +checkout's records. + ## Text handling Do not run a dedicated line-ending or BOM check, normalization, repair, phase,