♻️ refactor(cook-it-through): move workflow engine into skill

This commit is contained in:
csh
2026-08-20 17:02:34 +08:00
parent f6854d065a
commit 9b95bf2682
19 changed files with 3905 additions and 1474 deletions
+4 -3
View File
@@ -13,6 +13,7 @@
| ---------------------- | -------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
| `commit-message` | 根据 staged diff 生成符合仓库规范的提交信息,并判断是否应拆分提交 | 写 commit message、检查 staged 改动是否适合一个提交 |
| `gitea-fix-ci` | 基于 Gitea Actions run/job/log 诊断失败 CI,先形成修复计划再改代码;含 `fetch_ci_logs.py` 取证脚本 | Gitea PR checks 失败、远端 CI 红但本地需要定位 |
| `cook-it-through` | 运行任务路由、Matt 工程主链和本地 Markdown ticket 生命周期 | 处理实质性工程任务,或操作 `.scratch` queue、claim、finish、integrate |
| `tsl-syntax-reference` | 查询 TSL 语法条目,验证具体写法和错误边界;不负责 API、命名、风格、工具链或模块集成 | 写/改/审 TSL 时确认语言结构、表达式、控制流、对象模型和语法限制 |
| `tsl-api-reference` | 查询随 skill 分发的 TSL API 参考:按名精确查条目,或按中文关键词发现候选 | 写/审 TSL 时确认 builtin、dotnet、模块 API 的签名、参数、返回值和示例 |
@@ -32,9 +33,9 @@
| `code-review` | 对固定点后的提交做 Standards/Spec 双轴审查 |
| `handoff` | 跨 session 交接未完成上下文 |
安装时使用 `mode = "all"`,或在 `skills` 中逐项列出主链及其依赖;安装器不推导
skill 依赖。正式流程的调度、branch、worktree 和集成门禁由 `scripts/main_loop.py`
管理
安装时使用 `mode = "all"`,或在 `skills` 中逐项列出主链`cook-it-through` 及其依赖;
安装器不推导 skill 依赖。`cook-it-through` 同时定义工程和本地 ticket 协议,并由其内部
执行引擎强制执行状态机
### brooks-lint Suite
+73
View File
@@ -0,0 +1,73 @@
---
name: cook-it-through
description: "当用户要在一个目标项目里推进实质性工程任务、需要先确定执行深度和验证强度时使用;也用于该项目 .scratch 本地 ticket 队列的规划、领取、交付与集成。触发说法包括「按流程做这个」「这个需求怎么落地」「继续这个 ticket」「集成这个 feature」「.scratch 里还有什么可做」「谁在跑这个队列」。不用于纯 TSL 语法/API 查询、只写 commit message、诊断远端 Gitea CI。"
---
# Cook It Through
## 先确定项目与边界
- `<COOK_IT_THROUGH_ROOT>` 是本 skill 的绝对目录,只用于定位随 skill 安装的执行引擎;
它不是目标项目、状态目录或工作区。
- `<PROJECT_ROOT>` 只能来自用户或任务明确指定的 checkout,并由
`git rev-parse --show-toplevel` 验证。无法确定时保留占位符并暂停;不得把 skill source、
snapshot 或偶然 cwd 当项目根。
- 先读取目标项目的 `AGENT_RULES.md``AGENT_RULES.local.md``.agents/index.md`;不存在则跳过。
入口 1 只保留这些最小边界,不加载按需文件。
- 主循环只使用共享 local Markdown tracker、同一文件系统和 Git common directory
不得接入远程 tracker、独立 clone 或跨机器状态。
- 只从 `<COOK_IT_THROUGH_ROOT>/scripts/main_loop.py` 调用机器入口。构造任何命令前先运行
`python <COOK_IT_THROUGH_ROOT>/scripts/main_loop.py <command> --help`;帮助输出独占参数、
状态字段与 evidence schema 的权威。
## 任务路由
按成本从低到高选择第一个满足条件的入口;升级时把已有改动当作事实输入,不视为已批准方案。
路由前阅读 `AGENT_RULES.local.md` 声明的高爆炸半径路径:清单内文件即使改动看似机械,
也不得走入口 1。未声明时,触及构建、CI 或分发配置的改动最低入口 2。
### 入口 1:直接执行
只读分析、定位、审查和不改变可观察行为的机械修改;不生成 spec/tickets,不建 branch。
入口 1 不加载按需文件。
**升级条件**:出现可观察行为变化,或路径地板要求更高入口时转入口 2。
### 入口 2:单切片改动
单模块/既有接口、既有测试 seam、无新公开接口、配置、数据格式、依赖、迁移、兼容或并发,
且一个 session 可完成;不属于入口 3 时适用。详情读
[`workflows/single-session.md`](workflows/single-session.md)。
**升级条件**:需要新 seam、越过边界或出现未确认取舍时转入口 4。
### 入口 3:已明确预期行为的 bug
预期行为和失败反馈已明确;与入口 2 一起读
[`workflows/single-session.md`](workflows/single-session.md),由该文件决定诊断循环和升级。
### 入口 4:新 feature 或设计变更
产品/架构决策、新 seam/公开接口/配置/数据格式、跨模块、迁移/兼容、跨 session/并发,
或单 session 无法完成的工作。边界不清时先按入口 2 起步;确认后读
[`workflows/feature-planning.md`](workflows/feature-planning.md)。
enqueue 完成、恢复已入队工作或调用 status/claim 后读
[`workflows/ticket-execution.md`](workflows/ticket-execution.md)stdout 出现
`INTEGRATION_REQUIRED=<feature>@integrated` 后读
[`workflows/feature-integration.md`](workflows/feature-integration.md)。到阶段边界或上下文有压力时读
[`rules/session-boundary.md`](rules/session-boundary.md)。
## 常驻红线
- 禁止手工修改 ticket `Status``main-loop:ticket-state`、queue block 或 feature state。
- 禁止伪造或复用证据 artifactintegration dependency 不可见时禁止继续。
- 禁止 stash、reset 或覆盖其他 session 改动;禁止绕过 integration frontier 或运行
`git add .scratch`
- 只允许 local Markdown tracker;禁止把远程 tracker、独立 clone 或跨机器状态接入主循环。
## 恢复与完成
`clear``compact``handoff`、上下文压缩或新 session 恢复时,必须重新加载本文件与当前路由文件;
不得只依据 `status` 输出继续。保持活动 claim 的 heartbeat;结束前做 fresh verification
并报告完成、未完成、证据与风险。
@@ -0,0 +1,29 @@
# 阶段边界与稳定知识
到达阶段边界或感到上下文压力时,只按下列顺序选择首个满足项:
1. **继续当前 session**:下一阶段需要当前 session 作为 primary source,或剩余 smart zone 足够
(约 150k tokens)。
2. **使用 `clear`**:上下文与下一阶段无关,且没有必须随身携带的未交接事实。
3. **使用 `handoff`**:跨 harness、目录、仓库或同事,或需要 mid-phase 旁支;
`handoff` 解决的是可移植性,不是上下文过长。
4. **交给 subagent**:工作可独立、边界清楚并可 AFK,且主 session 不需要实时决策。
5. **使用 `compact`**:其余情况;说明要保留的事实、当前路由和下一动作。
`clear``compact``handoff`、压缩或恢复后,重新加载 `SKILL.md` 与当前路由文件,
并重新读取会改变下一步动作的项目规则;不得只依据 `status` 输出继续。
稳定知识不能替代阶段上下文。
## 稳定知识准入与归属
只把已经验证且可复现的事实写入稳定知识,并且至少满足一项:重新发现成本高、
不能从代码直接看出、下一 session 仍需要。
- `memory-bank/project-brief.md`:稳定定位、边界和目标。
- `memory-bank/tech-context.md`:已验证的工具链和验证入口。
- `memory-bank/system-patterns.md`:当前模块边界与不变量。
- `CONTEXT.md`:稳定领域词汇;关键取舍及理由写入 `docs/adr/`;项目执行规则写
`AGENT_RULES.local.md`
- `handoff` 产物写入 OS 临时目录,不写稳定知识文件。
按需使用 `domain-modeling``codebase-design` 只作词汇来源,不单独起会话。
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,387 @@
"""Pure identities and dependency scheduling for the ticket main loop.
The module deliberately has no filesystem, Git, clock, or locking dependency.
Callers provide an immutable snapshot and receive deterministic domain results.
"""
from dataclasses import dataclass
import re
from typing import TypeAlias
FEATURE_SLUG_RE = re.compile(r"^[a-z0-9][a-z0-9-]*$")
TICKET_NUMBER_RE = re.compile(r"^\d{2,}$")
TICKET_ID_RE = re.compile(
r"^(?P<feature>[a-z0-9][a-z0-9-]*)/(?P<number>\d{2,})$"
)
FEATURE_INTEGRATION_ID_RE = re.compile(
r"^(?P<feature>[a-z0-9][a-z0-9-]*)@integrated$"
)
SATISFIED_TICKET_STATUSES = frozenset({"resolved", "skipped"})
VALID_TICKET_STATUSES = frozenset(
{
"ready-for-agent",
"claimed",
"blocked",
"resolved",
"skipped",
}
)
class SchedulerError(ValueError):
"""The queued dependency graph violates the scheduler contract."""
@dataclass(frozen=True, order=True)
class FeatureId:
value: str
def __post_init__(self) -> None:
if not isinstance(self.value, str) or not FEATURE_SLUG_RE.fullmatch(self.value):
raise SchedulerError(f"invalid feature identity: {self.value}")
@classmethod
def parse(cls, raw: str) -> "FeatureId":
if not isinstance(raw, str):
raise SchedulerError(f"invalid feature identity: {raw}")
return cls(raw)
def __str__(self) -> str:
return self.value
@dataclass(frozen=True)
class TicketId:
feature: FeatureId
number: str
def __post_init__(self) -> None:
if not isinstance(self.feature, FeatureId) or not isinstance(
self.number, str
) or not TICKET_NUMBER_RE.fullmatch(self.number):
raise SchedulerError(
f"invalid ticket identity: {self.feature}/{self.number}"
)
@classmethod
def parse(cls, raw: str) -> "TicketId":
if not isinstance(raw, str):
raise SchedulerError(
f"invalid ticket identity '{raw}'; expected <feature>/<number>"
)
match = TICKET_ID_RE.fullmatch(raw)
if not match:
raise SchedulerError(
f"invalid ticket identity '{raw}'; expected <feature>/<number>"
)
return cls(
FeatureId.parse(match.group("feature")),
match.group("number"),
)
def __str__(self) -> str:
return f"{self.feature}/{self.number}"
@dataclass(frozen=True)
class FeatureIntegrationId:
feature: FeatureId
@classmethod
def parse(cls, raw: str) -> "FeatureIntegrationId":
if not isinstance(raw, str):
raise SchedulerError(
"invalid feature integration identity "
f"'{raw}'; expected <feature>@integrated"
)
match = FEATURE_INTEGRATION_ID_RE.fullmatch(raw)
if not match:
raise SchedulerError(
"invalid feature integration identity "
f"'{raw}'; expected <feature>@integrated"
)
return cls(FeatureId.parse(match.group("feature")))
def __str__(self) -> str:
return f"{self.feature}@integrated"
Dependency: TypeAlias = TicketId | FeatureIntegrationId
@dataclass(frozen=True)
class TicketRecord:
id: TicketId
slug: str
status: str
dependencies: tuple[Dependency, ...]
@dataclass(frozen=True)
class FeatureRecord:
id: FeatureId
tickets: tuple[TicketRecord, ...]
integrated: bool = False
integration_blocked: bool = False
class Scheduler:
"""Validate and query one immutable, queue-ordered global graph snapshot."""
def __init__(self, features: tuple[FeatureRecord, ...]) -> None:
self._features = features
feature_ids = [feature.id for feature in features]
for feature_id in feature_ids:
if not isinstance(feature_id, FeatureId):
raise SchedulerError(f"invalid feature identity: {feature_id}")
if len(feature_ids) != len(set(feature_ids)):
duplicate = next(
feature_id
for feature_id in feature_ids
if feature_ids.count(feature_id) > 1
)
raise SchedulerError(f"duplicate queued feature: {duplicate}")
self._feature_by_id = {feature.id: feature for feature in features}
self._ticket_by_id: dict[TicketId, TicketRecord] = {}
for feature in features:
for ticket in feature.tickets:
if not isinstance(ticket.id, TicketId):
raise SchedulerError(f"invalid ticket identity: {ticket.id}")
if ticket.id.feature != feature.id:
raise SchedulerError(
f"ticket {ticket.id} is stored under feature {feature.id}"
)
if ticket.status not in VALID_TICKET_STATUSES:
raise SchedulerError(
f"{ticket.id}: invalid status {ticket.status}"
)
if ticket.id in self._ticket_by_id:
raise SchedulerError(f"duplicate ticket identity: {ticket.id}")
self._ticket_by_id[ticket.id] = ticket
self._validate_integration_state()
self._queue_index = {
feature.id: index for index, feature in enumerate(features)
}
self._validate_dependency_targets()
self._edges = self._build_edges()
self._validate_acyclic()
def _validate_integration_state(self) -> None:
first_pending: FeatureIntegrationId | None = None
for feature in self._features:
integration_id = FeatureIntegrationId(feature.id)
if feature.integrated:
if first_pending is not None:
raise SchedulerError(
f"{integration_id}: earlier integration is pending: "
f"{first_pending}"
)
for ticket in sorted(feature.tickets, key=lambda item: str(item.id)):
if ticket.status not in SATISFIED_TICKET_STATUSES:
raise SchedulerError(
f"{integration_id}: unsatisfied ticket {ticket.id} "
f"has status {ticket.status}"
)
elif first_pending is None:
first_pending = integration_id
def _validate_dependency_targets(self) -> None:
for ticket_id in sorted(self._ticket_by_id, key=str):
ticket = self._ticket_by_id[ticket_id]
for dependency in ticket.dependencies:
if not isinstance(dependency, (TicketId, FeatureIntegrationId)):
raise SchedulerError(
f"{ticket.id}: invalid dependency {dependency}"
)
if len(ticket.dependencies) != len(set(ticket.dependencies)):
duplicate = next(
dependency
for dependency in ticket.dependencies
if ticket.dependencies.count(dependency) > 1
)
raise SchedulerError(
f"{ticket.id}: duplicate dependency {duplicate}"
)
for dependency in sorted(ticket.dependencies, key=str):
if dependency == ticket.id:
raise SchedulerError(
f"{ticket.id}: self dependency {dependency}"
)
if dependency.feature not in self._feature_by_id:
raise SchedulerError(
f"{ticket.id}: dependency feature not queued: "
f"{dependency.feature}"
)
if (
isinstance(dependency, TicketId)
and dependency not in self._ticket_by_id
):
raise SchedulerError(
f"{ticket.id}: dependency ticket not found: {dependency}"
)
def _build_edges(self) -> dict[Dependency, tuple[Dependency, ...]]:
edges: dict[Dependency, tuple[Dependency, ...]] = {
ticket_id: tuple(sorted(ticket.dependencies, key=str))
for ticket_id, ticket in self._ticket_by_id.items()
}
previous: FeatureIntegrationId | None = None
for feature in self._features:
integration_id = FeatureIntegrationId(feature.id)
dependencies: list[Dependency] = [
ticket.id for ticket in feature.tickets
]
if previous is not None:
dependencies.append(previous)
edges[integration_id] = tuple(sorted(dependencies, key=str))
previous = integration_id
return edges
def _validate_acyclic(self) -> None:
visited: set[Dependency] = set()
active: set[Dependency] = set()
stack: list[Dependency] = []
def visit(node: Dependency) -> None:
if node in visited:
return
if node in active:
start = stack.index(node)
cycle = (*stack[start:], node)
raise SchedulerError(
"dependency cycle: " + " -> ".join(map(str, cycle))
)
active.add(node)
stack.append(node)
for dependency in self._edges[node]:
visit(dependency)
stack.pop()
active.remove(node)
visited.add(node)
for node in sorted(self._edges, key=str):
visit(node)
def _dependency_is_satisfied(self, dependency: Dependency) -> bool:
if isinstance(dependency, TicketId):
return (
self._ticket_by_id[dependency].status
in SATISFIED_TICKET_STATUSES
)
return self._feature_by_id[dependency.feature].integrated
def unsatisfied_dependencies(
self, node: Dependency
) -> tuple[Dependency, ...]:
return tuple(
dependency
for dependency in self._edges[node]
if not self._dependency_is_satisfied(dependency)
)
def integration_dependencies(
self,
ticket_id: TicketId,
) -> tuple[FeatureIntegrationId, ...]:
return tuple(
dependency
for dependency in self._edges[ticket_id]
if isinstance(dependency, FeatureIntegrationId)
)
@property
def integration_frontier(self) -> FeatureIntegrationId | None:
for feature in self._features:
if not feature.integrated:
return FeatureIntegrationId(feature.id)
return None
@property
def integration_frontier_ready(self) -> bool:
frontier = self.integration_frontier
if frontier is None:
return False
feature = self._feature_by_id[frontier.feature]
return (
not feature.integration_blocked
and not self.unsatisfied_dependencies(frontier)
)
def feature_state(self, feature_id: FeatureId) -> str:
feature = self._feature_by_id[feature_id]
if feature.integrated:
return "integrated"
if feature.integration_blocked:
return "blocked"
statuses = {ticket.status for ticket in feature.tickets}
if statuses <= SATISFIED_TICKET_STATUSES:
return "ready-to-integrate"
if "claimed" in statuses or statuses & SATISFIED_TICKET_STATUSES:
return "active"
if any(ticket_id.feature == feature_id for ticket_id in self.ticket_frontier):
return "queued"
return "blocked"
@property
def ticket_frontier(self) -> tuple[TicketId, ...]:
claimable = (
ticket
for feature in self._features
if not feature.integrated
for ticket in feature.tickets
if ticket.status == "ready-for-agent"
and not self.unsatisfied_dependencies(ticket.id)
)
return tuple(
ticket.id
for ticket in sorted(
claimable,
key=lambda ticket: (
self._queue_index[ticket.id.feature],
int(ticket.id.number),
ticket.id.number,
ticket.slug,
),
)
)
def parse_dependencies(raw: str, owner: TicketId) -> tuple[Dependency, ...]:
"""Parse the one canonical ``Blocked by`` representation."""
if not isinstance(raw, str):
raise SchedulerError(
f"{owner}: invalid dependency value; expected a Markdown string"
)
if not isinstance(owner, TicketId):
raise SchedulerError(f"invalid dependency owner: {owner}")
value = raw.strip()
if value == "None":
return ()
entries = value.split(";")
dependencies: list[Dependency] = []
for raw_entry in entries:
entry = raw_entry.strip()
ticket_match = TICKET_ID_RE.fullmatch(entry)
integration_match = FEATURE_INTEGRATION_ID_RE.fullmatch(entry)
if ticket_match:
dependency: Dependency = TicketId(
FeatureId.parse(ticket_match.group("feature")),
ticket_match.group("number"),
)
elif integration_match:
dependency = FeatureIntegrationId(
FeatureId.parse(integration_match.group("feature"))
)
else:
raise SchedulerError(
f"{owner}: invalid dependency '{entry}'; expected "
"<feature>/<number> or <feature>@integrated separated by ';'"
)
if dependency in dependencies:
raise SchedulerError(f"{owner}: duplicate dependency {dependency}")
if dependency == owner:
raise SchedulerError(f"{owner}: self dependency {dependency}")
dependencies.append(dependency)
return tuple(dependencies)
@@ -0,0 +1,26 @@
# Feature 集成
stdout 出现 `INTEGRATION_REQUIRED=<feature>@integrated` 后,先读 `main_loop.py integrate --help`
只在全局锁下处理严格 integration frontier;后序 feature 即使完成也不得越序。
feature 必须先显式吸收最新 main,再完成 feature verification、main candidate verification 和
最终双轴 review。收到 `RETRY: feature needs main sync` 时同步后全部重跑;不得自行 merge 或
绕过 frontier。
merge 冲突令该 feature integration blocked,但不阻止后续 ticket 开发;必要时用
`main_loop.py block-feature` 记录边界,解决后用 `main_loop.py release-feature`
## 三道证据门禁
三个独立门禁,不能互相替代:feature、main candidate 和 review 都必须是 fresh UTF-8 JSON
artifact,精确字段以 `integrate --help` 为准。调用 `main_loop.py integrate` 时只提交 frontier
feature、已验证的 `FEATURE_HEAD`、feature verification、main verification 和 review
`skipped` ticket 还必须显式授权 partial integration。收到 `NOOP`/`RETRY` 仍按 stdout 处理。
## 持久化最终状态
集成成功后,先完成 merge commit,再只暂存 `.scratch/<feature>/` 及确被改写的
`.scratch/queue.md`,提交 final workflow state;不要运行 `git add .scratch`,不得 amend/squash。
保持 `.main-loop.json``integration_commit` 指向 `MAIN_INTEGRATION_COMMIT`,解析并报告全部
`WARNING` 与残留 worktree。已集成 feature 的重试必须幂等。
@@ -0,0 +1,55 @@
# 入口 4feature 规划与入队
## 选择与建立上下文
巨大而模糊的工作先用 `wayfinder`;需要外部事实时用 `research`。先进入 `grill-with-docs`
研究报告不能替代 grilling。进入 `grill-with-docs` 前,重读
`memory-bank/project-brief.md``memory-bank/tech-context.md`
`memory-bank/system-patterns.md` 以及会改变设计的项目规则、领域文档和 ADR。
首次运行 `setup-matt-pocock-skills` 时选择 local Markdown tracker。
`grilling` 清空 design frontier 并取得用户确认;seam confirmation 在 `to-spec``tdd`
`tdd` 不得在未经确认的 seam 上开始。按顺序推进:
`setup-matt-pocock-skills -> grill-with-docs -> to-spec -> to-tickets`
`-> main_loop.py enqueue -> 提交 planning baseline -> main_loop.py claim`
关键规划链是 `to-spec -> to-tickets`
## Ticket 身份与依赖格式
- `FeatureId`:小写字母或数字开头,只含小写字母、数字和连字符。
- `TicketId`qualified `feature-slug/NN`feature slug 与至少两位数字共同构成身份。
- `FeatureIntegrationId``feature-slug@integrated`
- Dependency 只能是后两类身份;标题和文件名中的可读 slug 不是身份。
每个 ticket 必须恰有一行 `**Blocked by:** None`,或
`**Blocked by:** feature-a/01; feature-b@integrated`。执行 hard cut`None` 只能单独出现;
同 feature 也写完整身份;只用分号;拒绝裸数字、标题描述、逗号、隐式当前 feature、
非法/重复/缺失目标、自依赖和跨 feature cycle。旧格式必须在 enqueue 前人工迁移;
禁止增加 fallback、双解析器或自动重写。
## 全局图与两个 Frontier
- 锁内加载全部 queued features:每个 ticket 是节点,每个 feature 增加 integration node。
- ticket 使用 `Blocked by` 边;integration node 依赖本 feature 全部 ticketsqueue 顺序只连接
integration nodes,不形成 ticket claim 门槛。
- ticket frontier 是依赖已满足且 `ready-for-agent` 的 tickets,按 queue feature、ticket number、
稳定 slug 排序;integration frontier 是首个尚未集成的 feature,两者独立。
- ticket 在 `resolved``skipped` 时满足;integration dependency 只在持久状态含有效
`integration_commit` 时满足。`skipped` 令 feature partial,集成时必须显式授权。
- 枚举顺序不得改变 frontier、claim 或错误顺序。
## 入队与 planning baseline
第三方 `to-tickets` 只定义通用 tracker 行为;主循环格式以本文件和执行引擎为准。
跨 feature 前向依赖必须把相关 feature 放在同一批次;同批重复 `--feature` 与现有 queue 一起校验。
任一解析、目标或 DAG 校验失败,都不得写 queue、ticket metadata/status 或 feature state
`enqueue` 是最终机器校验边界。参数与结果只查相应 `--help`,不维护命令职责表。
生成和入队不隐式提交。任何 claim 前,提交 `.scratch/<feature>/spec.md`
`.scratch/<feature>/issues/*.md``.scratch/queue.md` 作为 planning baseline;随后读
`workflows/ticket-execution.md`,由 `main_loop.py status` / `main_loop.py claim` 取得正式 assignment。
尚未 claim ticket 时,加载 `to-questionnaire`,把问题写入 control checkout 的
`.scratch/questions/<slug>.md`,并从 `grill-with-docs` 恢复;不得把问题写入稳定知识。
@@ -0,0 +1,27 @@
# 入口 2/3:单 session 工作
## 入口 2:单切片改动
修改前记录当前 `HEAD` 为 review fixed point。确认工作仍局限于单模块或既有接口、
既有测试 seam,且没有新公开接口、配置、数据格式、依赖、迁移、兼容或并发边界。
`tdd` 实现并验证,提交全部实现;再以 `<fixed-point>` 作为 `code-review` 的 fixed point
仅运行 Standards axis。修复硬 finding 后重新提交、验证和 review。
这里不生成 spec/tickets,也不提前创建 ticket branch。
## 入口 3:已明确预期行为的 bug
`diagnosing-bugs` 完整执行 Phase 1-6;它的 Phase 5 已包含 test-first
不重复 `tdd` 或 grilling。先建立可重复的失败反馈;若环境、artifact 或许可不足,
记录缺口并请求补充。
缺少 seam 时可记录后完成修复,落地后再用 `improve-codebase-architecture`
不要因为 bug 已经明确而跳过根因验证,也不要把未证实的猜测当修复依据。
## 完成与升级
完成后运行 fresh verification,并保留 fixed point、验证命令和结果。
若出现新 seam、兼容/架构取舍、跨模块或跨 session 影响,先完成当前入口可验证的收尾,
再转 `workflows/feature-planning.md`;不要把已有修改当作已批准设计。
入口 3 的 bug 段不进入 `grill-with-docs`;只有暴露新的产品或架构取舍时才升级入口 4。
@@ -0,0 +1,78 @@
# 入队后的 ticket 执行
## 建立机器上下文
先读 `main_loop.py status --help`,再按 stdout 分开读取 qualified `TICKET_FRONTIER`
`INTEGRATION_FRONTIER`,并处理 `CLAIM``STALE``BLOCKED``TICKET_ERROR`
`BLOCKED_DEPENDENCY``WAITING_ON``FEATURE_BLOCKED``MAIN_INTEGRATION_COMMIT`
单 feature 空 frontier 不是全局 BUSY。
claim 前使用 `--state-root "<PROJECT_ROOT>/.scratch"`claim 必须显式传绝对
`--repo-root "<PROJECT_ROOT>"`、全局唯一 `--owner``--isolation`。只从
`main_loop.py claim --help` 取得参数。成功 assignment 的键是 `FEATURE``TICKET`
`CONTROL_ROOT``STATE_ROOT``WORKSPACE``BRANCH``BASE``ISOLATION`
领取后立即从返回的绝对 `STATE_ROOT` 读取 feature spec/ticket。领取后实现前重读三个
memory-bank 文件;不要预猜 ticket,也不要使用 worktree 中的 `.scratch` 副本。
`NO FEATURES``NOOP``BUSY``RETRY``INTEGRATION_REQUIRED=<feature>@integrated`
是 stdout/0 的机器结果,`ERROR` 是 stderr/2;始终解析 stdout。只有全局无可领取 ticket 且
integration frontier 可推进时,才把 `INTEGRATION_REQUIRED` 交给 feature integration 路由。
## 🔴 Integration visibility RETRY
stdout 出现 `RETRY: dependency integration is not visible` 时,立即停止 claim 和实现。
必须原样取得 `TICKET``DEPENDENCY``INTEGRATION_COMMIT``WORKSPACE``BRANCH`
`BRANCH_HEAD``TICKET_BRANCH``TICKET_BRANCH_HEAD``SYNC_BRANCH``MAIN_BRANCH`
`MAIN_HEAD``SYNC_COMMAND`
任一字段缺失就报告原始 stdout 并停止,不自行推导 fetch、merge、rebase 或 cherry-pick。
从任一现有目录执行返回的 `SYNC_COMMAND`;它负责创建或使用 `WORKSPACE`,不要预先切换到
可能不存在的目录。成功后重新运行 status/claim;取得正式 assignment 前不得继续。
## 隔离、租约与恢复
- 请求 in-place 时,任意活动 claim 都令其 BUSY;活动 in-place claim 也阻止新 claim 和
pending integration。
- worktree ticket 可跨 feature 并发;活动 worktree 不阻止更早 feature integration。
- `auto` 在全局无 claim 时选 in-place,否则选 worktree;同 owner 只恢复自己的 ticket。
- in-place 要求 control checkout 除 `.scratch` 外干净;不要 stash、reset 或覆盖其他 session 改动。
claim 租约固定为 30 分钟,至少每 10 分钟运行 `main_loop.py heartbeat`,并在长验证/review
前后续租。`reclaim` 只接管 stale 的 `claimed` ticket,保留 branch、workspace、未提交改动和
`BASE`;接管后原 owner 不得 heartbeat/finish。
blocked ticket 用原 owner 的 `main_loop.py finish --result released` 释放;原 session 丢失时才用
`main_loop.py release-ticket`,不要用 reclaim。claim 环境准备失败会占住该 ticket,也按此恢复;
blocked/skipped 必须给 reasonreleased 回到 ready 并保留 workspace。禁止猜 ticket、复用通用
owner 或自动转移 stale claim。
## 实现、Review 与 Finish
按顺序执行:读取已领取 ticket 的 spec → 按 `tdd` 实现与验证 → 提交全部实现 →
运行 `code-review` 的 Standards/Spec 双轴 → 修复硬 finding 后重新提交、验证、完整 review →
结构化证据调用 main_loop.py finish。
ticket review 的 fixed point 是 claim 返回的 `BASE`,或 RETRY 返回的 `FEATURE_HEAD`
Spec sources 是 `<STATE_ROOT>/<feature>/spec.md`
`<STATE_ROOT>/<feature>/issues/<NN>-*.md`。只有对应 axis 零个未解决的硬 finding 才能映射为 pass
`code-review` 不给 pass/fail 判定,来源缺失、axis 跳过或仍有 finding 时不得填写 pass。
收到 `RETRY: feature advanced` 时,把该 `FEATURE_HEAD` 合入 ticket branch,重新验证并以它为新
review base;只有成功合入 feature branch 后 ticket 才能 resolved。调用前读取
`main_loop.py finish --help`,不得复用旧 artifact。
无人值守或当前 session 无法继续时,按 `main_loop.py finish --result blocked` 记录具体 reason。
按各自 `--help` 调用 `main_loop.py status``main_loop.py heartbeat``main_loop.py finish`
`main_loop.py reclaim``main_loop.py release-ticket``main_loop.py block-feature`
`main_loop.py release-feature`;不维护命令职责表。
## 证据门禁
ticket、feature、main candidate 验证是三个独立门禁,不能互相替代。只接受 fresh UTF-8 JSON artifact
精确 verification/review schema 以 `finish --help``integrate --help` 为准。
主循环校验摘要、Git object、branch tip、review base 与 workspace,并快照到
`.scratch/<feature>/evidence/`
artifact 无法证明命令真的执行过或报告来自真实 review;无法证明命令真的执行过时不得伪造 pass。
完成前做 fresh verification,并按帮助输出构造结构化参数。禁止手工修改 ticket `Status`
禁止为旧 Blocked by 格式增加 fallback,禁止伪造或复用证据 artifact。