♻️ refactor(cook-it-through): move workflow engine into skill
This commit is contained in:
+4
-3
@@ -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
|
||||
|
||||
|
||||
@@ -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。
|
||||
- 禁止伪造或复用证据 artifact;integration 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 @@
|
||||
# 入口 4:feature 规划与入队
|
||||
|
||||
## 选择与建立上下文
|
||||
|
||||
巨大而模糊的工作先用 `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 全部 tickets;queue 顺序只连接
|
||||
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 必须给 reason,released 回到 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。
|
||||
Reference in New Issue
Block a user