diff --git a/.github/scripts/bump_version.py b/.github/scripts/bump_version.py index 7a6b514f..b0ea4ea9 100644 --- a/.github/scripts/bump_version.py +++ b/.github/scripts/bump_version.py @@ -114,7 +114,6 @@ def load_targets(root: Path = ROOT) -> list[ManifestTarget]: return targets -# Refactor (iter3/skill-release-pipeline): Old: 无机械 release 管道 New: bump_version + release.yml minimal option A(#32 minimal 共识) def assert_versions_sync(targets: list[ManifestTarget]) -> str: versions = {target.version for target in targets} if len(versions) != 1: @@ -124,7 +123,6 @@ def assert_versions_sync(targets: list[ManifestTarget]) -> str: return targets[0].version -# Refactor (iter3/skill-release-pipeline): Old: 无机械 release 管道 New: bump_version + release.yml minimal option A(#32 minimal 共识) def write_version(targets: list[ManifestTarget], version: str, dry_run: bool) -> None: parse_version(version) for target in targets: @@ -135,7 +133,6 @@ def write_version(targets: list[ManifestTarget], version: str, dry_run: bool) -> write_json(target.path, target.data) -# Refactor (iter3/skill-release-pipeline): Old: 无机械 release 管道 New: bump_version + release.yml minimal option A(#32 minimal 共识) def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--check", action="store_true", help="validate mapped manifest versions are synchronized") diff --git a/README.md b/README.md index 958babaa..d71aaca9 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ This repository is not an application runtime. Its deliverables are skills under | skill | What it is for | Runtime shape | |---|---|---| -| `codex-refactor-loop` | Heavy autonomous loop for ongoing repository work: daemon supervision, Codex workers, GitHub orchestration, review gates, and automated release publication when the host opts in. | Uses checked-in scripts, `.refactor-loop/` state, GitHub, git, and host-provided `host.env` facts. | +| `codex-refactor-loop` | Heavy autonomous Consensus R&D work-unit loop for issue/PR resolution, ongoing repository R&D, daemon supervision, Codex workers, GitHub orchestration, review gates, and automated release publication when the host opts in. Audit/refactor is a fallback issue producer when no actionable managed work is open. | Uses checked-in scripts, `.refactor-loop/` state, GitHub, git, and host-provided `host.env` facts. | | `sshx` | Lightweight worker-delegated inline consensus methodology (轻量 worker-delegated inline 共识方法论) for high-risk decisions or implementation plans that need isolated perspectives but do not need daemon, GitHub, or git orchestration. | WorkerMode dispatches isolated thinking and review workers; no daemon, no lifecycle authority, no runtime control plane, and not a duplicate alias for `codex-refactor-loop`. | ## Core @@ -56,7 +56,7 @@ Copy `skills//` into the agent's personal skills directory, such as Claude ### Downstream Host Setup -The host installation sequence for `codex-refactor-loop` is centralized in the [`Downstream install walkthrough`](./skills/codex-refactor-loop/SKILL.md#downstream-install-walkthrough). Use that walkthrough to install the skill, copy and fill the host-owned `host.env`, configure user-level cron or launchd, and connect the Claude Code `statusLine`; this README does not duplicate the command matrix. +The host installation sequence for `codex-refactor-loop` is centralized in the [`Downstream install walkthrough`](./skills/codex-refactor-loop/SKILL.md#downstream-install-walkthrough). Use that walkthrough to install the skill, copy and fill the host-owned `host.env`, configure user-level cron or launchd, and connect the Claude Code `statusLine`; this README does not duplicate the command matrix. Host GitHub workflow portability uses the folded [`GitHub workflow portability checklist`](./skills/codex-refactor-loop/SKILL.md#github-workflow-portability-checklist), not a standalone setup skill. ## Architecture @@ -84,7 +84,7 @@ Host projects inject runtime facts through `host.env`: repository root, GitHub s ## Roadmap -The public product identity is Consensus R&D. `codex-refactor-loop` remains the heavy autonomous loop entrypoint, while `sshx` carries the same consensus philosophy as a lightweight worker-delegated inline method, not as a duplicate alias for the heavy loop. Future work should generalize the engine spine so the work-unit source can vary, parameterize host assumptions where appropriate, and keep runtime authority narrow enough to verify mechanically. +The public product identity is Consensus R&D. Refactoring, issue-solving, and repository R&D are different entry surfaces for the same work-unit loop. `codex-refactor-loop` remains the stable heavy autonomous loop entrypoint, while `sshx` carries the same consensus philosophy as a lightweight worker-delegated inline method, not as a duplicate alias for the heavy loop. Future work should continue generalizing host/project assumptions and producer inputs while keeping runtime authority narrow enough to verify mechanically. ## License diff --git a/skills/codex-refactor-loop/SKILL.md b/skills/codex-refactor-loop/SKILL.md index ed36b378..e766a5eb 100644 --- a/skills/codex-refactor-loop/SKILL.md +++ b/skills/codex-refactor-loop/SKILL.md @@ -1,6 +1,6 @@ --- name: codex-refactor-loop -description: Use when the user wants an unattended Consensus R&D work-unit loop driven by codex CLI in isolated git worktrees, with audit/refactor as the default compatibility intake, dynamic /loop wakeups, GitHub status, and per-work-unit merges. +description: Use when the user wants an unattended Consensus R&D issue/PR resolution and work-unit loop driven by codex CLI in isolated git worktrees, with audit/refactor as a fallback compatibility issue producer, dynamic /loop wakeups, GitHub status, and per-work-unit merges. --- > Refactor (iter319/issue-319): Old pattern: 物理拆 REFERENCE.md 后跨平台加载/维护退化. > New principle: 单文件 SKILL.md 用 Controller Contract Index + Detailed reference anchors 分层,禁止重新拆 REFERENCE.md 作为默认修复. @@ -9,7 +9,7 @@ description: Use when the user wants an unattended Consensus R&D work-unit loop > New principle: Downstream install walkthrough 是唯一装机主段;README 链到 SKILL 锚点,SKILL 内部段落互链;source-regression 锁住单文件链接与必备 surface,bounded scheduler behavior test 锁住 consensus-rnd-cli restart-daemons 不无限阻塞。 > Refactor (iter218/issue-218): Old pattern: ensure-project-rules 是 public CLI 默认写 host policy 文件($PROJECT_RULES),违反 skill 无 host 改动权边界 > New principle: 改为 read-only check-project-rules probe + patch artifact:probe 只读判 sentinel block,非 current 写 .refactor-loop/runs/ patch 并 fail-closed 不派 actor;删 ensure-project-rules/_atomic_write,不引入 PROJECT_RULES_WRITE_ENABLE。严格按 plan 逐条改。 -# Codex Refactor Loop — Controller Contract +# Consensus R&D Work-Unit Loop — Controller Contract This SKILL.md is the single controller contract and detailed reference by maintainer directive. It must be enough to run the loop safely on first load while keeping heavy schemas, full templates, command bodies, and recovery runbooks reachable by intra-file anchors. Use intra-file anchors when a phase needs the detailed body, such as [host runtime details](#host-runtime-details); do not force-load unrelated sections. @@ -38,14 +38,14 @@ Use intra-file anchors when a phase needs the detailed body, such as [host runti | Hard rules | All worker prompts inherit controller-level hard rules. | Include scope, git, test, language, and no-scope-creep constraints in every spawned prompt. | [hard rules details](#hard-rules-details) | prompt templates | | Language | Source files are English-only; external user-facing artifacts are 中文 by default. README.md + README.zh-CN.md is the only English-canonical public-doc carve-out. No mandatory parallel English section. | Enforce on prompts, GitHub posts, commits, docs, source comments/logs. | [language policy details](#language-policy-details), [historical bilingual notes](#historical-bilingual-notes) | prompts, docs, commit text | -## Two entry modes + +## Main path and fallback producer -The loop has two supported entry modes: +The default main path is open actionable catalog-managed GitHub issue/PR resolution. The controller dispatches the next-step actor for managed issues and PRs before starting any producer for new work. -- `audit-driven`: run the default audit producer, project accepted clusters into work-unit items, then route design decisions through Consensus-rnd Phase design-consensus. -- `issue-driven / Path A`: create or reuse a concrete GitHub issue, apply the catalog-derived design issue label bundle (`crnd:lifecycle:managed`, `crnd:phase:design-solving`, and `crnd:human:auto`), then let the controller sweep dispatch Consensus-rnd Phase design-consensus directly. Legacy issue-entry labels are migration aliases only and must not be written as the active bundle. +`issue-driven / Path A` is the main-path issue entry surface: create or reuse a concrete GitHub issue, apply the catalog-derived design issue label bundle (`crnd:lifecycle:managed`, `crnd:phase:design-solving`, and `crnd:human:auto`), then let the controller sweep dispatch Consensus-rnd Phase design-consensus directly. Legacy issue-entry labels are migration aliases only and must not be written as the active bundle. -Audit is a seed producer, not the only entry. Issue-driven work uses the GitHub issue body/comments as the work-unit source when no local audit artifact is provided. Both entry modes still require Consensus-rnd Phase design-consensus solver consensus and meta-judge consensus before implementation. +`audit` remains a stable compatibility producer value and fallback issue producer. It runs only after no open actionable managed issue/PR, queued dispatch, clean marker route, CI/no-gap route, maintainer-comment route, or higher-priority wakeup route exists. Audit produces or updates issues that feed back into the main path; it is not a co-equal entry mode or a parallel R&D lane. Issue-driven work uses the GitHub issue body/comments as the work-unit source when no local audit artifact is provided. Concrete plans still require Consensus-rnd Phase design-consensus solver consensus and meta-judge consensus before implementation. Workflow stage display names are sourced from `scripts/codex_refactor_loop/workflow_stages.py`. The built-in registry remains the default compatibility vocabulary; public built-in stage display must use `Consensus-rnd Phase `. Legacy `phase9-router` and `phase9-issue...` strings are compatibility command and artifact dialects only. @@ -68,7 +68,7 @@ Owner map: | `scripts/codex_refactor_loop/cli.py::COMMANDS` | public command names and authority tokens | Public command catalog and authority fact source; controller lifecycle primitives stay outside `COMMANDS`. | | `scripts/codex_refactor_loop/workflow_stages.py` | workflow stage display names and slugs | Sole built-in stage catalog; HostWorkflowSpec may project `host:` data without overwriting built-ins. | -`HOST_WORKFLOW_SPEC` may point at one repo-relative JSON HostWorkflowSpec. Empty or unset keeps built-in behavior. The file is data-only route vocabulary for events, host stages, work-unit kinds, roles, prompt bindings, consensus policies, and issue-intake mappings. All host-added names must use the reserved `host:` namespace, and `WorkflowInvariantValidator` rejects attempts to overwrite built-ins, public compatibility aliases, marker families, producers, or cluster aliases. HostWorkflowSpec grants no lifecycle authority: no command, shell, argv, git, commit, push, merge, close, label mutation, assignee, milestone, import, or executor fields are allowed. It also cannot downgrade consensus: design-consensus-shaped host policy still requires at least three independent solvers, exactly one independent judge, peer-output isolation, and fixed marker families. First-version scope is bounded to status/prompt/intake projection; it is not a DAG executor and does not create public marker aliases. Consensus-rnd Phase design-consensus router direct-spawn-intent ignores host `roles`, `dispatch`, and `consensus_policies` completely; its allowlist is always the built-in `minimal`/`structural`/`delete` solver triplet plus built-in `judge`. +`HOST_WORKFLOW_SPEC` may point at one repo-relative JSON HostWorkflowSpec. Empty or unset keeps built-in behavior. The file is data-only route vocabulary and a seven-surface data-only projection for events, host stages, work-unit kinds, roles, prompt bindings, consensus policies, and issue-intake mappings (`events`, `stages`, `work_unit_kinds`, `roles`, `prompt_bindings`, `consensus_policies`, and `issue_intake_mappings`). All host-added names must use the reserved `host:` namespace, and `WorkflowInvariantValidator` rejects attempts to overwrite built-ins, public compatibility aliases, marker families, producers, or cluster aliases. HostWorkflowSpec grants no lifecycle authority: no command, shell, argv, git, commit, push, merge, close, label mutation, assignee, milestone, import, or executor fields are allowed. It also cannot downgrade consensus: design-consensus-shaped host policy still requires at least three independent solvers, exactly one independent judge, peer-output isolation, and fixed marker families. First-version scope is bounded to status/prompt/intake projection; it is not a DAG executor and does not create public marker aliases. Consensus-rnd Phase design-consensus router direct-spawn-intent ignores host `roles`, `dispatch`, and `consensus_policies` completely; its allowlist is always the built-in `minimal`/`structural`/`delete` solver triplet plus built-in `judge`. ## Host 配置(通用化注入点) -`$HOST_REFACTOR_COMMENT_POLICY` controls only refactor-history self-documentation source-comment semantics: whether Old/New refactor comments are allowed, required, or rejected. `${HOST_COMMENT_RULE}` only supplies comment syntax in `self-doc-comment` mode; it does not override the policy. +`$HOST_REFACTOR_COMMENT_POLICY` controls only refactor-history self-documentation source-comment semantics. Missing, empty, or default policy is `none`, which rejects Old/New refactor-history source comments and keeps rationale in external artifacts. Explicit `self-doc-comment` is a downstream compatibility opt-in; `${HOST_COMMENT_RULE}` only supplies comment syntax in that mode and does not override source English-only. Host config rules: 1. `host.env` is the only loop runtime fact injection point. It is not host production configuration schema. @@ -183,7 +183,20 @@ export CONSENSUS_RND_HOST_ENV=.config/consensus-rnd/host.env Fill the host-owned `host.env` according to the Host env surface matrix: required values must be set, defaulted values may keep their template defaults, optional/noop values may stay empty, and conditional fail-closed surfaces such as `MAINTAINER_WHITELIST` are required only when their surface is enabled. The optional `HOST_*` language-policy variables are empty by default and may stay empty unless the host has explicit policy text to inject. Legacy `.refactor-loop/host.env` remains a compatibility read fallback only. -### Guided GitHub consensus workflow setup + +### GitHub workflow portability checklist + +#104 setup is folded into this skill's existing owner surface. It may only generate or fill host-owned `.config/consensus-rnd/host.env`, a repo-relative JSON file named by `HOST_WORKFLOW_SPEC`, and optional repo-relative prompt or body binding files referenced from that JSON. It must not create a standalone setup skill or a second protocol owner. + +Allowed host artifacts: + +- `.config/consensus-rnd/host.env` with `CONSENSUS_RND_HOST_ENV=.config/consensus-rnd/host.env`. +- A repo-relative HostWorkflowSpec JSON with exactly seven data-only surfaces: `events`, `stages`, `work_unit_kinds`, `roles`, `prompt_bindings`, `consensus_policies`, and `issue_intake_mappings`. +- Optional repo-relative prompt/body binding files referenced by HostWorkflowSpec. + +Forbidden setup actions: no host `.github` edits, no label mutation, no issue/PR mutation, no branch-protection probing or edits, no git mutation, no branch creation, and no merge/close side effects. Future #357 interactive configuration may guide a maintainer through the same contract, but it must output these same host-owned artifacts rather than owning a new setup protocol. + +#### Guided GitHub consensus workflow setup When a host user asks for guided setup, do not add a renderer, CLI command, setup skill, installer, template directory, or root install document. Follow this walkthrough and write advisory artifacts by hand under `.refactor-loop/runs/github-workflow-setup//`: @@ -256,7 +269,7 @@ Command contract: | `python3 /scripts/consensus-rnd-cli release-gate --dispatch` | Compute a ready decision and write `.refactor-loop/state/release-decision.json` plus `.refactor-loop/state/release-candidate.json`; print a hint that the controller-owned publisher owns bump/commit/push/tag/release after preflight. | | `python3 /scripts/consensus-rnd-cli release-gate --score-only` | Compute and print stability only; it does not require release opt-in and does not write the decision file. | -Stability requires all signals green and fail-closed handling on missing or red evidence: the shared Checks API projection must see exact check-run name success for `contract-tests`, `manifest-version-sync`, and `skill-degradation` on both `$REVIEW_BASE_BRANCH` and `$INTEGRATION_BRANCH` (host.env); zero open `crnd:phase:blocked` PRs; zero `crnd:human:maintainer-decision` labels; zero Consensus-rnd Phase review-gate reject churn at three or more consecutive rounds; last 30 minutes P0 alert streak at most 3; at least `RELEASE_AUTO_MIN_MERGES` recent merge commits in `.refactor-loop/state/recent-pr-merges.json` for the last two hours(default 1); at least five fresh daemon heartbeats; and zero unresolved `META_RESOLVED:escalate-human` records. `recent-pr-merges.json` is a controller-owned post-merge projection produced by `merge_pr` after successful `gh pr merge`; the release decider only reads it and never discovers merge facts from `git` or GitHub. Release cadence also requires more than `RELEASE_AUTO_MIN_INTERVAL_HOURS` hours since last release(default 2). Detailed scoring and the release decision schema live in [release decision schema](#release-decision-schema). +Stability requires all signals green and fail-closed handling on missing or red evidence: the shared Checks API projection must see exact check-run name success for every name in `$HOST_GITHUB_RELEASE_REQUIRED_CHECKS` on both `$REVIEW_BASE_BRANCH` and `$INTEGRATION_BRANCH` (host.env); zero open `crnd:phase:blocked` PRs; zero `crnd:human:maintainer-decision` labels; zero Consensus-rnd Phase review-gate reject churn at three or more consecutive rounds; last 30 minutes P0 alert streak at most 3; at least `RELEASE_AUTO_MIN_MERGES` recent merge commits in `.refactor-loop/state/recent-pr-merges.json` for the last two hours(default 1); at least five fresh daemon heartbeats; and zero unresolved `META_RESOLVED:escalate-human` records. `RELEASE_AUTO_ENABLE=true` with missing or empty `$HOST_GITHUB_RELEASE_REQUIRED_CHECKS` fails closed with `missing_host_required_release_checks`. `recent-pr-merges.json` is a controller-owned post-merge projection produced by `merge_pr` after successful non-admin `gh pr merge`; the release decider only reads it and never discovers merge facts from `git` or GitHub. Release cadence also requires more than `RELEASE_AUTO_MIN_INTERVAL_HOURS` hours since last release(default 2). Detailed scoring and the release decision schema live in [release decision schema](#release-decision-schema). `release-decision.json` records `from_version`, `to_version`, `bump_type`, `commits`, `decided_at`, `stability_score`, `signals`, `ready`, `blocked_reasons`, and `release_interval`. `release-candidate.json` records the artifact-only handoff metadata, including the decision artifact path, target version, target ref, expiry, decision digest, required signal projection, host opt-in name, publish preflight name, and controller lifecycle owner. @@ -318,7 +331,7 @@ Authorization mirror: `skills/codex-refactor-loop/authorizations/runtime-excepti ## Skill degradation source-repo validation -`skill-degradation` is source-repo CI/release validation, not downstream host runtime authority. Source validation runs through `consensus-rnd-cli check-degradation --static`; CI required `skill-degradation` runs `/scripts/consensus-rnd-cli check-degradation --static`; `consensus-rnd-cli release-gate` requires it beside `contract-tests` and `manifest-version-sync`. Static degradation checking against a non consensus-rnd source repo root must return rc=0 with `not-source-repo`, without emitting source-repo required-file findings, writing host artifacts, or creating runtime alerts/pending events; any source repo candidate remains fail-closed. A downstream host has no runtime watch, no alert log, no pending event, no peek lens, and no host.env knobs for skill-degradation. **Forbidden actions**: no source mutation; no git reset/rebase/merge/push; no GitHub issue/PR/body/label lifecycle mutation; no codex dispatch; no standalone daemon creation; no WorkUnit/schema/envelope changes; no protocol/plugin registry; no auto-clean root garbage; no auto-fix API. Details: [skill degradation source-repo validation details](#skill-degradation-source-repo-validation-details). +`skill-degradation` is source-repo CI/release validation, not downstream host runtime authority. Source validation runs through `consensus-rnd-cli check-degradation --static`; CI required `skill-degradation` runs `/scripts/consensus-rnd-cli check-degradation --static`; release required checks are not hardcoded by source-repo CI job names. `consensus-rnd-cli release-gate` consumes `required_release_checks()` from `$HOST_GITHUB_RELEASE_REQUIRED_CHECKS` and checks whichever host-owned exact GitHub check-run names are listed there. Static degradation checking against a non consensus-rnd source repo root must return rc=0 with `not-source-repo`, without emitting source-repo required-file findings, writing host artifacts, or creating runtime alerts/pending events; any source repo candidate remains fail-closed. A downstream host has no runtime watch, no alert log, no pending event, no peek lens, and no host.env knobs for skill-degradation. **Forbidden actions**: no source mutation; no git reset/rebase/merge/push; no GitHub issue/PR/body/label lifecycle mutation; no codex dispatch; no standalone daemon creation; no WorkUnit/schema/envelope changes; no protocol/plugin registry; no auto-clean root garbage; no auto-fix API. Details: [skill degradation source-repo validation details](#skill-degradation-source-repo-validation-details). ## Claude Code statusline(per #51 consensus) `skills/codex-refactor-loop/scripts/consensus-rnd-cli statusline` 是 fast (<200ms) read-only Claude Code statusline reader,显示本仓库 loop 实时状态(codex 计数、PR/issue 数、daemon 健康、P0 streak、freeze 指示、optional update notice)。 @@ -420,6 +433,7 @@ Every `/loop`, task notification, ScheduleWakeup resume, or daemon pending-event - **Allowed git topology observation(issue #190 only)**: `git fetch origin --quiet`, `git -C worktree list --porcelain`, `git -C rev-parse --verify HEAD`, `git -C rev-parse --verify refs/remotes/origin/`, and `git -C rev-list --count refs/remotes/origin/..HEAD`, solely for committed-but-unpushed worker output detection on open auto-loop PR heads. Committed `FIX_DONE` / `IMPLEMENT_DONE` output is not reviewer/CI visible until `origin/` contains it; ahead local output emits actionable `UNPUSHED_WORKER_OUTPUT::`. - **Forbidden / no lifecycle authority**: no restart, no spawn, no git lifecycle or mutation commands, no checkout/switch, no branch create/delete/update, no worktree add/remove/prune, no commit, no push, no reset, no rebase, no merge, no label mutation, no issue/PR create-close-edit, no tag/release, and no GitHub lifecycle mutation. - **Hard-gate**: computes canonical `actual`, `target=max(CODEX_FLOOR, expected_from_active_tasks)`, `deficit=max(0,target-actual)`, and when `deficit>0` emits `HARD_GATE:dispatch_required=N` plus structured `hard_gate`; this is not advisory and requires dispatching enough ordered actionable tasks or legal audit fallback before ending the wakeup. There is no general low-floor exemption, and `AUDIT_DONE:none:0` still does not exempt the floor. The only single active audit boundary is when no actionable open work and no queue candidate exists, expected is 0, and the same-iteration `audit-iter-N` is already active; then the plan emits `WAIT:single-active-audit`, `dispatch_required=0`, `reason=single_active_audit_in_flight`, and `blocked_deficit=N` instead of duplicating the same audit; no duplicate same-iteration audit. +- **Release countdown**: `crnd:milestone:release-target` may add a `release-countdown` status action sourced from release-gate scoring; release-countdown status is status-only and not dispatchable. - Output priority order mirrors the controller checklist: bootstrap or missing wake source, maintainer comment, unpushed worker output, completed `EXIT=0` marker, CI red, no-gap violation, `crnd:milestone:current` open issue/PR, ordinary open existing issue/PR, then producer or audit fixed-point recommendation. - If no actionable open work exists, it emits `RECOMMEND:audit`; ordinary audit is the floor fallback only when no same-iteration audit is already active. @@ -430,7 +444,7 @@ The workflow stage index is the local routing map. It intentionally links to hea | Phase | Local controller contract | Detail anchor | |---|---|---| | Consensus-rnd Phase bootstrap | Session bootstrap. Must complete before normal routing. | [Consensus-rnd Phase bootstrap details](#bootstrap-details) | -| Consensus-rnd Phase work-intake | Produce work-unit items. Audit remains the default compatibility producer; manual issue intake is separate. | [work-unit contract](#work-unit-contract), [batching heuristics](#batching-heuristics) | +| Consensus-rnd Phase work-intake | Fallback issue production when no actionable managed issue/PR exists; audit is the built-in compatibility producer. | [work-unit contract](#work-unit-contract), [batching heuristics](#batching-heuristics) | | Consensus-rnd Phase implementation | Implement one codex per active work unit in the batch. Controller owns branch/worktree topology and prompt construction. | [phase routing details](#phase-routing-details) | | Consensus-rnd Phase verification | Verify with a separate codex from the implementer. Verification may return ok, rework, partial, or blocked. | [recovery playbook](#recovery-playbook) | | Consensus-rnd Phase publish | Controller commits, merges, pushes, and opens PRs. Workers never commit/push/checkout. | [merge and push details](#merge-and-push-details) | @@ -574,7 +588,7 @@ The floor is local because it prevents loop stalls. - 自 PR #<本>: `consensus-rnd-cli concurrency` 不仅 alert; actual < floor 且 dispatch-queue 非空时自动派发(per host 实证 "低于预期数就继续派发"). controller 写 queue 即可,无需自己 ps grep + spawn. - controller 每次 wakeup 的 step 1.5 checks the count and 必须在任何 `ScheduleWakeup` 之前执行. - If below floor, consume real work first: existing dispatch queue, then higher-priority actionable marker, then maintainer comment, CI red, no-gap violation, or Consensus-rnd Phase design-intake / Consensus-rnd Phase design-consensus actionable route. "Actionable marker" 限定为:log tail `EXIT=0` 后的完成 verdict (FIX_DONE / REVIEW_DONE / IMPLEMENT_DONE / SOLVER_DONE / META_JUDGE_DONE / TEST_ADD_DONE / AUDIT_DONE / VERIFY_DONE),或新 maintainer comment、CI red、no-gap violation。in-flight codex (没 EXIT=0) 不是 actionable marker——以"等 cascade / fix 完会派 reviewers"为由 defer floor top-up 是绕规则。 -- If `deficit>0`, there is no general exemption: dispatch existing/open actionable work first, then legal audit fallback. The audit fallback remains: envsubst 下一 iteration `prompts/audit.md` 到 `.refactor-loop/prompts/audit-iter-N.md` → `consensus-rnd-cli spawn-codex` 用 harness background task 启动。 +- If `deficit>0`, there is no general exemption: dispatch existing/open actionable managed issue/PR work first, then legal fallback issue production through audit only when no higher-priority route exists. The audit fallback remains: envsubst 下一 iteration `prompts/audit.md` 到 `.refactor-loop/prompts/audit-iter-N.md` → `consensus-rnd-cli spawn-codex` 用 harness background task 启动。 - `AUDIT_DONE:none:0` still does not exempt the concurrency floor; when no real queued/actionable open work exists and no same-iteration audit is active, emit `RECOMMEND:audit` and the hard gate line `HARD_GATE:dispatch_required=N`. - Ordinary audit fallback has one same-iteration active slot. If that slot is occupied and no other legal work exists, expose the remaining capacity as `WAIT:single-active-audit` with `dispatch_required=0`, `reason=single_active_audit_in_flight`, and `blocked_deficit=N`; do not duplicate same-iteration audit; no duplicate same-iteration audit. - "派 audit 重 / daemon target stale / 等 cascade / 和已有工作冲突" 都不接受作为 defer 理由; the correct visible state for a positive deficit is hard-gate dispatch or the single active audit boundary WAIT, not low-floor exemption. @@ -589,6 +603,10 @@ Authorization: `skills/codex-refactor-loop/authorizations/runtime-exceptions.md# GitHub label `crnd:milestone:current` marks issue/PRs related to the current period's main task. It is orthogonal third axis beside phase labels and human labels: it changes dispatch priority, not phase semantics, human escalation semantics, marker semantics, or label exclusivity for those axes. Legacy milestone labels are migration aliases only and must be normalized through `codex_refactor_loop.labels`. +GitHub label `crnd:milestone:release-target` marks open catalog-managed issue/PRs whose existence should surface release countdown status. It is a non-exclusive milestone fact and may coexist with `crnd:milestone:current`; `crnd:milestone:current` remains dispatch priority only and must not trigger release countdown by itself. + +Release countdown is wakeup-plan-only and read-only. `consensus-rnd-cli wakeup-plan` may append a status-only, non-dispatchable `release-countdown` action only when an open actionable managed issue/PR has `crnd:milestone:release-target`. Its fields come from the release-gate scoring source, `.version-bump.json`, and the existing release commits projection: `targets`, `from_version`, `to_version`, `stability_score`, `ready`, `red_signals`, `blocked_reasons`, `no_lifecycle_authority`, and `source: "release-gate"`. It must not create a daemon, write state, update statusline, update peek, create a top-level duplicate object, write a release decision, mutate labels, tag, publish a release, or add lifecycle authority. + Milestone active means at least one open catalog-managed issue/PR carries `crnd:milestone:current`. Before any non-milestone existing-issue work or ordinary audit fallback, controller MUST first dispatch the next-step actor for milestone-labeled issue/PRs that lack in-flight codex coverage for their current phase label. Non-milestone design/audit work is downgraded while milestone is active; do not kill already-running non-milestone codexes solely because milestone became active. Only these actions stay above milestone priority: bootstrap failure / missing wake source, maintainer comment, completed marker same-wakeup route, CI red, and no-gap violation. Correctness still wins: no-gap violation means an active item has no required worker and must be repaired before discretionary prioritization. @@ -657,6 +675,11 @@ mutate labels except the #238 `closed-label-reconciler`, which may mutate only CLOSED `crnd:lifecycle:managed` item phase/cleanup/stuck labels into exactly one terminal phase, `crnd:phase:merged` or `crnd:phase:closed`. +Label exclusivity is per `LabelSpec.exclusive_axis`, not per group. Phase and +human labels remain exactly-one axes; non-exclusive catalog labels such as +`crnd:milestone:current` and `crnd:milestone:release-target` may coexist when +`LabelSpec.exclusive_axis is None`. + ## `crnd:human:maintainer-decision` 严格语义(强制) # Refactor (iter4/human-label-semantics-guard): Old pattern: label 当 architect reject workaround. New principle: 严语义 + reflector self-check + controller helper guard + source-regression test. @@ -744,8 +767,7 @@ Policy:the loop continues until an explicit stop condition or a visible `crnd:hu 1. No new features; only clean the authorized violation or implement the consensus plan. 2. No external repo changes; `$EXTERNAL_REPOS` are out of scope unless the user explicitly expands scope. - -3. Code self-documents refactors according to `$HOST_REFACTOR_COMMENT_POLICY`: `self-doc-comment` requires host-style refactor-history comments; `none` forbids those source comments and requires the rationale in external artifacts. +3. Code refactor rationale follows `$HOST_REFACTOR_COMMENT_POLICY`: missing, empty, or default policy is `none`, which forbids refactor-history source comments and keeps rationale in external artifacts; explicit `self-doc-comment` is a downstream compatibility opt-in and must still obey source English-only. 4. No `commit`, `push`, `checkout`, PR create/merge, or issue close inside worker prompts; controller owns git topology. 5. No sleep/delay-based test pacing; use deterministic awaiters. 6. No `[Skip]`, disabled tests, ignored tests, or manual category escapes to make CI green. @@ -879,22 +901,25 @@ The controller recognizes two producers: | Producer | Intake | Controller behavior | |---|---|---| -| `audit` | Default compatibility audit/refactor intake. | Run audit prompt, project accepted clusters into work-unit items, batch by dependencies/risk. | -| `manual-issue` | Explicit GitHub issue intake via labels/triage. | Normalize problem and verification hints into a design issue, then use Consensus-rnd Phase design-consensus. | +| `audit` | Compatibility audit/refactor fallback issue producer. | Run audit prompt only when no actionable managed issue/PR or higher-priority route exists; project accepted clusters into managed issues or work-unit items, then feed the main path. | +| `manual-issue` | Explicit managed GitHub issue intake for main-path resolution. | Normalize problem and verification hints into a design issue, then use Consensus-rnd Phase design-consensus. | Producer rules: -1. Audit is the default when the user asks for the unattended loop without a narrower producer. -2. Manual issues enter only through explicit maintainer label or triage monitor routing. -3. `requires_design` audit clusters open GitHub issues and do not auto-implement until Consensus-rnd Phase design-consensus consensus. -4. Direct implementation is allowed only for clusters already authorized by policy and not requiring design. -5. Batching should prefer independent, low-risk work and preserve dependency ordering. -6. Detailed producer fields and batching heuristics live in [work-unit contract](#work-unit-contract) and [batching heuristics](#batching-heuristics). +1. Issue/PR resolution is the main-path state, not a producer that creates new work. +2. Audit runs only as a compatibility fallback issue producer when no actionable managed issue/PR, queued dispatch, clean marker route, CI/no-gap route, maintainer-comment route, or higher-priority wakeup route exists. +3. Manual issues enter only through explicit maintainer label or triage monitor routing. +4. `requires_design` audit clusters open GitHub issues and do not auto-implement until Consensus-rnd Phase design-consensus consensus. +5. Direct implementation is allowed only for clusters already authorized by policy and not requiring design. +6. Batching should prefer independent, low-risk work and preserve dependency ordering. +7. Detailed producer fields and batching heuristics live in [work-unit contract](#work-unit-contract) and [batching heuristics](#batching-heuristics). ## Phase Guardrails Consensus-rnd Phase work-intake guardrails: +This stage covers fallback issue production through the audit compatibility producer. The main issue/PR resolution path does not start here; it starts from open actionable managed GitHub issues and PRs already visible to the controller. + 1. Run the producer with host-injected `$SOURCE_GLOBS`. 2. Write audit output to `.refactor-loop/runs/audit-iter-N.md`; `N` must be nonempty and unique among currently active audit fallback runs. 3. Convert accepted units into work-unit items before dispatch. @@ -1079,7 +1104,7 @@ Stability score is the percentage of the eight boolean signals that pass. `ready | Signal key | Pass condition | |---|---| -| `required_checks_recent_green` | Shared Checks API projection sees exact check-run name success for `contract-tests`, `manifest-version-sync`, and `skill-degradation` on both `$REVIEW_BASE_BRANCH` and `$INTEGRATION_BRANCH` (host.env) within two hours. | +| `required_checks_recent_green` | Shared Checks API projection sees exact check-run name success for every name in `$HOST_GITHUB_RELEASE_REQUIRED_CHECKS` on both `$REVIEW_BASE_BRANCH` and `$INTEGRATION_BRANCH` (host.env) within two hours; auto-release with an empty list fails closed. | | `no_open_blocked_pr` | No open PR has `crnd:phase:blocked`. | | `no_human_decision_label` | No open issue or PR has `crnd:human:maintainer-decision`. | | `no_phase8_reject_churn` | `.refactor-loop/state/phase8-review-state.json` reports fewer than three consecutive reject rounds. | @@ -1116,7 +1141,7 @@ dogfood 运行中固化的操作经验。host 注入的 loop runtime 配置集 ### Skill degradation source-repo validation details The skill-degradation checker is intentionally source-repo scoped: no standalone watchdog, no seventh daemon, no `DegradationCheck` protocol, no plugin registry, no new event envelope, no auto-clean, no auto-fix, no GitHub lifecycle mutation, and no codex dispatch path. -Static checker: `python3 skills/codex-refactor-loop/scripts/consensus-rnd-cli check-degradation --static`; CI job `.github/workflows/consensus-rnd-ci.yml` `skill-degradation`; release gate `consensus-rnd-cli release-gate:required_checks_recent_green` requires `skill-degradation` beside `contract-tests` and `manifest-version-sync`, mirrored by `release.yml`. The checker is read-only and returns nonzero on missing source-repo validation text, CI/release wiring, forbidden runtime files, forbidden expansion surfaces, or downstream runtime watch markers. +Static checker: `python3 skills/codex-refactor-loop/scripts/consensus-rnd-cli check-degradation --static`; CI job `.github/workflows/consensus-rnd-ci.yml` `skill-degradation`; release gate `consensus-rnd-cli release-gate:required_checks_recent_green` does not name source-repo CI jobs directly. It consumes `required_release_checks()` from `$HOST_GITHUB_RELEASE_REQUIRED_CHECKS`, so each host configures the exact required GitHub check-run names in `host.env`; this source repo may list its own CI names in its host-owned `host.env`, but that example is not downstream runtime authority. The checker is read-only and returns nonzero on missing source-repo validation text, CI/release wiring, forbidden runtime files, forbidden expansion surfaces, or downstream runtime watch markers. Downstream plugin-installed hosts have no skill-degradation runtime watch, no degradation alert log, no degradation pending event, no degradation peek lens, and no degradation host.env knobs. `consensus-rnd-cli concurrency` must not invoke `check-degradation` as a runtime watch against a host repo root. Forbidden: no source mutation, git operations, GitHub issue/PR/body/label lifecycle mutation, codex dispatch, standalone daemon creation, WorkUnit/schema/envelope changes, protocol/plugin registry, auto-clean root garbage, and auto-fix API. ### Worktree 位置约定(强制) @@ -1451,7 +1476,10 @@ and controller helpers. Prose may name individual canonical labels when explaining a single guard, but active label bundles and transition tables must come from catalog-backed helpers instead of SKILL.md examples. The invariant is still exactly one canonical phase label and exactly one canonical human label -for managed open items after migration. +for managed open items after migration. For loop-managed issue/PR routing, that +means exactly one loop-owned `crnd:phase:*` label and exactly one loop-owned +`crnd:human:*` label. Host business labels may coexist, but they are not +routing authority and must not replace the loop-owned phase/human axes. ### Spawn pattern — Bash `run_in_background: true`(强制) @@ -1531,12 +1559,12 @@ You are the **Controller**. You never edit production code yourself. You orchest 1. **runtime dirs + integration 分支**:`mkdir -p .refactor-loop/{logs,runs,clusters,prompts,worktrees,state}` + idempotent 建/推 `$INTEGRATION_BRANCH`(下方细节)。Do not create or maintain root `.refactor-loop/state.json`. 2. **建全套 labels**:跑「Label 系统」节的 catalog validation / GitHub drift plan, then controller-owned apply if authorized. **漏建 = 后续 phase transition 无 canonical label 可挂、comment-monitor 查 catalog-managed items 漏掉 PR**。 3. **起并挂载全部 6 个 daemon**:按「Host 运行编排 → Daemon 启动」节的 `bash -c 'source host.env && exec'` pattern 起齐 `consensus-rnd-cli concurrency` / `consensus-rnd-cli progress-reporter` / `consensus-rnd-cli comment-monitor` / `consensus-rnd-cli dev-sync` / `consensus-rnd-cli phase9-router` / `consensus-rnd-cli closed-label-reconciler`。随后运行 `python3 /scripts/consensus-rnd-cli restart-daemons` 规范化 heartbeat-managed daemon,再读 `python3 /scripts/consensus-rnd-cli daemon-status --json` / `.refactor-loop/state/statusline-snapshot.json` / `python3 /scripts/consensus-rnd-cli peek | tail -80` 确认健康面可见;Consensus-rnd Phase design-consensus router 读其 lock/ledger/log/fallback event surface。**首轮就必须把 6 个全起起来——它不是「以后某次 wakeup 才做的 liveness 检查」**。 -4. **派默认 work-unit producer**(Consensus-rnd Phase work-intake,默认 audit,`consensus-rnd-cli spawn-codex` + Bash `run_in_background:true`)+ ScheduleWakeup 兜底 + end turn。 +4. **派主路径或 fallback producer**:先扫 open actionable managed issue/PR 并派 next-step actor;只有没有 open actionable work / queued dispatch / clean marker route / CI-no-gap route / maintainer-comment route / higher-priority wakeup route 时,才派 Consensus-rnd Phase work-intake audit fallback(`consensus-rnd-cli spawn-codex` + Bash `run_in_background:true`)+ ScheduleWakeup 兜底 + end turn。 每步做完才进下一步。3 漏起任一 daemon、2 漏建 labels = bootstrap 失败,下次 wakeup 第一件事补齐。 #### ❌ 严禁(首次唤醒反模式 — 均来自 baseline 失败) -- ❌ 只建本地目录 + 派默认 producer,不起 6 daemon(baseline 默认失败模式) +- ❌ 只建本地目录 + 直接派 audit fallback,不起 6 daemon(baseline 默认失败模式) - ❌ 不建 labels 就派 codex(phase transition 时无 label 可挂) - ❌ 把整个 skill 降级成「本地读代码 + 出 markdown 报告 + 本地 commit」而不碰 GitHub、不起 daemon、不派 audit - ❌ host.env 缺失时猜值硬跑 @@ -1577,11 +1605,13 @@ Create top-level TaskCreate items: audit / dispatch / merge. --- -## Consensus-rnd Phase work-intake — Work-unit production (audit default) +## Consensus-rnd Phase work-intake — Fallback issue production -The default work-unit producer is `audit`. Producer normalization is documented in +The default main path is open actionable managed issue/PR resolution; the controller must dispatch those +next-step actors before starting any new-work producer. Producer normalization is documented in [work-unit contract](#work-unit-contract): accepts only `producer: audit` and `producer: manual-issue`. -`audit` is the raw artifact producer for this phase; `manual-issue` enters through Consensus-rnd Phase design-intake +`audit` is the fallback raw artifact issue producer for this phase and runs only when no actionable managed +issue/PR or higher-priority route exists. `manual-issue` enters through Consensus-rnd Phase design-intake triage and must already be reshaped before Consensus-rnd Phase design-consensus. 1. Copy `prompts/audit.md` (this skill's template) to `.refactor-loop/prompts/audit-iter-N.md`. @@ -2271,8 +2301,16 @@ Policy: all three solver outputs are mandatory, and all implementation-bearing p ### Solver source contract + + Solver scope comes from the prompt header `WORK_UNIT_SOURCE_REF`, the work-unit `source_ref`, or a local source artifact explicitly pointed to by either field. For issue-driven / Path A work, `WORK_UNIT_PRODUCER=manual-issue (prompt-only provenance)` and `WORK_UNIT_SOURCE_REF=gh-issue-` mean `gh issue view ` issue body/comments are the scope source. If an issue body or source_ref points to an existing audit artifact, solvers may read and verify that artifact; otherwise missing audit artifacts are valid for issue-driven work and must not be fabricated. For Path A greenfield identity, absence of existing local code to delete is neutral evidence for the delete solver and is compatible with `SOLVER_DONE:delete:abstain:` when deletion/collapse is not justified. +For Path A issue body/comments that cite files absent from the current checkout, solvers must not directly emit a generic `no-plan`. The issue body/comments may carry a read-only source locator: `git show :` for a named ref/path, a raw URL, `gh api` for a GitHub object, or a host-provided path from `.refactor-loop/host.env`. Use the locator only to read the cited source; do not fetch, checkout, switch, merge, rebase, reset, create a source worktree, or add a source directory. If the locator is missing or invalid and the current checkout cannot verify the cited source, classify the no-plan reason precisely as `source-location-missing-or-invalid`. + Audit-backed sources require verification of the audit `evidence:` file:line. Issue-driven sources require verification of the cited files, symbols, problem statement, or repo rules present in the issue body/comments. A missing audit `evidence:` block is not by itself a defect for manual issues. The router renders the same producer/source-ref provenance into meta-judge prompts so the judge can recognize Path A greenfield framing instead of treating delete-solver abstain as a failed deletion proof. ### Default solver roles @@ -3053,8 +3091,7 @@ Bash( 1. **No new features** — only clean violations of CLAUDE.md philosophy. 2. **No external repo changes** — $EXTERNAL_REPOS are out of scope. - -3. **Code self-documents the refactor according to policy** — `$HOST_REFACTOR_COMMENT_POLICY` empty/`self-doc-comment` requires a 3-5 line host-style source comment with `Refactor (iterN/cluster-XXX)`, `Old pattern`, and `New principle`; `none` forbids refactor-history source comments and moves the rationale to external artifacts. +3. **Code refactor rationale follows policy** — `$HOST_REFACTOR_COMMENT_POLICY` missing/empty/default is `none`: source refactor-history comments are forbidden and rationale belongs in external artifacts. Explicit `self-doc-comment` is a downstream compatibility opt-in for a 3-5 line host-style source comment with `Refactor (iterN/cluster-XXX)`, `Old pattern`, and `New principle`; it must still obey source English-only. 4. **No `commit`/`push`/`checkout` inside codex prompts** — the controller owns git topology. 5. **No `sleep/delay`-based test pacing** — tests must use deterministic awaiters. 6. **No `[Skip]` / disabled tests** as a way to make CI green. @@ -3191,10 +3228,12 @@ producer enum. The only allowed sidecar provenance values are `audit` and `manua ### `audit` producer -`audit` remains the default producer. It reads the raw artifact contract from -`prompts/audit.md` and the resulting `.refactor-loop/runs/audit-iter-N.md` cluster sections. -The controller leaves `prompts/audit.md` unchanged and projects each accepted audit cluster into -the work-unit contract before dispatching or opening a design issue: +`audit` remains the stable compatibility producer value and fallback issue producer, not the default +main path. It runs only after no open actionable managed issue/PR, queued dispatch, clean marker route, +CI/no-gap route, maintainer-comment route, or higher-priority wakeup route exists. It reads the raw +artifact contract from `prompts/audit.md` and the resulting `.refactor-loop/runs/audit-iter-N.md` +cluster sections. The controller leaves `prompts/audit.md` unchanged and projects each accepted audit +cluster into the work-unit contract before dispatching or opening a design issue: - `work_unit_id: ` - `id: ` diff --git a/skills/codex-refactor-loop/host.env.example b/skills/codex-refactor-loop/host.env.example index efff48af..a9bf4ff5 100644 --- a/skills/codex-refactor-loop/host.env.example +++ b/skills/codex-refactor-loop/host.env.example @@ -62,6 +62,9 @@ export PROJECT_RULES="CLAUDE.md" # Default: release auto gate opt-in; false/empty exits 0 with noop reason. export RELEASE_AUTO_ENABLE="false" +# Default: host-owned exact GitHub check-run names required before automatic release. +export HOST_GITHUB_RELEASE_REQUIRED_CHECKS="ci,lint,typecheck" + # Default/noop: notify-only skill update check; false/empty exits 0 with noop reason. export UPDATE_CHECK_ENABLE="false" @@ -103,8 +106,8 @@ export COMMENT_MONITOR_INTERVAL="30" export COMMENT_MONITOR_LOOKBACK="" # Default: refactor-history source comment policy. Valid values: -# self-doc-comment, none. Empty is treated as self-doc-comment. -export HOST_REFACTOR_COMMENT_POLICY="self-doc-comment" +# none, self-doc-comment. Empty is treated as none. +export HOST_REFACTOR_COMMENT_POLICY="none" # ─── optional-empty-or-noop / compatibility / conditional-fail-closed ─ diff --git a/skills/codex-refactor-loop/prompts/implement.md b/skills/codex-refactor-loop/prompts/implement.md index e9a6e6e5..93487be0 100644 --- a/skills/codex-refactor-loop/prompts/implement.md +++ b/skills/codex-refactor-loop/prompts/implement.md @@ -24,16 +24,15 @@ Artifact profile: marker-only-work-unit 1. **作用域**:仅修改下列文件;扩展前必须打印 `SCOPE_EXTEND: `: ${SCOPE_PATHS} - -2. **Refactor comment policy**:读取 `${HOST_REFACTOR_COMMENT_POLICY}`。empty/`self-doc-comment` 归一化为 `self-doc-comment`;`none` 是 no source refactor-history comments 模式;其它值 invalid, fail-closed:停止实施并在摘要说明 invalid `HOST_REFACTOR_COMMENT_POLICY`; do not guess. - - empty/`self-doc-comment`:被重构的每个类/关键方法必须按 `${HOST_COMMENT_RULE}` 新增/更新一段 Refactor self-documentation;`${HOST_COMMENT_RULE}` 为空时匹配目标文件已有注释风格,文件类型不支持注释时在实施摘要说明 not applicable。内容必须包含: +2. **Refactor comment policy**:读取 `${HOST_REFACTOR_COMMENT_POLICY}`。missing/empty/default/`none` 归一化为 `none`;`self-doc-comment` 是 explicit downstream compatibility opt-in;其它值 invalid, fail-closed:停止实施并在摘要说明 invalid `HOST_REFACTOR_COMMENT_POLICY`; do not guess. + - `self-doc-comment`:被重构的每个类/关键方法必须按 `${HOST_COMMENT_RULE}` 新增/更新一段 Refactor self-documentation;`${HOST_COMMENT_RULE}` 为空时匹配目标文件已有注释风格,文件类型不支持注释时在实施摘要说明 not applicable。源码注释必须 English-only。内容必须包含: ``` Refactor (iter${ITERATION}/${CLUSTER_ID}): Old pattern: ${OLD_PATTERN} New principle: ${NEW_PRINCIPLE} ``` 3-5 行内;不是 changelog,是代码自我说明。 - - `none`:MUST NOT add `Refactor (...)`, `Old pattern`, `New principle`, or `iterN/cluster` refactor-history source comments. Put the rationale in the implementation summary and include exactly: `refactor self-doc: not applicable (HOST_REFACTOR_COMMENT_POLICY=none)`. + - missing/empty/default/`none`:MUST NOT add `Refactor (...)`, `Old pattern`, `New principle`, or `iterN/cluster` refactor-history source comments. Put the rationale in the implementation summary and include exactly: `refactor self-doc: not applicable (HOST_REFACTOR_COMMENT_POLICY=none)`. 3. **不新增功能**:不引入新接口、新 flag、新模块;只清理违反点。新增极小辅助类型须注释 "refactor helper, no behavior change"。 4. **测试**:按 `verification_hints` 跑测试,必须通过;测试不足必须补;任何 `sleep/delay` 轮询测试必须改为确定性断言。 5. **架构守卫**:跑 host 配置的 `$CI_GUARDS`,必须通过。其它 cluster 特定守卫见 verification hints。 diff --git a/skills/codex-refactor-loop/prompts/review-fix.md b/skills/codex-refactor-loop/prompts/review-fix.md index 323f0fc1..657316c6 100644 --- a/skills/codex-refactor-loop/prompts/review-fix.md +++ b/skills/codex-refactor-loop/prompts/review-fix.md @@ -44,8 +44,7 @@ Categorize each demand into one of: For each fix: - Open the file fully (not just the hunk) to make a context-aware change. - -- Preserve/add refactor self-doc comments only when `${HOST_REFACTOR_COMMENT_POLICY}` is empty/`self-doc-comment`. When `${HOST_REFACTOR_COMMENT_POLICY}=none`, do not add `Refactor (...)`, `Old pattern`, `New principle`, or `iterN/cluster` refactor-history source comments; if a reviewer demands those comments, classify it as a host-policy conflict/false-positive and record that evidence in the fix report. Any other policy value is invalid and fail-closed; do not guess. +- Preserve/add refactor self-doc comments only when `${HOST_REFACTOR_COMMENT_POLICY}=self-doc-comment`, and keep those source comments English-only. When `${HOST_REFACTOR_COMMENT_POLICY}` is missing/empty/default/`none`, do not add `Refactor (...)`, `Old pattern`, `New principle`, or `iterN/cluster` refactor-history source comments; keep rationale in the fix report/external artifact and include `refactor self-doc: not applicable (HOST_REFACTOR_COMMENT_POLICY=none)`. If a reviewer demands those comments under `none`, classify it as a host-policy conflict/false-positive and record that evidence in the fix report. Any other policy value is invalid and fail-closed; do not guess. - New test files: follow existing host test naming conventions, single behavior per test, no `sleep/delay`, no `[Skip]`, no mock-only assertions. - New non-test code stays minimal and reuses existing patterns. diff --git a/skills/codex-refactor-loop/prompts/reviewer-architect.md b/skills/codex-refactor-loop/prompts/reviewer-architect.md index 2f721e9e..11de9941 100644 --- a/skills/codex-refactor-loop/prompts/reviewer-architect.md +++ b/skills/codex-refactor-loop/prompts/reviewer-architect.md @@ -17,8 +17,7 @@ You are **one of N independent reviewers**; you do not see the other reviewers' ## Your checklist (architect angle only — other reviewers cover other angles) - -- [ ] **Old/New pattern comment policy**: read `${HOST_REFACTOR_COMMENT_POLICY}`. empty/`self-doc-comment` normalizes to `self-doc-comment`: each refactored type/method follows `${HOST_COMMENT_RULE}` for refactor self-documentation, or surrounding file comment style when `${HOST_COMMENT_RULE}` is empty; if the file type cannot carry comments, accept a documented not-applicable reason. `none`: absence is compliant, and new Old/New/iteration refactor-history source comments must be rejected under the `$PROJECT_RULES` no-comment clause. Any other value is invalid and fail-closed; do not guess. +- [ ] **Old/New pattern comment policy**: read `${HOST_REFACTOR_COMMENT_POLICY}`. missing/empty/default/`none` normalizes to `none`: absence is compliant, rationale belongs in external artifacts, and new Old/New/iteration refactor-history source comments must be rejected. Explicit `self-doc-comment` is a downstream compatibility opt-in: each refactored type/method follows `${HOST_COMMENT_RULE}` for English-only refactor self-documentation, or surrounding file comment style when `${HOST_COMMENT_RULE}` is empty; if the file type cannot carry comments, accept a documented not-applicable reason. Any other value is invalid and fail-closed; do not guess. - [ ] **CLAUDE clause compliance**: each net-changed concept maps to a clause; no new violation introduced. Use `$PROJECT_RULES`, `$SOURCE_GLOBS`, actual diff evidence, `$CI_GUARDS`, and `${HOST_ARCHITECTURE_GREP_CHECKS}` for host-specific grep checks. If `${HOST_ARCHITECTURE_GREP_CHECKS}` is empty, do not invent language/framework-specific anti-patterns. - [ ] **Scope honesty**: diff stays within the cluster's declared `scope_paths` (or has a documented SCOPE_EXTEND in implement summary). Diff drift → comment. - [ ] **Single business entity per actor**: no new `*WriteActor` / `*ReadActor` / `*Store` splits of one entity. diff --git a/skills/codex-refactor-loop/prompts/reviewer-quality.md b/skills/codex-refactor-loop/prompts/reviewer-quality.md index df120c42..39f27cf0 100644 --- a/skills/codex-refactor-loop/prompts/reviewer-quality.md +++ b/skills/codex-refactor-loop/prompts/reviewer-quality.md @@ -26,8 +26,7 @@ You are **one of N independent reviewers**. - [ ] **No under-engineering**: ≥3 near-identical inline copies of a snippet should be extracted. Inline duplication that violates DRY → comment. - [ ] **Method size & cyclomatic complexity**: a single new/modified method ≤ 80 lines and ≤ ~15 branches is preferred. Existing host 项目的复杂度分析器 warnings carried unchanged ≠ regression; but adding new ones → comment. - [ ] **Comments add value**: new comments explain *why* not *what* (the code already says what). Filler comments / commented-out code → comment. - -- [ ] **Refactor self-doc comment policy**: read `${HOST_REFACTOR_COMMENT_POLICY}`. empty/`self-doc-comment` normalizes to `self-doc-comment`: refactor self-doc comments must be present and clear, with Old/New blocks readable to a non-audit reader (no `see issue #X` placeholders, no truncated sentences). `none`: missing/illegible self-doc must not be a reject reason; still comment/reject for naming, dead code, complexity, scope creep, or code whose intent cannot be reviewed from names/structure/external artifacts. Any other value is invalid and fail-closed; do not guess. +- [ ] **Refactor self-doc comment policy**: read `${HOST_REFACTOR_COMMENT_POLICY}`. missing/empty/default/`none` normalizes to `none`: missing/illegible self-doc must not be a reject reason, rationale belongs in external artifacts, and new Refactor/Old/New/iteration source comments are defects. Explicit `self-doc-comment` is downstream compatibility opt-in: English-only refactor self-doc comments must be present and clear, with Old/New blocks readable to a non-audit reader (no `see issue #X` placeholders, no truncated sentences). Still comment/reject for naming, dead code, complexity, scope creep, or code whose intent cannot be reviewed from names/structure/external artifacts. Any other value is invalid and fail-closed; do not guess. - [ ] **No unrelated drive-by changes**: diff stays focused on the cluster intent; one-line "fix typo over there" or "tidy this whitespace" sneaking into a behavior PR → comment. ## Out of scope @@ -62,7 +61,7 @@ Verdict semantics: - **approve**: code is readable, focused, no over/under-engineering smell, and refactor self-doc handling complies with `${HOST_REFACTOR_COMMENT_POLICY}`. - **comment**: small naming/clarity nits; unrelated drive-by changes worth surfacing; host 项目的复杂度分析器 borderline. -- **reject**: significant dead code, harmful single-implementer abstraction, scope creep into unrelated cleanup, or a major refactor that lacks/garbles self-doc only when `HOST_REFACTOR_COMMENT_POLICY` is empty/`self-doc-comment`. Under `HOST_REFACTOR_COMMENT_POLICY=none`, missing/illegible self-doc alone is not a reject reason. +- **reject**: significant dead code, harmful single-implementer abstraction, scope creep into unrelated cleanup, or a major refactor that lacks/garbles self-doc only when `HOST_REFACTOR_COMMENT_POLICY=self-doc-comment`. Under missing/empty/default/`HOST_REFACTOR_COMMENT_POLICY=none`, missing/illegible self-doc alone is not a reject reason. - In-scope must-fix-before-merge findings must be `reject`. - Out-of-scope, non-flippable, or advisory findings must be `comment`. diff --git a/skills/codex-refactor-loop/prompts/solver-delete.md b/skills/codex-refactor-loop/prompts/solver-delete.md index 09a0a736..c80acb9a 100644 --- a/skills/codex-refactor-loop/prompts/solver-delete.md +++ b/skills/codex-refactor-loop/prompts/solver-delete.md @@ -19,12 +19,19 @@ You explicitly resist adding code. If after honest evaluation the feature must s ## Inputs + + 1. `gh issue view ${ISSUE_NUMBER}` — full body + comments. 2. Work-unit scope source, by precedence: - Read the prompt header `WORK_UNIT_SOURCE_REF` / `source_ref` first. - Use only the router-injected validated transition projection lines (`TRANSITION_TYPE`, `TRANSITION_CONFIDENCE`, `TRANSITION_EVIDENCE_REFS`) for transition assessment context. Missing, malformed, or untrusted sidecars are projected as `unknown` with confidence `0`; the sidecar is not approval, not a consensus substitute, and cannot override the meta-judge truth table. `positive-discovery` is valid only with classifier-surface delta and `net_positive_signal=true`. - If it points to an existing local artifact or audit section, read that source and verify it. - If it is `gh-issue-` or a referenced local artifact is missing, treat the GitHub issue body/comments from `gh issue view ${ISSUE_NUMBER}` as the scope spec. + - If issue body/comments cite files absent from the current checkout, look for a read-only source locator in that source: `git show :`, raw URL, `gh api`, or a host-provided path from `.refactor-loop/host.env`. Use it only to read source; must not fetch/checkout/switch/merge/rebase/reset; must not create source worktree/add-dir. If the locator is missing or invalid and the current checkout cannot verify the cited source, emit `SOLVER_DONE:delete:escalate:no-plan:source-location-missing-or-invalid` instead of a generic no-plan. - `audit-iter-${ITERATION}.md if present` is an audit-backed source only when the current `WORK_UNIT_SOURCE_REF` / `source_ref` points to it; do not fabricate audit artifacts. - For issue-driven / Path A greenfield work, `WORK_UNIT_PRODUCER=manual-issue (prompt-only provenance)` with `WORK_UNIT_SOURCE_REF=gh-issue-` means absence of existing local code to delete is neutral evidence: classify as genuinely needed/no current deletion dependency and abstain when deletion/collapse is not justified. 3. `$REPO_ROOT/${PROJECT_RULES:-CLAUDE.md}` "删除优先" clause; "Deletion-first" principle. `$REPO_ROOT/AGENTS.md` is supporting input when present. diff --git a/skills/codex-refactor-loop/prompts/solver-minimal.md b/skills/codex-refactor-loop/prompts/solver-minimal.md index c2d8ecc7..64eb4903 100644 --- a/skills/codex-refactor-loop/prompts/solver-minimal.md +++ b/skills/codex-refactor-loop/prompts/solver-minimal.md @@ -8,12 +8,19 @@ Your bias: **smallest viable change** that resolves the audit's flagged violatio ## Inputs + + 1. `gh issue view ${ISSUE_NUMBER}` — full body + comments (skip controller `## 🤖` markers). 2. Work-unit scope source, by precedence: - Read the prompt header `WORK_UNIT_SOURCE_REF` / `source_ref` first. - Use only the router-injected validated transition projection lines (`TRANSITION_TYPE`, `TRANSITION_CONFIDENCE`, `TRANSITION_EVIDENCE_REFS`) for transition assessment context. Missing, malformed, or untrusted sidecars are projected as `unknown` with confidence `0`; the sidecar is not approval, not a consensus substitute, and cannot override the meta-judge truth table. `positive-discovery` is valid only with classifier-surface delta and `net_positive_signal=true`. - If it points to an existing local artifact or audit section, read that source and verify it. - If it is `gh-issue-` or a referenced local artifact is missing, treat the GitHub issue body/comments from `gh issue view ${ISSUE_NUMBER}` as the scope spec. + - If issue body/comments cite files absent from the current checkout, look for a read-only source locator in that source: `git show :`, raw URL, `gh api`, or a host-provided path from `.refactor-loop/host.env`. Use it only to read source; must not fetch/checkout/switch/merge/rebase/reset; must not create source worktree/add-dir. If the locator is missing or invalid and the current checkout cannot verify the cited source, emit `SOLVER_DONE:minimal:escalate:no-plan:source-location-missing-or-invalid` instead of a generic no-plan. - `audit-iter-${ITERATION}.md if present` is an audit-backed source only when the current `WORK_UNIT_SOURCE_REF` / `source_ref` points to it; do not fabricate audit artifacts. 3. `$REPO_ROOT/${PROJECT_RULES:-CLAUDE.md}` — primary rules that frame the violation; `$REPO_ROOT/AGENTS.md` — supporting rules when present. 4. The actual source files cited by the current work-unit source (issue body/comments, local artifact, audit evidence, or repo rules). Open them; do NOT trust line numbers without verifying. diff --git a/skills/codex-refactor-loop/prompts/solver-structural.md b/skills/codex-refactor-loop/prompts/solver-structural.md index 19e2de93..9350e557 100644 --- a/skills/codex-refactor-loop/prompts/solver-structural.md +++ b/skills/codex-refactor-loop/prompts/solver-structural.md @@ -8,12 +8,19 @@ Your bias: **CLAUDE-philosophy-aligned, structurally clean**. You accept higher ## Inputs + + 1. `gh issue view ${ISSUE_NUMBER}` — full body + comments (skip controller `## 🤖` markers). 2. Work-unit scope source, by precedence: - Read the prompt header `WORK_UNIT_SOURCE_REF` / `source_ref` first. - Use only the router-injected validated transition projection lines (`TRANSITION_TYPE`, `TRANSITION_CONFIDENCE`, `TRANSITION_EVIDENCE_REFS`) for transition assessment context. Missing, malformed, or untrusted sidecars are projected as `unknown` with confidence `0`; the sidecar is not approval, not a consensus substitute, and cannot override the meta-judge truth table. `positive-discovery` is valid only with classifier-surface delta and `net_positive_signal=true`. - If it points to an existing local artifact or audit section, read that source and verify it. - If it is `gh-issue-` or a referenced local artifact is missing, treat the GitHub issue body/comments from `gh issue view ${ISSUE_NUMBER}` as the scope spec. + - If issue body/comments cite files absent from the current checkout, look for a read-only source locator in that source: `git show :`, raw URL, `gh api`, or a host-provided path from `.refactor-loop/host.env`. Use it only to read source; must not fetch/checkout/switch/merge/rebase/reset; must not create source worktree/add-dir. If the locator is missing or invalid and the current checkout cannot verify the cited source, emit `SOLVER_DONE:structural:escalate:no-plan:source-location-missing-or-invalid` instead of a generic no-plan. - `audit-iter-${ITERATION}.md if present` is an audit-backed source only when the current `WORK_UNIT_SOURCE_REF` / `source_ref` points to it; do not fabricate audit artifacts. 3. `$REPO_ROOT/${PROJECT_RULES:-CLAUDE.md}` — primary rules that frame the violation; `$REPO_ROOT/AGENTS.md` — supporting rules when present. 4. `$REPO_ROOT/$REPO_ROOT 的架构/词汇文档(若有)` — repo vocabulary (Module / Interface / Depth / Seam / Adapter / Leverage / Locality). diff --git a/skills/codex-refactor-loop/prompts/verify.md b/skills/codex-refactor-loop/prompts/verify.md index ef2fa39e..6d9d0762 100644 --- a/skills/codex-refactor-loop/prompts/verify.md +++ b/skills/codex-refactor-loop/prompts/verify.md @@ -20,10 +20,9 @@ Artifact profile: marker-only-work-unit ### 1. 改动与设计原则一致 - -- 检查 `${HOST_REFACTOR_COMMENT_POLICY}`。empty/`self-doc-comment` 归一化为 `self-doc-comment`;`none` 禁用 refactor-history source comments;其它值 invalid, fail-closed → 标 rework; do not guess. -- empty/`self-doc-comment`:检查每个被重构的关键类/方法是否按 `${HOST_COMMENT_RULE}` 或目标文件现有注释风格带有 Refactor self-documentation,包含 Old pattern + New principle。缺失任何一处且无合理 not-applicable 说明 → 标记缺陷。 -- `none`:missing Refactor self-documentation is not a defect and must not trigger rework. 新增 `Refactor (...)`, `Old pattern`, `New principle`, or `iterN/cluster` refactor-history source comments → 标记缺陷;外部 artifact/实施摘要必须说明 rationale,包括 `refactor self-doc: not applicable (HOST_REFACTOR_COMMENT_POLICY=none)` 或等价理由。 +- 检查 `${HOST_REFACTOR_COMMENT_POLICY}`。missing/empty/default/`none` 归一化为 `none`;`self-doc-comment` 是 explicit downstream compatibility opt-in;其它值 invalid, fail-closed → 标 rework; do not guess. +- `self-doc-comment`:检查每个被重构的关键类/方法是否按 `${HOST_COMMENT_RULE}` 或目标文件现有注释风格带有 Refactor self-documentation,包含 Old pattern + New principle 且源码注释 English-only。缺失任何一处且无合理 not-applicable 说明 → 标记缺陷。 +- missing/empty/default/`none`:missing Refactor self-documentation is not a defect and must not trigger rework. 新增 `Refactor (...)`, `Old pattern`, `New principle`, or `iterN/cluster` refactor-history source comments → 标记缺陷;外部 artifact/实施摘要必须说明 rationale,包括 `refactor self-doc: not applicable (HOST_REFACTOR_COMMENT_POLICY=none)` 或等价理由。 - 检查改动是否真正消除了 `old_pattern` 描述的违反(用 `rg` 抽样确认 anti-pattern 不再出现在 scope_paths 内)。 ### 2. 作用域诚实 diff --git a/skills/codex-refactor-loop/scripts/codex_refactor_loop/active_controller.py b/skills/codex-refactor-loop/scripts/codex_refactor_loop/active_controller.py index cd45faf0..dab68822 100644 --- a/skills/codex-refactor-loop/scripts/codex_refactor_loop/active_controller.py +++ b/skills/codex-refactor-loop/scripts/codex_refactor_loop/active_controller.py @@ -81,10 +81,6 @@ class _LeaseRead: class ActiveControllerLeaseStore: - # Refactor (impl/issue191-single-active-controller): Old pattern: each device - # could run controller write daemons independently. New principle: one - # pushed-ref CAS lease elects a single active controller; non-owners fail - # closed before controller writes. def __init__( self, repo_root: Path, diff --git a/skills/codex-refactor-loop/scripts/codex_refactor_loop/banners.py b/skills/codex-refactor-loop/scripts/codex_refactor_loop/banners.py index 5ecde56b..7f794aba 100644 --- a/skills/codex-refactor-loop/scripts/codex_refactor_loop/banners.py +++ b/skills/codex-refactor-loop/scripts/codex_refactor_loop/banners.py @@ -18,16 +18,9 @@ AUTHORIZATION_ARTIFACT = "skills/codex-refactor-loop/authorizations/runtime-exceptions.md#observability-comment-writers-53" OBSERVABILITY_COMMENT_WRITER = "observability-comment-writers" -# Refactor (issue160/p3-banners): Old pattern: status banner rendering and -# GitHub posting lived only in the executable script. New principle: package -# code owns the reusable contract while legacy callers keep using post_banner.py -# until the caller migration phase. ROLE_NEXT_STEPS = { "test-add": "1. test-add 完成 marker `TEST_ADD_DONE:...` 2. controller 自动 commit + push 3. codecov 重测", "fix": "1. fix r 完成 marker `FIX_DONE:...` 2. controller commit + push 3. 派 reviewer r", - # Refactor (iter3/skill-merge-policy): Old pattern: unanimous-approve merge - # gate + contradictory review-gate wording. New principle: fixed truth table - # reject=0 && approve>=1 -> MERGE; comments are advisory (#26 minimal option B consensus). "reviewer": "1. 三 reviewer 完成 verdict marker 2. controller 计算 consensus 3. reject=0 + approve>=1 -> merge; all-comment -> wait explicit approval; reject -> fix", "implement": "1. implement 完成 marker `IMPLEMENT_DONE::` 2. controller commit + push 3. open PR + 派 reviewer r1", "solver": "1. 三 solver `SOLVER_DONE:...` 2. controller 派 meta-judge r 3. consensus → implement / converge → fresh round", diff --git a/skills/codex-refactor-loop/scripts/codex_refactor_loop/checks/degradation.py b/skills/codex-refactor-loop/scripts/codex_refactor_loop/checks/degradation.py index 01043b54..35cd3867 100644 --- a/skills/codex-refactor-loop/scripts/codex_refactor_loop/checks/degradation.py +++ b/skills/codex-refactor-loop/scripts/codex_refactor_loop/checks/degradation.py @@ -121,7 +121,8 @@ "Skill degradation source-repo validation details", "consensus-rnd-cli check-degradation --static", "CI job `.github/workflows/consensus-rnd-ci.yml` `skill-degradation`", - "release gate `consensus-rnd-cli release-gate:required_checks_recent_green` requires `skill-degradation`", + "release gate `consensus-rnd-cli release-gate:required_checks_recent_green` does not name source-repo CI jobs directly", + "consumes `required_release_checks()` from `$HOST_GITHUB_RELEASE_REQUIRED_CHECKS`", "Downstream plugin-installed hosts have no skill-degradation runtime watch", "no degradation alert log", "no degradation pending event", @@ -144,10 +145,9 @@ ) REQUIRED_RELEASE_PROJECTION_MARKERS = ( - '"skill-degradation"', - '"contract-tests"', - '"manifest-version-sync"', - "REQUIRED_RELEASE_CHECKS", + "HOST_GITHUB_RELEASE_REQUIRED_CHECKS", + "required_release_checks", + "ReleaseRequiredChecksProjection", ) REQUIRED_RELEASE_GATE_MARKERS = ( @@ -316,12 +316,13 @@ def release_workflow_required_check_present(self) -> list[Finding]: f"shared release required-check projection missing {marker}", ) ) - if projection_text and '"skill-degradation"' not in projection_text: + host_env_example = self.read(SKILL_RELATIVE / "host.env.example") + if host_env_example and "HOST_GITHUB_RELEASE_REQUIRED_CHECKS" not in host_env_example: findings.append( Finding( "release-workflow", - str(SCRIPT_RELATIVE / "codex_refactor_loop" / "release" / "required_checks.py"), - "shared release required checks must include skill-degradation", + str(SKILL_RELATIVE / "host.env.example"), + "host.env.example must expose host-owned release required checks", ) ) return findings @@ -334,12 +335,12 @@ def release_gate_required_check_present(self) -> list[Finding]: ) text = self.read(SCRIPT_RELATIVE / "codex_refactor_loop" / "release" / "gate.py") projection_text = self.read(SCRIPT_RELATIVE / "codex_refactor_loop" / "release" / "required_checks.py") - if projection_text and '"skill-degradation"' not in projection_text: + if projection_text and "required_release_checks" not in projection_text: findings.append( Finding( "release-gate", str(SCRIPT_RELATIVE / "codex_refactor_loop" / "release" / "required_checks.py"), - "required_checks_recent_green must require skill-degradation", + "required_checks_recent_green must use host.env configured required checks", ) ) if text and "gh\", \"run\", \"list" in text: diff --git a/skills/codex-refactor-loop/scripts/codex_refactor_loop/checks/manifest.py b/skills/codex-refactor-loop/scripts/codex_refactor_loop/checks/manifest.py index b3f74269..b6684c22 100644 --- a/skills/codex-refactor-loop/scripts/codex_refactor_loop/checks/manifest.py +++ b/skills/codex-refactor-loop/scripts/codex_refactor_loop/checks/manifest.py @@ -12,13 +12,6 @@ from ..context import LoopContext -# Refactor (issue160-p3-checks): -# Old: scripts/check_manifest_version_sync.py owned the manifest-version-sync -# CI check as a standalone script. -# New: expose the same fail-closed JSON field resolution and version parity -# check from codex_refactor_loop.checks for future CLI import. - - class ManifestVersionSyncError(Exception): """Raised when manifest version mappings cannot be resolved or synced.""" diff --git a/skills/codex-refactor-loop/scripts/codex_refactor_loop/cli.py b/skills/codex-refactor-loop/scripts/codex_refactor_loop/cli.py index a50f2515..225e158a 100644 --- a/skills/codex-refactor-loop/scripts/codex_refactor_loop/cli.py +++ b/skills/codex-refactor-loop/scripts/codex_refactor_loop/cli.py @@ -33,7 +33,6 @@ def release_commits_command(argv: Sequence[str] | None) -> int: - # Refactor (fix/pr236-split-release-commits-command): Old pattern: release-gate inlined the git-reading release commit producer and gained read-git authority. New principle: release commits are produced by a separate narrow CLI surface whose only powers are read-git and write-artifact, keeping release-gate decider-only. from .release.commits import main as release_commits_main return release_commits_main(argv) @@ -46,13 +45,6 @@ class CommandSpec: authority: tuple[str, ...] -# Refactor (iter201/issue-201): Old pattern: public consensus-rnd-cli exposed -# lifecycle commands and wakeup_plan/peek rendered copyable suggested_command, -# forming a generic lifecycle authority surface. New principle: COMMANDS keeps -# only public non-lifecycle CLI primitives; controller lifecycle actions stay -# controller-internal, with dev-sync's narrow integration-worktree carveout. -# Refactor (iter218/issue-218): Old pattern: ensure-project-rules 是 public CLI 默认写 host policy 文件($PROJECT_RULES),违反 skill 无 host 改动权边界 -# New principle: 改为 read-only check-project-rules probe + patch artifact:probe 只读判 sentinel block,非 current 写 .refactor-loop/runs/ patch 并 fail-closed 不派 actor;删 ensure-project-rules/_atomic_write,不引入 PROJECT_RULES_WRITE_ENABLE。严格按 plan 逐条改。 COMMANDS: dict[str, CommandSpec] = { "spawn-codex": CommandSpec(spawn.main, "run the Python codex spawn supervisor", ("spawn", "write-log")), "peek": CommandSpec(peek_main, "run the Python read-only state sweep", ("read-state", "read-gh")), @@ -67,10 +59,6 @@ class CommandSpec: "run the Python daemon restart helper", ("spawn-daemon", "write-state", "delete-log"), ), - # Refactor (issue-298): Old: public CLI had restart-daemons as the only - # daemon health surface, nudging controllers toward write-side repair for - # status reads. New: daemon-status is read-only and restart-daemons remains - # the sole repair/reload command. "daemon-status": CommandSpec( daemon_status_main, "read restart-helper-managed daemon status", @@ -117,7 +105,6 @@ class CommandSpec: "phase9-router": CommandSpec( phase9_router_main, "compatibility alias for the Python design-consensus router", - # Refactor (fix/pr245-router-authority-anchor): Old: phase9-router's public CommandSpec omitted the state-only GitHub read used by the source-OPEN gate. New: include read-gh in the closed-token authority tuple while keeping lifecycle mutation tokens absent. ("read-log", "read-gh", "write-event", "write-artifact"), ), "release-gate": CommandSpec( diff --git a/skills/codex-refactor-loop/scripts/codex_refactor_loop/closed_label_reconciler.py b/skills/codex-refactor-loop/scripts/codex_refactor_loop/closed_label_reconciler.py index 148e3836..e2387a3d 100644 --- a/skills/codex-refactor-loop/scripts/codex_refactor_loop/closed_label_reconciler.py +++ b/skills/codex-refactor-loop/scripts/codex_refactor_loop/closed_label_reconciler.py @@ -20,11 +20,7 @@ class ClosedLabelReconciler: - """Refactor (issue238/closed-label-reconciler): - Old pattern: closed managed items relied on controller close-path cleanup - or peek hints. New principle: a restart-managed closed-only daemon owns - the narrow gh-label-closed-reconcile authority. - """ + """Restart-managed closed-only phase label reconciler.""" def __init__(self, ctx: LoopContext, *, dry_run: bool = False) -> None: self.ctx = ctx diff --git a/skills/codex-refactor-loop/scripts/codex_refactor_loop/closed_phase_labels.py b/skills/codex-refactor-loop/scripts/codex_refactor_loop/closed_phase_labels.py index 0e1d48af..2e388308 100644 --- a/skills/codex-refactor-loop/scripts/codex_refactor_loop/closed_phase_labels.py +++ b/skills/codex-refactor-loop/scripts/codex_refactor_loop/closed_phase_labels.py @@ -13,11 +13,7 @@ @dataclass(frozen=True) class ClosedPhaseLabelPlan: - """Refactor (issue238/closed-label-reconciler): - Old pattern: peek exposed closed-label remediation text as a side channel. - New principle: a read-only projection computes terminal phase-label plans; - only the named reconciler may apply them. - """ + """Read-only terminal phase-label plan for closed managed items.""" kind: str number: int diff --git a/skills/codex-refactor-loop/scripts/codex_refactor_loop/context.py b/skills/codex-refactor-loop/scripts/codex_refactor_loop/context.py index eac40190..7af71cfa 100644 --- a/skills/codex-refactor-loop/scripts/codex_refactor_loop/context.py +++ b/skills/codex-refactor-loop/scripts/codex_refactor_loop/context.py @@ -45,19 +45,7 @@ class LoopPaths: @dataclass(frozen=True) class LoopContext: - """Resolved host repository and skill context. - - Refactor (iter219/issue-219): - Old pattern: host 无法按 GitHub 模板自定义事件流/工作流/issue/prompt;workflow vocabulary 是闭集硬编码 - New principle: 引入 data-only HostWorkflowSpec(HOST_WORKFLOW_SPEC,repo-relative JSON)+ WorkflowInvariantValidator;空/未设=built-in 行为;host 只能在 host: 命名空间加 data,不能覆盖 built-in/降共识闸/夺 lifecycle authority。严格按 plan 'Concrete plan' 逐条改,首版 scope 受限。 - - Refactor (iter202/issue-202): Old pattern: durable artifact(ledger log_path、pending-event JSON log_path、meta-judge/reflector evidence、dev-sync resolver prompt、DEV_SYNC_REQUEST marker)写入 host absolute repo/worktree/log path,违反 CLAUDE.md R24『artifact 路径相对 $REPO_ROOT,不引入具体 host 事实』。 - New principle: 分层 durable-text-path vs execution-path:写入时所有 durable artifact/prompt/marker 只存 repo-relative POSIX text;读取或传 subprocess 时由 LoopContext.repo_root/rel_path 解析回 absolute;spawn-codex --cd/--add-dir/--prompt/--log 与 Popen argv 仍用 absolute(execution boundary 非 durable truth)。配套 behavior(写入存相对、读取解析绝对)+ source-regression(无 host absolute prefix)测试。不改 daemon lifecycle authority,不加规则例外。 - - Refactor (iter316/issue-316): - Old pattern: host runtime facts were parsed in wakeup_plan/release-gate/sync/controller_actions/heartbeat and read legacy aliases plus root host.env. - New principle: LoopContext/context.py is the single shared host.env parser; read only CONSENSUS_RND_HOST_ENV or legacy .refactor-loop/host.env; no root host.env or unlisted aliases. - """ + """Resolved host repository, host.env, and durable artifact context.""" repo_root: Path skill_root: Path @@ -176,9 +164,6 @@ def resolve(cls, repo_root: Path, env: Mapping[str, str], cwd: Path | None = Non root = repo_root.resolve() raw_explicit = env.get(cls.EXPLICIT_ENV) if raw_explicit is not None: - # Refactor (iter1/issue-310): - # Old pattern: host.env lookup made .refactor-loop/host.env look like the host production fact home. - # New principle: CONSENSUS_RND_HOST_ENV is only a locator for a host-owned loop runtime injection file; .refactor-loop/host.env stays a compatibility read. return HostEnvLocation( path=cls._resolve_explicit(root, raw_explicit), ) @@ -290,9 +275,6 @@ def _github_repo_slug(env: Mapping[str, str]) -> str | None: def _paths(repo_root: Path) -> LoopPaths: - # Refactor (iter1/issue-310): - # Old pattern: .refactor-loop paths could be read as host production configuration or ledger authority. - # New principle: .refactor-loop is only the skill-private runtime home while callers keep the historical Path contract. refactor_loop = repo_root / ".refactor-loop" state = refactor_loop / "state" return LoopPaths( diff --git a/skills/codex-refactor-loop/scripts/codex_refactor_loop/controller_actions.py b/skills/codex-refactor-loop/scripts/codex_refactor_loop/controller_actions.py index f8fefd97..c02300f3 100644 --- a/skills/codex-refactor-loop/scripts/codex_refactor_loop/controller_actions.py +++ b/skills/codex-refactor-loop/scripts/codex_refactor_loop/controller_actions.py @@ -40,33 +40,17 @@ SAFE_WORKTREE_ITERATION_RE = re.compile(r"^[0-9]+$") SAFE_WORKTREE_CLUSTER_RE = re.compile(r"^[A-Za-z0-9._-]+$") GITHUB_LIFECYCLE_TARGET_RE = re.compile(r"^[1-9][0-9]*$") -# Refactor (issue-276): validate body-linked lifecycle targets without swallowing escaped line boundaries. BODY_CLOSING_ISSUE_TARGET_RE = re.compile(r"(?im)\bCloses\s+#([^\s,;:.)\]}\\]*)") class ControllerActions: - # Refactor (iter201/issue-201): Old pattern: public consensus-rnd-cli exposed - # merge/open/safe-push/apply lifecycle commands as generic callable verbs. - # New principle: keep these as controller-internal primitives only; callers - # construct ControllerActions directly and public CLI routing cannot reach them. - # - # Refactor (iter217/issue-217): - # Old pattern: release.yml 保留 tag/release mutation,无法可靠读本地 runtime fact,绕过 release-gate decider-only 边界 - # New principle: controller-only publication:新增 ReleasePublishPreflight+ReleasePublisher 替代 workflow 发布权;release.yml 降为 read-only preview(contents:read,禁 gh release create)。严格按 plan 'Concrete plan' 逐条改。 def __init__(self, ctx: LoopContext) -> None: self.ctx = ctx merged_env = {**os.environ, **ctx.host_env} - # Refactor (iter316/issue-316): - # Old pattern: controller actions accepted legacy branch aliases from process env. - # New principle: use ctx.host_env or canonical process env only, then defaults. self.integration_branch = merged_env.get("INTEGRATION_BRANCH") or "auto-refact-dev" self.review_base_branch = merged_env.get("REVIEW_BASE_BRANCH") or "dev" def gh(self, args: Sequence[str], *, check: bool = True) -> subprocess.CompletedProcess[str]: - # Refactor (loop/gh-arg-coercion): Old pattern: gh() assumed every arg was - # already a str, so an int caller (e.g. a raw PR number via merge_pr) crashed - # with AttributeError on full[3].startswith before any gh process ran. - # New principle: coerce all args to str at the gh() boundary. full = ["gh", *(str(a) for a in args)] if self.ctx.gh_repo_slug: insert_at = 4 if len(full) > 3 and not full[3].startswith("-") else min(3, len(full)) @@ -153,9 +137,6 @@ def publish_release_candidate( target_ref: str = "", ) -> ReleasePublishResult: self._require_owner_or_raise("publish-release") - # Refactor (iter217/issue-217): - # Old pattern: release.yml 保留 tag/release mutation,无法可靠读本地 runtime fact,绕过 release-gate decider-only 边界 - # New principle: controller-only publication:新增 ReleasePublishPreflight+ReleasePublisher 替代 workflow 发布权;release.yml 降为 read-only preview(contents:read,禁 gh release create)。严格按 plan 'Concrete plan' 逐条改。 target = target_ref or os.environ.get("RELEASE_TARGET_REF", "") if not target: raise RuntimeError("publish_release_candidate: RELEASE_TARGET_REF is required") @@ -191,9 +172,6 @@ def safe_sync_main(self, remote: str = "origin", branch: str = "") -> int: return 0 def safe_worktree(self, iteration: str, cluster: str, base: str) -> tuple[Path, str]: - # Refactor (iter81/issue-81): - # Old pattern: 文件/分支/marker/label/role 命名混乱;松散 regex(parse_target ^phase9-issue([0-9]+).*)解析,缺 owner-local operational-name 契约 - # New principle: owner-local operational-name contract:CLAUDE.md 扩写命名不动点为 operational-name invariant + SKILL.md 增 owner map;收窄现有 owner parser/validation(progress.py parse_target 精确文法、safe_worktree 字段校验);behavior test + source-regression production-literal allowlist 防偷抄;**无**生产 OperationalNameRegistry/names.py/check_naming.py/全仓审美 lint _validate_safe_worktree_fields(str(iteration), cluster) wt_path = self.ctx.repo_root / ".worktrees" / f"iter{iteration}-{cluster}" branch = f"refactor/iter{iteration}-{cluster}" @@ -209,9 +187,6 @@ def safe_worktree(self, iteration: str, cluster: str, base: str) -> tuple[Path, return wt_path, branch def _ensure_pr_ready_for_merge(self, pr_target: str) -> int: - # Refactor (issue-300): PRs are opened as draft by default; merge_pr is - # the controller-owned post-decision boundary that marks them ready only - # after MERGE or MERGE_WITH_COMMENTS has already been decided. draft = self.gh(["pr", "view", pr_target, "--json", "isDraft", "--jq", ".isDraft"], check=False) if draft.returncode != 0: return draft.returncode @@ -256,12 +231,13 @@ def merge_pr(self, pr: str, linked_issue: str = "") -> int: ready = self._ensure_pr_ready_for_merge(pr_target) if ready != 0: return ready - merge = self.gh(["pr", "merge", pr_target, "--admin", "--squash", "--delete-branch"], check=False) + merge = self.gh(["pr", "merge", pr_target, "--squash", "--delete-branch"], check=False) if merge.stdout: print(merge.stdout.splitlines()[-1]) elif merge.stderr: print(merge.stderr.splitlines()[-1]) if merge.returncode != 0: + self._append_pending_event(f"CONTROLLER_ACTION_BLOCKED:blocked-by-host-policy:merge-pr:pr:{pr_target}") return merge.returncode self.record_recent_pr_merge(pr_target) args = ["pr", "edit", pr_target] @@ -305,7 +281,6 @@ def open_pr_with_label(self, title: str, body_file: str, base: str | None = None output = created.stdout + created.stderr match = re.search(r"https://github\.com/[^/]+/[^/]+/pull/([0-9]+)", output) if not match: - # Refactor (issue-276): preserve the PR-create parse failure instead of routing it through target normalization. raise RuntimeError(f"open_pr_with_label: failed to extract PR num from: {output.strip()}") pr_target = self._normalize_lifecycle_target_or_raise( match.group(1), @@ -338,9 +313,6 @@ def open_pr_with_label(self, title: str, body_file: str, base: str | None = None def open_design_issue_with_labels(self, title: str, body_file: str) -> tuple[int, str]: self._require_owner_or_raise("open-design-issue") - # Refactor (issue-297): Old: controller runbook exposed raw issue-open - # plus label recipes. New: design issue opening is a narrow internal - # ControllerActions primitive gated by the active controller lease. if not title.strip(): raise RuntimeError("open_design_issue_with_labels: title required") self._validate_design_issue_body_file(body_file) @@ -394,12 +366,6 @@ def open_release_rollup_pr_from_pending_event( title: str = "Release rollup", ) -> tuple[int, str]: self._require_owner_or_raise("open-release-rollup-pr") - # Refactor (issue174-rollup-throwaway-head): - # Old pattern: the rollup PR used the shared integration branch as - # its head, so GitHub merge/delete-branch flows could delete the - # integration branch itself. New principle: re-check the pending-event - # SHA, push a controller-owned rollup/ head, and open - # the PR from that disposable head only. try: event = json.loads(event_json) except json.JSONDecodeError as exc: @@ -510,10 +476,6 @@ def record_recent_pr_merge(self, pr: str) -> None: def apply_triage_decision_marker(self, marker: str) -> int: if not self._require_owner_or_return("apply-triage", code=3): return 3 - # Refactor (iter201/issue-201): Old pattern: controller marker handling - # subprocessed consensus-rnd-cli apply-triage, preserving public lifecycle - # reachability. New principle: direct internal call keeps validation and - # applied/rejected artifacts without exposing a public lifecycle command. match = re.fullmatch(r"TRIAGE_DECISION_DONE:([0-9]+):(accept|reject):(\.refactor-loop/runs/.*\.json)", marker) if not match: sys.stderr.write("apply_triage_decision_marker: invalid marker\n") @@ -523,9 +485,6 @@ def apply_triage_decision_marker(self, marker: str) -> int: return apply_decision(config, self.ctx.repo_root / rel_path, issue_number=int(issue), verdict=verdict) def render_template(self, input_path: str, output_path: str, env: Mapping[str, str] | None = None) -> None: - # Refactor (iter219/issue-219): - # Old pattern: host 无法按 GitHub 模板自定义事件流/工作流/issue/prompt;workflow vocabulary 是闭集硬编码 - # New principle: 引入 data-only HostWorkflowSpec(HOST_WORKFLOW_SPEC,repo-relative JSON)+ WorkflowInvariantValidator;空/未设=built-in 行为;host 只能在 host: 命名空间加 data,不能覆盖 built-in/降共识闸/夺 lifecycle authority。严格按 plan 'Concrete plan' 逐条改,首版 scope 受限。 values = dict(os.environ) if env: values.update(env) @@ -553,9 +512,6 @@ def render_review_fix_prompt( round_number: int, env: Mapping[str, str] | None = None, ) -> ReviewFixDispatchSpec: - # Refactor (issue-267): Old: FIX_OUTPUT_PATH was a prompt-only oral - # variable and workers could drift to root FIX_REPORT.md. New: - # controller render binds the canonical runs artifact before dispatch. spec = ReviewFixDispatchSpec.for_round(pr_number, round_number) render_env = dict(env or {}) render_env.update(spec.as_render_env()) @@ -591,10 +547,6 @@ def _worktree_for_branch(self, branch: str) -> Path | None: return None def _require_owner_or_return(self, action: str, *, code: int) -> bool: - # Refactor (impl/issue191-single-active-controller): Old pattern: - # controller lifecycle helpers could mutate GitHub/git from any device. - # New principle: every lifecycle mutation fails closed unless this - # process owns the singleton active-controller lease. decision = require_active_controller(self.ctx, action) write_active_controller_status(self.ctx, decision) if decision.allowed: @@ -609,10 +561,6 @@ def _require_owner_or_raise(self, action: str) -> None: raise RuntimeError(f"active_controller=noop:not-owner action={action} owner={decision.owner_device}") def _normalize_lifecycle_target_or_block(self, value: object, *, kind: str, action: str, source: str) -> str | None: - # Refactor (iter276/issue-276): Old pattern: controller lifecycle - # targets accepted empty or non-canonical GitHub ids before gh calls. - # New principle: require canonical positive decimal target ids and - # record invalid target blocks before lifecycle side effects. try: return _normalize_lifecycle_target(value, kind=kind, action=action, source=source) except ValueError as exc: @@ -627,17 +575,14 @@ def _normalize_lifecycle_target_or_raise(self, value: object, *, kind: str, acti return target def _append_invalid_github_target_event(self, *, kind: str, action: str, source: str) -> None: + self._append_pending_event(f"CONTROLLER_ACTION_BLOCKED:invalid-github-target:{action}:{kind}:{source}") + + def _append_pending_event(self, line: str) -> None: self.ctx.paths.pending_events.parent.mkdir(parents=True, exist_ok=True) with self.ctx.paths.pending_events.open("a", encoding="utf-8") as handle: - handle.write(f"CONTROLLER_ACTION_BLOCKED:invalid-github-target:{action}:{kind}:{source}\n") + handle.write(f"{line}\n") def _single_body_linked_issue_or_block(self, body: str, *, action: str) -> str | None: - # Refactor (issue-276): Old pattern: body-derived `Closes #...` - # targets used the read-only projection parser, so malformed body links - # looked identical to no link and skipped lifecycle target validation. - # New principle: body-link lifecycle targets fail closed before any - # gh side effect; absent or ambiguous valid links still mean no issue - # lifecycle mutation. for target in _body_closing_issue_targets(body): if self._normalize_lifecycle_target_or_block( target, @@ -676,11 +621,6 @@ def _normalize_lifecycle_target(value: object, *, kind: str, action: str, source def _single_linked_issue(body: str) -> str: - # Refactor (impl/issue239-linkage): - # Old pattern: controller parsed `Closes #N` with a caller-local regex - # while other runtime surfaces used different interpretations. - # New principle: use the shared managed-work projection parser and only - # mutate a parent issue when there is exactly one durable PR-body link. numbers = extract_closing_issue_numbers(body) return str(numbers[0]) if len(numbers) == 1 else "" diff --git a/skills/codex-refactor-loop/scripts/codex_refactor_loop/daemon_status.py b/skills/codex-refactor-loop/scripts/codex_refactor_loop/daemon_status.py index cc842e21..5c53c22e 100644 --- a/skills/codex-refactor-loop/scripts/codex_refactor_loop/daemon_status.py +++ b/skills/codex-refactor-loop/scripts/codex_refactor_loop/daemon_status.py @@ -25,10 +25,6 @@ ) -# Refactor (issue-298): Old: controller daemon health was inferred by -# restart-daemons side effects or local process probes. New: daemon-status is a -# read-only projection over RestartDaemons facts and cached active-controller -# status; repair/reload stays exclusively with restart-daemons. @dataclass(frozen=True) class DaemonStatusProjection: name: str diff --git a/skills/codex-refactor-loop/scripts/codex_refactor_loop/git.py b/skills/codex-refactor-loop/scripts/codex_refactor_loop/git.py index 540f6d17..4cecacad 100644 --- a/skills/codex-refactor-loop/scripts/codex_refactor_loop/git.py +++ b/skills/codex-refactor-loop/scripts/codex_refactor_loop/git.py @@ -28,9 +28,6 @@ def run(self, args: Sequence[str], *, check: bool = True) -> subprocess.Complete return result def safe_worktree(self, iteration: str | int, cluster: str, base_ref: str) -> tuple[Path, str]: - # Refactor (iter81/issue-81): - # Old pattern: 文件/分支/marker/label/role 命名混乱;松散 regex(parse_target ^phase9-issue([0-9]+).*)解析,缺 owner-local operational-name 契约 - # New principle: owner-local operational-name contract:CLAUDE.md 扩写命名不动点为 operational-name invariant + SKILL.md 增 owner map;收窄现有 owner parser/validation(progress.py parse_target 精确文法、safe_worktree 字段校验);behavior test + source-regression production-literal allowlist 防偷抄;**无**生产 OperationalNameRegistry/names.py/check_naming.py/全仓审美 lint _validate_safe_worktree_fields(str(iteration), cluster) wt_path = self.repo_root / ".worktrees" / f"iter{iteration}-{cluster}" branch = f"refactor/iter{iteration}-{cluster}" diff --git a/skills/codex-refactor-loop/scripts/codex_refactor_loop/github_body.py b/skills/codex-refactor-loop/scripts/codex_refactor_loop/github_body.py index b9e16d92..e4080adb 100644 --- a/skills/codex-refactor-loop/scripts/codex_refactor_loop/github_body.py +++ b/skills/codex-refactor-loop/scripts/codex_refactor_loop/github_body.py @@ -1,6 +1,6 @@ """Read-only GitHub body renderer and self-contained authority validator. -Runtime boundary: this module may read local artifact files and print rendered +Runtime boundary: this read-only helper may read local artifact files and print rendered Markdown to stdout. It must not write files, call Git/GitHub, spawn background processes, or change controller state; behavior and source-regression tests verify that boundary. @@ -45,18 +45,7 @@ def render_github_body( debug_paths: Sequence[str | Path] = (), max_bytes: int = MAX_BODY_BYTES, ) -> str: - """Render a self-contained Chinese GitHub body from local artifacts. - - Refactor (iter191/issue-191): - Old pattern: multi-device / multi-loop runtime lacks a single-active-controller guard; GitHub-facing authority/consensus bodies risk referencing local .refactor-loop paths instead of inlining artifacts - New principle: single active controller lease (no per-work claims, no cross-device floor); strengthen the self-contained github_body.py validator so authority/consensus/plan bodies inline raw artifacts and .refactor-loop/runs/*.md appears only as debug detail - - Refactor (issue192/self-contained-github-body): - Old pattern: GitHub bodies could cite `.refactor-loop/runs/*.md` as the - sole authorization source, leaving reviewers with machine-local dead links. - New principle: this read-only helper inlines the complete artifact text and - demotes local paths to optional debug hints under a collapsed section. - """ + """Render a self-contained Chinese GitHub body from local artifacts.""" _validate_kind(kind) if not title.strip(): @@ -121,12 +110,7 @@ def validate_self_contained_github_body( authority_required: bool = False, max_bytes: int = MAX_BODY_BYTES, ) -> None: - """Fail closed when a GitHub body uses local run paths as sole authority. - - Refactor (iter191/issue-191): - Old pattern: multi-device / multi-loop runtime lacks a single-active-controller guard; GitHub-facing authority/consensus bodies risk referencing local .refactor-loop paths instead of inlining artifacts - New principle: single active controller lease (no per-work claims, no cross-device floor); strengthen the self-contained github_body.py validator so authority/consensus/plan bodies inline raw artifacts and .refactor-loop/runs/*.md appears only as debug detail - """ + """Fail closed when a GitHub body uses local run paths as sole authority.""" if not isinstance(text, str) or not text.strip(): raise GitHubBodyError("empty GitHub body") diff --git a/skills/codex-refactor-loop/scripts/codex_refactor_loop/heartbeat.py b/skills/codex-refactor-loop/scripts/codex_refactor_loop/heartbeat.py index 19d5446c..b57c57a9 100644 --- a/skills/codex-refactor-loop/scripts/codex_refactor_loop/heartbeat.py +++ b/skills/codex-refactor-loop/scripts/codex_refactor_loop/heartbeat.py @@ -13,9 +13,6 @@ class DaemonHeartbeatLease: """Write and renew the daemon heartbeat from the actor process. - Refactor (iter316/issue-316): - Old pattern: heartbeat fell back to cwd when repo root was absent. - New principle: require explicit repo root, canonical REPO_ROOT/LoopContext, or explicit heartbeat file. """ def __init__( diff --git a/skills/codex-refactor-loop/scripts/codex_refactor_loop/labels.py b/skills/codex-refactor-loop/scripts/codex_refactor_loop/labels.py index 1c00ffc6..973c482f 100644 --- a/skills/codex-refactor-loop/scripts/codex_refactor_loop/labels.py +++ b/skills/codex-refactor-loop/scripts/codex_refactor_loop/labels.py @@ -2,10 +2,6 @@ from __future__ import annotations -# Refactor (iter4/issue-183): Old pattern: legacy emoji, Chinese, and -# auto-loop label literals were scattered across controller code, prompts, and -# SKILL prose. New principle: one crnd:: catalog owns canonical -# labels, legacy aliases, and dual-read migration planning. import argparse import json import os @@ -157,6 +153,7 @@ def _spec( _spec("triage", "pending", "External issue is pending manual intake triage.", "fbca04", aliases=("auto-loop-triage",)), _spec("triage", "resume-requested", "Maintainer requested resumed implementation.", "1d76db", aliases=("auto-loop-resume",)), _spec("milestone", "current", "Milestone-priority item.", "f9d0c4", aliases=("🎯 milestone",)), + _spec("milestone", "release-target", "Release countdown target issue/PR.", "f9d0c4"), ) CLEANUP_ONLY_ALIASES = frozenset({"🆘 human:卡死", "🆘 human:卡死-需-rework"}) @@ -187,6 +184,7 @@ def _spec( TRIAGE_PENDING = canonical_name("triage", "pending") TRIAGE_RESUME_REQUESTED = canonical_name("triage", "resume-requested") MILESTONE_CURRENT = canonical_name("milestone", "current") +MILESTONE_RELEASE_TARGET = canonical_name("milestone", "release-target") HUMAN_AUTO = canonical_name("human", "auto") HUMAN_MAINTAINER_DECISION = canonical_name("human", "maintainer-decision") PHASE_DESIGN_SOLVING = canonical_name("phase", "design-solving") diff --git a/skills/codex-refactor-loop/scripts/codex_refactor_loop/monitors/comment.py b/skills/codex-refactor-loop/scripts/codex_refactor_loop/monitors/comment.py index 3a545dc6..32cae6ca 100644 --- a/skills/codex-refactor-loop/scripts/codex_refactor_loop/monitors/comment.py +++ b/skills/codex-refactor-loop/scripts/codex_refactor_loop/monitors/comment.py @@ -70,10 +70,6 @@ def tick(self) -> None: self._poll_once() def _poll_once(self) -> None: - # Refactor (fix/comment-monitor-only-new): Old pattern: every tick fetched - # recent comments for every managed item. New principle: use the search - # node updatedAt as the per-item freshness gate before spending a comments - # query. for number, updated_at in self._search_active().items(): if not self._should_fetch_comments(number, updated_at): continue @@ -207,10 +203,6 @@ def handle_comment(self, number: str, comment: Mapping[str, object]) -> None: self.mark_seen(comment_id) print(f"new-outsider-comment: {number} {author} {comment_id} (skipped reply per security gate)", flush=True) return - # Refactor (impl/issue191-single-active-controller): Old pattern: - # comment monitors on multiple devices could react and post banners for - # the same maintainer comment. New principle: GitHub comment mutations - # are active-controller-owner-only; non-owners stay read-only. decision = require_active_controller(self.ctx, "comment-monitor-write") write_active_controller_status(self.ctx, decision) if not decision.allowed: diff --git a/skills/codex-refactor-loop/scripts/codex_refactor_loop/monitors/concurrency.py b/skills/codex-refactor-loop/scripts/codex_refactor_loop/monitors/concurrency.py index 2dc73d58..1dd931fc 100644 --- a/skills/codex-refactor-loop/scripts/codex_refactor_loop/monitors/concurrency.py +++ b/skills/codex-refactor-loop/scripts/codex_refactor_loop/monitors/concurrency.py @@ -94,10 +94,6 @@ def single_active_audit_boundary( gh_items: Any | None, queue_state: Any | None, ) -> Boundary | None: - # Refactor (issue-277): - # Old pattern: floor deficits treated audit fallback as endlessly repeatable. - # New principle: one active same-iteration audit occupies the only ordinary - # fallback slot; callers expose blocked capacity without duplicate audit. if has_open_actionable_managed_work(gh_items or []): return None if not _queue_state_empty(repo_root, monitor, queue_state): @@ -349,15 +345,8 @@ def list_auto_loop_issues(self) -> list[dict]: return items def compute_expected(self, items: list[dict]) -> tuple[int, list[dict]]: - # Refactor (iter3/skill-human-label-taxonomy): - # Old: four Human labels, including two escalation labels, scattered no-gap and escalation decisions across the codebase. - # New principle: exactly two active Human labels; causes move to the reason surface (#15 structural consensus). breakdown = [] total = 0 - # Refactor (impl/issue239-linkage): - # Old pattern: parent issues and child PRs were counted independently. - # New principle: shared ManagedWorkProjection folds an issue represented - # by an open managed PR body `Closes #N` before worker expectation math. for item in ManagedWorkProjection(items).effective_worker_items(): if label_catalog.HUMAN_MAINTAINER_DECISION in label_catalog.normalize_label_set([item.human]).canonical: continue @@ -407,9 +396,6 @@ def archive_dispatched(self, path: Path, payload: dict, task_id: str) -> Path: return archive def append_harness_spawn_intent(self, payload: dict, task_id: str, priority: str, reason: str) -> dict[str, object]: - # Refactor (iterissue-330/issue-330): - # Old pattern: daemon nohup spawn bypassed the harness-visible contract; command could mean argv/shell. - # New principle: HARNESS_SPAWN_INTENT.command is closed enum Literal['spawn-codex']; argv is built by controller/harness. intent = { "intent_id": f"dispatch:{task_id}", "source": "concurrency-monitor", @@ -430,13 +416,7 @@ def append_harness_spawn_intent(self, payload: dict, task_id: str, priority: str self.write_pending_event(f"HARNESS_SPAWN_INTENT {json.dumps(intent, ensure_ascii=False, sort_keys=True)}") return intent - # Refactor (iter6/issue-133): - # Old pattern: concurrency monitor passed queue payload[cd] straight to spawn-codex.sh --cd, letting a mutable task run in the repo-root/main worktree. - # New principle: structural consensus dispatch queue mutable-prefix cwd guard, no shared workspace policy. See .refactor-loop/runs/phase9-issue133-r4-judge.md. def validate_dispatch_cwd(self, payload: dict, task_id: str) -> tuple[bool, str]: - # Refactor (iter81/issue-81): - # Old pattern: 文件/分支/marker/label/role 命名混乱;松散 regex(parse_target ^phase9-issue([0-9]+).*)解析,缺 owner-local operational-name 契约 - # New principle: owner-local operational-name contract:CLAUDE.md 扩写命名不动点为 operational-name invariant + SKILL.md 增 owner map;收窄现有 owner parser/validation(progress.py parse_target 精确文法、safe_worktree 字段校验);behavior test + source-regression production-literal allowlist 防偷抄;**无**生产 OperationalNameRegistry/names.py/check_naming.py/全仓审美 lint cd_raw = payload.get("cd") if not cd_raw: return False, "missing-cd" @@ -466,9 +446,6 @@ def validate_dispatch_cwd(self, payload: dict, task_id: str) -> tuple[bool, str] return False, "worktrees-root-cd" return True, "worktrees-cd" - # Refactor (iter6/issue-133): - # Old pattern: concurrency monitor passed queue payload[cd] straight to spawn-codex.sh --cd, letting a mutable task run in the repo-root/main worktree. - # New principle: structural consensus dispatch queue mutable-prefix cwd guard, no shared workspace policy. See .refactor-loop/runs/phase9-issue133-r4-judge.md. def archive_rejected(self, path: Path, payload: dict, task_id: str, priority: str, reason: str) -> Path: self.dispatch_rejected.mkdir(parents=True, exist_ok=True) payload["rejected_at"] = utc_ts() @@ -483,14 +460,7 @@ def archive_rejected(self, path: Path, payload: dict, task_id: str, priority: st path.unlink() return archive - # Refactor (iter6/issue-133): - # Old pattern: concurrency monitor passed queue payload[cd] straight to spawn-codex.sh --cd, letting a mutable task run in the repo-root/main worktree. - # New principle: structural consensus dispatch queue mutable-prefix cwd guard, no shared workspace policy. See .refactor-loop/runs/phase9-issue133-r4-judge.md. def dispatch_one_from_queue(self) -> tuple[str, str, str] | None: - # Refactor (impl/issue191-single-active-controller): Old pattern: - # concurrency monitors on multiple devices could consume/archive the - # same local queue and spawn duplicate workers. New principle: dispatch - # and queue archive require the single active-controller owner. decision = require_active_controller(self.ctx, "concurrency-dispatch") write_active_controller_status(self.ctx, decision) if not decision.allowed: @@ -519,9 +489,6 @@ def dispatch_one_from_queue(self) -> tuple[str, str, str] | None: return task_id, priority, reason return None - # Refactor (iter4/concurrency-auto-topup): - # Old pattern: monitor only alerted; actual int: if actual >= floor: return actual @@ -535,13 +502,9 @@ def top_up_from_dispatch_queue(self, actual: int, floor: int) -> int: break return actual - # Refactor (iter4/concurrency-auto-topup): - # Old pattern: single no-gap sentinel path could alert and leave deficit repair to a later controller wakeup. - # New principle: no-gap alerting continues into deficit detection so queued work can be fired in the same tick. def tick(self) -> None: state = self.load_state() zero_streak = int(state.get("zero_streak", 0)) - # Refactor (impl/issue235-delete-downstream-watch): Old pattern: concurrency tick ran source-repo skill-degradation checks against downstream host roots. New principle: downstream hosts have no skill-degradation runtime watch; source-repo static validation stays in CI/release gates. decision = require_active_controller(self.ctx, "concurrency-tick") write_active_controller_status(self.ctx, decision) owner_allowed = decision.allowed diff --git a/skills/codex-refactor-loop/scripts/codex_refactor_loop/monitors/progress.py b/skills/codex-refactor-loop/scripts/codex_refactor_loop/monitors/progress.py index 6b7e4169..de3ad25b 100644 --- a/skills/codex-refactor-loop/scripts/codex_refactor_loop/monitors/progress.py +++ b/skills/codex-refactor-loop/scripts/codex_refactor_loop/monitors/progress.py @@ -78,9 +78,6 @@ def tick(self) -> None: self.post_or_update(base, log) def parse_target(self, base: str) -> str: - # Refactor (iter81/issue-81): - # Old pattern: 文件/分支/marker/label/role 命名混乱;松散 regex(parse_target ^phase9-issue([0-9]+).*)解析,缺 owner-local operational-name 契约 - # New principle: owner-local operational-name contract:CLAUDE.md 扩写命名不动点为 operational-name invariant + SKILL.md 增 owner map;收窄现有 owner parser/validation(progress.py parse_target 精确文法、safe_worktree 字段校验);behavior test + source-regression production-literal allowlist 防偷抄;**无**生产 OperationalNameRegistry/names.py/check_naming.py/全仓审美 lint for pattern in (PROGRESS_REVIEW_TARGET_RE, PROGRESS_FIX_TARGET_RE, PROGRESS_PHASE9_TARGET_RE): match = pattern.fullmatch(base) if match: @@ -129,10 +126,6 @@ def build_body(self, base: str, log: Path, finished: str) -> str: return body def post_or_update(self, base: str, log: Path) -> None: - # Refactor (impl/issue191-single-active-controller): Old pattern: - # progress reporters on multiple devices could create/edit/delete the - # same GitHub progress comment. New principle: GitHub comment writes - # require the single active-controller owner. decision = require_active_controller(self.ctx, "progress-reporter-write") write_active_controller_status(self.ctx, decision) if not decision.allowed: diff --git a/skills/codex-refactor-loop/scripts/codex_refactor_loop/peek.py b/skills/codex-refactor-loop/scripts/codex_refactor_loop/peek.py index c151d1f8..607b2e2d 100644 --- a/skills/codex-refactor-loop/scripts/codex_refactor_loop/peek.py +++ b/skills/codex-refactor-loop/scripts/codex_refactor_loop/peek.py @@ -38,7 +38,6 @@ def render(self) -> str: lines.extend(_prefixed_tail(self.ctx.paths.refactor_loop / "phase9-router-ledger.jsonl", 10, " ")) lines.append(" pending events tail:") lines.extend(_prefixed_tail(self.ctx.paths.pending_events, 10, " ")) - # Refactor (impl/issue235-delete-downstream-watch): Old pattern: peek rendered a downstream skill-degradation alert log. New principle: downstream hosts have no skill-degradation alert log or peek lens. lines.extend(["", "▍Milestone (优先) issues:"]) lines.extend(self._milestone_items()) lines.extend(["", "▍Open auto-loop PRs:"]) @@ -47,10 +46,6 @@ def render(self) -> str: lines.extend(self._unpushed_worker_output()) lines.extend(["", "▍Monitor zero_streak (last 10 ticks):"]) lines.extend(self._zero_streak()) - # Refactor (iter203/issue-203): Old pattern: controller decisions were - # split across peek, wakeup-plan, phase9-router, and concurrency. New - # principle: peek is observability-only; review-gate policy stays in - # existing wakeup-plan completed markers and controller truth tables. lines.extend(["", "▍Stale labels (CLOSED but still carrying in-flight phase labels):"]) lines.extend(self._stale_labels()) lines.extend(["", "▍Issue/PR linkage mismatch:"]) @@ -147,10 +142,6 @@ def _open_prs(self) -> list[str]: return lines def _unpushed_worker_output(self) -> list[str]: - # Refactor (iter201/issue-201): Old pattern: peek displayed a copyable - # consensus-rnd-cli safe-push command, making a status lens look like a - # lifecycle dispatcher. New principle: show fixed facts only; controller - # lifecycle execution remains internal and non-public. out = [] for action in unpushed_worker_output_actions(self.ctx.repo_root, load_github_items(self.ctx.repo_root)): out.append( @@ -175,10 +166,6 @@ def _zero_streak(self) -> list[str]: return out def _stale_labels(self) -> list[str]: - # Refactor (issue238/closed-label-reconciler): - # Old pattern: peek printed remediation text that told the controller - # to clean labels. New principle: peek remains a read-only display of - # the closed-label-reconciler projection and emits no edit guidance. out = [] for kind in ("issue", "pr"): for item in self._list_by_any_label(kind, label_catalog.query_labels_for(label_catalog.MANAGED), "number,state,labels", state="closed", limit="30"): @@ -193,10 +180,6 @@ def _stale_labels(self) -> list[str]: return out def _linkage_mismatch(self) -> list[str]: - # Refactor (impl/issue239-linkage): - # Old pattern: peek searched implementing issues locally and guessed - # PR linkage. New principle: render the shared read-only projection so - # missing or ambiguous `Closes #N` links remain visible. return list(self._managed_work_projection().linkage_mismatches()) def _spawn_drop(self) -> list[str]: @@ -258,9 +241,6 @@ def _managed_work_projection(self) -> ManagedWorkProjection: return ManagedWorkProjection(items) def _stale_worktrees(self) -> list[str]: - # Refactor (iter/issue-332): - # Old pattern: peek 只读 status lens 在输出里打印可复制的破坏性 lifecycle cleanup 命令(git worktree remove --force && git branch -D) - # New principle: read-only lens 只报 fact 字段(path/branch/remote_missing/cleanup_owner/no_lifecycle_authority),lifecycle cleanup 命令只属授权 controller runbook;保留 stale worktree 只读检测 out = [] result = subprocess.run(["git", "-C", str(self.ctx.repo_root), "worktree", "list", "--porcelain"], capture_output=True, text=True, check=False) current: Path | None = None @@ -348,8 +328,6 @@ def _list_by_any_label( return rows def _checks(self, pr_num: str) -> tuple[int, int, int]: - # Refactor (issue-297): Old: peek counted check buckets through a naked - # PR checks CLI. New: reuse the narrow read-only PR-head projection. if not self.ctx.gh_repo_slug: return 0, 0, 0 status = PrChecksProjection(cwd=self.ctx.repo_root).check_pr(self.ctx.gh_repo_slug, pr_num) @@ -404,12 +382,6 @@ def _safe_int(value: object) -> int | None: def main(argv: Sequence[str] | None = None) -> int: - # Refactor (iter1/issue-116): - # Old pattern: `peek --help` ignored argv, loaded LoopContext, fetched - # git, and ran the live status sweep, so bounded help could hang. - # New principle: argparse owns the human status-lens help surface before - # any repository, git, or GitHub access. `peek` remains text-only; the - # machine-readable next-action surface is `wakeup-plan`. parser = argparse.ArgumentParser( prog="consensus-rnd-cli peek", description="render the human-readable codex-refactor-loop status lens", diff --git a/skills/codex-refactor-loop/scripts/codex_refactor_loop/phase9/router.py b/skills/codex-refactor-loop/scripts/codex_refactor_loop/phase9/router.py index b3793ae3..dfd6e86c 100644 --- a/skills/codex-refactor-loop/scripts/codex_refactor_loop/phase9/router.py +++ b/skills/codex-refactor-loop/scripts/codex_refactor_loop/phase9/router.py @@ -1,9 +1,4 @@ #!/usr/bin/env python3 -# Refactor (iter3/skill-daemon-first-refactor): Old pattern: all numeric design-consensus routes -# were manually dispatched by the LLM controller, which easily missed markers. -# New principle: narrow allowlist daemon directly dispatches SOLVER_DONE -# triplet/converge/stalled routes; all other markers append fallback events -# (#37 structural B consensus). """Narrow design-consensus deterministic router daemon. This daemon owns only three design-consensus direct-dispatch routes: @@ -56,12 +51,6 @@ class Phase9MarkerGrammar: - # Refactor (iter1/issue-149): refactor helper, no behavior change outside existing routes. - # Old pattern: phase9_router_daemon marker parsing rejected judge markers - # with non-ASCII convergence bodies or route suffixes, so triplet judge - # and converge dispatches fell back to the controller. - # New principle: route-specific marker grammar keeps non-ASCII bodies - # valid for route markers without adding a design-consensus round projection layer. ROUTE_TOKEN = re.compile(r"^[A-Za-z0-9_./-]+$") VERDICT_TOKEN = re.compile(r"^[A-Za-z0-9_./-]+$") CONVERGE_RE = re.compile(r"^META_JUDGE_DONE:converge:round-(\d+)(?::.*)?$") @@ -122,9 +111,6 @@ class Phase9LogIdentity: @dataclass(frozen=True) class Phase9SourceIssueDecision: - # Refactor (iter229/issue-229): refactor helper, no behavior change outside the source issue gate. - # Old pattern: phase9-router 3 条 direct route(solver-triplet->judge / converge->next solvers / stalled->reflector)仅凭本地 clean EXIT=0 历史 marker/ledger/in-flight 状态派发,不校验 source GitHub issue 是否仍 OPEN - # New principle: 三条 direct route 在 prompt/spawn/ledger side-effect 前必须 read-only 确认 source GitHub issue state=OPEN;非 OPEN 或 state 不可证明则 fail-closed(不 spawn、不写 dispatch ledger,只追加 existing-format phase9-router-fallback pending event,reason ∈ phase9-source-not-open / phase9-source-state-unavailable);GitHub access 仅 state-only read,无 label/close/merge/release lifecycle authority;删旧 stale-marker-only dispatch authority allowed: bool state: str | None reason: Literal["phase9-source-open", "phase9-source-not-open", "phase9-source-state-unavailable"] @@ -132,9 +118,6 @@ class Phase9SourceIssueDecision: @dataclass(frozen=True) class MetaJudgePromptContext: - # Refactor (issue-262): Old: meta-judge prompt context had no validated - # transition projection. New: carry the same read-only sidecar projection - # used by solver headers without creating a second prompt injection path. issue: str round: int solver_paths: dict[str, str] @@ -159,9 +142,6 @@ def convergence_round_plus_one(self) -> int: class MetaJudgePromptRenderer: - # Refactor (iter9/issue-260): - # Old pattern: phase9-router meta-judge dispatch rendered a short no-rubric stub prompt. - # New principle: render full prompts/meta-judge.md with same issue/round scoped solver paths and fail closed on template/scope errors. PLACEHOLDERS = { "ISSUE_NUMBER", "WORK_UNIT_ID", @@ -246,11 +226,6 @@ def parse_phase9_log_identity(name: str) -> Phase9LogIdentity | None: class Phase9Router: - # Refactor (iter229/issue-229): - # Old pattern: phase9-router 3 条 direct route(solver-triplet->judge / converge->next solvers / stalled->reflector)仅凭本地 clean EXIT=0 历史 marker/ledger/in-flight 状态派发,不校验 source GitHub issue 是否仍 OPEN - # New principle: 三条 direct route 在 prompt/spawn/ledger side-effect 前必须 read-only 确认 source GitHub issue state=OPEN;非 OPEN 或 state 不可证明则 fail-closed(不 spawn、不写 dispatch ledger,只追加 existing-format phase9-router-fallback pending event,reason ∈ phase9-source-not-open / phase9-source-state-unavailable);GitHub access 仅 state-only read,无 label/close/merge/release lifecycle authority;删旧 stale-marker-only dispatch authority - # Refactor (iter202/issue-202): Old pattern: durable artifact(ledger log_path、pending-event JSON log_path、meta-judge/reflector evidence、dev-sync resolver prompt、DEV_SYNC_REQUEST marker)写入 host absolute repo/worktree/log path,违反 CLAUDE.md R24『artifact 路径相对 $REPO_ROOT,不引入具体 host 事实』。 - # New principle: 分层 durable-text-path vs execution-path:写入时所有 durable artifact/prompt/marker 只存 repo-relative POSIX text;读取或传 subprocess 时由 LoopContext.repo_root/rel_path 解析回 absolute;spawn-codex --cd/--add-dir/--prompt/--log 与 Popen argv 仍用 absolute(execution boundary 非 durable truth)。配套 behavior(写入存相对、读取解析绝对)+ source-regression(无 host absolute prefix)测试。不改 daemon lifecycle authority,不加规则例外。 def __init__( self, repo_root: Path | None = None, @@ -276,20 +251,10 @@ def __init__( self.pending_events_path = ctx.paths.pending_events self.lock_path = self.loop_dir / "phase9-router.lock" self.command_runner = command_runner or self._default_runner - # Refactor (iter4/skill-router-fallback-flood-fix): Old pattern: - # memory-only dedup was lost on daemon restart, re-emitting historical - # fallback events and flooding Monitor. New principle: __init__ scans - # existing phase9-router-fallback lines in pending-events to seed the - # dedup set, making restarts idempotent. self._fallback_seen: set[str] = self._load_persisted_fallback_seen() self._source_issue_decisions: dict[str, Phase9SourceIssueDecision] = {} def tick(self) -> None: - # Refactor (impl/issue191-single-active-controller): Old pattern: - # phase9 router instances on multiple devices could write prompts, - # append ledgers, spawn solvers, and emit fallback events in parallel. - # New principle: all router write routes require the global - # active-controller owner. decision = require_active_controller(self.ctx, "phase9-router") write_active_controller_status(self.ctx, decision) if not decision.allowed: @@ -316,24 +281,11 @@ def singleton(self) -> Iterable[None]: lock.flush() yield - # Refactor (iter5/skill-marker-tail-only-scope): - # Old pattern: scan entire log body for markers; codex worker logs that - # happen to echo prompt-body / test-fixture / grep-output marker text - # (e.g. `META_JUDGE_DONE:converge:round-3:echoed-from-prompt-body` from - # test_phase9_router_daemon.py source listing) were classified as real - # verdicts and triggered cascading dispatches. - # New principle: real worker verdict markers always appear in the tail - # alongside `EXIT=0`. Scan only the last MARKER_TAIL_LINES of each log. - # Body-position prompt-body echoes never reach the marker parser. MARKER_TAIL_LINES = 30 - TAIL_READ_BYTES = 8192 # Refactor (iter5/issue122-phase9-tail-perf): bound tail read to ~8KB so per-tick scan stays O(num_logs), not O(total log bytes). + TAIL_READ_BYTES = 8192 @staticmethod def _read_tail_lines(path: Path, num_lines: int) -> list[str]: - # Refactor (iter5/issue122-phase9-tail-perf): Old: read full log via - # read_text() then splitlines()[-N:]. New: seek to file end and read - # only the last TAIL_READ_BYTES, decode, return tail num_lines. Keeps - # per-tick CPU bounded as logs/ grows. try: with path.open("rb") as fh: fh.seek(0, 2) @@ -368,11 +320,6 @@ def _collect_markers(self) -> list[Marker]: return markers def _extract_marker(self, line: str) -> str | None: - # Refactor (iter1/issue-149): - # Old pattern: phase9_router_daemon marker parsing rejected judge - # markers with non-ASCII convergence bodies or route suffixes. - # New principle: route-specific marker grammar accepts all route - # markers, including non-ASCII bodies, without broad payload gates. stripped = line.strip().strip("`") if self._is_placeholder_or_echo(stripped): return None @@ -399,11 +346,6 @@ def _is_placeholder_or_echo(self, text: str) -> bool: return True if "round-n" in lowered: return True - # Refactor (iter4/skill-router-fallback-flood-fix): common traits of - # regex/grep alternation or template placeholders are `|` choices, - # `\"` escaped quotes, `r+1` placeholders, and `*` wildcards. These - # lines are almost certainly prompt-template or grep-command marker - # references, not real codex output. if "|" in text and any(prefix in text for prefix in KNOWN_PREFIXES): return True if "\\\"" in text or '\\"' in text: @@ -451,35 +393,14 @@ def _load_persisted_fallback_seen(self) -> set[str]: return seen def _identity_from_path(self, path: Path) -> Phase9LogIdentity | None: - # Refactor (issue-100/router-filename-identity): Old pattern: one loose regex - # accepted non-owned design-consensus-ish names. New principle: router-private filename - # identity allowlist accepts only phase9-issue, solver-issue, and meta-judge-issue - # dialects; public markers remain role-local. return parse_phase9_log_identity(path.name) def _is_clean_exit(self, path: Path) -> bool: - # Refactor (iter5/issue122-phase9-tail-perf): tail-only read via - # _read_tail_lines bounds CPU as logs/ grows. tail = self._read_tail_lines(path, 5) if not tail: return False return any(re.match(r"^EXIT=0$", line) for line in tail) - # Refactor (iter1/issue-167): - # Old pattern: solver triplet handoff recorded only the base dispatch row, - # so judge dispatch could proceed without durable triplet provenance or a - # visible same-round peer artifact reference failure. - # New principle: keep row-level router-private ledger provenance and a - # narrow fail-closed peer artifact token check on this route; do not add a - # standalone evidence file, hash, or lifecycle authority. - # Refactor (iter229/issue-229): - # Old pattern: phase9-router 3 条 direct route(solver-triplet->judge / converge->next solvers / stalled->reflector)仅凭本地 clean EXIT=0 历史 marker/ledger/in-flight 状态派发,不校验 source GitHub issue 是否仍 OPEN - # New principle: 三条 direct route 在 prompt/spawn/ledger side-effect 前必须 read-only 确认 source GitHub issue state=OPEN;非 OPEN 或 state 不可证明则 fail-closed(不 spawn、不写 dispatch ledger,只追加 existing-format phase9-router-fallback pending event,reason ∈ phase9-source-not-open / phase9-source-state-unavailable);GitHub access 仅 state-only read,无 label/close/merge/release lifecycle authority;删旧 stale-marker-only dispatch authority - # Refactor (iter284/issue-284): - # Old pattern: target log exists / in-flight and ledgered triplet - # duplicates were both silent, hiding unledgered solver-triplet suppression. - # New principle: ledgered duplicates stay silent; unledgered suppression - # appends one existing-format fallback event with a narrow private reason. def _dispatch_solver_triplets(self, markers: list[Marker], ledger: set[str]) -> None: solver_roles = self._solver_roles() judge_role = self._judge_role() @@ -526,28 +447,6 @@ def _dispatch_solver_triplets(self, markers: list[Marker], ledger: set[str]) -> ledger.add(key) def _dispatch_meta_judge_routes(self, markers: list[Marker], ledger: set[str]) -> None: - # Refactor (iter5/skill-converge-source-and-monotonic-guard): - # Old pattern: any log with `META_JUDGE_DONE:converge:round-N` marker - # could trigger solver dispatch, and `target_round` was accepted even - # if it equaled or preceded the source log's round. Result: solver - # logs echoing prompt-body marker examples plus judge-self-referential - # verdicts spawned cascading r3..r8 solver rounds with judge gaps. - # New principle: only judge-role source logs may authorize a converge - # dispatch (JUDGE markers come from JUDGE logs), and converge - # projection must stay adjacent to the source round. - # Refactor (iter229/issue-229): - # Old pattern: phase9-router 3 条 direct route(solver-triplet->judge / converge->next solvers / stalled->reflector)仅凭本地 clean EXIT=0 历史 marker/ledger/in-flight 状态派发,不校验 source GitHub issue 是否仍 OPEN - # New principle: 三条 direct route 在 prompt/spawn/ledger side-effect 前必须 read-only 确认 source GitHub issue state=OPEN;非 OPEN 或 state 不可证明则 fail-closed(不 spawn、不写 dispatch ledger,只追加 existing-format phase9-router-fallback pending event,reason ∈ phase9-source-not-open / phase9-source-state-unavailable);GitHub access 仅 state-only read,无 label/close/merge/release lifecycle authority;删旧 stale-marker-only dispatch authority - # Refactor (iter6/issue-244): Old pattern: converge payload was treated as - # target round only, so clean rS judge logs with canonical round-S - # markers fell back. New principle: accept source-round and legacy - # adjacent payloads locally, both dispatching r(S+1). - # Refactor (issue-304): Old pattern: fresh judge-emitted - # `META_JUDGE_DONE:escalate:stalled` was normal stalled authority. - # New principle: judge emits converge; before spawning r(S+1) solvers, - # router-owned stalled predicate may dispatch the round-S reflector and - # suppress solver churn. Legacy stalled markers remain read-only replay - # compatibility under the same predicate/source gates. for marker in markers: if marker.marker.startswith("META_JUDGE_DONE:converge:round-"): if marker.role != self._judge_role(): @@ -588,13 +487,6 @@ def _dispatch_meta_judge_routes(self, markers: list[Marker], ledger: set[str]) - self._dispatch_stalled_reflector(marker, ledger) def _append_fallbacks(self, markers: list[Marker], ledger: set[str]) -> None: - # Refactor (iter4/skill-router-fallback-flood-fix): Old pattern: dedup - # used a (log_path, marker) tuple, but marker text changes with extractor - # tweaks, so it was unstable across versions/restarts. New principle: - # dedup by log_path only. Once any marker from a log has surfaced to the - # controller, later markers from that log are not re-emitted; the - # controller can read the log directly if it needs details. This keeps - # dedup stable across versions. for marker in markers: if self._directly_handled(marker, ledger): continue @@ -632,10 +524,6 @@ def _round_from_converge(self, marker: str) -> int | None: return Phase9MarkerGrammar.parse_converge_round(marker) def _converge_target_round(self, marker_text: str, source_round: int) -> int | None: - # Refactor (iter6/issue-244): Old pattern: target-round math was - # duplicated at dispatch and fallback dedupe. New principle: one - # router-local adjacent helper maps canonical source-round and legacy - # next-round payloads to r(S+1); non-adjacent payloads fall back. payload_round = Phase9MarkerGrammar.parse_converge_round(marker_text) if payload_round in {source_round, source_round + 1}: return source_round + 1 @@ -684,11 +572,6 @@ def _stalled_predicate_holds(self, issue: str, round_no: int) -> bool: return recent[0] == recent[1] == recent[2] def _collect_markers_from_path(self, path: Path) -> list[str]: - # Refactor (iter5/skill-marker-tail-only-scope): same tail-only invariant - # as _collect_markers: _stalled_predicate_holds must not trust body- - # position SOLVER_DONE echoes when classifying convergence verdicts. - # Refactor (iter5/issue122-phase9-tail-perf): tail-only read via - # _read_tail_lines bounds CPU as logs/ grows. if not path.exists(): return [] tail = self._read_tail_lines(path, self.MARKER_TAIL_LINES) @@ -791,9 +674,6 @@ def _require_open_source_issue( marker: str, log_path: Path, ) -> bool: - # Refactor (iter229/issue-229): - # Old pattern: phase9-router 3 条 direct route(solver-triplet->judge / converge->next solvers / stalled->reflector)仅凭本地 clean EXIT=0 历史 marker/ledger/in-flight 状态派发,不校验 source GitHub issue 是否仍 OPEN - # New principle: 三条 direct route 在 prompt/spawn/ledger side-effect 前必须 read-only 确认 source GitHub issue state=OPEN;非 OPEN 或 state 不可证明则 fail-closed(不 spawn、不写 dispatch ledger,只追加 existing-format phase9-router-fallback pending event,reason ∈ phase9-source-not-open / phase9-source-state-unavailable);GitHub access 仅 state-only read,无 label/close/merge/release lifecycle authority;删旧 stale-marker-only dispatch authority decision = self._source_issue_decision(issue) if decision.allowed: return True @@ -936,9 +816,6 @@ def _append_solver_triplet_suppression_event( pending.write(f"{self._now()} phase9-router-fallback {json.dumps(event, ensure_ascii=False, sort_keys=True)}\n") def _spawn(self, prompt: Path, log_path: Path) -> bool: - # Refactor (iterissue-330/issue-330): - # Old pattern: daemon nohup spawn bypassed the harness-visible contract; command could mean argv/shell. - # New principle: HARNESS_SPAWN_INTENT.command is closed enum Literal['spawn-codex']; argv is built by controller/harness. if self.dry_run: return False intent = { @@ -1062,13 +939,6 @@ def _solver_prompt(self, issue: str, round_no: int, role: str, marker: str) -> s f"Convergence marker: {marker}\n\nUse prompts/solver-{role}.md contract and emit SOLVER_DONE:{role}:...\n" ) - # Refactor (issue-114/phase9-issue-source-header): - # Old pattern: converge-dispatched solver prompts had issue and round only, - # so issue-driven Path A depended on hidden prompt-template fallback and - # could be mistaken for a mandatory audit-backed cluster. - # New principle: render a router-private source header from known issue - # identity only; do not add state, producer registries, or lifecycle - # authority. def _solver_work_unit_header(self, issue: str, round_no: int, role: str) -> str: output_path = f".refactor-loop/runs/phase9-issue{issue}-r{round_no}-{role}.md" transition_lines = "\n".join(projection_lines(self._transition_assessment(issue))) @@ -1105,12 +975,6 @@ def _transition_projection_summary(self, assessment: TransitionAssessment) -> st ) ) - # Refactor (iter5/issue-85-stalled-reflector-template): - # Old pattern: generic 3-line fallback reflector prompt without template - # body or solver evidence. - # New principle: embed the full meta-reflector-stalled.md template plus - # 9 solver log-path evidence lines; missing template fails closed with an - # explicit missing-template prompt containing META_RESOLVED:escalate-human. def _reflector_prompt(self, marker: Marker) -> str: template = self._stalled_reflector_template() evidence_lines = "\n".join(self._stalled_evidence_lines(marker.issue, marker.round)) @@ -1249,9 +1113,7 @@ def _now(self) -> str: return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") def _solver_roles(self) -> tuple[str, ...]: - # Refactor (iter219/issue-219): - # Old pattern: host 无法按 GitHub 模板自定义事件流/工作流/issue/prompt;workflow vocabulary 是闭集硬编码 - # New principle: 引入 data-only HostWorkflowSpec(HOST_WORKFLOW_SPEC,repo-relative JSON)+ WorkflowInvariantValidator;空/未设=built-in 行为;host 只能在 host: 命名空间加 data,不能覆盖 built-in/降共识闸/夺 lifecycle authority。严格按 plan 'Concrete plan' 逐条改,首版 scope 受限。Phase9 direct-spawn-intent ignores HostWorkflowSpec role/dispatch/policy data entirely and keeps the built-in allowlist. + # Phase9 direct-spawn-intent ignores HostWorkflowSpec role/dispatch/policy data entirely. return ROLES def _judge_role(self) -> str: @@ -1284,10 +1146,6 @@ def main(argv: list[str] | None = None, command_runner: Callable[[dict[str, obje if args.once: router.tick() return 0 - # Refactor (iter1/issue-143): - # Old pattern: restart wrapper sidecar refreshed heartbeat even if this loop hung. - # New principle: actor loop beats after tick/caught exception, then lease-sleeps. - # --once stays outside the lease loop; daemon mode owns heartbeat progress. lease = DaemonHeartbeatLease("phase9_router_daemon", repo_root) while True: try: diff --git a/skills/codex-refactor-loop/scripts/codex_refactor_loop/pr_checks.py b/skills/codex-refactor-loop/scripts/codex_refactor_loop/pr_checks.py index 080c6efb..92b725db 100644 --- a/skills/codex-refactor-loop/scripts/codex_refactor_loop/pr_checks.py +++ b/skills/codex-refactor-loop/scripts/codex_refactor_loop/pr_checks.py @@ -91,9 +91,6 @@ def as_dict(self) -> dict[str, Any]: class PrChecksProjection: - # Refactor (issue-297): Old: controller CI watch used a naked PR checks CLI - # and mixed PR bucket policy into runbook recipes. New: PR-head checks are - # read through one narrow REST projection with no lifecycle authority. def __init__( self, *, diff --git a/skills/codex-refactor-loop/scripts/codex_refactor_loop/project_rules.py b/skills/codex-refactor-loop/scripts/codex_refactor_loop/project_rules.py index 32b263b9..0ccde81b 100644 --- a/skills/codex-refactor-loop/scripts/codex_refactor_loop/project_rules.py +++ b/skills/codex-refactor-loop/scripts/codex_refactor_loop/project_rules.py @@ -72,9 +72,6 @@ def needs_patch(self) -> bool: class ProjectRulesPatchArtifact: - # Refactor (iter218/issue-218): - # Old pattern: ensure-project-rules 是 public CLI 默认写 host policy 文件($PROJECT_RULES),违反 skill 无 host 改动权边界 - # New principle: 改为 read-only check-project-rules probe + patch artifact:probe 只读判 sentinel block,非 current 写 .refactor-loop/runs/ patch 并 fail-closed 不派 actor;删 ensure-project-rules/_atomic_write,不引入 PROJECT_RULES_WRITE_ENABLE。严格按 plan 逐条改。 def __init__(self, repo_root: Path, filename: str = "project-rules-fixed-point.patch") -> None: self.repo_root = repo_root self.path = repo_root / ".refactor-loop" / "runs" / filename @@ -98,9 +95,6 @@ def write(self, report: ProjectRulesFixedPointReport) -> Path: class ProjectRulesFixedPointProbe: - # Refactor (iter218/issue-218): - # Old pattern: ensure-project-rules 是 public CLI 默认写 host policy 文件($PROJECT_RULES),违反 skill 无 host 改动权边界 - # New principle: 改为 read-only check-project-rules probe + patch artifact:probe 只读判 sentinel block,非 current 写 .refactor-loop/runs/ patch 并 fail-closed 不派 actor;删 ensure-project-rules/_atomic_write,不引入 PROJECT_RULES_WRITE_ENABLE。严格按 plan 逐条改。 def __init__(self, repo_root: str, project_rules: str | None = None) -> None: self.repo_root = self._resolve_repo_root(repo_root) self.target = self._resolve_target(project_rules if project_rules else "CLAUDE.md") @@ -111,9 +105,6 @@ def from_env(cls) -> "ProjectRulesFixedPointProbe": return cls(os.environ.get("REPO_ROOT", ""), os.environ.get("PROJECT_RULES")) def inspect(self) -> ProjectRulesFixedPointReport: - # Refactor (iter218/issue-218): - # Old pattern: ensure-project-rules 是 public CLI 默认写 host policy 文件($PROJECT_RULES),违反 skill 无 host 改动权边界 - # New principle: 改为 read-only check-project-rules probe + patch artifact:probe 只读判 sentinel block,非 current 写 .refactor-loop/runs/ patch 并 fail-closed 不派 actor;删 ensure-project-rules/_atomic_write,不引入 PROJECT_RULES_WRITE_ENABLE。严格按 plan 逐条改。 original = self._read_target() return self._inspect_text(original) @@ -146,9 +137,6 @@ def _read_target(self) -> str: return text def _inspect_text(self, text: str) -> ProjectRulesFixedPointReport: - # Refactor (iter218/issue-218): - # Old pattern: ensure-project-rules 是 public CLI 默认写 host policy 文件($PROJECT_RULES),违反 skill 无 host 改动权边界 - # New principle: 改为 read-only check-project-rules probe + patch artifact:probe 只读判 sentinel block,非 current 写 .refactor-loop/runs/ patch 并 fail-closed 不派 actor;删 ensure-project-rules/_atomic_write,不引入 PROJECT_RULES_WRITE_ENABLE。严格按 plan 逐条改。 starts = list(START_RE.finditer(text)) end_count = text.count(END_MARKER) if len(starts) != end_count: diff --git a/skills/codex-refactor-loop/scripts/codex_refactor_loop/release/commits.py b/skills/codex-refactor-loop/scripts/codex_refactor_loop/release/commits.py index 7a9489e8..608b4e32 100644 --- a/skills/codex-refactor-loop/scripts/codex_refactor_loop/release/commits.py +++ b/skills/codex-refactor-loop/scripts/codex_refactor_loop/release/commits.py @@ -61,7 +61,6 @@ def write_release_commits( target_ref: str | None = None, fetch_tags: bool = False, ) -> Path: - # Refactor (fix/pr236-split-release-commits-command): Old pattern: release-gate consumed release-commits.json but also inlined the git-reading producer. New principle: a separate release-commits command reads git, then atomically writes the state artifact while the decider stays git-free. repo_root = Path(repo_root).expanduser().resolve() commits = collect_release_commits( repo_root, diff --git a/skills/codex-refactor-loop/scripts/codex_refactor_loop/release/gate.py b/skills/codex-refactor-loop/scripts/codex_refactor_loop/release/gate.py index 9e1358f7..47241256 100644 --- a/skills/codex-refactor-loop/scripts/codex_refactor_loop/release/gate.py +++ b/skills/codex-refactor-loop/scripts/codex_refactor_loop/release/gate.py @@ -17,28 +17,10 @@ from ..context import HostEnvLocator, parse_host_env from ..restart import restart_managed_daemon_names from ..state import read_json, write_json -from .required_checks import REQUIRED_RELEASE_CHECKS, ReleaseRequiredChecksProjection +from .required_checks import ReleaseRequiredChecksProjection, required_release_checks from .versions import SEMVER_RE, bump_semver, compare_semver, next_release_version, parse_semver -# Refactor (issue160-p3-auto-release-gate): -# Old pattern: scripts/auto_release_gate.py owned release-decision behavior as -# a top-level script, which made later package CLI routing import-unsafe. -# New principle: keep every gate signal and artifact byte-for-byte compatible, -# but expose package functions/classes from codex_refactor_loop.release.gate. -# This module remains decision-artifact-only per -# skills/codex-refactor-loop/authorizations/runtime-exceptions.md#autonomous-release-gate-56; -# it has no lifecycle authority and does not bump, commit, push, tag, -# publish, merge, close, or mutate issue/PR labels. -# -# Refactor (iter217/issue-217): -# Old pattern: release.yml 保留 tag/release mutation,无法可靠读本地 runtime fact,绕过 release-gate decider-only 边界 -# New principle: controller-only publication:新增 ReleasePublishPreflight+ReleasePublisher 替代 workflow 发布权;release.yml 降为 read-only preview(contents:read,禁 gh release create)。严格按 plan 'Concrete plan' 逐条改。 -# -# Refactor (iter316/issue-316): -# Old pattern: release-gate parsed root and nested host.env with a private parser. -# New principle: use context.py shared host.env parser and locator contract. - SIGNAL_NAMES = ( "required_checks_recent_green", "no_open_blocked_pr", @@ -50,7 +32,7 @@ "no_unresolved_human_escalation", ) DAEMON_NAMES = restart_managed_daemon_names() -REQUIRED_CHECKS = REQUIRED_RELEASE_CHECKS +REQUIRED_CHECKS = required_release_checks HEARTBEAT_FRESH_SECONDS = 90 @@ -94,9 +76,6 @@ def isoformat(value: datetime) -> str: def load_host_env(repo_root: Path, env: Mapping[str, str] | None = None) -> dict[str, str]: - # Refactor (iter1/fix-release-hostenv): - # Old pattern: release authority read only legacy .refactor-loop/host.env, bypassing CONSENSUS_RND_HOST_ENV. - # New principle: release authority uses HostEnvLocator, matching LoopContext's explicit-first host.env contract. location = HostEnvLocator.resolve(repo_root, os.environ if env is None else env, repo_root) return parse_host_env(location.path) if location is not None else {} @@ -251,16 +230,17 @@ def required_checks_recent_green(self, since: datetime) -> dict[str, Any]: reason = "empty GH_REPO_SLUG, REVIEW_BASE_BRANCH, or INTEGRATION_BRANCH; unsafe to infer release check branch" print(f"auto-release unsafe abort: {reason}", file=sys.stderr) return {"passed": False, "reason": reason, "source": "env"} + checks = required_release_checks(os.environ) + if not checks and os.environ.get("RELEASE_AUTO_ENABLE") == "true": + reason = "missing_host_required_release_checks" + print(f"auto-release unsafe abort: {reason}", file=sys.stderr) + return {"passed": False, "reason": reason, "source": "host.env"} branches = (review_base, integration) print(f"check branches: {review_base}, {integration}") - # Refactor (issue157 release gate): - # Old pattern: gh run list matched workflow run names, so a workflow - # called consensus-rnd-ci could not prove exact required check-runs. - # New principle: read the shared Checks API projection by exact - # check-run name for both release branches, fail-closed on drift. projection = ReleaseRequiredChecksProjection( runner=lambda cmd: self.runner(cmd, self.repo_root), now=self.now, + required_checks=checks, ) evidence: dict[str, Any] = {} red_checks: list[dict[str, str]] = [] @@ -354,11 +334,6 @@ def p0_alert_streak_ok(self, since: datetime) -> dict[str, Any]: return {"passed": streak <= 3 and recent_lines <= 3, "zero_streak": streak, "recent_p0_alerts": recent_lines, "source": "state"} def recent_pr_merges_min(self, since: datetime, minimum: int) -> dict[str, Any]: - # Refactor (iter1/issue-145): - # Old pattern: merge_pr success did not write recent-pr-merges.json, - # so recent_pr_merges_min stayed red and blocked release. - # New principle: read the controller-owned post-merge projection only; - # the release gate does not discover merge facts from git or GitHub. raw = read_json(self.recent_merges_path, {}) count = raw.get("count") if isinstance(raw, dict) else None if count is None and minimum <= 0: @@ -379,14 +354,6 @@ def recent_pr_merges_min(self, since: datetime, minimum: int) -> dict[str, Any]: return signal def fresh_heartbeats(self) -> dict[str, Any]: - # Refactor (iterissue-331/issue-331): - # Old pattern: release gate and wakeup_plan each kept local - # daemon-name literals, drifting from restart.py DAEMON_COMMANDS and - # duplicating the source of truth. - # New principle: restart.py::restart_managed_daemon_names() is the - # canonical daemon-name projection; release keeps DAEMON_NAMES only - # as a derived alias, wakeup deletes EXPECTED_DAEMONS, and health - # requires every restart-managed heartbeat to be fresh. now = self.now() required_names = restart_managed_daemon_names() fresh: dict[str, bool] = {} @@ -434,9 +401,6 @@ def commits_since_latest_release(self) -> list[CommitInfo]: return commits def decide_release(self, stability: StabilityResult, min_interval_hours: int) -> dict[str, Any]: - # Refactor (iter272/issue-272): - # Old pattern: release-gate semver 不遵循预发布阶梯:beta.3+patch commits → 误算 1.0.1(GA,三重越阶) - # New principle: 结构化修复:新增 versions.next_release_version helper 按预发布阶梯递推(beta.N→beta.N+1,绝不自动升阶/off-ladder),preflight 增 off-ladder validation 拒绝越阶 target;不引入 schema v3 / ReleaseCoordinatePolicy now = self.now() from_version = self.current_version() interval = self.release_interval_status(now, min_interval_hours) @@ -477,9 +441,6 @@ def dispatch_release(self, decision: dict[str, Any]) -> None: write_json(self.candidate_path, self.release_candidate(decision)) def release_candidate(self, decision: dict[str, Any]) -> dict[str, Any]: - # Refactor (iter217/issue-217): - # Old pattern: release.yml 保留 tag/release mutation,无法可靠读本地 runtime fact,绕过 release-gate decider-only 边界 - # New principle: controller-only publication:新增 ReleasePublishPreflight+ReleasePublisher 替代 workflow 发布权;release.yml 降为 read-only preview(contents:read,禁 gh release create)。严格按 plan 'Concrete plan' 逐条改。 now = self.now() required_signals = { name: { diff --git a/skills/codex-refactor-loop/scripts/codex_refactor_loop/release/publish_preflight.py b/skills/codex-refactor-loop/scripts/codex_refactor_loop/release/publish_preflight.py index c841a480..fdd57d9c 100644 --- a/skills/codex-refactor-loop/scripts/codex_refactor_loop/release/publish_preflight.py +++ b/skills/codex-refactor-loop/scripts/codex_refactor_loop/release/publish_preflight.py @@ -11,7 +11,7 @@ from ..state import read_json from .gate import isoformat, load_host_env, parse_time, resolve_field -from .required_checks import REQUIRED_RELEASE_CHECKS +from .required_checks import required_release_checks from .versions import compare_semver, validate_release_version_coordinate @@ -23,9 +23,6 @@ class PublishPreflightResult: """Release publication authorization result. - Refactor (iter217/issue-217): - Old pattern: release.yml 保留 tag/release mutation,无法可靠读本地 runtime fact,绕过 release-gate decider-only 边界 - New principle: controller-only publication:新增 ReleasePublishPreflight+ReleasePublisher 替代 workflow 发布权;release.yml 降为 read-only preview(contents:read,禁 gh release create)。严格按 plan 'Concrete plan' 逐条改。 """ allowed: bool @@ -42,9 +39,6 @@ class PublishPreflightResult: class ReleasePublishPreflight: """Validate controller release publication inputs before mutation. - Refactor (iter217/issue-217): - Old pattern: release.yml 保留 tag/release mutation,无法可靠读本地 runtime fact,绕过 release-gate decider-only 边界 - New principle: controller-only publication:新增 ReleasePublishPreflight+ReleasePublisher 替代 workflow 发布权;release.yml 降为 read-only preview(contents:read,禁 gh release create)。严格按 plan 'Concrete plan' 逐条改。 """ def __init__( @@ -65,9 +59,6 @@ def validate( target_ref: str, manifest_version: str | None = None, ) -> PublishPreflightResult: - # Refactor (iter217/issue-217): - # Old pattern: release.yml 保留 tag/release mutation,无法可靠读本地 runtime fact,绕过 release-gate decider-only 边界 - # New principle: controller-only publication:新增 ReleasePublishPreflight+ReleasePublisher 替代 workflow 发布权;release.yml 降为 read-only preview(contents:read,禁 gh release create)。严格按 plan 'Concrete plan' 逐条改。 reasons: list[str] = [] candidate_file, candidate_path_error = self._resolve_repo_path(candidate_path) if candidate_path_error: @@ -196,9 +187,6 @@ def _current_manifest_version(self, reasons: list[str]) -> str: return next(iter(versions)) def _validate_version(self, candidate: dict[str, Any], decision: dict[str, Any], version: str, reasons: list[str]) -> None: - # Refactor (iter279/issue-279): - # Old pattern: preflight compared pre-bump manifests against to_version and rejected valid beta.N to beta.N+1 candidates. - # New principle: preflight validates current manifests against from_version, while coordinate validation still gates from_version to to_version. to_version = candidate.get("to_version") if not isinstance(to_version, str) or not to_version: reasons.append("candidate_version_missing") @@ -227,7 +215,11 @@ def _validate_required_signals(self, candidate: dict[str, Any], decision: dict[s for name, signal in signals.items(): if not isinstance(signal, dict) or signal.get("passed") is not True: reasons.append(f"required_signal_red:{name}") - missing = [name for name in REQUIRED_RELEASE_CHECKS if not _required_check_is_green(signals, name)] + checks = required_release_checks(load_host_env(self.repo_root)) + if not checks: + reasons.append("missing_host_required_release_checks") + return + missing = [name for name in checks if not _required_check_is_green(signals, name)] if missing: reasons.append("required_check_red:" + ",".join(missing)) decision_signals = decision.get("signals") if isinstance(decision, dict) else None diff --git a/skills/codex-refactor-loop/scripts/codex_refactor_loop/release/publisher.py b/skills/codex-refactor-loop/scripts/codex_refactor_loop/release/publisher.py index 689e2f80..95b9b0e1 100644 --- a/skills/codex-refactor-loop/scripts/codex_refactor_loop/release/publisher.py +++ b/skills/codex-refactor-loop/scripts/codex_refactor_loop/release/publisher.py @@ -12,7 +12,7 @@ from ..state import write_json from .gate import isoformat, load_host_env from .publish_preflight import PublishPreflightResult, ReleasePublishPreflight, load_manifest_targets -from .required_checks import ReleaseRequiredChecksProjection +from .required_checks import ReleaseRequiredChecksProjection, required_release_checks from .versions import parse_semver_full @@ -23,9 +23,6 @@ class ReleasePublishResult: """Result of the controller-owned release publication primitive. - Refactor (iter217/issue-217): - Old pattern: release.yml 保留 tag/release mutation,无法可靠读本地 runtime fact,绕过 release-gate decider-only 边界 - New principle: controller-only publication:新增 ReleasePublishPreflight+ReleasePublisher 替代 workflow 发布权;release.yml 降为 read-only preview(contents:read,禁 gh release create)。严格按 plan 'Concrete plan' 逐条改。 """ published: bool @@ -42,12 +39,7 @@ class ReleasePublishResult: @dataclass(frozen=True) class ReleasePublicationState: - """Private publication classifier. - - Refactor (iter341/issue-341): - Old pattern: ReleasePublisher.publish() 线性 bump/add/commit→push→green-gate;push 后 CI pending 即陷入不可恢复授权态(manifests 已 bump,re-run git commit nothing-to-commit 失败)——beta.5 靠 controller hand-complete 绕过 - New principle: 单一 publish() 主链路加 already-bumped reentry:仅当唯一 preflight mismatch 是 mapped manifests 已==to_version 且 git show -s --format=%s HEAD 证明 HEAD subject 精确为 'Release v' 时跳过 bump/add/commit 三步,随后仍必须 _safe_push + exact-SHA required-checks green gate + gh release create + result artifact。严格按 DESIGN_DECISION_PATH verbatim Concrete plan;不新增 resume ticket/public CLI/workflow 发版权/host.env 事实源 - """ + """Private publication classifier.""" phase: ReleasePublicationPhase version: str @@ -58,20 +50,7 @@ class ReleasePublicationState: class ReleasePublisher: - """Publish a preflight-approved release from the controller boundary only. - - Refactor (iter341/issue-341): - Old pattern: ReleasePublisher.publish() 线性 bump/add/commit→push→green-gate;push 后 CI pending 即陷入不可恢复授权态(manifests 已 bump,re-run git commit nothing-to-commit 失败)——beta.5 靠 controller hand-complete 绕过 - New principle: 单一 publish() 主链路加 already-bumped reentry:仅当唯一 preflight mismatch 是 mapped manifests 已==to_version 且 git show -s --format=%s HEAD 证明 HEAD subject 精确为 'Release v' 时跳过 bump/add/commit 三步,随后仍必须 _safe_push + exact-SHA required-checks green gate + gh release create + result artifact。严格按 DESIGN_DECISION_PATH verbatim Concrete plan;不新增 resume ticket/public CLI/workflow 发版权/host.env 事实源 - - Refactor (iter334/issue-334): - Old pattern: fresh manifest-bump commits could be released before exact-SHA checks were green. - New principle: after safe push, gate the same fresh SHA with ReleaseRequiredChecksProjection before release creation. - - Refactor (iter217/issue-217): - Old pattern: release.yml 保留 tag/release mutation,无法可靠读本地 runtime fact,绕过 release-gate decider-only 边界 - New principle: controller-only publication:新增 ReleasePublishPreflight+ReleasePublisher 替代 workflow 发布权;release.yml 降为 read-only preview(contents:read,禁 gh release create)。严格按 plan 'Concrete plan' 逐条改。 - """ + """Publish a preflight-approved release from the controller boundary only.""" def __init__( self, @@ -96,12 +75,6 @@ def publish( candidate_path: str | Path = ".refactor-loop/state/release-candidate.json", target_ref: str, ) -> ReleasePublishResult: - # Refactor (iter217/issue-217): - # Old pattern: release.yml 保留 tag/release mutation,无法可靠读本地 runtime fact,绕过 release-gate decider-only 边界 - # New principle: controller-only publication:新增 ReleasePublishPreflight+ReleasePublisher 替代 workflow 发布权;release.yml 降为 read-only preview(contents:read,禁 gh release create)。严格按 plan 'Concrete plan' 逐条改。 - # Refactor (iter341/issue-341): - # Old pattern: ReleasePublisher.publish() 线性 bump/add/commit→push→green-gate;push 后 CI pending 即陷入不可恢复授权态(manifests 已 bump,re-run git commit nothing-to-commit 失败)——beta.5 靠 controller hand-complete 绕过 - # New principle: 单一 publish() 主链路加 already-bumped reentry:仅当唯一 preflight mismatch 是 mapped manifests 已==to_version 且 git show -s --format=%s HEAD 证明 HEAD subject 精确为 'Release v' 时跳过 bump/add/commit 三步,随后仍必须 _safe_push + exact-SHA required-checks green gate + gh release create + result artifact。严格按 DESIGN_DECISION_PATH verbatim Concrete plan;不新增 resume ticket/public CLI/workflow 发版权/host.env 事实源 result = self.preflight.validate(candidate_path=candidate_path, target_ref=target_ref) state = self._inspect_publication_state(result) if not result.allowed and not state.skip_bump_commit: @@ -117,11 +90,6 @@ def publish( self._ensure_success(add, "git add") commit = self._run(["git", "commit", "-m", self._release_bump_subject(version)]) self._ensure_success(commit, "git commit") - # Refactor (fix/publisher-tag-target): Old pattern: publisher committed - # synchronized release manifests, pushed HEAD, then created the release tag - # on the preflight candidate ref whose manifests could still be old. - # New principle: the release tag target is the freshly committed manifest - # bump SHA, so tag version and mapped manifest versions are coupled. release_target_ref = self._current_head_sha() release_push_started_at = self.now() self._safe_push() @@ -249,11 +217,6 @@ def _current_head_sha(self) -> str: return sha def _safe_push(self) -> None: - # Refactor (iter217/issue-217): - # Old pattern: release.yml kept tag/release mutation authority and - # could not reliably read local runtime facts. - # New principle: controller-only publication owns release mutation; - # release.yml is a read-only preview. fetch = self._run(["git", "fetch", "origin", "HEAD"]) self._ensure_success(fetch, "git fetch") behind = self._run(["git", "rev-list", "--count", "HEAD..origin/HEAD"]) @@ -272,6 +235,7 @@ def _ensure_fresh_release_commit_checks_green(self, release_target_ref: str, *, projection = ReleaseRequiredChecksProjection( runner=self._run_check_command, now=self.now, + required_checks=required_release_checks(load_host_env(self.repo_root)), ) status = projection.check_ref(repo_slug, release_target_ref, since=since, wait_seconds=0) if not status.passed: diff --git a/skills/codex-refactor-loop/scripts/codex_refactor_loop/release/required_checks.py b/skills/codex-refactor-loop/scripts/codex_refactor_loop/release/required_checks.py index fb556794..0351516d 100644 --- a/skills/codex-refactor-loop/scripts/codex_refactor_loop/release/required_checks.py +++ b/skills/codex-refactor-loop/scripts/codex_refactor_loop/release/required_checks.py @@ -4,20 +4,30 @@ import argparse import json +import os import subprocess import sys import time from dataclasses import dataclass from datetime import datetime, timedelta, timezone -from typing import Any, Callable, Iterable, Sequence +from typing import Any, Callable, Iterable, Mapping, Sequence -# Refactor (issue157 release gate): -# Old pattern: release.yml and release-gate each interpreted required checks -# through separate readers, including workflow-run names from gh run list. -# New principle: one read-only Checks API projection owns exact check-run name -# matching for release required checks; it never mutates lifecycle state. -REQUIRED_RELEASE_CHECKS = ("contract-tests", "manifest-version-sync", "skill-degradation") +# Compatibility alias only. Runtime callers must use required_release_checks() +# or inject check names into ReleaseRequiredChecksProjection. +REQUIRED_RELEASE_CHECKS: tuple[str, ...] = () +HOST_REQUIRED_CHECKS_ENV = "HOST_GITHUB_RELEASE_REQUIRED_CHECKS" + + +def required_release_checks(env_or_ctx: Mapping[str, str] | Any | None = None) -> tuple[str, ...]: + if env_or_ctx is None: + source: Mapping[str, str] = os.environ + elif isinstance(env_or_ctx, Mapping): + source = env_or_ctx + else: + source = getattr(env_or_ctx, "host_env", {}) or {} + raw = str(source.get(HOST_REQUIRED_CHECKS_ENV, "") or "") + return tuple(part.strip() for part in raw.split(",") if part.strip()) @dataclass(frozen=True) @@ -99,10 +109,13 @@ def __init__( runner: Callable[[Sequence[str]], subprocess.CompletedProcess[str]] = run_command, now: Callable[[], datetime] = utc_now, sleep: Callable[[float], None] = time.sleep, + required_checks: Sequence[str] | None = None, + env: Mapping[str, str] | None = None, ) -> None: self.runner = runner self.now = now self.sleep = sleep + self.required_checks = tuple(required_checks) if required_checks is not None else required_release_checks(env) def check_ref( self, @@ -134,6 +147,8 @@ def check_refs( return {ref: self.check_ref(repo_slug, ref, since=since) for ref in refs} def _check_ref_once(self, repo_slug: str, ref: str, *, since: datetime | None) -> RequiredCheckStatus: + if not self.required_checks: + return self._failed(ref, "missing_host_required_release_checks", {}, [], [], []) result = self.runner( [ "gh", @@ -158,7 +173,7 @@ def _check_ref_once(self, repo_slug: str, ref: str, *, since: datetime | None) - if not isinstance(check, dict): continue name = check.get("name") - if name not in REQUIRED_RELEASE_CHECKS: + if name not in self.required_checks: continue previous = latest_by_name.get(name) if previous is None or _check_sort_time(check) >= _check_sort_time(previous): @@ -169,7 +184,7 @@ def _check_ref_once(self, repo_slug: str, ref: str, *, since: datetime | None) - pending_checks: list[str] = [] stale_checks: list[str] = [] missing_checks: list[str] = [] - for name in REQUIRED_RELEASE_CHECKS: + for name in self.required_checks: check = latest_by_name.get(name) if check is None: checks[name] = False @@ -223,7 +238,7 @@ def _failed( return RequiredCheckStatus( passed=False, reason=reason, - checks={name: checks.get(name, False) for name in REQUIRED_RELEASE_CHECKS}, + checks={name: checks.get(name, False) for name in self.required_checks}, red_checks=red_checks, pending_checks=pending_checks, stale_checks=stale_checks, @@ -240,8 +255,10 @@ def check_ref( poll_seconds: int = 10, runner: Callable[[Sequence[str]], subprocess.CompletedProcess[str]] = run_command, now: Callable[[], datetime] = utc_now, + required_checks: Sequence[str] | None = None, + env: Mapping[str, str] | None = None, ) -> RequiredCheckStatus: - return ReleaseRequiredChecksProjection(runner=runner, now=now).check_ref( + return ReleaseRequiredChecksProjection(runner=runner, now=now, required_checks=required_checks, env=env).check_ref( repo_slug, ref, since=since, @@ -257,8 +274,10 @@ def check_refs( since: datetime, runner: Callable[[Sequence[str]], subprocess.CompletedProcess[str]] = run_command, now: Callable[[], datetime] = utc_now, + required_checks: Sequence[str] | None = None, + env: Mapping[str, str] | None = None, ) -> dict[str, RequiredCheckStatus]: - return ReleaseRequiredChecksProjection(runner=runner, now=now).check_refs(repo_slug, refs, since=since) + return ReleaseRequiredChecksProjection(runner=runner, now=now, required_checks=required_checks, env=env).check_refs(repo_slug, refs, since=since) def main(argv: Sequence[str] | None = None) -> int: @@ -290,6 +309,7 @@ def main(argv: Sequence[str] | None = None) -> int: __all__ = [ "REQUIRED_RELEASE_CHECKS", + "HOST_REQUIRED_CHECKS_ENV", "ReleaseRequiredChecksProjection", "RequiredCheckStatus", "check_ref", @@ -297,4 +317,5 @@ def main(argv: Sequence[str] | None = None) -> int: "isoformat", "main", "parse_time", + "required_release_checks", ] diff --git a/skills/codex-refactor-loop/scripts/codex_refactor_loop/release/versions.py b/skills/codex-refactor-loop/scripts/codex_refactor_loop/release/versions.py index 95effa77..ba870120 100644 --- a/skills/codex-refactor-loop/scripts/codex_refactor_loop/release/versions.py +++ b/skills/codex-refactor-loop/scripts/codex_refactor_loop/release/versions.py @@ -7,11 +7,6 @@ from typing import Any -# Refactor (issue231-update-check): -# Old pattern: release semver parsing lived inside release-gate and callers -# compared versions as exact strings. -# New principle: keep parse/bump compatibility while sharing SemVer ordering -# for release preflight and notify-only update checks. SEMVER_RE = re.compile( r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)" r"(?:-((?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)" @@ -89,9 +84,6 @@ def next_release_version(version: str, bump_type: str) -> str: bump_type records commit impact; for beta.N / rc.N it never authorizes stage/core promotion. """ - # Refactor (iter272/issue-272): - # Old pattern: release-gate semver 不遵循预发布阶梯:beta.3+patch commits → 误算 1.0.1(GA,三重越阶) - # New principle: 结构化修复:新增 versions.next_release_version helper 按预发布阶梯递推(beta.N→beta.N+1,绝不自动升阶/off-ladder),preflight 增 off-ladder validation 拒绝越阶 target;不引入 schema v3 / ReleaseCoordinatePolicy current = parse_semver_full(version) if not current.prerelease: return bump_semver(version, bump_type) @@ -107,9 +99,6 @@ def next_release_version(version: str, bump_type: str) -> str: def validate_release_version_coordinate(from_version: str, to_version: str, bump_type: str | None) -> str | None: - # Refactor (iter272/issue-272): - # Old pattern: release-gate semver 不遵循预发布阶梯:beta.3+patch commits → 误算 1.0.1(GA,三重越阶) - # New principle: 结构化修复:新增 versions.next_release_version helper 按预发布阶梯递推(beta.N→beta.N+1,绝不自动升阶/off-ladder),preflight 增 off-ladder validation 拒绝越阶 target;不引入 schema v3 / ReleaseCoordinatePolicy if not isinstance(bump_type, str) or not bump_type: return "release_coordinate_off_ladder" try: diff --git a/skills/codex-refactor-loop/scripts/codex_refactor_loop/restart.py b/skills/codex-refactor-loop/scripts/codex_refactor_loop/restart.py index 821d6125..072c9ade 100644 --- a/skills/codex-refactor-loop/scripts/codex_refactor_loop/restart.py +++ b/skills/codex-refactor-loop/scripts/codex_refactor_loop/restart.py @@ -21,11 +21,6 @@ from .update_check import maybe_run_update_check -# Refactor (issue238/closed-label-reconciler): Old: Python kept a five-daemon -# static allowlist without a closed-item label reconciler. New: the sixth -# restart-managed daemon is the named #238 closed-only phase-label reconciler; -# wrapper authority remains local pid, actor-owned heartbeat, and fingerprint -# maintenance. CLI_ENTRYPOINT_NAME = "consensus-rnd-cli" DAEMON_COMMANDS: tuple[tuple[str, tuple[str, ...]], ...] = ( @@ -37,14 +32,6 @@ ("closed_label_reconciler", ("python3", "{skill_root}/scripts/consensus-rnd-cli", "closed-label-reconciler", "--daemon")), ) -# Refactor (iterissue-331/issue-331): -# Old pattern: release gate and wakeup_plan each kept local daemon-name -# literals, drifting from restart.py DAEMON_COMMANDS and duplicating the -# source of truth. -# New principle: restart.py::restart_managed_daemon_names() is the canonical -# daemon-name projection; release keeps DAEMON_NAMES only as a derived alias, -# wakeup deletes EXPECTED_DAEMONS, and health requires every restart-managed -# heartbeat to be fresh. def restart_managed_daemon_names() -> tuple[str, ...]: return tuple(name for name, _command in DAEMON_COMMANDS) @@ -64,10 +51,6 @@ class RestartConfig: stop_grace_seconds: int = int(os.environ.get("RESTART_DAEMONS_STOP_GRACE_SECONDS", "5")) -# Refactor (issue-298): Old: status guidance made controllers infer daemon -# state from write-side restart or ad hoc process probes. New: helper-private -# readers expose pid, heartbeat age, expected fingerprint, and static allowlist -# resolution for read-only daemon-status without adding lifecycle verbs. @dataclass(frozen=True) class DaemonTarget: """refactor helper, no behavior change outside read-only status projection.""" @@ -80,9 +63,6 @@ class DaemonTarget: died_file: Path -# Refactor (iter204/issue-204): -# Old pattern: restart-daemons kill daemon 后读 stale pidfile + 90s 内 heartbeat 误判存活、跳过 respawn(实测手 kill 5 daemon 后未 respawn 造成 outage);且无代码变更重启(daemon import 缓存旧代码)。 -# New principle: 按 r2 consensus structural 锁定:引入 restart-daemons 代码指纹 artifact(检测 daemon 脚本 mtime/hash vs 启动时,变更则 force-restart)+ 值对象边界,kill 后不误判 stale-pid 存活。配套 behavior(指纹变更触发 restart、kill 后正确 respawn)+ source-regression 测试。不扩大 process authority surface。 @dataclass(frozen=True) class DaemonLaunchFingerprint: """refactor helper, no behavior change outside restart skip eligibility.""" @@ -162,8 +142,6 @@ def to_json(self) -> dict[str, Any]: } -# Refactor (issue-264): Old: one fresh pidfile wrapper could hide duplicate canonical wrappers. -# New: helper-private process inventory reconciles static-allowlist duplicates before skip/start. @dataclass(frozen=True) class DaemonProcess: """refactor helper, no behavior change outside duplicate reconciliation.""" @@ -263,9 +241,6 @@ def heartbeat_is_fresh(target: DaemonTarget, config: RestartConfig, *, now: int return age is not None and age < config.heartbeat_fresh_seconds -# Refactor (iter204/issue-204): -# Old pattern: restart-daemons kill daemon 后读 stale pidfile + 90s 内 heartbeat 误判存活、跳过 respawn(实测手 kill 5 daemon 后未 respawn 造成 outage);且无代码变更重启(daemon import 缓存旧代码)。 -# New principle: 按 r2 consensus structural 锁定:引入 restart-daemons 代码指纹 artifact(检测 daemon 脚本 mtime/hash vs 启动时,变更则 force-restart)+ 值对象边界,kill 后不误判 stale-pid 存活。配套 behavior(指纹变更触发 restart、kill 后正确 respawn)+ source-regression 测试。不扩大 process authority surface。 class RestartDaemons: def __init__(self, ctx: LoopContext, config: RestartConfig | None = None) -> None: self.ctx = ctx @@ -276,10 +251,6 @@ def __init__(self, ctx: LoopContext, config: RestartConfig | None = None) -> Non def run(self) -> int: self._prepare_dirs() - # Refactor (issue238/closed-label-reconciler): Old pattern: every - # device could restart all controller write daemons. New principle: - # only the active-controller owner starts or maintains those daemons; - # non-owners leave local noop status and do not kill/start wrappers. decision = require_active_controller(self.ctx, "restart-daemons") write_active_controller_status(self.ctx, decision) if not decision.allowed: @@ -373,11 +344,6 @@ def _run_log_retention(self) -> None: self._log(f"log_retention: ttl_hours=24 deleted={deleted} kept={kept} target={target}{suffix}") def _run_update_check(self) -> None: - # Refactor (issue231-update-check): - # Old pattern: restart-daemons maintained only daemon wrappers and had - # no startup projection for installed skill version drift. - # New principle: after the fixed daemon start/skip pass, run the opt-in - # notify-only probe and log warnings without blocking daemon restart. try: result = maybe_run_update_check(self.ctx, startup=True) except Exception as exc: diff --git a/skills/codex-refactor-loop/scripts/codex_refactor_loop/review_fix_dispatch.py b/skills/codex-refactor-loop/scripts/codex_refactor_loop/review_fix_dispatch.py index 5ec4b137..8002c380 100644 --- a/skills/codex-refactor-loop/scripts/codex_refactor_loop/review_fix_dispatch.py +++ b/skills/codex-refactor-loop/scripts/codex_refactor_loop/review_fix_dispatch.py @@ -24,8 +24,6 @@ class ReviewFixDispatchSpec: def for_round(cls, pr_number: int, round_number: int) -> "ReviewFixDispatchSpec": pr = _validate_positive_int(pr_number, "pr_number") round_no = _validate_positive_int(round_number, "round_number") - # Refactor (issue-267): Old: review-fix output was a prompt-only - # convention. New: controller render owns the canonical artifact path. fix_output_path = f".refactor-loop/runs/fix-pr{pr}-round-{round_no}-report.md" return cls( prompt_path=f".refactor-loop/prompts/fixes/fix-pr{pr}-round-{round_no}.md", diff --git a/skills/codex-refactor-loop/scripts/codex_refactor_loop/sync/dev.py b/skills/codex-refactor-loop/scripts/codex_refactor_loop/sync/dev.py index fb285c2d..2c241fb9 100644 --- a/skills/codex-refactor-loop/scripts/codex_refactor_loop/sync/dev.py +++ b/skills/codex-refactor-loop/scripts/codex_refactor_loop/sync/dev.py @@ -1,10 +1,5 @@ """Dev integration sync daemon. -Refactor (issue160/p3-dev-sync-daemon): -Old pattern: `dev_sync_daemon.py` owned the integration sync state machine as a -top-level script with import-time host resolution. -New principle: expose the same daemon behavior from an import-safe package -module; legacy callers remain on the old script until the caller switch. """ from __future__ import annotations @@ -49,9 +44,6 @@ def load_dev_sync_config(env: dict[str, str] | None = None, cwd: Path | str | No source_env = dict(os.environ if env is None else env) ctx = LoopContext.load(env=source_env, cwd=cwd or os.getcwd()) merged_env = {**source_env, **ctx.host_env} - # Refactor (iter316/issue-316): - # Old pattern: dev-sync accepted private branch/worktree aliases beside host.env. - # New principle: use canonical LoopContext host facts only; derive the dedicated worktree. integration = merged_env.get("INTEGRATION_BRANCH") or DEFAULT_INTEGRATION_BRANCH review_base = merged_env.get("REVIEW_BASE_BRANCH") or DEFAULT_REVIEW_BASE_BRANCH worktree = ctx.repo_root / ".worktrees" / "dev-sync" @@ -186,8 +178,6 @@ def dispatch_codex_resolve( ) -> None: """Spawn a codex to resolve the in-progress merge conflicts in worktree. - Refactor (iter202/issue-202): Old pattern: durable artifact(ledger log_path、pending-event JSON log_path、meta-judge/reflector evidence、dev-sync resolver prompt、DEV_SYNC_REQUEST marker)写入 host absolute repo/worktree/log path,违反 CLAUDE.md R24『artifact 路径相对 $REPO_ROOT,不引入具体 host 事实』。 - New principle: 分层 durable-text-path vs execution-path:写入时所有 durable artifact/prompt/marker 只存 repo-relative POSIX text;读取或传 subprocess 时由 LoopContext.repo_root/rel_path 解析回 absolute;spawn-codex --cd/--add-dir/--prompt/--log 与 Popen argv 仍用 absolute(execution boundary 非 durable truth)。配套 behavior(写入存相对、读取解析绝对)+ source-regression(无 host absolute prefix)测试。不改 daemon lifecycle authority,不加规则例外。 """ ctx = LoopContext.load(repo_root=main_repo) ts = int(time.time()) @@ -276,8 +266,6 @@ def working_tree_dirty(cwd: Path, command_runner=run) -> bool: def merge_in_progress(cwd: Path, command_runner=run) -> bool: """Detect an in-progress merge in normal checkouts and linked worktrees.""" - # Refactor (issue-264): Old: stale MERGE_MSG was treated as live merge evidence. - # New: only MERGE_HEAD proves an in-progress merge; stale messages fall through. result = command_runner(["git", "-C", str(cwd), "rev-parse", "--git-path", "MERGE_HEAD"]) return result.returncode == 0 and Path(result.stdout.strip()).exists() @@ -295,22 +283,9 @@ class RollupDetection: adoption: RollupAdoption | None = None -# Refactor (iter4/skill-dev-sync-state-machine): Old pattern: scattered active -# controller-owned sync recipe + implicit daemon transition. New principle: -# named IntegrationSyncDaemon state machine boundary. -# Refactor (iter/issue-199): -# Old pattern: daemon emitted request artifacts and a controller side-channel -# marker. New principle: daemon writes IntegrationSyncOperation evidence and -# executes the #53 integration-branch git allowlist itself. -# Refactor (iter5/issue107-python-identifier-rename): Old pattern: version -# suffix in daemon class/schema names. New principle: naked responsibility names -# carry stable artifact intent; compatibility/version policy lives in -# contracts/tests, not identifier suffixes. class IntegrationSyncDaemon: """Narrow detector and executor for integration-branch sync transitions. - Refactor (iter202/issue-202): Old pattern: durable artifact(ledger log_path、pending-event JSON log_path、meta-judge/reflector evidence、dev-sync resolver prompt、DEV_SYNC_REQUEST marker)写入 host absolute repo/worktree/log path,违反 CLAUDE.md R24『artifact 路径相对 $REPO_ROOT,不引入具体 host 事实』。 - New principle: 分层 durable-text-path vs execution-path:写入时所有 durable artifact/prompt/marker 只存 repo-relative POSIX text;读取或传 subprocess 时由 LoopContext.repo_root/rel_path 解析回 absolute;spawn-codex --cd/--add-dir/--prompt/--log 与 Popen argv 仍用 absolute(execution boundary 非 durable truth)。配套 behavior(写入存相对、读取解析绝对)+ source-regression(无 host absolute prefix)测试。不改 daemon lifecycle authority,不加规则例外。 """ def __init__( @@ -521,11 +496,6 @@ def release_rollup_event_recently_emitted(self, integration_sha: str, now: datet return True return False - # Refactor (iter5/issue-65-release-rollup-pending-event): - # Old pattern: no release-rollup detection when integration was ahead of - # the review base without an open PR. - # New principle: detect ahead + no open PR, then emit the existing - # DEV_SYNC_PENDING release-rollup line format for controller sweep. def detect_release_rollup_needed(self, cwd: Path) -> bool: if self.release_rollup_min_commits <= 0: return False @@ -644,10 +614,6 @@ def detect_merged_rollup(self, cwd: Path) -> RollupDetection | None: ), ) - # Refactor (fix/issue282-devsync-adoption-ambiguity): - # Old pattern: adoption metadata ambiguity consumed the whole daemon tick. - # New principle: adoption ambiguity blocks only force-with-lease adoption; - # later reset, forward-sync, and release-rollup gates still run. def execute_merged_rollup_adoption(self, cwd: Path, adoption: RollupAdoption) -> bool: if not adoption.expected_remote_sha: self.append_pending_event("rollup-adoption-ambiguous", "missing-expected-remote-sha") @@ -729,11 +695,6 @@ def execute_reset_to_remote(self, cwd: Path) -> bool: return True def tick(self) -> None: - # Refactor (impl/issue191-single-active-controller): Old pattern: - # multiple dev-sync daemons could touch the integration worktree and - # execute the #53 git allowlist concurrently. New principle: the #53 - # allowlist remains narrow, and only the active-controller owner may - # enter it. if self.context is not None: decision = require_active_controller(self.context, "dev-sync") write_active_controller_status(self.context, decision) @@ -789,10 +750,6 @@ def tick(self) -> None: if self.execute_clean_local_ahead(cwd): return - # Refactor (fix/issue282-devsync-adoption-ambiguity): - # Old pattern: ambiguous rollup detection returned before later sync gates. - # New principle: only a constructed adoption operation consumes the tick; - # ambiguity falls through to the existing fail-closed sync gates. rollup = self.detect_merged_rollup(cwd) if rollup and rollup.status == "adopt" and rollup.adoption: if self.execute_merged_rollup_adoption(cwd, rollup.adoption): diff --git a/skills/codex-refactor-loop/scripts/codex_refactor_loop/sync/executor.py b/skills/codex-refactor-loop/scripts/codex_refactor_loop/sync/executor.py index 55828fbf..980de2c2 100644 --- a/skills/codex-refactor-loop/scripts/codex_refactor_loop/sync/executor.py +++ b/skills/codex-refactor-loop/scripts/codex_refactor_loop/sync/executor.py @@ -1,10 +1,5 @@ """Daemon-owned executor for typed IntegrationSyncOperation artifacts. -Refactor (iter/issue-199): - Old pattern: controller helper consumed request artifacts through a public - CLI and owned integration branch git mutation. - New principle: IntegrationSyncExecutor executes typed daemon operations - through the narrow #53 integration-branch git allowlist only. """ from __future__ import annotations @@ -96,9 +91,6 @@ def _ensure_clean_or_merge( def _expected_branches(self, env: dict[str, str] | None = None) -> tuple[str, str]: source_env = os.environ if env is None else env - # Refactor (iter316/issue-316): - # Old pattern: sync executor accepted unregistered branch aliases. - # New principle: only canonical branch names participate in operation validation. expected_integration = source_env.get("INTEGRATION_BRANCH") or DEFAULT_INTEGRATION_BRANCH expected_review_base = source_env.get("REVIEW_BASE_BRANCH") or DEFAULT_REVIEW_BASE_BRANCH return expected_integration, expected_review_base diff --git a/skills/codex-refactor-loop/scripts/codex_refactor_loop/sync/operations.py b/skills/codex-refactor-loop/scripts/codex_refactor_loop/sync/operations.py index 56bebb4e..f8ab8246 100644 --- a/skills/codex-refactor-loop/scripts/codex_refactor_loop/sync/operations.py +++ b/skills/codex-refactor-loop/scripts/codex_refactor_loop/sync/operations.py @@ -1,10 +1,5 @@ """Integration sync operation schema for daemon-owned execution. -Refactor (iter/issue-199): - Old pattern: controller-owned request artifacts carried lifecycle owner - fields and were applied by a public CLI. - New principle: daemon writes typed IntegrationSyncOperation artifacts with - #53 authority and no command envelope or controller lifecycle fields. """ from __future__ import annotations diff --git a/skills/codex-refactor-loop/scripts/codex_refactor_loop/transition_assessment.py b/skills/codex-refactor-loop/scripts/codex_refactor_loop/transition_assessment.py index 201a7062..cca51ee9 100644 --- a/skills/codex-refactor-loop/scripts/codex_refactor_loop/transition_assessment.py +++ b/skills/codex-refactor-loop/scripts/codex_refactor_loop/transition_assessment.py @@ -9,9 +9,6 @@ from typing import Any -# Refactor (issue-262): Old: transition ranking/prompt facts had no checked-in -# reader, so callers would need ad hoc sidecar parsing. New: this module is the -# only read-only transition_assessment reader and fails closed to unknown. SAFE_WORK_UNIT_ID_RE = re.compile(r"^[A-Za-z0-9._-]+$") ALLOWED_PRODUCERS = frozenset({"audit", "manual-issue"}) TRANSITION_BUCKET_ORDER = { diff --git a/skills/codex-refactor-loop/scripts/codex_refactor_loop/triage.py b/skills/codex-refactor-loop/scripts/codex_refactor_loop/triage.py index 829f1ecb..d7771890 100644 --- a/skills/codex-refactor-loop/scripts/codex_refactor_loop/triage.py +++ b/skills/codex-refactor-loop/scripts/codex_refactor_loop/triage.py @@ -1,9 +1,5 @@ """Manual issue triage decision artifacts and controller apply. -Refactor (issue160/p3-triage): -Old pattern: `triage_decisions.py` and `apply_triage_decision.py` split the -ManualIssueTriageDecision schema from controller-owned GitHub apply behavior. -New principle: expose the same schema validation and bounded apply behavior from an import-safe package module; legacy script callers remain unchanged until the caller switch. Host facts still resolve through host.env via LoopContext. @@ -48,9 +44,6 @@ class HostIssueIntakeMappingError(ValueError): @dataclass(frozen=True) class ManualIssueTriageDecision: - # Refactor (iter219/issue-219): - # Old pattern: host 无法按 GitHub 模板自定义事件流/工作流/issue/prompt;workflow vocabulary 是闭集硬编码 - # New principle: 引入 data-only HostWorkflowSpec(HOST_WORKFLOW_SPEC,repo-relative JSON)+ WorkflowInvariantValidator;空/未设=built-in 行为;host 只能在 host: 命名空间加 data,不能覆盖 built-in/降共识闸/夺 lifecycle authority。严格按 plan 'Concrete plan' 逐条改,首版 scope 受限。 issue_number: int verdict: str body_artifact_path: str @@ -163,9 +156,6 @@ def validate_decision_dict(data: dict[str, Any], *, expected_issue: int | None = def host_issue_intake_projection(ctx: LoopContext, mapping_name: str) -> dict[str, str]: - # Refactor (iter219/issue-219): - # Old pattern: host 无法按 GitHub 模板自定义事件流/工作流/issue/prompt;workflow vocabulary 是闭集硬编码 - # New principle: 引入 data-only HostWorkflowSpec(HOST_WORKFLOW_SPEC,repo-relative JSON)+ WorkflowInvariantValidator;空/未设=built-in 行为;host 只能在 host: 命名空间加 data,不能覆盖 built-in/降共识闸/夺 lifecycle authority。严格按 plan 'Concrete plan' 逐条改,首版 scope 受限。 try: spec = load_validated_workflow_spec(ctx) except WorkflowSpecError as exc: diff --git a/skills/codex-refactor-loop/scripts/codex_refactor_loop/update_check.py b/skills/codex-refactor-loop/scripts/codex_refactor_loop/update_check.py index 52fce60f..85c5f7bb 100644 --- a/skills/codex-refactor-loop/scripts/codex_refactor_loop/update_check.py +++ b/skills/codex-refactor-loop/scripts/codex_refactor_loop/update_check.py @@ -66,11 +66,6 @@ class GitHubReleaseVersion: class UpdateCheckProbe: """Read local version and GitHub release/tag state, then write local notice state. - Refactor (issue231-update-check): - Old pattern: downstream installs had no checked-in version snapshot or - notify-only update surface. - New principle: opt-in probe only reads VERSION.json and GitHub release/tag - metadata, writes local state, and never applies, installs, or mutates hosts. """ def __init__( diff --git a/skills/codex-refactor-loop/scripts/codex_refactor_loop/wakeup_plan.py b/skills/codex-refactor-loop/scripts/codex_refactor_loop/wakeup_plan.py index e97af2f7..ed6b6d6a 100755 --- a/skills/codex-refactor-loop/scripts/codex_refactor_loop/wakeup_plan.py +++ b/skills/codex-refactor-loop/scripts/codex_refactor_loop/wakeup_plan.py @@ -1,27 +1,6 @@ #!/usr/bin/env python3 """Read-only wakeup planner for codex-refactor-loop controllers. -Refactor (iter1/wakeup-plan-script): - Old pattern: controller wakeups assembled priority from peek.sh, log greps, - GitHub checks, and floor rules by hand, making milestone and audit-none - ordering easy to drift. - New principle: one read-only prioritized-next-action script emits structured - JSON from local evidence plus GitHub labels while leaving every lifecycle - action to the controller. - -Allowed: read `.refactor-loop` files, read daemon heartbeats, run read-only -GitHub list/check/view commands, observe git topology with the issue-190 -allowlist (`git fetch origin --quiet`, `git worktree list --porcelain`, -`git rev-parse --verify HEAD`, `git rev-parse --verify refs/remotes/origin/`, -and `git rev-list --count refs/remotes/origin/..HEAD`), and print JSON -recommendations. Forbidden: no restart/spawn, no git lifecycle or mutation -commands, no GitHub lifecycle mutation, no commit, push, checkout/switch, -branch create/delete/update, worktree add/remove/prune, reset, rebase, merge, -label, issue/PR create/close/edit, tag, release, or worker dispatch. -Authorization source: -`skills/codex-refactor-loop/authorizations/runtime-exceptions.md#maintainer-directive-wakeup-plan-script`. -Issue-190 consensus source: -`.refactor-loop/runs/phase9-issue190-r3-judge.md`. """ from __future__ import annotations @@ -41,6 +20,7 @@ from codex_refactor_loop import labels as label_catalog from codex_refactor_loop.context import LoopContext from codex_refactor_loop.pr_checks import PrChecksProjection +from codex_refactor_loop.release.gate import decide_release_artifact from codex_refactor_loop.restart import restart_managed_daemon_names from codex_refactor_loop.transition_assessment import TransitionAssessmentReader, transition_rank_key from codex_refactor_loop.work_items import ManagedWorkProjection, open_actionable_managed_items @@ -124,9 +104,6 @@ def run_json(cmd: list[str], *, cwd: Path) -> Any: def load_host_workflow_projection(repo_root: Path) -> tuple[list[dict[str, Any]], str | None]: - # Refactor (iter219/issue-219): - # Old pattern: host 无法按 GitHub 模板自定义事件流/工作流/issue/prompt;workflow vocabulary 是闭集硬编码 - # New principle: 引入 data-only HostWorkflowSpec(HOST_WORKFLOW_SPEC,repo-relative JSON)+ WorkflowInvariantValidator;空/未设=built-in 行为;host 只能在 host: 命名空间加 data,不能覆盖 built-in/降共识闸/夺 lifecycle authority。严格按 plan 'Concrete plan' 逐条改,首版 scope 受限。 try: ctx = LoopContext.load(repo_root=repo_root, env=os.environ, cwd=repo_root, read_only=True) spec = load_validated_workflow_spec(ctx) @@ -162,9 +139,6 @@ def _canonical_in_flight_for_log(log_path: Path, monitor: Any | None) -> bool: def harness_spawn_intent_actions(repo_root: Path, ctx: LoopContext, monitor: Any | None = None) -> list[dict[str, Any]]: - # Refactor (iterissue-330/issue-330): - # Old pattern: daemon nohup spawn bypassed the harness-visible contract; command could mean argv/shell. - # New principle: HARNESS_SPAWN_INTENT.command is closed enum Literal['spawn-codex']; argv is built by controller/harness. pending_path = ctx.paths.pending_events if not pending_path.exists(): return [] @@ -278,20 +252,11 @@ def configured_floor() -> int: def resolve_repo_root(arg_root: str | None) -> Path: - # Refactor (iter316/issue-316): - # Old pattern: wakeup_plan guessed repo root from cwd and parsed host.env itself. - # New principle: LoopContext owns repo-root/host.env loading; no private cwd default or parser. ctx = LoopContext.load(repo_root=arg_root, env=os.environ, cwd=Path.cwd(), read_only=True) return ctx.repo_root def import_concurrency_monitor(repo_root: Path) -> Any | None: - # Refactor (iter2/wakeup-plan-hardgate): - # Old pattern: wakeup routing could finish with a hidden concurrency gap - # because only the daemon knew the canonical count. - # New principle: wakeup_plan imports the daemon's read-only count/expected - # helpers after pinning REPO_ROOT, so the controller sees a hard gate before - # it can end the wakeup. os.environ["REPO_ROOT"] = str(repo_root) try: module_name = "codex_refactor_loop.monitors.concurrency" @@ -350,10 +315,6 @@ def canonical_expected_from_active_tasks(monitor: Any | None) -> tuple[int, list def expected_from_open_items(items: list[GhItem]) -> tuple[int, list[dict[str, Any]]]: breakdown: list[dict[str, Any]] = [] total = 0 - # Refactor (impl/issue239-linkage): - # Old pattern: wakeup hard-gate counted parent issue and child PR as - # separate active work. New principle: share ManagedWorkProjection with - # the daemon so represented parents are non-action expected_workers=0. for item in ManagedWorkProjection(_projection_items(items)).effective_worker_items(): labels = set(item.labels) if label_catalog.HUMAN_MAINTAINER_DECISION in label_catalog.normalize_label_set(labels).canonical: @@ -375,11 +336,6 @@ def concurrency_plan( monitor: Any | None = None, concurrency_module: Any | None = None, ) -> dict[str, Any]: - # Refactor (issue-277): - # Old pattern: AUDIT_DONE:none:0 converted a positive deficit into an - # exemption, then floor-no-exemption made audit repeatable without a slot. - # New principle: positive deficits stay visible; duplicate same-iteration - # audit is not legal dispatch when that audit is already active. if concurrency_module is None: concurrency_module = import_concurrency_monitor(repo_root) if monitor is None: @@ -415,7 +371,7 @@ def concurrency_plan( "dispatch_required": deficit if hard_gate_active else 0, "line": hard_gate_line, "semantics": ( - "controller must dispatch this many actionable tasks or audit fallback before ending the wakeup" + "controller must dispatch this many actionable managed issue/PR tasks or legal fallback issue production through audit before ending the wakeup" if hard_gate_active else None ), @@ -427,14 +383,6 @@ def concurrency_plan( def daemon_health(repo_root: Path, now: float | None = None) -> dict[str, Any]: - # Refactor (iterissue-331/issue-331): - # Old pattern: release gate and wakeup_plan each kept local daemon-name - # literals, drifting from restart.py DAEMON_COMMANDS and duplicating the - # source of truth. - # New principle: restart.py::restart_managed_daemon_names() is the - # canonical daemon-name projection; release keeps DAEMON_NAMES only as a - # derived alias, wakeup deletes EXPECTED_DAEMONS, and health requires - # every restart-managed heartbeat to be fresh. if now is None: now = time.time() heartbeat_dir = repo_root / ".refactor-loop" / "heartbeats" @@ -712,10 +660,6 @@ def github_repo_slug() -> str | None: def load_github_items(repo_root: Path) -> list[GhItem]: - # Refactor (issue-162/wakeup-open-only): - # Old pattern: action planning risked mixing closed or merged auto-loop - # records into dispatch candidates. - # New principle: actions are derived only from open auto-loop issues/PRs. slug = github_repo_slug() items: list[GhItem] = [] for kind, gh_kind in (("issue", "issue"), ("PR", "pr")): @@ -781,10 +725,6 @@ def safe_head_ref(value: str | None) -> str | None: def unpushed_worker_output_actions(repo_root: Path, gh_items: list[GhItem]) -> list[dict[str, Any]]: - # Refactor (iter201/issue-201): Old pattern: wakeup_plan rendered a copyable - # consensus-rnd-cli safe-push suggested_command, exposing public lifecycle - # reachability. New principle: emit only a fixed controller_action fact with - # no_lifecycle_authority; controller maps it to an internal primitive. prs = [item for item in gh_items if item.kind == "PR" and safe_head_ref(item.head_ref)] if not prs: return [] @@ -845,8 +785,6 @@ def ci_red_actions(repo_root: Path, items: list[GhItem]) -> list[dict[str, Any]] for item in items: if item.kind != "PR": continue - # Refactor (issue-297): Old: ci-red routed through a naked PR checks CLI. - # New: wakeup-plan consumes the named PR-head Checks API projection. status = projection.check_pr(slug, item.number) if not status.ok: continue @@ -872,9 +810,6 @@ def existing_issue_actions(items: list[GhItem], repo_root: Path | None = None) - actions: list[dict[str, Any]] = [] raw_by_key = {(item.kind.lower(), item.number): item for item in items} actionable = open_actionable_managed_items(_projection_items(items)) - # Refactor (issue-262): Old: existing issue ranking only used milestone, - # kind, and number. New: a checked-in caller may use the validated - # transition_assessment sidecar bucket before the existing tie-breakers. ordered = sorted(actionable, key=lambda item: _existing_issue_sort_key(item, repo_root)) for item in ordered: raw = raw_by_key.get((item.kind, item.number)) @@ -896,6 +831,46 @@ def existing_issue_actions(items: list[GhItem], repo_root: Path | None = None) - return actions +def release_countdown_actions(repo_root: Path, items: list[GhItem], scorer: Any | None = None) -> list[dict[str, Any]]: + targets = [] + for item in open_actionable_managed_items(_projection_items(items)): + projection = label_catalog.normalize_label_set(item.labels) + if label_catalog.MILESTONE_RELEASE_TARGET not in projection.canonical: + continue + targets.append( + { + "kind": "PR" if item.kind == "pr" else item.kind, + "number": item.number, + "item": f"{'PR' if item.kind == 'pr' else item.kind} #{item.number}", + "title": item.title, + } + ) + if not targets: + return [] + + score = (scorer or decide_release_artifact)(repo_root) + signals = score.get("signals") if isinstance(score.get("signals"), dict) else {} + red_signals = [name for name, signal in signals.items() if isinstance(signal, dict) and not signal.get("passed")] + blocked = score.get("blocked_reasons") + blocked_reasons = [str(reason) for reason in blocked] if isinstance(blocked, list) else red_signals + return [ + { + "priority": 7, + "kind": "release-countdown", + "status_only": True, + "no_lifecycle_authority": True, + "targets": targets, + "from_version": score.get("from_version"), + "to_version": score.get("to_version"), + "stability_score": score.get("stability_score"), + "ready": bool(score.get("ready")), + "red_signals": red_signals, + "blocked_reasons": blocked_reasons, + "source": "release-gate", + } + ] + + def has_dispatchable_action(actions: list[dict[str, Any]]) -> bool: dispatchable = { "maintainer-comment", @@ -920,7 +895,7 @@ def restore_hard_gate_for_dispatchable_actions(concurrency: dict[str, Any], acti "dispatch_required": deficit if deficit > 0 else 0, "line": f"HARD_GATE:dispatch_required={deficit}" if deficit > 0 else None, "semantics": ( - "controller must dispatch this many actionable tasks or audit fallback before ending the wakeup" + "controller must dispatch this many actionable managed issue/PR tasks or legal fallback issue production through audit before ending the wakeup" if deficit > 0 else None ), @@ -1043,6 +1018,7 @@ def build_plan(repo_root: Path) -> dict[str, Any]: ) else: actions.extend(host_actions) + actions.extend(release_countdown_actions(repo_root, gh_items)) actions.extend(existing_issue_actions(gh_items, repo_root)) actions.sort(key=lambda action: action["priority"]) restore_hard_gate_for_dispatchable_actions(concurrency, actions) diff --git a/skills/codex-refactor-loop/scripts/codex_refactor_loop/work_items.py b/skills/codex-refactor-loop/scripts/codex_refactor_loop/work_items.py index 8c1b8661..2099e7d8 100644 --- a/skills/codex-refactor-loop/scripts/codex_refactor_loop/work_items.py +++ b/skills/codex-refactor-loop/scripts/codex_refactor_loop/work_items.py @@ -55,11 +55,6 @@ def extract_closing_issue_numbers(body: str) -> tuple[int, ...]: class ManagedWorkProjection: - # Refactor (impl/issue239-linkage): - # Old pattern: controller, concurrency, wakeup, and peek each interpreted - # PR body `Closes #N` linkage independently or not at all. - # New principle: one read-only projection owns the represented-parent - # semantics; lifecycle mutations stay in ControllerActions. def __init__(self, items: Iterable[ManagedItem | Mapping[str, object]]) -> None: self.items = tuple(_coerce_item(item) for item in items) self.links = self._links() diff --git a/skills/codex-refactor-loop/scripts/codex_refactor_loop/workflow_spec.py b/skills/codex-refactor-loop/scripts/codex_refactor_loop/workflow_spec.py index 2542fce4..fef86944 100644 --- a/skills/codex-refactor-loop/scripts/codex_refactor_loop/workflow_spec.py +++ b/skills/codex-refactor-loop/scripts/codex_refactor_loop/workflow_spec.py @@ -69,13 +69,19 @@ class WorkflowSpecError(ValueError): *(stage.slug for stage in WORKFLOW_STAGES), } FIXED_MARKER_FAMILIES = ("SOLVER_DONE", "META_JUDGE_DONE", "META_RESOLVED") +WORKFLOW_PROJECTION_KEYS = ( + "events", + "stages", + "work_unit_kinds", + "roles", + "prompt_bindings", + "consensus_policies", + "issue_intake_mappings", +) @dataclass(frozen=True) class HostWorkflowEvent: - # Refactor (iter219/issue-219): - # Old pattern: host 无法按 GitHub 模板自定义事件流/工作流/issue/prompt;workflow vocabulary 是闭集硬编码 - # New principle: 引入 data-only HostWorkflowSpec(HOST_WORKFLOW_SPEC,repo-relative JSON)+ WorkflowInvariantValidator;空/未设=built-in 行为;host 只能在 host: 命名空间加 data,不能覆盖 built-in/降共识闸/夺 lifecycle authority。严格按 plan 'Concrete plan' 逐条改,首版 scope 受限。 name: str stage: str status: str = "" @@ -84,18 +90,12 @@ class HostWorkflowEvent: @dataclass(frozen=True) class HostWorkflowRole: - # Refactor (iter219/issue-219): - # Old pattern: host 无法按 GitHub 模板自定义事件流/工作流/issue/prompt;workflow vocabulary 是闭集硬编码 - # New principle: 引入 data-only HostWorkflowSpec(HOST_WORKFLOW_SPEC,repo-relative JSON)+ WorkflowInvariantValidator;空/未设=built-in 行为;host 只能在 host: 命名空间加 data,不能覆盖 built-in/降共识闸/夺 lifecycle authority。严格按 plan 'Concrete plan' 逐条改,首版 scope 受限。 name: str prompt_binding: str = "" @dataclass(frozen=True) class HostConsensusPolicy: - # Refactor (iter219/issue-219): - # Old pattern: host 无法按 GitHub 模板自定义事件流/工作流/issue/prompt;workflow vocabulary 是闭集硬编码 - # New principle: 引入 data-only HostWorkflowSpec(HOST_WORKFLOW_SPEC,repo-relative JSON)+ WorkflowInvariantValidator;空/未设=built-in 行为;host 只能在 host: 命名空间加 data,不能覆盖 built-in/降共识闸/夺 lifecycle authority。严格按 plan 'Concrete plan' 逐条改,首版 scope 受限。 name: str solver_roles: tuple[str, ...] judge_role: str @@ -106,9 +106,6 @@ class HostConsensusPolicy: @dataclass(frozen=True) class HostIssueIntakeMapping: - # Refactor (iter219/issue-219): - # Old pattern: host 无法按 GitHub 模板自定义事件流/工作流/issue/prompt;workflow vocabulary 是闭集硬编码 - # New principle: 引入 data-only HostWorkflowSpec(HOST_WORKFLOW_SPEC,repo-relative JSON)+ WorkflowInvariantValidator;空/未设=built-in 行为;host 只能在 host: 命名空间加 data,不能覆盖 built-in/降共识闸/夺 lifecycle authority。严格按 plan 'Concrete plan' 逐条改,首版 scope 受限。 name: str work_unit_kind: str producer: str @@ -118,9 +115,6 @@ class HostIssueIntakeMapping: @dataclass(frozen=True) class ValidatedWorkflowSpec: - # Refactor (iter219/issue-219): - # Old pattern: host 无法按 GitHub 模板自定义事件流/工作流/issue/prompt;workflow vocabulary 是闭集硬编码 - # New principle: 引入 data-only HostWorkflowSpec(HOST_WORKFLOW_SPEC,repo-relative JSON)+ WorkflowInvariantValidator;空/未设=built-in 行为;host 只能在 host: 命名空间加 data,不能覆盖 built-in/降共识闸/夺 lifecycle authority。严格按 plan 'Concrete plan' 逐条改,首版 scope 受限。 source_path: Path | None stages: tuple[WorkflowStage, ...] events: tuple[HostWorkflowEvent, ...] @@ -140,12 +134,49 @@ def stage_for_event(self, event_name: str) -> str | None: def prompt_binding_path(self, name: str) -> str | None: return self.prompt_bindings.get(name) + def projection(self) -> dict[str, Any]: + return { + "events": [ + {"name": event.name, "stage": event.stage, "status": event.status, "actor": event.actor} + for event in self.events + ], + "stages": [ + { + "slug": stage.slug, + "title": stage.title, + "contract": stage.contract, + "detail_anchor": stage.detail_anchor, + } + for stage in self.stages + ], + "work_unit_kinds": list(self.work_unit_kinds), + "roles": [{"name": role.name, "prompt_binding": role.prompt_binding} for role in self.roles], + "prompt_bindings": dict(self.prompt_bindings), + "consensus_policies": [ + { + "name": policy.name, + "solver_roles": list(policy.solver_roles), + "judge_role": policy.judge_role, + "peer_output_isolation": policy.peer_output_isolation, + "marker_families": list(policy.marker_families), + "stage": policy.stage, + } + for policy in self.consensus_policies + ], + "issue_intake_mappings": [ + { + "name": mapping.name, + "work_unit_kind": mapping.work_unit_kind, + "producer": mapping.producer, + "stage": mapping.stage, + "prompt_binding": mapping.prompt_binding, + } + for mapping in self.issue_intake_mappings + ], + } -class HostWorkflowSpec: - # Refactor (iter219/issue-219): - # Old pattern: host 无法按 GitHub 模板自定义事件流/工作流/issue/prompt;workflow vocabulary 是闭集硬编码 - # New principle: 引入 data-only HostWorkflowSpec(HOST_WORKFLOW_SPEC,repo-relative JSON)+ WorkflowInvariantValidator;空/未设=built-in 行为;host 只能在 host: 命名空间加 data,不能覆盖 built-in/降共识闸/夺 lifecycle authority。严格按 plan 'Concrete plan' 逐条改,首版 scope 受限。 +class HostWorkflowSpec: def __init__(self, data: Mapping[str, Any], *, source_path: Path, repo_root: Path) -> None: self.data = dict(data) self.source_path = source_path @@ -156,19 +187,7 @@ def validate(self) -> ValidatedWorkflowSpec: class WorkflowInvariantValidator: - # Refactor (iter219/issue-219): - # Old pattern: host 无法按 GitHub 模板自定义事件流/工作流/issue/prompt;workflow vocabulary 是闭集硬编码 - # New principle: 引入 data-only HostWorkflowSpec(HOST_WORKFLOW_SPEC,repo-relative JSON)+ WorkflowInvariantValidator;空/未设=built-in 行为;host 只能在 host: 命名空间加 data,不能覆盖 built-in/降共识闸/夺 lifecycle authority。严格按 plan 'Concrete plan' 逐条改,首版 scope 受限。 - - ALLOWED_TOP_LEVEL = { - "events", - "stages", - "work_unit_kinds", - "roles", - "prompt_bindings", - "consensus_policies", - "issue_intake_mappings", - } + ALLOWED_TOP_LEVEL = set(WORKFLOW_PROJECTION_KEYS) def __init__(self, repo_root: Path, source_path: Path | None = None) -> None: self.repo_root = repo_root.resolve() @@ -403,9 +422,6 @@ def _repo_relative_file(self, text: str, label: str) -> str: class WorkflowSpecLoader: - # Refactor (iter219/issue-219): - # Old pattern: host 无法按 GitHub 模板自定义事件流/工作流/issue/prompt;workflow vocabulary 是闭集硬编码 - # New principle: 引入 data-only HostWorkflowSpec(HOST_WORKFLOW_SPEC,repo-relative JSON)+ WorkflowInvariantValidator;空/未设=built-in 行为;host 只能在 host: 命名空间加 data,不能覆盖 built-in/降共识闸/夺 lifecycle authority。严格按 plan 'Concrete plan' 逐条改,首版 scope 受限。 _cache: dict[tuple[Path, int, int], ValidatedWorkflowSpec] = {} @classmethod diff --git a/skills/codex-refactor-loop/scripts/codex_refactor_loop/workflow_stages.py b/skills/codex-refactor-loop/scripts/codex_refactor_loop/workflow_stages.py index fae4214d..d501142a 100644 --- a/skills/codex-refactor-loop/scripts/codex_refactor_loop/workflow_stages.py +++ b/skills/codex-refactor-loop/scripts/codex_refactor_loop/workflow_stages.py @@ -1,11 +1,5 @@ """Closed workflow stage registry for codex-refactor-loop. -Refactor (iter3/workflow-stage-registry): - Old pattern: controller-facing workflow vocabulary was encoded as numeric - phase display text plus ad hoc string literals across docs, prompts, and - wakeup routing. - New principle: one closed registry owns public stage slugs and display text; - legacy numbers remain private migration metadata only. """ from __future__ import annotations @@ -15,9 +9,6 @@ @dataclass(frozen=True) class WorkflowStage: - # Refactor (iter219/issue-219): - # Old pattern: host 无法按 GitHub 模板自定义事件流/工作流/issue/prompt;workflow vocabulary 是闭集硬编码 - # New principle: 引入 data-only HostWorkflowSpec(HOST_WORKFLOW_SPEC,repo-relative JSON)+ WorkflowInvariantValidator;空/未设=built-in 行为;host 只能在 host: 命名空间加 data,不能覆盖 built-in/降共识闸/夺 lifecycle authority。严格按 plan 'Concrete plan' 逐条改,首版 scope 受限。 slug: str title: str contract: str @@ -36,7 +27,7 @@ class WorkflowStage: WorkflowStage( "work-intake", "Work Intake", - "Produce work-unit items. Audit remains the default compatibility producer; manual issue intake is separate.", + "Fallback issue production when no actionable managed issue/PR exists; audit is the built-in compatibility producer.", "work-unit-contract", 1, ), @@ -102,9 +93,6 @@ class WorkflowStage: def stage_by_slug(slug: str, extra_stages: tuple[WorkflowStage, ...] = ()) -> WorkflowStage: - # Refactor (iter219/issue-219): - # Old pattern: host 无法按 GitHub 模板自定义事件流/工作流/issue/prompt;workflow vocabulary 是闭集硬编码 - # New principle: 引入 data-only HostWorkflowSpec(HOST_WORKFLOW_SPEC,repo-relative JSON)+ WorkflowInvariantValidator;空/未设=built-in 行为;host 只能在 host: 命名空间加 data,不能覆盖 built-in/降共识闸/夺 lifecycle authority。严格按 plan 'Concrete plan' 逐条改,首版 scope 受限。 stages = {**_STAGES_BY_SLUG, **{stage.slug: stage for stage in extra_stages}} try: return stages[slug] diff --git a/skills/codex-refactor-loop/scripts/test_auto_release_gate.py b/skills/codex-refactor-loop/scripts/test_auto_release_gate.py index cdcd52b6..ed399bca 100644 --- a/skills/codex-refactor-loop/scripts/test_auto_release_gate.py +++ b/skills/codex-refactor-loop/scripts/test_auto_release_gate.py @@ -86,6 +86,7 @@ def write_opt_in( f"export GH_REPO_SLUG={repo_slug}", f"export REVIEW_BASE_BRANCH={review_base}", f"export INTEGRATION_BRANCH={integration}", + "export HOST_GITHUB_RELEASE_REQUIRED_CHECKS=contract-tests,manifest-version-sync,skill-degradation", "", ] ), diff --git a/skills/codex-refactor-loop/scripts/test_check_skill_degradation.py b/skills/codex-refactor-loop/scripts/test_check_skill_degradation.py index 12c1a6fd..491e8a31 100644 --- a/skills/codex-refactor-loop/scripts/test_check_skill_degradation.py +++ b/skills/codex-refactor-loop/scripts/test_check_skill_degradation.py @@ -219,11 +219,11 @@ def test_checker_detects_forbidden_runtime_file(self) -> None: self.assertTrue(any(f.check == "forbidden-runtime-file" and "degradation_watchdog.py" in f.path for f in findings)) - def test_checker_detects_release_projection_missing_skill_degradation(self) -> None: + def test_checker_detects_release_projection_missing_host_required_checks_parser(self) -> None: with copy_minimal_repo() as tmp: repo = Path(tmp) / "repo" projection = repo / "skills/codex-refactor-loop/scripts/codex_refactor_loop/release/required_checks.py" - projection.write_text(projection.read_text(encoding="utf-8").replace(', "skill-degradation"', ""), encoding="utf-8") + projection.write_text(projection.read_text(encoding="utf-8").replace("required_release_checks", "removed_release_checks"), encoding="utf-8") findings = self.checker_module.SkillDriftChecker(repo).run_static() diff --git a/skills/codex-refactor-loop/scripts/test_cli_command_router.py b/skills/codex-refactor-loop/scripts/test_cli_command_router.py index 3aece53a..1d18e46a 100644 --- a/skills/codex-refactor-loop/scripts/test_cli_command_router.py +++ b/skills/codex-refactor-loop/scripts/test_cli_command_router.py @@ -222,11 +222,15 @@ def test_daemon_status_is_read_only_status_projection(self) -> None: cli = (SCRIPT_DIR / "codex_refactor_loop" / "cli.py").read_text(encoding="utf-8") daemon_status = (SCRIPT_DIR / "codex_refactor_loop" / "daemon_status.py").read_text(encoding="utf-8") for token in ( - "Refactor (issue-298)", - "daemon-status is read-only", - "restart-daemons remains", - "read-only projection", - "repair/reload stays exclusively", + '"daemon-status": CommandSpec(', + "daemon_status_main", + '("read-state", "read-process")', + "def collect(", + "DaemonStatusProjection", + "DaemonStatusReport", + "read_daemon_pid", + "read_heartbeat_age_seconds", + "duplicate_canonical_wrappers", ): with self.subTest(token=token): self.assertIn(token, cli + daemon_status) @@ -365,13 +369,13 @@ def test_pr_checks_command_declares_read_gh_only_and_no_lifecycle_authority(self def test_authority_refactor_self_doc_source_regression(self) -> None: cli = (SCRIPT_DIR / "codex_refactor_loop" / "cli.py").read_text(encoding="utf-8") for token in ( - "Refactor (iter201/issue-201)", - "public consensus-rnd-cli exposed", - "lifecycle commands", - "generic lifecycle authority surface", - "only public non-lifecycle CLI primitives", - "controller lifecycle actions stay", - "dev-sync's narrow integration-worktree carveout", + "COMMANDS: dict[str, CommandSpec]", + "authority: tuple[str, ...]", + '"dev-sync": CommandSpec(', + '"release-gate": CommandSpec(', + '"check-project-rules": CommandSpec(', + '("read-git", "write-artifact")', + '"phase9-router": CommandSpec(', ): with self.subTest(token=token): self.assertIn(token, cli) diff --git a/skills/codex-refactor-loop/scripts/test_controller_actions.py b/skills/codex-refactor-loop/scripts/test_controller_actions.py index a515c061..bbb6563c 100644 --- a/skills/codex-refactor-loop/scripts/test_controller_actions.py +++ b/skills/codex-refactor-loop/scripts/test_controller_actions.py @@ -196,9 +196,9 @@ def fake_gh(args: list[str], *, check: bool = True) -> mock.Mock: self.assertEqual(0, self.actions.merge_pr("77", linked_issue="239")) ready_index = gh_calls.index(["pr", "view", "77", "--json", "isDraft", "--jq", ".isDraft"]) - merge_index = gh_calls.index(["pr", "merge", "77", "--admin", "--squash", "--delete-branch"]) + merge_index = gh_calls.index(["pr", "merge", "77", "--squash", "--delete-branch"]) self.assertLess(ready_index, merge_index) - self.assertIn(["pr", "merge", "77", "--admin", "--squash", "--delete-branch"], gh_calls) + self.assertIn(["pr", "merge", "77", "--squash", "--delete-branch"], gh_calls) self.assertFalse(any(call[:5] == ["pr", "view", "77", "--json", "body"] for call in gh_calls), gh_calls) self.assertTrue(any(call[:3] == ["pr", "edit", "77"] for call in gh_calls), gh_calls) self.assertTrue(any(call[:3] == ["issue", "close", "239"] for call in gh_calls), gh_calls) @@ -240,7 +240,7 @@ def fake_gh(args: list[str], *, check: bool = True) -> mock.Mock: self.assertEqual(0, self.actions.merge_pr("77")) self.assertIn(["pr", "ready", "77"], gh_calls) - self.assertLess(gh_calls.index(["pr", "ready", "77"]), gh_calls.index(["pr", "merge", "77", "--admin", "--squash", "--delete-branch"])) + self.assertLess(gh_calls.index(["pr", "ready", "77"]), gh_calls.index(["pr", "merge", "77", "--squash", "--delete-branch"])) def test_merge_pr_ready_failure_fails_closed_before_merge_side_effects(self) -> None: gh_calls: list[list[str]] = [] @@ -263,6 +263,28 @@ def fake_gh(args: list[str], *, check: bool = True) -> mock.Mock: self.assertFalse(any(call[:2] == ["pr", "merge"] for call in gh_calls), gh_calls) self.assertFalse((self.tmp / ".refactor-loop" / "state" / "recent-pr-merges.json").exists()) + def test_merge_pr_failure_surfaces_blocked_by_host_policy_without_cleanup(self) -> None: + gh_calls: list[list[str]] = [] + + def fake_gh(args: list[str], *, check: bool = True) -> mock.Mock: + gh_calls.append(args) + if args[:5] == ["pr", "view", "77", "--json", "body"]: + return mock.Mock(returncode=0, stdout="", stderr="") + if args[:5] == ["pr", "view", "77", "--json", "isDraft"]: + return mock.Mock(returncode=0, stdout="false\n", stderr="") + if args[:2] == ["pr", "merge"]: + return mock.Mock(returncode=9, stdout="", stderr="merge blocked by host policy") + raise AssertionError(f"unexpected gh side effect after merge failure: {args}") + + with mock.patch.object(self.actions, "gh", side_effect=fake_gh): + with mock.patch.object(self.actions, "git", side_effect=AssertionError("git should not be called")): + self.assertEqual(9, self.actions.merge_pr("77")) + + self.assertIn(["pr", "merge", "77", "--squash", "--delete-branch"], gh_calls) + self.assertFalse(any(call[:2] == ["pr", "edit"] for call in gh_calls), gh_calls) + self.assertFalse((self.tmp / ".refactor-loop" / "state" / "recent-pr-merges.json").exists()) + self.assertIn("CONTROLLER_ACTION_BLOCKED:blocked-by-host-policy:merge-pr:pr:77", self.pending_events()) + def test_merge_pr_already_ready_merges_without_pr_ready_call(self) -> None: gh_calls: list[list[str]] = [] @@ -297,7 +319,7 @@ def fake_gh(args: list[str], *, check: bool = True) -> mock.Mock: self.assertIn(["pr", "view", "77", "--json", "isDraft", "--jq", ".isDraft"], gh_calls) self.assertFalse(any(call[:2] == ["pr", "ready"] for call in gh_calls), gh_calls) - self.assertIn(["pr", "merge", "77", "--admin", "--squash", "--delete-branch"], gh_calls) + self.assertIn(["pr", "merge", "77", "--squash", "--delete-branch"], gh_calls) def test_open_pr_with_label_rejects_malformed_create_url_before_post_create_edit(self) -> None: cases = ( @@ -780,7 +802,7 @@ def fake_gh(args: list[str], *, check: bool = True) -> mock.Mock: with mock.patch.object(self.actions, "gh", side_effect=fake_gh): self.assertEqual(0, self.actions.merge_pr("77")) - self.assertIn(["pr", "merge", "77", "--admin", "--squash", "--delete-branch"], gh_calls) + self.assertIn(["pr", "merge", "77", "--squash", "--delete-branch"], gh_calls) self.assertTrue(any(call[:3] == ["issue", "close", "239"] for call in gh_calls), gh_calls) issue_edit = next(call for call in gh_calls if call[:3] == ["issue", "edit", "239"]) self.assertEqual(labels.PHASE_MERGED, issue_edit[issue_edit.index("--add-label") + 1]) @@ -815,7 +837,7 @@ def fake_gh(args: list[str], *, check: bool = True) -> mock.Mock: with mock.patch.object(self.actions, "gh", side_effect=fake_gh): self.assertEqual(0, self.actions.merge_pr("77")) - self.assertIn(["pr", "merge", "77", "--admin", "--squash", "--delete-branch"], gh_calls) + self.assertIn(["pr", "merge", "77", "--squash", "--delete-branch"], gh_calls) self.assertTrue(any(call[:3] == ["issue", "close", "239"] for call in gh_calls), gh_calls) def test_merge_pr_does_not_guess_issue_when_body_closes_multiple_issues(self) -> None: @@ -848,7 +870,7 @@ def fake_gh(args: list[str], *, check: bool = True) -> mock.Mock: with mock.patch.object(self.actions, "gh", side_effect=fake_gh): self.assertEqual(0, self.actions.merge_pr("77")) - self.assertIn(["pr", "merge", "77", "--admin", "--squash", "--delete-branch"], gh_calls) + self.assertIn(["pr", "merge", "77", "--squash", "--delete-branch"], gh_calls) self.assertTrue(any(call[:3] == ["pr", "edit", "77"] for call in gh_calls), gh_calls) self.assertFalse(any(call[:2] == ["issue", "close"] for call in gh_calls), gh_calls) self.assertFalse(any(call[:2] == ["issue", "edit"] for call in gh_calls), gh_calls) @@ -1001,13 +1023,21 @@ def test_body_linked_issue_parser_validates_malformed_closing_refs(self) -> None def test_issue_300_draft_pr_ready_before_merge_contract(self) -> None: text = (SCRIPT_DIR / "codex_refactor_loop" / "controller_actions.py").read_text(encoding="utf-8") - self.assertIn("Refactor (issue-300)", text) + self.assertNotIn("Refactor (issue-300)", text) + self.assertNotIn("Old pattern", text) + self.assertNotIn("New principle", text) self.assertIn('"pr", "create", "--draft"', text) self.assertIn('def _ensure_pr_ready_for_merge(self, pr_target: str) -> int:', text) self.assertIn('"pr", "view", pr_target, "--json", "isDraft", "--jq", ".isDraft"', text) self.assertIn('"pr", "ready", pr_target', text) self.assertIn("ready = self._ensure_pr_ready_for_merge(pr_target)", text) self.assertLess(text.index("ready = self._ensure_pr_ready_for_merge(pr_target)"), text.index('"pr", "merge", pr_target')) + + def test_merge_pr_uses_non_admin_merge_and_surfaces_host_policy_block(self) -> None: + text = (SCRIPT_DIR / "codex_refactor_loop" / "controller_actions.py").read_text(encoding="utf-8") + self.assertIn('["pr", "merge", pr_target, "--squash", "--delete-branch"]', text) + self.assertIn("blocked-by-host-policy", text) + self.assertNotIn("--admin", text) self.assertNotIn("ReviewGateAction", text) self.assertNotIn("review_gate.py", text) diff --git a/skills/codex-refactor-loop/scripts/test_controller_lib_label_helper.py b/skills/codex-refactor-loop/scripts/test_controller_lib_label_helper.py index d229a731..6c2d813b 100644 --- a/skills/codex-refactor-loop/scripts/test_controller_lib_label_helper.py +++ b/skills/codex-refactor-loop/scripts/test_controller_lib_label_helper.py @@ -335,7 +335,7 @@ def test_merge_pr_records_recent_merge_after_success(self) -> None: calls = self.gh_calls() self.assertIn("pr view 55 --repo test-owner/test-repo --json isDraft --jq .isDraft", calls) self.assertNotIn("pr ready 55 --repo test-owner/test-repo", calls) - self.assertIn("pr merge 55 --repo test-owner/test-repo --admin --squash --delete-branch", calls) + self.assertIn("pr merge 55 --repo test-owner/test-repo --squash --delete-branch", calls) expected_pr_edit = ["pr", "edit", "55", "--repo", "test-owner/test-repo"] for label in ( *labels.labels_for_group("phase"), @@ -463,7 +463,7 @@ def test_merge_pr_marks_draft_ready_before_merge(self) -> None: self.assertEqual(result.returncode, 0, result.stderr) calls = self.gh_calls() ready_call = "pr ready 55 --repo test-owner/test-repo" - merge_call = "pr merge 55 --repo test-owner/test-repo --admin --squash --delete-branch" + merge_call = "pr merge 55 --repo test-owner/test-repo --squash --delete-branch" self.assertIn(ready_call, calls) self.assertIn(merge_call, calls) self.assertLess(calls.index(ready_call), calls.index(merge_call)) diff --git a/skills/codex-refactor-loop/scripts/test_github_body_renderer.py b/skills/codex-refactor-loop/scripts/test_github_body_renderer.py index 7c95510b..6247830e 100644 --- a/skills/codex-refactor-loop/scripts/test_github_body_renderer.py +++ b/skills/codex-refactor-loop/scripts/test_github_body_renderer.py @@ -147,11 +147,11 @@ def test_no_new_daemon_or_lifecycle_authority_for_renderer(self) -> None: self.assertIn("read-only helper", src) self.assertIn("must not write files", src) - def test_issue191_refactor_documentation_pins_validator_contract(self) -> None: + def test_validator_contract_uses_self_contained_authority_literals(self) -> None: src = (SCRIPT_DIR / "codex_refactor_loop" / "github_body.py").read_text(encoding="utf-8") - self.assertIn("Refactor (iter191/issue-191):", src) - self.assertIn("single active controller lease (no per-work claims, no cross-device floor)", src) - self.assertIn("authority/consensus/plan bodies inline raw artifacts", src) + self.assertNotIn("Refactor (iter191/issue-191):", src) + self.assertNotIn("Old pattern", src) + self.assertNotIn("New principle", src) self.assertIn("INLINE_ARTIFACT_DETAILS_RE", src) self.assertIn("authority body must inline raw artifact text in inline artifact details", src) diff --git a/skills/codex-refactor-loop/scripts/test_host_env_surface_matrix.py b/skills/codex-refactor-loop/scripts/test_host_env_surface_matrix.py index 488a62af..0907c174 100644 --- a/skills/codex-refactor-loop/scripts/test_host_env_surface_matrix.py +++ b/skills/codex-refactor-loop/scripts/test_host_env_surface_matrix.py @@ -149,6 +149,7 @@ def test_host_env_example_exports_match_skill_matrix(self) -> None: def test_defaults_and_missing_behaviors_match(self) -> None: cases = { "RELEASE_AUTO_ENABLE": ("false", "false or empty exits 0 with noop reason"), + "HOST_GITHUB_RELEASE_REQUIRED_CHECKS": ("ci,lint,typecheck", "missing_host_required_release_checks"), "UPDATE_CHECK_ENABLE": ("false", "disabled update-check state"), "UPDATE_CHECK_INTERVAL_SECONDS": ("21600", "fresh local update-check state"), "UPDATE_CHECK_TIMEOUT_SECONDS": ("5", "failures write unknown state"), @@ -201,11 +202,17 @@ def test_defaults_and_missing_behaviors_match(self) -> None: self.assertGreaterEqual(len(host_rows), 7) for key, row in host_rows.items(): with self.subTest(key=key): - self.assertEqual("", self.exports[key]["value"]) if key == "HOST_WORKFLOW_SPEC": + self.assertEqual("", self.exports[key]["value"]) self.assertEqual("optional-noop", row["Category"]) self.assertIn("built-in behavior", row["Missing/empty behavior"]) + elif key == "HOST_GITHUB_RELEASE_REQUIRED_CHECKS": + self.assertEqual("defaulted", row["Category"]) + self.assertIn("exact GitHub check-run names", row["Missing/empty behavior"]) + self.assertIn("host-owned comma-separated exact check-run names", row["Default/example"]) + self.assertEqual("ci,lint,typecheck", self.exports[key]["value"]) else: + self.assertEqual("", self.exports[key]["value"]) self.assertEqual("prompt-empty-infer", row["Category"]) self.assertRegex(row["Missing/empty behavior"], r"infer|mirror|match|omit|diff") self.assertIn("do not invent a host language default", self.rows["HOST_CODE_FENCE_LANG"]["Missing/empty behavior"]) @@ -220,12 +227,15 @@ def test_refactor_comment_policy_is_defaulted_and_registered(self) -> None: self.assertEqual("defaulted", row["Category"]) self.assertEqual("prompt templates", row["Owner"]) self.assertEqual("prompt templates", row["Consumer"]) - self.assertIn("`self-doc-comment`", row["Default/example"]) - self.assertEqual("self-doc-comment", self.exports[key]["value"]) - self.assertIn("missing/empty normalizes to `self-doc-comment`", row["Missing/empty behavior"]) - self.assertIn("`none` disables refactor-history source comments", row["Missing/empty behavior"]) + self.assertIn("`none`", row["Default/example"]) + self.assertEqual("none", self.exports[key]["value"]) + self.assertIn("missing/empty/default normalizes to `none`", row["Missing/empty behavior"]) + self.assertIn("rationale belongs in external artifacts", row["Missing/empty behavior"]) + self.assertIn("explicit `self-doc-comment` is downstream compatibility opt-in", row["Missing/empty behavior"]) + self.assertIn("source English-only", row["Missing/empty behavior"]) self.assertIn("invalid and fail-closed", row["Missing/empty behavior"]) self.assertIn("test_refactor_comment_policy_prompt_contract.py", row["Test owner"]) + self.assertIn("test_source_language_policy.py", row["Test owner"]) self.assertIn("defaulted", self.exports[key]["section"]) text = "\n".join( @@ -236,7 +246,7 @@ def test_refactor_comment_policy_is_defaulted_and_registered(self) -> None: ] ) self.assertIn("${HOST_REFACTOR_COMMENT_POLICY}", text) - self.assertIn("HOST_REFACTOR_COMMENT_POLICY=\"self-doc-comment\"", read(HOST_ENV_EXAMPLE)) + self.assertIn("HOST_REFACTOR_COMMENT_POLICY=\"none\"", read(HOST_ENV_EXAMPLE)) for alias in ("HOST_SOURCE_COMMENT_POLICY", "HOST_REFACTOR_SELF_DOC_POLICY"): with self.subTest(alias=alias): self.assertNotIn(alias, text) diff --git a/skills/codex-refactor-loop/scripts/test_host_workflow_spec.py b/skills/codex-refactor-loop/scripts/test_host_workflow_spec.py index abb8b07c..40e2874b 100644 --- a/skills/codex-refactor-loop/scripts/test_host_workflow_spec.py +++ b/skills/codex-refactor-loop/scripts/test_host_workflow_spec.py @@ -15,6 +15,7 @@ from codex_refactor_loop.context import LoopContext # noqa: E402 from codex_refactor_loop.workflow_spec import ( # noqa: E402 + WORKFLOW_PROJECTION_KEYS, WorkflowSpecError, load_validated_workflow_spec, ) @@ -101,6 +102,27 @@ def test_repo_relative_spec_declares_host_event_stage_kind_and_prompt_binding(se self.assertEqual(spec.prompt_binding_path("host:solver"), "prompts/host-solver.md") self.assertTrue(any(mapping.name == "host:template" for mapping in spec.issue_intake_mappings)) + def test_projection_outputs_exact_seven_data_only_surfaces(self) -> None: + spec = load_validated_workflow_spec(self.ctx("workflow.json")) + projection = spec.projection() + + self.assertEqual(tuple(projection), WORKFLOW_PROJECTION_KEYS) + self.assertEqual(set(projection), set(WORKFLOW_PROJECTION_KEYS)) + self.assertIn("minimal", [role["name"] for role in projection["roles"]]) + self.assertIn("host:solver-a", [role["name"] for role in projection["roles"]]) + self.assertEqual(projection["prompt_bindings"]["host:solver"], "prompts/host-solver.md") + forbidden_fields = {"label", "labels", "assignee", "assignees", "milestone", "merge", "command", "executor", "git"} + stack = [projection] + seen_keys: set[str] = set() + while stack: + value = stack.pop() + if isinstance(value, dict): + seen_keys.update(value) + stack.extend(value.values()) + elif isinstance(value, list): + stack.extend(value) + self.assertTrue(forbidden_fields.isdisjoint(seen_keys), seen_keys & forbidden_fields) + def test_spec_rejects_absolute_parent_or_symlink_escape_paths(self) -> None: data = self.valid_spec() data["prompt_bindings"]["host:solver"] = "/tmp/outside.md" diff --git a/skills/codex-refactor-loop/scripts/test_label_contract_source.py b/skills/codex-refactor-loop/scripts/test_label_contract_source.py index 4cef7d7e..fe705143 100644 --- a/skills/codex-refactor-loop/scripts/test_label_contract_source.py +++ b/skills/codex-refactor-loop/scripts/test_label_contract_source.py @@ -29,12 +29,16 @@ def test_skill_names_catalog_and_does_not_restore_bootstrap_truth_table(self) -> self.assertNotIn("issue/PR 状态 → 期望 label", text) self.assertNotIn("crnd:phase:pr-open + crnd:phase:reviewing", text) self.assertNotIn('"crnd:lifecycle:managed,crnd:phase:design-solving,crnd:human:auto"', text) + self.assertIn("exactly one loop-owned `crnd:phase:*` label", text) + self.assertIn("exactly one loop-owned\n`crnd:human:*` label", text) + self.assertIn("Host business labels may coexist", text) + self.assertIn("not\nrouting authority", text) def test_active_skill_sections_use_catalog_managed_labels(self) -> None: text = (SKILL_ROOT / "SKILL.md").read_text(encoding="utf-8") active_contracts = { - "entry mode": self._section(text, "## Two entry modes", "## Host 配置"), + "entry mode": self._section(text, "## Main path and fallback producer", "## Host 配置"), "bootstrap": self._section(text, "## Label bootstrap loops", "## Codex invocation details"), "pr open": self._section(text, "### Consensus-rnd Phase publish stacked", "### Consensus-rnd Phase publish stack-depth cap"), "existing priority": self._section(text, "### Existing-issue priority route table", "### Stale-issue revival"), @@ -102,6 +106,24 @@ def test_closed_phase_is_catalog_owned_ascii_phase_exclusive(self) -> None: self.assertIn("Closed terminal protocol state", source) self.assertEqual(labels.PHASE_CLOSED, "crnd:phase:closed") + def test_release_target_label_contract_stays_in_milestone_catalog(self) -> None: + skill = (SKILL_ROOT / "SKILL.md").read_text(encoding="utf-8") + labels_source = (SCRIPT_DIR / "codex_refactor_loop" / "labels.py").read_text(encoding="utf-8") + wakeup_source = (SCRIPT_DIR / "codex_refactor_loop" / "wakeup_plan.py").read_text(encoding="utf-8") + combined = "\n".join((skill, labels_source, wakeup_source)) + + self.assertEqual(labels.MILESTONE_RELEASE_TARGET, "crnd:milestone:release-target") + self.assertIn('_spec("milestone", "release-target", "Release countdown target issue/PR.", "f9d0c4")', labels_source) + self.assertIn('MILESTONE_RELEASE_TARGET = canonical_name("milestone", "release-target")', labels_source) + self.assertIn("crnd:milestone:release-target", skill) + self.assertIn("crnd:milestone:current` remains dispatch priority only and must not trigger release countdown by itself", skill) + self.assertIn("Label exclusivity is per `LabelSpec.exclusive_axis`, not per group", skill) + self.assertNotIn("crnd:release-target", combined) + self.assertNotIn('"release"', labels_source) + self.assertNotIn("release-countdown.json", combined) + self.assertIn("label_catalog.MILESTONE_RELEASE_TARGET", wakeup_source) + self.assertNotIn("label_catalog.MILESTONE_CURRENT in projection.canonical", wakeup_source) + def test_runtime_code_has_no_legacy_routing_literals_outside_catalog(self) -> None: allow = { SCRIPT_DIR / "codex_refactor_loop" / "labels.py", diff --git a/skills/codex-refactor-loop/scripts/test_label_taxonomy.py b/skills/codex-refactor-loop/scripts/test_label_taxonomy.py index 6f13e4f5..c0529d07 100644 --- a/skills/codex-refactor-loop/scripts/test_label_taxonomy.py +++ b/skills/codex-refactor-loop/scripts/test_label_taxonomy.py @@ -38,11 +38,25 @@ def test_group_helpers_and_expected_axes_are_catalog_backed(self) -> None: self.assertEqual(labels.canonical_name("phase", "design-solving"), labels.PHASE_DESIGN_SOLVING) self.assertIn(labels.HUMAN_AUTO, labels.labels_for_group("human")) self.assertIn(labels.PHASE_CLOSED, labels.labels_for_group("phase")) + self.assertEqual(labels.MILESTONE_RELEASE_TARGET, "crnd:milestone:release-target") + self.assertIn(labels.MILESTONE_RELEASE_TARGET, labels.labels_for_group("milestone")) labels.PHASE_CLOSED.encode("ascii") self.assertEqual(labels.phase_expected_workers(labels.PHASE_FIXING), 1) self.assertEqual(labels.actor_for_phase(labels.PHASE_REVIEWING), "reviewer-codex") self.assertEqual(labels.actor_for_phase(labels.PHASE_CLOSED), "closed-label-reconciler") + def test_release_target_uses_existing_milestone_grammar_without_groupless_exception(self) -> None: + self.assertEqual(labels.LABEL_GROUPS, ("phase", "human", "lifecycle", "triage", "milestone")) + self.assertRegex(labels.MILESTONE_RELEASE_TARGET, labels.CANONICAL_RE) + self.assertIsNone(re.fullmatch(labels.CANONICAL_RE, "crnd:release-target")) + + projection = labels.normalize_label_set([labels.MILESTONE_CURRENT, labels.MILESTONE_RELEASE_TARGET]) + + self.assertEqual( + projection.canonical, + frozenset({labels.MILESTONE_CURRENT, labels.MILESTONE_RELEASE_TARGET}), + ) + def test_managed_query_labels_include_canonical_and_legacy_aliases(self) -> None: self.assertEqual( labels.query_labels_for(labels.MANAGED), diff --git a/skills/codex-refactor-loop/scripts/test_package_checks.py b/skills/codex-refactor-loop/scripts/test_package_checks.py index f8b296bb..e8db4c95 100644 --- a/skills/codex-refactor-loop/scripts/test_package_checks.py +++ b/skills/codex-refactor-loop/scripts/test_package_checks.py @@ -147,7 +147,7 @@ def test_degradation_source_preserves_issue66_contract_literals_and_forbidden_bo ) for required in ( "skill-degradation", - "manifest-version-sync", + "HOST_GITHUB_RELEASE_REQUIRED_CHECKS", "consensus-rnd-cli check-degradation --static", "source-repo CI/release validation", "downstream host has no runtime watch", @@ -170,12 +170,11 @@ def test_degradation_source_preserves_issue66_contract_literals_and_forbidden_bo with self.subTest(required=required): self.assertIn(required, evaluated_markers) - self.assertIn("Old: scripts/check_skill_degradation.py", source) - self.assertIn("New: expose the same read-only checks", source) - self.assertIn("Old pattern: the checker required downstream runtime watch hooks", source) - self.assertIn("New principle: skill-degradation is source-repo CI/release validation only", source) + self.assertIn("SOURCE_REPO_SENTINELS", source) + self.assertIn("FORBIDDEN_RUNTIME_FILES", source) + self.assertIn("REQUIRED_DETAILED_REFERENCE_MARKERS", source) self.assertIn("not-source-repo", source) - self.assertIn("source-repo CI/release validation only", source) + self.assertIn("source-repo CI/release validation", source) for sentinel in ( "skills/codex-refactor-loop/SKILL.md", ".version-bump.json", diff --git a/skills/codex-refactor-loop/scripts/test_package_triage.py b/skills/codex-refactor-loop/scripts/test_package_triage.py index cc32530b..9b729ae7 100644 --- a/skills/codex-refactor-loop/scripts/test_package_triage.py +++ b/skills/codex-refactor-loop/scripts/test_package_triage.py @@ -245,6 +245,9 @@ def test_host_issue_intake_requires_host_namespace_and_no_lifecycle_authority(se self.assertEqual(projection["producer"], "host:github-template") self.assertEqual(projection["lifecycle_authority"], "false") + for forbidden in ("labels", "assignees", "milestones", "spawn", "merge"): + with self.subTest(forbidden=forbidden): + self.assertNotIn(forbidden, projection) spec["issue_intake_mappings"][0]["producer"] = "manual-issue" (self.repo / "workflow.json").write_text(json.dumps(spec), encoding="utf-8") diff --git a/skills/codex-refactor-loop/scripts/test_phase9_router_package.py b/skills/codex-refactor-loop/scripts/test_phase9_router_package.py index cf414d8f..1f52c8fc 100644 --- a/skills/codex-refactor-loop/scripts/test_phase9_router_package.py +++ b/skills/codex-refactor-loop/scripts/test_phase9_router_package.py @@ -342,10 +342,14 @@ def test_package_router_source_preserves_narrow_allowlist_and_forbidden_tokens(s "phase9-triplet-target-log-exists", "phase9-triplet-equivalent-log-exists", "phase9-triplet-in-flight", - "Refactor (iter1/issue-167)", - "Old pattern: solver triplet handoff recorded only the base dispatch row", - "New principle: keep row-level router-private ledger provenance", - "narrow fail-closed peer artifact token check", + "_solver_triplet_ledger_fields", + "_peer_solver_reference_violation", + "_peer_solver_reference_tokens", + "clean_exit_solver_logs", + "solver_input_prompts", + "judge_input_solver_logs", + "judge_prompt_scope", + "independence_check", "Dispatch ledger evidence:", ".controller-pending-events.log", "phase9-router-fallback", diff --git a/skills/codex-refactor-loop/scripts/test_refactor_comment_policy_prompt_contract.py b/skills/codex-refactor-loop/scripts/test_refactor_comment_policy_prompt_contract.py index b514b449..b3775786 100644 --- a/skills/codex-refactor-loop/scripts/test_refactor_comment_policy_prompt_contract.py +++ b/skills/codex-refactor-loop/scripts/test_refactor_comment_policy_prompt_contract.py @@ -17,7 +17,7 @@ def read_prompt(name: str) -> str: class RefactorCommentPolicyPromptContractTests(unittest.TestCase): - def test_default_policy_preserves_old_new_requirement(self) -> None: + def test_default_policy_is_none_and_external_rationale(self) -> None: implement = read_prompt("implement.md") verify = read_prompt("verify.md") architect = read_prompt("reviewer-architect.md") @@ -33,15 +33,33 @@ def test_default_policy_preserves_old_new_requirement(self) -> None: }.items(): with self.subTest(prompt=name): self.assertIn("${HOST_REFACTOR_COMMENT_POLICY}", text) - self.assertIn("empty/`self-doc-comment`", text) + self.assertIn("missing/empty/default", text) + self.assertIn("`none`", text) self.assertIn("self-doc-comment", text) + self.assertIn("missing/empty/default/`none` 归一化为 `none`", implement) + self.assertIn("MUST NOT add `Refactor (...)`, `Old pattern`, `New principle`, or `iterN/cluster`", implement) + self.assertIn("refactor self-doc: not applicable (HOST_REFACTOR_COMMENT_POLICY=none)", implement) + self.assertIn("missing Refactor self-documentation is not a defect and must not trigger rework", verify) + self.assertIn("absence is compliant, rationale belongs in external artifacts", architect) + self.assertIn("missing/illegible self-doc must not be a reject reason", quality) + self.assertIn("keep rationale in the fix report/external artifact", review_fix) + + def test_explicit_self_doc_policy_preserves_old_new_requirement(self) -> None: + implement = read_prompt("implement.md") + verify = read_prompt("verify.md") + quality = read_prompt("reviewer-quality.md") + review_fix = read_prompt("review-fix.md") + + self.assertIn("`self-doc-comment`:被重构的每个类/关键方法必须", implement) self.assertIn("Refactor (iter${ITERATION}/${CLUSTER_ID}):", implement) self.assertIn("Old pattern: ${OLD_PATTERN}", implement) self.assertIn("New principle: ${NEW_PRINCIPLE}", implement) + self.assertIn("源码注释必须 English-only", implement) self.assertIn("缺失任何一处且无合理 not-applicable 说明 → 标记缺陷", verify) + self.assertIn("HOST_REFACTOR_COMMENT_POLICY=self-doc-comment", quality) self.assertIn("must be present and clear", quality) - self.assertIn("Preserve/add refactor self-doc comments only when", review_fix) + self.assertIn("Preserve/add refactor self-doc comments only when `${HOST_REFACTOR_COMMENT_POLICY}=self-doc-comment`", review_fix) def test_none_policy_forbids_refactor_history_source_comments(self) -> None: expected = ( @@ -59,11 +77,11 @@ def test_none_policy_forbids_refactor_history_source_comments(self) -> None: self.assertIn("refactor self-doc: not applicable (HOST_REFACTOR_COMMENT_POLICY=none)", verify) architect = read_prompt("reviewer-architect.md") - self.assertIn("`none`: absence is compliant", architect) + self.assertIn("missing/empty/default/`none` normalizes to `none`: absence is compliant", architect) self.assertIn("new Old/New/iteration refactor-history source comments must be rejected", architect) review_fix = read_prompt("review-fix.md") - self.assertIn("When `${HOST_REFACTOR_COMMENT_POLICY}=none`", review_fix) + self.assertIn("When `${HOST_REFACTOR_COMMENT_POLICY}` is missing/empty/default/`none`", review_fix) self.assertIn("classify it as a host-policy conflict/false-positive", review_fix) def test_none_policy_disables_missing_self_doc_rejects(self) -> None: @@ -72,12 +90,14 @@ def test_none_policy_disables_missing_self_doc_rejects(self) -> None: self.assertIn("missing Refactor self-documentation is not a defect and must not trigger rework", verify) self.assertIn("missing/illegible self-doc must not be a reject reason", quality) - self.assertIn("Under `HOST_REFACTOR_COMMENT_POLICY=none`, missing/illegible self-doc alone is not a reject reason", quality) - self.assertIn("still comment/reject for naming, dead code, complexity, scope creep", quality) + self.assertIn("Under missing/empty/default/`HOST_REFACTOR_COMMENT_POLICY=none`, missing/illegible self-doc alone is not a reject reason", quality) + self.assertIn("Still comment/reject for naming, dead code, complexity, scope creep", quality) forbidden_unconditional = ( "the cluster mandates `// Refactor (iterN/cluster-XXX):` Old/New blocks", "missing/illegible self-doc on a major refactor, or scope creep", + "empty/`self-doc-comment`", + "empty/`self-doc-comment` normalizes to `self-doc-comment`", "缺失任何一处且无合理 not-applicable 说明 → 标记缺陷。\n- 检查改动是否真正消除了", ) combined = "\n".join( diff --git a/skills/codex-refactor-loop/scripts/test_release_commits.py b/skills/codex-refactor-loop/scripts/test_release_commits.py index 62ee7056..306f36c8 100644 --- a/skills/codex-refactor-loop/scripts/test_release_commits.py +++ b/skills/codex-refactor-loop/scripts/test_release_commits.py @@ -256,9 +256,11 @@ def test_source_regression_keeps_producer_controller_side_and_gate_git_free(self gate_executable_source = "\n".join(line for line in gate_source.splitlines() if not line.lstrip().startswith("#")) skill_source = (SCRIPT_PATH.parents[1] / "SKILL.md").read_text(encoding="utf-8") - self.assertIn("Refactor (fix/pr236-split-release-commits-command)", producer_source) - self.assertIn("separate release-commits command reads git", producer_source) + self.assertIn("RELEASE_COMMITS_RELATIVE_PATH", producer_source) self.assertIn("def write_release_commits(", producer_source) + self.assertIn("run_git", producer_source) + self.assertIn("write_json", producer_source) + self.assertIn("latest_release_ref", producer_source) self.assertIn('"release-commits": CommandSpec(', cli_source) self.assertIn('("read-git", "write-artifact")', cli_source) self.assertNotIn("release_gate_with_pre_gate_commits", cli_source) diff --git a/skills/codex-refactor-loop/scripts/test_release_gate_module.py b/skills/codex-refactor-loop/scripts/test_release_gate_module.py index 8d0026f9..2f38fddf 100644 --- a/skills/codex-refactor-loop/scripts/test_release_gate_module.py +++ b/skills/codex-refactor-loop/scripts/test_release_gate_module.py @@ -93,7 +93,7 @@ def __call__(self, cmd: list[str], cwd: Path) -> subprocess.CompletedProcess[str created = gate.isoformat(NOW) runs = [ {"id": index + 1, "created_at": created, "started_at": created, "completed_at": created, "conclusion": "success", "status": "completed", "name": name} - for index, name in enumerate(gate.REQUIRED_CHECKS) + for index, name in enumerate(("contract-tests", "manifest-version-sync", "skill-degradation")) ] return subprocess.CompletedProcess(cmd, 0, stdout=json.dumps({"check_runs": runs}), stderr="") if len(cmd) >= 2 and cmd[0] == "gh" and cmd[2:3] == ["list"]: @@ -159,11 +159,12 @@ def test_live_signal_parity_for_required_checks_labels_and_heartbeats(self) -> N write_live_state(repo) runner = FakeRunner() release_gate = gate.AutoReleaseGate(repo, now=lambda: NOW, runner=runner) - old_env = {key: gate.os.environ.get(key) for key in ("GH_REPO_SLUG", "REVIEW_BASE_BRANCH", "INTEGRATION_BRANCH")} + old_env = {key: gate.os.environ.get(key) for key in ("GH_REPO_SLUG", "REVIEW_BASE_BRANCH", "INTEGRATION_BRANCH", "HOST_GITHUB_RELEASE_REQUIRED_CHECKS")} try: gate.os.environ["GH_REPO_SLUG"] = "owner/repo" gate.os.environ["REVIEW_BASE_BRANCH"] = "review-base" gate.os.environ["INTEGRATION_BRANCH"] = "integration-branch" + gate.os.environ["HOST_GITHUB_RELEASE_REQUIRED_CHECKS"] = "contract-tests,manifest-version-sync,skill-degradation" stability = release_gate.compute_stability(min_recent_merges=1) finally: for key, value in old_env.items(): @@ -189,6 +190,43 @@ def test_live_signal_parity_for_required_checks_labels_and_heartbeats(self) -> N self.assertEqual(sum(1 for value in heartbeat_signal["heartbeats"].values() if value), 6) self.assertTrue(heartbeat_signal["heartbeats"]["closed_label_reconciler"]) + def test_live_release_gate_fails_closed_when_auto_release_lacks_host_required_checks(self) -> None: + for value in (None, ""): + with self.subTest(value=value), copy_repo_fixture() as tmp: + repo = Path(tmp) / "repo" + write_live_state(repo) + runner = FakeRunner() + release_gate = gate.AutoReleaseGate(repo, now=lambda: NOW, runner=runner) + keys = ("GH_REPO_SLUG", "REVIEW_BASE_BRANCH", "INTEGRATION_BRANCH", "RELEASE_AUTO_ENABLE", "HOST_GITHUB_RELEASE_REQUIRED_CHECKS") + old_env = {key: gate.os.environ.get(key) for key in keys} + try: + gate.os.environ["GH_REPO_SLUG"] = "owner/repo" + gate.os.environ["REVIEW_BASE_BRANCH"] = "review-base" + gate.os.environ["INTEGRATION_BRANCH"] = "integration-branch" + gate.os.environ["RELEASE_AUTO_ENABLE"] = "true" + if value is None: + gate.os.environ.pop("HOST_GITHUB_RELEASE_REQUIRED_CHECKS", None) + else: + gate.os.environ["HOST_GITHUB_RELEASE_REQUIRED_CHECKS"] = value + + stability = release_gate.compute_stability(min_recent_merges=1) + finally: + for key, previous in old_env.items(): + if previous is None: + gate.os.environ.pop(key, None) + else: + gate.os.environ[key] = previous + + self.assertFalse(stability.ready) + signal = stability.signals["required_checks_recent_green"] + self.assertFalse(signal["passed"]) + self.assertEqual(signal["source"], "host.env") + self.assertEqual( + signal["reason"], + "required_checks_recent_green:missing_host_required_release_checks", + ) + self.assertFalse(any(cmd[:2] == ["gh", "api"] for cmd in runner.commands)) + def test_release_gate_blocks_on_legacy_blocked_and_human_labels(self) -> None: with copy_repo_fixture() as tmp: repo = Path(tmp) / "repo" @@ -197,11 +235,12 @@ def test_release_gate_blocks_on_legacy_blocked_and_human_labels(self) -> None: runner.label_results["⏸️ phase:blocked"] = [{"number": 10}] runner.label_results["👤 human:需-maintainer-决策"] = [{"number": 11}] release_gate = gate.AutoReleaseGate(repo, now=lambda: NOW, runner=runner) - old_env = {key: gate.os.environ.get(key) for key in ("GH_REPO_SLUG", "REVIEW_BASE_BRANCH", "INTEGRATION_BRANCH")} + old_env = {key: gate.os.environ.get(key) for key in ("GH_REPO_SLUG", "REVIEW_BASE_BRANCH", "INTEGRATION_BRANCH", "HOST_GITHUB_RELEASE_REQUIRED_CHECKS")} try: gate.os.environ["GH_REPO_SLUG"] = "owner/repo" gate.os.environ["REVIEW_BASE_BRANCH"] = "review-base" gate.os.environ["INTEGRATION_BRANCH"] = "integration-branch" + gate.os.environ["HOST_GITHUB_RELEASE_REQUIRED_CHECKS"] = "contract-tests,manifest-version-sync,skill-degradation" stability = release_gate.compute_stability(min_recent_merges=1) finally: for key, value in old_env.items(): @@ -285,8 +324,15 @@ def test_source_has_no_release_lifecycle_or_daemon_event_authority(self) -> None source = (SCRIPT_PATH.parent / "codex_refactor_loop/release/gate.py").read_text(encoding="utf-8") cli_source = (SCRIPT_PATH.parent / "codex_refactor_loop/cli.py").read_text(encoding="utf-8") executable_source = "\n".join(line for line in source.splitlines() if not line.lstrip().startswith("#")) - self.assertIn("skills/codex-refactor-loop/authorizations/runtime-exceptions.md#autonomous-release-gate-56", source) self.assertIn("decision-artifact-only", source) + self.assertIn('parser.add_argument("--dispatch"', source) + self.assertIn('host_env.get("RELEASE_AUTO_ENABLE") != "true"', source) + self.assertIn("ReleaseRequiredChecksProjection", source) + self.assertIn("write_json(self.decision_path, decision)", source) + self.assertIn("write_json(self.candidate_path, self.release_candidate(decision))", source) + self.assertNotIn("Refactor (", source) + self.assertNotIn("Old pattern", source) + self.assertNotIn("New principle", source) self.assertNotIn('["git"', executable_source) self.assertNotIn('"git"', executable_source) self.assertNotIn("release.commits", source) @@ -311,7 +357,7 @@ def test_source_has_no_release_lifecycle_or_daemon_event_authority(self) -> None self.assertNotIn(forbidden, executable_source) def test_required_check_names_and_daemon_heartbeat_allowlist_are_stable(self) -> None: - self.assertEqual(gate.REQUIRED_CHECKS, ("contract-tests", "manifest-version-sync", "skill-degradation")) + self.assertEqual(gate.REQUIRED_CHECKS({"HOST_GITHUB_RELEASE_REQUIRED_CHECKS": "contract-tests,manifest-version-sync,skill-degradation"}), ("contract-tests", "manifest-version-sync", "skill-degradation")) self.assertEqual(gate.HEARTBEAT_FRESH_SECONDS, 90) self.assertEqual(gate.DAEMON_NAMES, restart.restart_managed_daemon_names()) self.assertEqual(6, len(gate.DAEMON_NAMES)) diff --git a/skills/codex-refactor-loop/scripts/test_release_pipeline_contract.py b/skills/codex-refactor-loop/scripts/test_release_pipeline_contract.py index b8961f31..132ff662 100644 --- a/skills/codex-refactor-loop/scripts/test_release_pipeline_contract.py +++ b/skills/codex-refactor-loop/scripts/test_release_pipeline_contract.py @@ -226,9 +226,12 @@ def test_release_guards_and_rejected_files(self) -> None: ): with self.subTest(needle=needle): self.assertIn(needle, self.workflow) - for needle in ("contract-tests", "manifest-version-sync", "skill-degradation", "REQUIRED_RELEASE_CHECKS"): + for needle in ("HOST_GITHUB_RELEASE_REQUIRED_CHECKS", "required_release_checks", "ReleaseRequiredChecksProjection"): with self.subTest(required_check_projection=needle): self.assertIn(needle, self.required_checks) + for forbidden in ('"contract-tests"', '"manifest-version-sync"', '"skill-degradation"'): + with self.subTest(forbidden_runtime_default=forbidden): + self.assertNotIn(forbidden, self.required_checks) def test_release_publish_preflight_source_guards_candidate_artifact_path(self) -> None: for needle in ( diff --git a/skills/codex-refactor-loop/scripts/test_release_publish_preflight.py b/skills/codex-refactor-loop/scripts/test_release_publish_preflight.py index 2499a8f2..6353ae52 100644 --- a/skills/codex-refactor-loop/scripts/test_release_publish_preflight.py +++ b/skills/codex-refactor-loop/scripts/test_release_publish_preflight.py @@ -71,16 +71,26 @@ def set_mapped_version(repo: Path, version: str) -> None: def write_host_opt_in(repo: Path, enabled: bool = True) -> None: (repo / ".refactor-loop").mkdir(parents=True, exist_ok=True) (repo / ".refactor-loop/host.env").write_text( - f"export RELEASE_AUTO_ENABLE={'true' if enabled else 'false'}\n", + f"export RELEASE_AUTO_ENABLE={'true' if enabled else 'false'}\n" + 'export HOST_GITHUB_RELEASE_REQUIRED_CHECKS="contract-tests,manifest-version-sync,skill-degradation"\n', encoding="utf-8", ) +def write_host_auto_release_without_required_checks(repo: Path, *, value: str | None = None) -> None: + (repo / ".refactor-loop").mkdir(parents=True, exist_ok=True) + lines = ["export RELEASE_AUTO_ENABLE=true"] + if value is not None: + lines.append(f"export HOST_GITHUB_RELEASE_REQUIRED_CHECKS={value}") + (repo / ".refactor-loop/host.env").write_text("\n".join(lines) + "\n", encoding="utf-8") + + def write_explicit_host_opt_in(repo: Path, enabled: bool = True) -> Path: path = repo / ".config/consensus-rnd/host.env" path.parent.mkdir(parents=True, exist_ok=True) path.write_text( - f"export RELEASE_AUTO_ENABLE={'true' if enabled else 'false'}\n", + f"export RELEASE_AUTO_ENABLE={'true' if enabled else 'false'}\n" + 'export HOST_GITHUB_RELEASE_REQUIRED_CHECKS="contract-tests,manifest-version-sync,skill-degradation"\n', encoding="utf-8", ) return path @@ -213,6 +223,20 @@ def test_ready_candidate_allows_matching_ref_version_and_green_checks(self) -> N self.assertEqual(result.version, "1.9.10") self.assertEqual(result.target_ref, "abc123") + def test_publish_preflight_fails_closed_when_auto_release_lacks_host_required_checks(self) -> None: + for value in (None, ""): + with self.subTest(value=value), copy_repo_fixture() as tmp: + repo = Path(tmp) / "repo" + write_host_auto_release_without_required_checks(repo, value=value) + write_ready_artifacts(repo, from_version="1.9.9", version="1.9.10", target_ref="abc123") + + result = ReleasePublishPreflight(repo, now=lambda: NOW).validate(target_ref="abc123") + + self.assertFalse(result.allowed) + self.assertIn("missing_host_required_release_checks", result.reasons) + other_reasons = set(result.reasons) - {"missing_host_required_release_checks"} + self.assertEqual(other_reasons, set()) + def test_explicit_host_env_opt_in_allows_preflight_without_legacy_file(self) -> None: with copy_repo_fixture() as tmp: repo = Path(tmp) / "repo" diff --git a/skills/codex-refactor-loop/scripts/test_release_publisher.py b/skills/codex-refactor-loop/scripts/test_release_publisher.py index 4ffc113a..c2dd8aed 100644 --- a/skills/codex-refactor-loop/scripts/test_release_publisher.py +++ b/skills/codex-refactor-loop/scripts/test_release_publisher.py @@ -22,7 +22,7 @@ from codex_refactor_loop.release.publish_preflight import PublishPreflightResult from codex_refactor_loop.release.publisher import ReleasePublisher -from codex_refactor_loop.release.required_checks import REQUIRED_RELEASE_CHECKS +FIXTURE_RELEASE_CHECKS = ("contract-tests", "manifest-version-sync", "skill-degradation") def write_json(path: Path, data: object) -> None: @@ -52,7 +52,11 @@ def copy_repo_fixture() -> tempfile.TemporaryDirectory[str]: target.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(source, target) (repo / ".refactor-loop").mkdir(parents=True, exist_ok=True) - (repo / ".refactor-loop/host.env").write_text('export GH_REPO_SLUG="owner/repo"\n', encoding="utf-8") + (repo / ".refactor-loop/host.env").write_text( + 'export GH_REPO_SLUG="owner/repo"\n' + 'export HOST_GITHUB_RELEASE_REQUIRED_CHECKS="contract-tests,manifest-version-sync,skill-degradation"\n', + encoding="utf-8", + ) return tmp @@ -103,7 +107,7 @@ def __call__(self, cmd: Sequence[str], cwd: Path) -> subprocess.CompletedProcess def _check_runs_payload(self) -> list[dict[str, object]]: check_runs = [] - for name in REQUIRED_RELEASE_CHECKS: + for name in FIXTURE_RELEASE_CHECKS: status = "completed" conclusion = "success" if self.check_status == "pending": diff --git a/skills/codex-refactor-loop/scripts/test_required_release_checks.py b/skills/codex-refactor-loop/scripts/test_required_release_checks.py index 88c82cdc..961906c2 100644 --- a/skills/codex-refactor-loop/scripts/test_required_release_checks.py +++ b/skills/codex-refactor-loop/scripts/test_required_release_checks.py @@ -16,11 +16,13 @@ sys.path.insert(0, str(SCRIPT_PATH.parent)) from codex_refactor_loop.release.required_checks import ( - REQUIRED_RELEASE_CHECKS, ReleaseRequiredChecksProjection, isoformat, + required_release_checks, ) +FIXTURE_RELEASE_CHECKS = ("contract-tests", "manifest-version-sync", "skill-degradation") + def check_run(name: str, *, conclusion: str = "success", status: str = "completed", at: datetime = NOW) -> dict[str, str]: return { @@ -52,8 +54,8 @@ def __call__(self, cmd: list[str]) -> subprocess.CompletedProcess[str]: class ReleaseRequiredChecksProjectionTests(unittest.TestCase): def test_all_required_exact_check_run_names_success(self) -> None: - runner = FakeRunner([{"check_runs": [check_run(name) for name in REQUIRED_RELEASE_CHECKS]}]) - projection = ReleaseRequiredChecksProjection(runner=runner, now=lambda: NOW) + runner = FakeRunner([{"check_runs": [check_run(name) for name in FIXTURE_RELEASE_CHECKS]}]) + projection = ReleaseRequiredChecksProjection(runner=runner, now=lambda: NOW, required_checks=FIXTURE_RELEASE_CHECKS) status = projection.check_ref("owner/repo", "dev", since=NOW - timedelta(hours=2)) @@ -64,7 +66,7 @@ def test_all_required_exact_check_run_names_success(self) -> None: def test_missing_required_check_fails_closed(self) -> None: runner = FakeRunner([{"check_runs": [check_run("consensus-rnd-ci")]}]) - projection = ReleaseRequiredChecksProjection(runner=runner, now=lambda: NOW) + projection = ReleaseRequiredChecksProjection(runner=runner, now=lambda: NOW, required_checks=FIXTURE_RELEASE_CHECKS) status = projection.check_ref("owner/repo", "dev", since=NOW - timedelta(hours=2)) @@ -80,7 +82,7 @@ def test_latest_red_beats_older_success_for_same_name(self) -> None: check_run("manifest-version-sync"), check_run("skill-degradation"), ]}]) - projection = ReleaseRequiredChecksProjection(runner=runner, now=lambda: NOW) + projection = ReleaseRequiredChecksProjection(runner=runner, now=lambda: NOW, required_checks=FIXTURE_RELEASE_CHECKS) status = projection.check_ref("owner/repo", "dev", since=NOW - timedelta(hours=2)) @@ -94,7 +96,7 @@ def test_pending_required_check_fails_after_timeout(self) -> None: check_run("manifest-version-sync"), check_run("skill-degradation"), ]}]) - projection = ReleaseRequiredChecksProjection(runner=runner, now=lambda: NOW) + projection = ReleaseRequiredChecksProjection(runner=runner, now=lambda: NOW, required_checks=FIXTURE_RELEASE_CHECKS) status = projection.check_ref("owner/repo", "dev", since=NOW - timedelta(hours=2), wait_seconds=0) @@ -109,7 +111,7 @@ def test_stale_completed_check_fails_closed(self) -> None: check_run("manifest-version-sync"), check_run("skill-degradation"), ]}]) - projection = ReleaseRequiredChecksProjection(runner=runner, now=lambda: NOW) + projection = ReleaseRequiredChecksProjection(runner=runner, now=lambda: NOW, required_checks=FIXTURE_RELEASE_CHECKS) status = projection.check_ref("owner/repo", "dev", since=NOW - timedelta(hours=2)) @@ -124,13 +126,33 @@ def test_invalid_json_and_api_failure_fail_closed(self) -> None: ) for runner, expected in cases: with self.subTest(expected=expected): - projection = ReleaseRequiredChecksProjection(runner=runner, now=lambda: NOW) + projection = ReleaseRequiredChecksProjection(runner=runner, now=lambda: NOW, required_checks=FIXTURE_RELEASE_CHECKS) status = projection.check_ref("owner/repo", "dev", since=NOW - timedelta(hours=2)) self.assertFalse(status.passed) self.assertEqual(status.reason, expected) + def test_required_release_checks_parse_only_host_env_key(self) -> None: + self.assertEqual( + required_release_checks({"HOST_GITHUB_RELEASE_REQUIRED_CHECKS": "unit, lint,types"}), + ("unit", "lint", "types"), + ) + self.assertEqual(required_release_checks({"REQUIRED_RELEASE_CHECKS": "legacy"}), ()) + + def test_default_projection_fails_closed_when_host_required_checks_missing_or_empty(self) -> None: + for env in ({}, {"HOST_GITHUB_RELEASE_REQUIRED_CHECKS": ""}): + with self.subTest(env=env): + runner = FakeRunner([{"check_runs": [check_run("contract-tests")]}]) + projection = ReleaseRequiredChecksProjection(runner=runner, now=lambda: NOW, env=env) + + status = projection.check_ref("owner/repo", "dev", since=NOW - timedelta(hours=2)) + + self.assertFalse(status.passed) + self.assertEqual(status.reason, "missing_host_required_release_checks") + self.assertEqual(status.checks, {}) + self.assertEqual(runner.commands, []) + if __name__ == "__main__": unittest.main() diff --git a/skills/codex-refactor-loop/scripts/test_restart_daemons.py b/skills/codex-refactor-loop/scripts/test_restart_daemons.py index 88e1093f..59ed3c7b 100644 --- a/skills/codex-refactor-loop/scripts/test_restart_daemons.py +++ b/skills/codex-refactor-loop/scripts/test_restart_daemons.py @@ -515,12 +515,17 @@ def test_restart_helper_source_mentions_launch_fingerprint_contract(self) -> Non "package_tree_sha256", "entrypoint_sha256", "pid_alive(pid)", - "actor-owned heartbeat", "FORBIDDEN_LIFECYCLE_AUTHORITY", "def restart_managed_daemon_names(", "return tuple(name for name, _command in DAEMON_COMMANDS)", ): self.assertIn(needle, source) + self.assertIn("heartbeat_file", source) + self.assertIn("RESTART_DAEMON_HEARTBEAT_FILE", source) + self.assertIn("RESTART_DAEMON_HEARTBEAT_INTERVAL", source) + self.assertNotIn("Refactor (", source) + self.assertNotIn("Old pattern", source) + self.assertNotIn("New principle", source) for forbidden in ("gh issue", "gh pr", "git fetch", "git push", "git merge"): self.assertNotIn(forbidden, source) self.assertNotIn("RESTART_MANAGED_DAEMON_NAMES", source) diff --git a/skills/codex-refactor-loop/scripts/test_skill_entrypoint_contract.py b/skills/codex-refactor-loop/scripts/test_skill_entrypoint_contract.py index 9c467c78..33a88824 100644 --- a/skills/codex-refactor-loop/scripts/test_skill_entrypoint_contract.py +++ b/skills/codex-refactor-loop/scripts/test_skill_entrypoint_contract.py @@ -53,6 +53,22 @@ def test_frontmatter_contract_is_minimal_and_trigger_only(self) -> None: self.assertTrue(lines[1].startswith("description: Use when ")) self.assertLessEqual(len(body), 1024) + def test_audit_is_not_documented_as_default_main_path(self) -> None: + match = re.match(r"\A---\n(?P.*?)\n---\n", self.skill, flags=re.DOTALL) + self.assertIsNotNone(match) + body = match.group("body") + top = "\n".join(self.skill.splitlines()[:120]) + + self.assertIn("name: codex-refactor-loop", body) + self.assertIn("issue/PR resolution and work-unit loop", body) + self.assertIn("audit/refactor as a fallback compatibility issue producer", body) + self.assertIn("# Consensus R&D Work-Unit Loop", top) + self.assertIn("## Main path and fallback producer", top) + self.assertIn("open actionable catalog-managed GitHub issue/PR resolution", top) + self.assertNotIn("audit/refactor as the default compatibility intake", body) + self.assertNotIn("Audit is the default", top) + self.assertNotIn("The loop has two supported entry modes", top) + def test_entrypoint_line_budget_and_controller_contract_headings(self) -> None: headings = set(markdown_headings(self.skill)) @@ -319,17 +335,12 @@ def test_wakeup_plan_entrypoint_contract_is_read_only_and_authorized(self) -> No def test_wakeup_plan_script_declares_allowed_forbidden_boundary(self) -> None: script = read(PACKAGE_WAKEUP_PLAN) for needle in ( - "Allowed: read `.refactor-loop` files", - "issue-190", - "git fetch origin --quiet", - "git worktree list --porcelain", - "git rev-parse --verify HEAD", - "git rev-parse --verify refs/remotes/origin/", - "git rev-list --count refs/remotes/origin/..HEAD", - "Forbidden: no restart/spawn, no git lifecycle or mutation", - "no commit, push, checkout/switch", - "no GitHub lifecycle mutation", - ".refactor-loop/runs/phase9-issue190-r3-judge.md", + "def unpushed_worker_output_actions", + '["git", "-C", str(repo_root), "fetch", "origin", "--quiet"]', + '["git", "-C", str(repo_root), "worktree", "list", "--porcelain"]', + '["git", "-C", str(worktree), "rev-parse", "--verify", "HEAD"]', + '["git", "-C", str(worktree), "rev-parse", "--verify", remote_ref]', + '["git", "-C", str(worktree), "rev-list", "--count", f"{remote_ref}..HEAD"]', "no_lifecycle_authority", "count_in_flight_codex", "HARD_GATE:dispatch_required", @@ -338,6 +349,9 @@ def test_wakeup_plan_script_declares_allowed_forbidden_boundary(self) -> None: ): with self.subTest(needle=needle): self.assertIn(needle, script) + for forbidden in ("Refactor (", "Old pattern", "New principle", '"git", "push"', '"git", "commit"', '"git", "checkout"'): + with self.subTest(forbidden=forbidden): + self.assertNotIn(forbidden, script) self.assertNotIn("WorkerOutputProjection", script) self.assertNotIn("codex_refactor_loop.projections", script) diff --git a/skills/codex-refactor-loop/scripts/test_skill_reference_anchors.py b/skills/codex-refactor-loop/scripts/test_skill_reference_anchors.py index f6122444..ed102980 100644 --- a/skills/codex-refactor-loop/scripts/test_skill_reference_anchors.py +++ b/skills/codex-refactor-loop/scripts/test_skill_reference_anchors.py @@ -58,6 +58,18 @@ def section_after_heading(markdown: str, heading: str) -> str: return _section_after_first_heading(markdown[match.start() :]) +def section_after_anchor_until_heading(markdown: str, anchor: str, level: int) -> str: + marker = f'' + _, found, after_anchor = markdown.partition(marker) + if not found: + raise AssertionError(f"missing markdown anchor: {anchor}") + pattern = re.compile(rf"(?m)^#{{1,{level}}}\s+") + first_heading = pattern.search(after_anchor) + start = first_heading.end() if first_heading else 0 + match = pattern.search(after_anchor, start) + return after_anchor[: match.start()] if match else after_anchor + + def _section_after_first_heading(markdown: str) -> str: lines = markdown.splitlines(keepends=True) if not lines: @@ -162,22 +174,70 @@ def test_no_absolute_reference_links_in_entrypoint(self) -> None: self.assertNotRegex(self.skill, r"/Users/[^)\s]+") self.assertNotRegex(self.skill, r"REFERENCE\.md#/[^\s)]+") - def test_skill_documents_two_entry_modes_near_top(self) -> None: + def test_skill_documents_main_path_and_fallback_producer_near_top(self) -> None: top = "\n".join(self.skill.splitlines()[:200]) for needle in ( - "## Two entry modes", - "audit-driven", + "## Main path and fallback producer", + "open actionable catalog-managed GitHub issue/PR resolution", "issue-driven / Path A", "catalog-derived design issue label bundle", "crnd:lifecycle:managed", "crnd:phase:design-solving", "crnd:human:auto", "Legacy issue-entry labels are migration aliases only", - "Audit is a seed producer, not the only entry", + "`audit` remains a stable compatibility producer value and fallback issue producer", + "no open actionable managed issue/PR", + "Audit produces or updates issues that feed back into the main path", + "not a co-equal entry mode", ): with self.subTest(needle=needle): self.assertIn(needle, top) + self.assertIn('', top) + self.assertNotIn("The loop has two supported entry modes", top) + self.assertNotIn("audit-driven", top) + + def test_detailed_reference_uses_issue_pr_main_path_and_audit_fallback(self) -> None: + producers = section_after_heading(self.skill, "Producers") + work_intake = section_after_heading(self.skill, "Consensus-rnd Phase work-intake — Fallback issue production") + bootstrap = section_after_heading( + self.skill, + "Consensus-rnd Phase bootstrap — Bootstrap (first wakeup only)", + ) + detailed = "\n".join((producers, work_intake, bootstrap)) + + for needle in ( + "The default main path is open actionable managed issue/PR resolution", + "`audit` is the fallback raw artifact issue producer", + "`audit` remains the stable compatibility producer value and fallback issue producer, not the default\nmain path", + "先扫 open actionable managed issue/PR 并派 next-step actor", + ): + with self.subTest(needle=needle): + self.assertIn(needle, detailed) + for forbidden in ( + "`audit` remains the default producer", + "The default work-unit producer is `audit`", + "派默认 work-unit producer", + "默认 audit", + "默认 producer", + ): + with self.subTest(forbidden=forbidden): + self.assertNotIn(forbidden, detailed) + def test_project_rules_do_not_duplicate_skill_local_main_path_contract(self) -> None: + claude = read(REPO_ROOT / "CLAUDE.md") + for forbidden in ( + "issue resolution 是主路径", + "audit 是 fallback producer", + "open actionable managed GitHub issue/PR", + ): + with self.subTest(forbidden=forbidden): + self.assertNotIn(forbidden, claude) + self.assertIn("Refactoring, issue-solving, and repository R&D are different entry surfaces", self.readme) + self.assertIn("## Main path and fallback producer", self.skill) + + # Refactor (iter364/issue364): + # Old pattern: Path-A solvers dispatched with --cd $REPO_ROOT (integration checkout) can't see work-unit source when the issue references files on a divergent non-integration branch, emitting spurious no-plan and wasting rounds. + # New principle: Contract-only source locator: SKILL solver source contract + 3 solver prompts document a read-only source-locator recipe (git show : / raw URL / gh api / host.env), classify missing/invalid locator as source-location-missing-or-invalid; NO new projection/parser/header/module. def test_skill_documents_phase9_solver_source_contract(self) -> None: phase9 = section_after_heading( self.skill, @@ -192,10 +252,61 @@ def test_skill_documents_phase9_solver_source_contract(self) -> None: "issue body/comments are the scope source", "must not be fabricated", "A missing audit `evidence:` block is not by itself a defect for manual issues", + "Path A issue body/comments that cite files absent from the current checkout", + "read-only source locator", + "git show :", + "raw URL", + "gh api", + ".refactor-loop/host.env", + "must not directly emit a generic `no-plan`", + "source-location-missing-or-invalid", ): with self.subTest(needle=needle): self.assertIn(needle, phase9) + def test_release_countdown_contract_is_wakeup_plan_only_status_projection(self) -> None: + milestone = section_after_heading(self.skill, "Milestone priority(强制)") + wakeup = section_after_heading(self.skill, "Wakeup Skeleton") + + for needle in ( + "crnd:milestone:release-target", + "release countdown status", + "non-exclusive milestone fact", + "crnd:milestone:current` remains dispatch priority only and must not trigger release countdown by itself", + "wakeup-plan-only and read-only", + "status-only, non-dispatchable `release-countdown` action", + "release-gate scoring source", + ".version-bump.json", + "existing release commits projection", + "no_lifecycle_authority", + "targets", + "from_version", + "to_version", + "stability_score", + "ready", + "red_signals", + "blocked_reasons", + 'source: "release-gate"', + ): + with self.subTest(needle=needle): + self.assertIn(needle, milestone) + for forbidden in ( + "create a daemon", + "write state", + "update statusline", + "update peek", + "create a top-level duplicate object", + "write a release decision", + "mutate labels", + "tag", + "publish a release", + "add lifecycle authority", + ): + with self.subTest(forbidden=forbidden): + self.assertIn(forbidden, milestone) + self.assertIn("release-countdown status is status-only", wakeup) + self.assertIn("not dispatchable", wakeup) + def test_skill_documents_transition_assessment_sidecar_boundary(self) -> None: work_unit = section_after_anchor(self.skill, "work-unit-contract") producers = section_after_heading(self.skill, "Producers") @@ -275,11 +386,22 @@ def test_downstream_install_walkthrough_contract(self) -> None: walkthrough, ) - def test_guided_github_consensus_workflow_setup_is_artifact_only(self) -> None: + def test_github_workflow_portability_checklist_is_folded_into_skill(self) -> None: walkthrough = section_after_anchor(self.skill, "downstream-install-walkthrough") - self.assertIn("This walkthrough is the only downstream install runbook", walkthrough) - self.assertIn("### Guided GitHub consensus workflow setup", walkthrough) + checklist = section_after_anchor_until_heading(self.skill, "github-workflow-portability-checklist", 3) + self.assertIn("SKILL.md#github-workflow-portability-checklist", self.readme) + self.assertIn("Host GitHub workflow portability", self.readme) for needle in ( + "#104 setup is folded into this skill's existing owner surface", + ".config/consensus-rnd/host.env", + "HOST_WORKFLOW_SPEC", + "exactly seven data-only surfaces", + "`events`, `stages`, `work_unit_kinds`, `roles`, `prompt_bindings`, `consensus_policies`, and `issue_intake_mappings`", + "no host `.github` edits", + "no branch-protection probing or edits", + "Future #357 interactive configuration", + "must output these same host-owned artifacts", + "#### Guided GitHub consensus workflow setup", ".refactor-loop/runs/github-workflow-setup//", "host-env.patch.md", "labels-plan.json", @@ -299,7 +421,19 @@ def test_guided_github_consensus_workflow_setup_is_artifact_only(self) -> None: "advisory only", ): with self.subTest(needle=needle): - self.assertIn(needle, walkthrough) + self.assertIn(needle, checklist) + self.assertIn("GitHub workflow portability checklist", walkthrough) + self.assertFalse((REPO_ROOT / "skills" / "consensus-github-workflow-setup").exists()) + for forbidden in ( + "HostWorkflowPortabilityProjection", + "GitHubHostPolicy", + "HOST_GITHUB_LABEL_MAP", + "branch-protection probe", + "Projects adapter", + ): + with self.subTest(forbidden=forbidden): + self.assertNotIn(forbidden, self.skill) + self.assertNotIn(forbidden, self.readme) for forbidden in ( "summary.json", @@ -328,13 +462,39 @@ def test_guided_github_consensus_workflow_setup_is_artifact_only(self) -> None: "lifecycle surface", ): with self.subTest(forbidden=forbidden): - self.assertIn(forbidden, walkthrough) + self.assertIn(forbidden, checklist) self.assertNotIn("GuidedWorkflowSetupBundle", self.skill) self.assertFalse((REPO_ROOT / "skills" / "github-workflow-setup").exists()) self.assertFalse((SKILL_ROOT / "scripts" / "codex_refactor_loop" / "setup.py").exists()) self.assertEqual(0, len(list((SKILL_ROOT / "scripts" / "codex_refactor_loop").glob("*setup*")))) + def test_release_required_checks_contract_is_host_configurable(self) -> None: + source_repo_validation = section_after_heading(self.skill, "Skill degradation source-repo validation") + details = section_after_anchor_until_heading(self.skill, "skill-degradation-source-repo-validation-details", 3) + release_schema = section_after_anchor_until_heading(self.skill, "release-decision-schema", 3) + combined = "\n".join((source_repo_validation, details, release_schema)) + + for needle in ( + "$HOST_GITHUB_RELEASE_REQUIRED_CHECKS", + "required_release_checks()", + "host.env", + "host configures the exact required GitHub check-run names", + "release required checks are not hardcoded by source-repo CI job names", + "Shared Checks API projection sees exact check-run name success for every name in `$HOST_GITHUB_RELEASE_REQUIRED_CHECKS`", + "auto-release with an empty list fails closed", + ): + with self.subTest(needle=needle): + self.assertIn(needle, combined) + + stale_contracts = ( + "requires `skill-degradation` beside `contract-tests` and `manifest-version-sync`", + "release gate `consensus-rnd-cli release-gate:required_checks_recent_green` requires `skill-degradation` beside `contract-tests` and `manifest-version-sync`", + ) + for stale in stale_contracts: + with self.subTest(stale=stale): + self.assertNotIn(stale, combined) + def test_skill_documents_update_check_notify_only_contract(self) -> None: section = section_after_heading(self.skill, "Notify-only update check(per #231)") for needle in ( @@ -632,25 +792,22 @@ def test_stale_revival_documents_updated_at_as_metadata_only(self) -> None: def test_phase9_router_issue167_refactor_self_doc_source_regression(self) -> None: router = (SKILL_ROOT / "scripts" / "codex_refactor_loop" / "phase9" / "router.py").read_text(encoding="utf-8") for token in ( - "Refactor (iter1/issue-167)", - "Old pattern: solver triplet handoff recorded only the base dispatch row", - "durable triplet provenance", - "visible same-round peer artifact reference failure", - "New principle: keep row-level router-private ledger provenance", - "narrow fail-closed peer artifact token check", - "do not add a", - "standalone evidence file", - "hash", - "lifecycle authority", + "_solver_triplet_ledger_fields", + "_peer_solver_reference_violation", + "_peer_solver_reference_tokens", + "clean_exit_solver_logs", + "solver_input_prompts", + "judge_input_solver_logs", + "judge_prompt_scope", + "phase9-triplet-evidence-invalid", + "Dispatch ledger evidence:", + "phase9-router-fallback", ): with self.subTest(token=token): self.assertIn(token, router) def test_skill_documents_cli_runtime_authority_fact_source(self) -> None: required = ( - "Refactor (iter1/issue-166)", - "Old pattern: CLI command authority was represented by coarse read_only metadata", - "New principle: `cli.py::COMMANDS[*].authority` is the inline closed-token mechanical fact source", "dev-sync's integration-worktree git surface", "## CLI runtime authority fact source(per #166)", "cli.py::COMMANDS[*].authority", @@ -880,8 +1037,11 @@ def test_phase9_direct_spawn_intent_allowlist_ignores_host_workflow_spec_sources for required in ( "ROLES = (\"minimal\", \"structural\", \"delete\")", "JUDGE_ROLE = \"judge\"", + "def _solver_roles", "return ROLES", + "def _judge_role", "return JUDGE_ROLE", + "class Phase9Router", "Phase9 direct-spawn-intent ignores HostWorkflowSpec role/dispatch/policy data entirely", ): with self.subTest(required=required): @@ -901,7 +1061,10 @@ def test_phase9_router_filename_identity_source_regression_keeps_role_markers(se router = (SKILL_ROOT / "scripts" / "codex_refactor_loop" / "phase9" / "router.py").read_text(encoding="utf-8") helper = router + "\n" + (SKILL_ROOT / "scripts" / "consensus-rnd-cli").read_text(encoding="utf-8") combined = "\n".join((self.skill, router, helper)) - self.assertIn("Refactor (issue-100/router-filename-identity)", router) + self.assertIn("parse_phase9_log_identity", router) + self.assertIn("PHASE9_LOG_RE", router) + self.assertIn("SOLVER_LOG_RE", router) + self.assertIn("META_JUDGE_LOG_RE", router) self.assertIn("SOLVER_DONE::", combined) self.assertNotIn("SOLVER_DONE:::", combined) self.assertIn("consensus-rnd-cli", helper) @@ -919,8 +1082,9 @@ def test_phase9_converge_adjacent_round_helper_source_regression(self) -> None: for token in ( "_converge_target_round", "canonical payload is the judge log source round", - "source-round and legacy", - "non-adjacent payloads fall back", + "payload_round in {source_round, source_round + 1}", + "return source_round + 1", + "return None", "clean rS judge canonical payload is `round-S`", "legacy `round-(S+1)`", "non-adjacent payload mismatch falls back", @@ -945,7 +1109,6 @@ def test_phase9_stalled_is_router_owned_predicate_source_regression(self) -> Non combined = "\n".join((self.skill, meta_judge, router, marker_contract, profile_contract)) for token in ( - "Refactor (issue-304)", "meta-judge emits only consensus/converge", "router-owned stalled predicate", "_dispatch_stalled_reflector", diff --git a/skills/codex-refactor-loop/scripts/test_solver_prompt_scope_sources.py b/skills/codex-refactor-loop/scripts/test_solver_prompt_scope_sources.py index e0dc6f64..b74bb114 100644 --- a/skills/codex-refactor-loop/scripts/test_solver_prompt_scope_sources.py +++ b/skills/codex-refactor-loop/scripts/test_solver_prompt_scope_sources.py @@ -59,6 +59,27 @@ def test_solver_prompts_do_not_require_audit_evidence_for_issue_driven_work(self for needle in required: self.assertIn(needle, prompt) + # Refactor (iter364/issue364): + # Old pattern: Path-A solvers dispatched with --cd $REPO_ROOT (integration checkout) can't see work-unit source when the issue references files on a divergent non-integration branch, emitting spurious no-plan and wasting rounds. + # New principle: Contract-only source locator: SKILL solver source contract + 3 solver prompts document a read-only source-locator recipe (git show : / raw URL / gh api / host.env), classify missing/invalid locator as source-location-missing-or-invalid; NO new projection/parser/header/module. + def test_solver_prompts_document_divergent_source_locator_contract(self) -> None: + for prompt_name in SOLVER_PROMPTS: + prompt = (PROMPTS_DIR / prompt_name).read_text(encoding="utf-8") + with self.subTest(prompt=prompt_name): + for needle in ( + "source locator", + "current checkout", + "read-only", + "git show :", + "raw URL", + "gh api", + ".refactor-loop/host.env", + "must not fetch/checkout/switch/merge/rebase/reset", + "must not create source worktree/add-dir", + "source-location-missing-or-invalid", + ): + self.assertIn(needle, prompt) + if __name__ == "__main__": unittest.main() diff --git a/skills/codex-refactor-loop/scripts/test_source_language_policy.py b/skills/codex-refactor-loop/scripts/test_source_language_policy.py new file mode 100644 index 00000000..47c0afc5 --- /dev/null +++ b/skills/codex-refactor-loop/scripts/test_source_language_policy.py @@ -0,0 +1,310 @@ +#!/usr/bin/env python3 +"""Test-only source language policy guard.""" + +from __future__ import annotations + +import ast +import os +import re +import tokenize +import unittest +from dataclasses import dataclass +from pathlib import Path +from tempfile import TemporaryDirectory + + +SCRIPT_PATH = Path(__file__).resolve() +REPO_ROOT = SCRIPT_PATH.parents[3] +REF_HISTORY_TOKENS = ("Refactor (", "Old pattern", "New principle") +REF_HISTORY_ITER_CLUSTER_RE = re.compile(r"\biter(?:\d+|N)/cluster[A-Za-z0-9_-]*\b") +HAN_START = "\u4e00" +HAN_END = "\u9fff" +LOG_METHODS = {"debug", "info", "warning", "warn", "error", "exception", "critical"} +ERROR_TYPES = {"Exception", "RuntimeError", "ValueError", "KeyError", "TypeError", "SystemExit"} +COMMIT_TEMPLATE_NAMES = {"commit_body", "commit_message", "body_lines"} +REFACTOR_POLICY_NONE = "none" +REFACTOR_POLICY_SELF_DOC = "self-doc-comment" + + +@dataclass(frozen=True) +class Finding: + relative_path: str + owner: str + line: int + reason: str + text: str + + +@dataclass(frozen=True) +class AllowlistEntry: + relative_path: str + owner: str + reason: str + + +ALLOWLIST: tuple[AllowlistEntry, ...] = ( + AllowlistEntry("skills/codex-refactor-loop/scripts/codex_refactor_loop/banners.py", "ROLE_NEXT_STEPS", "GitHub banner body is intentionally Chinese user-facing output"), + AllowlistEntry("skills/codex-refactor-loop/scripts/codex_refactor_loop/banners.py", "build_status_banner", "GitHub banner body is intentionally Chinese user-facing output"), + AllowlistEntry("skills/codex-refactor-loop/scripts/codex_refactor_loop/github_body.py", "DEBUG_SUMMARY", "debug details summary is intentional Chinese GitHub output"), + AllowlistEntry("skills/codex-refactor-loop/scripts/codex_refactor_loop/github_body.py", "AUTHORITY_PATH_RE", "validator regex recognizes Chinese authority labels in GitHub bodies"), + AllowlistEntry("skills/codex-refactor-loop/scripts/codex_refactor_loop/github_body.py", "INLINE_ARTIFACT_DETAILS_RE", "validator regex recognizes Chinese inline artifact details"), + AllowlistEntry("skills/codex-refactor-loop/scripts/codex_refactor_loop/github_body.py", "render_github_body", "self-contained GitHub body text is intentionally Chinese user-facing output"), + AllowlistEntry("skills/codex-refactor-loop/scripts/codex_refactor_loop/github_body.py", "validate_self_contained_github_body", "validator error references intentional Chinese debug heading"), + AllowlistEntry("skills/codex-refactor-loop/scripts/codex_refactor_loop/github_body.py", "_kind_label", "GitHub body kind labels are intentionally Chinese user-facing output"), + AllowlistEntry("skills/codex-refactor-loop/scripts/codex_refactor_loop/labels.py", "CLEANUP_ONLY_ALIASES", "GitHub label names include legacy Chinese catalog entries"), + AllowlistEntry("skills/codex-refactor-loop/scripts/codex_refactor_loop/labels.py", "LABEL_SPECS", "GitHub label names include legacy Chinese catalog entries"), + AllowlistEntry("skills/codex-refactor-loop/scripts/codex_refactor_loop/monitors/comment.py", "CommentMonitor.post_banner.banner_body", "maintainer-facing GitHub notification text is intentionally Chinese"), + AllowlistEntry("skills/codex-refactor-loop/scripts/codex_refactor_loop/monitors/progress.py", "ProgressReporter.build_body", "progress comments are intentionally Chinese user-facing output"), + AllowlistEntry("skills/codex-refactor-loop/scripts/codex_refactor_loop/peek.py", "PeekStatusLens.render", "status lens renders existing Chinese labels and user-facing state"), + AllowlistEntry("skills/codex-refactor-loop/scripts/codex_refactor_loop/project_rules.py", "CANONICAL_BODY", "project-rules fixed point text is intentionally Chinese host-facing policy"), + AllowlistEntry("skills/codex-refactor-loop/scripts/codex_refactor_loop/project_rules.py", "OLD_CANONICAL_BODY", "legacy project-rules fixed point text is intentionally Chinese host-facing policy"), +) + + +def has_han(text: str) -> bool: + return any(HAN_START <= char <= HAN_END for char in text) + + +def has_refactor_history(text: str) -> bool: + return any(token in text for token in REF_HISTORY_TOKENS) or REF_HISTORY_ITER_CLUSTER_RE.search(text) is not None + + +def source_files(repo_root: Path = REPO_ROOT) -> list[Path]: + skill_root = repo_root / "skills" / "codex-refactor-loop" + python_source_roots = ( + repo_root / ".github" / "scripts", + skill_root / "scripts" / "codex_refactor_loop", + ) + files: list[Path] = [] + for root in python_source_roots: + if not root.exists(): + continue + files.extend(path for path in root.rglob("*.py") if path.name != "degradation.py") + return sorted(files) + + +def relative(path: Path, repo_root: Path = REPO_ROOT) -> str: + return path.relative_to(repo_root).as_posix() + + +def build_parent_map(tree: ast.AST) -> dict[ast.AST, ast.AST]: + parent_map: dict[ast.AST, ast.AST] = {} + for parent in ast.walk(tree): + for child in ast.iter_child_nodes(parent): + parent_map[child] = parent + return parent_map + + +def literal_owner(node: ast.Constant, parent_map: dict[ast.AST, ast.AST]) -> str: + parts: list[str] = [] + target: ast.AST | None = node + while target is not None: + if isinstance(target, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)): + parts.append(target.name) + assign_parent = parent_map.get(target) + if isinstance(assign_parent, ast.Assign): + for assigned in assign_parent.targets: + if isinstance(assigned, ast.Name): + parts.append(assigned.id) + elif isinstance(assign_parent, ast.AnnAssign) and isinstance(assign_parent.target, ast.Name): + parts.append(assign_parent.target.id) + target = assign_parent + return ".".join(reversed(parts)) or "module-string" + + +def call_name(node: ast.AST) -> str: + if isinstance(node, ast.Name): + return node.id + if isinstance(node, ast.Attribute): + return node.attr + return "" + + +def nearest_call(node: ast.AST, parent_map: dict[ast.AST, ast.AST]) -> ast.Call | None: + current: ast.AST | None = node + while current is not None: + if isinstance(current, ast.Call): + return current + current = parent_map.get(current) + return None + + +def assigned_names(node: ast.AST, parent_map: dict[ast.AST, ast.AST]) -> set[str]: + parent = parent_map.get(node) + names: set[str] = set() + if isinstance(parent, ast.Assign): + for target in parent.targets: + if isinstance(target, ast.Name): + names.add(target.id) + elif isinstance(parent, ast.AnnAssign) and isinstance(parent.target, ast.Name): + names.add(parent.target.id) + return names + + +def is_docstring_node(node: ast.Constant, parent_map: dict[ast.AST, ast.AST]) -> bool: + parent = parent_map.get(node) + grandparent = parent_map.get(parent) if parent is not None else None + if not isinstance(parent, ast.Expr): + return False + if not isinstance(grandparent, (ast.Module, ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)): + return False + return bool(grandparent.body and grandparent.body[0] is parent) + + +def is_selected_string(node: ast.Constant, parent_map: dict[ast.AST, ast.AST]) -> bool: + if is_docstring_node(node, parent_map): + return True + call = nearest_call(node, parent_map) + if call is not None and call_name(call.func) in LOG_METHODS | ERROR_TYPES: + return True + if assigned_names(node, parent_map) & COMMIT_TEMPLATE_NAMES: + return True + return False + + +def normalize_refactor_comment_policy(raw: str | None = None) -> str: + value = (raw if raw is not None else os.environ.get("HOST_REFACTOR_COMMENT_POLICY", "")).strip() + if not value or value == "default": + return REFACTOR_POLICY_NONE + if value in {REFACTOR_POLICY_NONE, REFACTOR_POLICY_SELF_DOC}: + return value + raise ValueError(f"invalid HOST_REFACTOR_COMMENT_POLICY: {value}") + + +def comment_findings(path: Path, repo_root: Path = REPO_ROOT, *, forbid_refactor_history: bool = True) -> list[Finding]: + findings: list[Finding] = [] + with tokenize.open(path) as fh: + tokens = tokenize.generate_tokens(fh.readline) + for token in tokens: + if token.type != tokenize.COMMENT: + continue + text = token.string + if has_han(text): + findings.append(Finding(relative(path, repo_root), "comment", token.start[0], "han-comment", text.strip())) + if forbid_refactor_history and has_refactor_history(text): + findings.append(Finding(relative(path, repo_root), "comment", token.start[0], "refactor-history-comment", text.strip())) + return findings + + +def string_findings(path: Path, repo_root: Path = REPO_ROOT, *, forbid_refactor_history: bool = True) -> list[Finding]: + source = path.read_text(encoding="utf-8") + tree = ast.parse(source) + parent_map = build_parent_map(tree) + findings: list[Finding] = [] + for node in ast.walk(tree): + if not isinstance(node, ast.Constant) or not isinstance(node.value, str): + continue + text = node.value + owner = literal_owner(node, parent_map) + if has_han(text): + reason = "han-docstring" if is_docstring_node(node, parent_map) else "han-string" + if is_selected_string(node, parent_map) and reason != "han-docstring": + reason = "han-selected-string" + findings.append(Finding(relative(path, repo_root), owner, node.lineno, reason, text[:160])) + if forbid_refactor_history and has_refactor_history(text): + findings.append(Finding(relative(path, repo_root), owner, node.lineno, "refactor-history-string", text[:160])) + return findings + + +def scan_python_source_language(repo_root: Path = REPO_ROOT, *, refactor_comment_policy: str | None = None) -> list[Finding]: + policy = normalize_refactor_comment_policy(refactor_comment_policy) + forbid_refactor_history = policy == REFACTOR_POLICY_NONE + findings: list[Finding] = [] + for path in source_files(repo_root): + findings.extend(comment_findings(path, repo_root, forbid_refactor_history=forbid_refactor_history)) + findings.extend(string_findings(path, repo_root, forbid_refactor_history=forbid_refactor_history)) + return [finding for finding in findings if not is_allowlisted(finding)] + + +def is_allowlisted(finding: Finding) -> bool: + return any( + entry.relative_path == finding.relative_path and finding.owner.startswith(entry.owner) + for entry in ALLOWLIST + ) + + +class SourceLanguagePolicyTests(unittest.TestCase): + def test_scan_python_source_language_is_clean(self) -> None: + findings = scan_python_source_language(REPO_ROOT) + details = "\n".join(f"{f.relative_path}:{f.line}:{f.owner}:{f.reason}:{f.text}" for f in findings[:50]) + self.assertEqual([], findings, details) + + def test_scanner_rejects_han_comments_docstrings_and_refactor_history(self) -> None: + with TemporaryDirectory() as temp_dir: + repo_root = Path(temp_dir) + source_path = repo_root / ".github" / "scripts" / "prohibited_sample.py" + source_path.parent.mkdir(parents=True) + source_path.write_text( + '"""Chinese text in source: 中文 docstring"""\n' + "# Refactor (iter1/example): Old pattern: 中文 comment history\n" + "# iter3/cluster-016 rationale\n" + "def run() -> None:\n" + ' raise ValueError("Chinese text in source: 中文 error")\n', + encoding="utf-8", + ) + + comment_results = comment_findings(source_path, repo_root) + string_results = string_findings(source_path, repo_root) + scan_results = scan_python_source_language(repo_root) + + expected_path = ".github/scripts/prohibited_sample.py" + comment_reasons = {(finding.relative_path, finding.line, finding.reason) for finding in comment_results} + string_reasons = {(finding.relative_path, finding.owner, finding.reason) for finding in string_results} + scan_reasons = {(finding.relative_path, finding.line, finding.reason) for finding in scan_results} + + self.assertIn((expected_path, 2, "han-comment"), comment_reasons) + self.assertIn((expected_path, 2, "refactor-history-comment"), comment_reasons) + self.assertIn((expected_path, 3, "refactor-history-comment"), comment_reasons) + self.assertIn((expected_path, "module-string", "han-docstring"), string_reasons) + self.assertIn((expected_path, "run", "han-selected-string"), string_reasons) + self.assertIn((expected_path, 1, "han-docstring"), scan_reasons) + self.assertIn((expected_path, 2, "han-comment"), scan_reasons) + self.assertIn((expected_path, 2, "refactor-history-comment"), scan_reasons) + self.assertIn((expected_path, 3, "refactor-history-comment"), scan_reasons) + self.assertIn((expected_path, 5, "han-selected-string"), scan_reasons) + + def test_self_doc_policy_allows_english_refactor_history_comments_only(self) -> None: + with TemporaryDirectory() as temp_dir: + repo_root = Path(temp_dir) + source_path = repo_root / ".github" / "scripts" / "allowed_self_doc.py" + source_path.parent.mkdir(parents=True) + source_path.write_text( + "# Refactor (iter1/example):\n" + "# Old pattern: Previous behavior was hard to review.\n" + "# New principle: Keep local rationale readable when explicitly enabled.\n" + "def run() -> None:\n" + " return None\n", + encoding="utf-8", + ) + + self.assertEqual([], scan_python_source_language(repo_root, refactor_comment_policy=REFACTOR_POLICY_SELF_DOC)) + none_findings = scan_python_source_language(repo_root, refactor_comment_policy=REFACTOR_POLICY_NONE) + + self.assertEqual( + {(1, "refactor-history-comment"), (2, "refactor-history-comment"), (3, "refactor-history-comment")}, + {(finding.line, finding.reason) for finding in none_findings}, + ) + + def test_invalid_refactor_comment_policy_fails_closed(self) -> None: + with TemporaryDirectory() as temp_dir: + repo_root = Path(temp_dir) + (repo_root / ".github" / "scripts").mkdir(parents=True) + + with self.assertRaises(ValueError): + scan_python_source_language(repo_root, refactor_comment_policy="maybe") + + def test_allowlist_entries_match_current_literals(self) -> None: + raw_findings: list[Finding] = [] + for path in source_files(): + raw_findings.extend(comment_findings(path)) + raw_findings.extend(string_findings(path)) + for entry in ALLOWLIST: + with self.subTest(entry=entry): + self.assertTrue( + any(f.relative_path == entry.relative_path and f.owner.startswith(entry.owner) for f in raw_findings), + entry, + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/skills/codex-refactor-loop/scripts/test_source_publication_boundary.py b/skills/codex-refactor-loop/scripts/test_source_publication_boundary.py index ea961a38..3402dde4 100644 --- a/skills/codex-refactor-loop/scripts/test_source_publication_boundary.py +++ b/skills/codex-refactor-loop/scripts/test_source_publication_boundary.py @@ -53,8 +53,10 @@ def test_publication_boundary_contract_is_release_gated(self) -> None: "python3 -m unittest discover -s skills/codex-refactor-loop/scripts -p 'test_*.py'", workflow, ) - self.assertIn('"contract-tests"', required_checks) - self.assertIn('"skill-degradation"', required_checks) + self.assertIn("HOST_GITHUB_RELEASE_REQUIRED_CHECKS", required_checks) + self.assertIn("required_release_checks", required_checks) + self.assertNotIn('"contract-tests"', required_checks) + self.assertNotIn('"skill-degradation"', required_checks) if __name__ == "__main__": diff --git a/skills/codex-refactor-loop/scripts/test_sync_dev.py b/skills/codex-refactor-loop/scripts/test_sync_dev.py index 3b4146b0..5f2daa9a 100644 --- a/skills/codex-refactor-loop/scripts/test_sync_dev.py +++ b/skills/codex-refactor-loop/scripts/test_sync_dev.py @@ -577,20 +577,27 @@ def test_dev_sync_source_does_not_read_legacy_branch_or_worktree_aliases(self) - def test_narrow_allowlist_contract_is_visible_in_module_source(self) -> None: src = SYNC_DEV.read_text(encoding="utf-8") - self.assertIn("daemon writes IntegrationSyncOperation", src) - self.assertIn("executes the #53 integration-branch git allowlist itself", src) + self.assertIn("IntegrationSyncOperation", src) + self.assertIn("write_operation_artifact", src) + self.assertIn("def execute_sync_operation", src) self.assertIn("DEV_SYNC_PENDING:release-rollup-needed:", src) self.assertIn('["git", "ls-remote", "--exit-code", "--heads", "origin", branch]', src) self.assertIn('append_pending_event("missing-integration-branch", self.integration)', src) self.assertIn('head_name.startswith("rollup/")', src) self.assertNotIn("DEV_SYNC_REQUEST:", src) + self.assertNotIn("Refactor (", src) + self.assertNotIn("Old pattern", src) + self.assertNotIn("New principle", src) def test_sync_source_regression_uses_durable_display_paths(self) -> None: src = SYNC_DEV.read_text(encoding="utf-8") self.assertIn("ctx.durable_artifact_path(worktree)", src) self.assertIn("ctx.durable_artifact_path(prompt_file)", src) self.assertIn("ctx.durable_artifact_path(log_file)", src) - self.assertIn("spawn-codex --cd/--add-dir/--prompt/--log", src) + self.assertIn("--cd", src) + self.assertIn("--add-dir", src) + self.assertIn("--prompt", src) + self.assertIn("--log", src) for forbidden in ( "`{worktree}`. Resolve conflicts", "`cd {worktree}`", diff --git a/skills/codex-refactor-loop/scripts/test_wakeup_plan.py b/skills/codex-refactor-loop/scripts/test_wakeup_plan.py index 00c869e3..b0225301 100644 --- a/skills/codex-refactor-loop/scripts/test_wakeup_plan.py +++ b/skills/codex-refactor-loop/scripts/test_wakeup_plan.py @@ -22,7 +22,13 @@ from codex_refactor_loop import labels as label_catalog # noqa: E402 from codex_refactor_loop.restart import restart_managed_daemon_names # noqa: E402 from codex_refactor_loop.workflow_stages import assert_stage_slug # noqa: E402 -from codex_refactor_loop.wakeup_plan import resolve_repo_root # noqa: E402 +from codex_refactor_loop.wakeup_plan import ( # noqa: E402 + GhItem, + existing_issue_actions, + has_dispatchable_action, + release_countdown_actions, + resolve_repo_root, +) class WakeupPlanBehaviorTests(unittest.TestCase): @@ -670,12 +676,115 @@ def test_milestone_labeled_items_route_before_ordinary_existing_issue(self) -> N self.assertTrue(actions[0]["milestone"]) self.assertFalse(actions[-1]["milestone"]) + def test_release_countdown_ignores_absent_or_current_only_milestone_labels(self) -> None: + def scorer(_: Path) -> dict: + raise AssertionError("release scorer should not run without crnd:milestone:release-target") + + no_target = [ + GhItem("issue", 10, "ordinary", (label_catalog.MANAGED, label_catalog.PHASE_IMPLEMENTING, label_catalog.HUMAN_AUTO)), + GhItem("issue", 20, "current", (label_catalog.MANAGED, label_catalog.PHASE_IMPLEMENTING, label_catalog.HUMAN_AUTO, label_catalog.MILESTONE_CURRENT)), + ] + + self.assertEqual(release_countdown_actions(self.repo, no_target, scorer=scorer), []) + + def test_release_countdown_projects_release_gate_score_without_dispatch_authority(self) -> None: + calls: list[Path] = [] + + def scorer(repo_root: Path) -> dict: + calls.append(repo_root) + return { + "from_version": "1.2.3-beta.4", + "to_version": "1.2.3-beta.5", + "stability_score": 75, + "ready": False, + "signals": { + "required_checks_recent_green": {"passed": False, "reason": "pending_required_checks"}, + "fresh_heartbeats": {"passed": True}, + }, + "blocked_reasons": ["required_checks_recent_green", "min_interval"], + } + + items = [ + GhItem( + "issue", + 344, + "release target", + ( + label_catalog.MANAGED, + label_catalog.PHASE_IMPLEMENTING, + label_catalog.HUMAN_AUTO, + label_catalog.MILESTONE_CURRENT, + label_catalog.MILESTONE_RELEASE_TARGET, + ), + ), + GhItem("PR", 345, "ordinary PR", (label_catalog.MANAGED, label_catalog.PHASE_REVIEWING, label_catalog.HUMAN_AUTO)), + ] + + actions = release_countdown_actions(self.repo, items, scorer=scorer) + + self.assertEqual(calls, [self.repo]) + self.assertEqual(len(actions), 1) + action = actions[0] + self.assertEqual(action["kind"], "release-countdown") + self.assertTrue(action["status_only"]) + self.assertTrue(action["no_lifecycle_authority"]) + self.assertEqual(action["source"], "release-gate") + self.assertEqual(action["targets"], [{"kind": "issue", "number": 344, "item": "issue #344", "title": "release target"}]) + self.assertEqual(action["from_version"], "1.2.3-beta.4") + self.assertEqual(action["to_version"], "1.2.3-beta.5") + self.assertEqual(action["stability_score"], 75) + self.assertFalse(action["ready"]) + self.assertEqual(action["red_signals"], ["required_checks_recent_green"]) + self.assertEqual(action["blocked_reasons"], ["required_checks_recent_green", "min_interval"]) + self.assertFalse(has_dispatchable_action(actions)) + + def test_release_countdown_status_does_not_change_existing_issue_order(self) -> None: + def scorer(_: Path) -> dict: + return { + "from_version": "1.2.3-beta.4", + "to_version": "1.2.3-beta.4", + "stability_score": 100, + "ready": False, + "signals": {}, + "blocked_reasons": ["no_commits_since_last_release"], + } + + items = [ + GhItem( + "issue", + 20, + "current release target", + ( + label_catalog.MANAGED, + label_catalog.PHASE_DESIGN_SOLVING, + label_catalog.HUMAN_AUTO, + label_catalog.MILESTONE_CURRENT, + label_catalog.MILESTONE_RELEASE_TARGET, + ), + ), + GhItem("issue", 10, "ordinary", (label_catalog.MANAGED, label_catalog.PHASE_FIXING, label_catalog.HUMAN_AUTO)), + ] + + combined = release_countdown_actions(self.repo, items, scorer=scorer) + existing_issue_actions(items, self.repo) + combined.sort(key=lambda action: action["priority"]) + + existing = [action for action in combined if action["kind"] == "existing-issue"] + self.assertEqual([action["item"] for action in existing], ["issue #20", "issue #10"]) + def test_existing_issue_routes_before_audit_fallback(self) -> None: - plan = self.run_plan(fixture="existing") + plan, stdout = self.run_plan_with_stdout(fixture="existing") self.assertEqual(plan["actions"][0]["kind"], "existing-issue") self.assertEqual(plan["actions"][0]["item"], "issue #10") - self.assertNotEqual(plan.get("recommendation"), "RECOMMEND:audit") + self.assertIsNone(plan.get("recommendation")) + self.assertNotIn("RECOMMEND:audit", stdout) + + def test_audit_fallback_only_when_no_actionable_issue_or_pr_exists(self) -> None: + plan, stdout = self.run_plan_with_stdout(fixture="empty") + + self.assertEqual(plan["actions"], []) + self.assertEqual(plan["recommendation"], "RECOMMEND:audit") + self.assertIn("RECOMMEND:audit", stdout) def write_transition_assessment(self, number: int, transition_type: str, confidence: float) -> None: path = self.repo / ".refactor-loop" / "runs" / "transition-assessments" / f"issue-{number}.json" @@ -1002,6 +1111,9 @@ def test_host_stages_are_status_projection_only_after_validation(self) -> None: self.assertEqual(host_actions[0]["phase"], "host:qa") self.assertEqual(host_actions[0]["route"], "host-workflow-status-projection") self.assertTrue(host_actions[0]["no_lifecycle_authority"]) + for forbidden in ("labels", "assignees", "milestones", "spawn", "merge"): + with self.subTest(forbidden=forbidden): + self.assertNotIn(forbidden, host_actions[0]) def test_invalid_host_workflow_spec_is_noop_error_reason(self) -> None: (self.repo / "workflow.json").write_text(json.dumps({"events": [{"name": "host:x", "stage": "missing"}]}), encoding="utf-8") diff --git a/skills/codex-refactor-loop/scripts/test_workflow_stages.py b/skills/codex-refactor-loop/scripts/test_workflow_stages.py index 2ed1d4d0..dc667789 100644 --- a/skills/codex-refactor-loop/scripts/test_workflow_stages.py +++ b/skills/codex-refactor-loop/scripts/test_workflow_stages.py @@ -60,6 +60,14 @@ def test_stage_by_slug_fails_closed(self) -> None: with self.assertRaises(ValueError): assert_stage_slug(bad_slug) + def test_work_intake_stage_contract_names_fallback_issue_production(self) -> None: + contract = stage_by_slug("work-intake").contract + + self.assertIn("Fallback issue production", contract) + self.assertIn("no actionable managed issue/PR", contract) + self.assertIn("audit is the built-in compatibility producer", contract) + self.assertNotIn("default compatibility producer", contract) + def test_validated_host_stage_projection_is_additive_only(self) -> None: host_stage = WorkflowStage("host:discovery", "Discovery", "Host projection only.", "host-discovery", -1)