agent-project-sdlc 0.1.10 → 0.1.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -1
- package/assets/agents/AGENTS_CORE.md +13 -9
- package/assets/docs/README.md +21 -3
- package/assets/make/sdlc-harness.mk +2 -1
- package/assets/policies/allowed_paths.yaml +1 -0
- package/assets/policies/gates.yaml +1 -1
- package/assets/policies/phase_contracts.yaml +5 -1
- package/assets/skills/pjsdlc_architect_design/SKILL.md +14 -3
- package/assets/skills/pjsdlc_dev_sprint/SKILL.md +14 -10
- package/assets/skills/pjsdlc_manager/SKILL.md +6 -6
- package/assets/skills/pjsdlc_pm_prd/SKILL.md +2 -0
- package/assets/templates/ADR_TEMPLATE.md +28 -2
- package/assets/templates/PLAN_TEMPLATE.yaml +2 -3
- package/dist/lib/init.js +6 -3
- package/dist/lib/migrations.js +31 -12
- package/dist/lib/validators.js +230 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ npx sdlc-harness init --adopt
|
|
|
25
25
|
| Managed file sync | `npx sdlc-harness sync` | Materializes package canonical assets into the configured Harness root while preserving project state, docs and local overrides. |
|
|
26
26
|
| Upgrade | `npx sdlc-harness upgrade` | Runs migrations and sync for already-adopted projects. |
|
|
27
27
|
| Diagnostics | `npx sdlc-harness doctor` | Reports Harness root, package version, schema version and key managed paths. |
|
|
28
|
-
| Validators | `npx sdlc-harness validate-*`, `make validate-current`, `make validate-harness` | Checks product, design, development, review, test, release, RFC, active plan shape, prompt language contract and generated overview freshness. |
|
|
28
|
+
| Validators | `npx sdlc-harness validate-*`, `make validate-current`, `make validate-harness` | Checks product, design slicing, development, review, test, release, RFC, active plan shape, prompt language contract and generated overview freshness. |
|
|
29
29
|
| Lifecycle workflow | `<harnessRoot>/state/lifecycle.yaml`, `<harnessRoot>/state/plan.yaml`, `.docs/**` | Tracks REQUIREMENT_GATHERING, ARCHITECTING, SPRINTING, REVIEWING, TESTING, RELEASING and RFC_RECALIBRATION facts. |
|
|
30
30
|
| Stage task control | `plan.yaml`, `make validate-plan`, `npx sdlc-harness validate-plan` | Keeps each stage's agent work in small `TASK-*` tasks with `phase` metadata and scoped paths/gates. |
|
|
31
31
|
| Natural-language control | `AGENTS.md` plus workflow skills | Lets users say things like "continue", "start development", "run tests" or "requirements changed"; agents map these to workflow actions. |
|
|
@@ -68,12 +68,26 @@ The default workflow is serial. Agents should only create `parallel_execution` i
|
|
|
68
68
|
- `runtime_managed`: use only when the current agent runtime can spawn subagents. The main agent assigns workers, waits for results, reviews, merges or cherry-picks, and runs the total gate.
|
|
69
69
|
- `user_orchestrated`: use when the runtime cannot spawn subagents. The main agent generates copyable worker prompts, and the user manually opens Codex conversations or worktrees and pastes them.
|
|
70
70
|
|
|
71
|
+
`parallel_execution` does not store duplicate current phase or current task fields. Agents read phase from `<harnessRoot>/state/lifecycle.yaml#current_phase` and task selection from `<harnessRoot>/state/plan.yaml#current_task_id`.
|
|
72
|
+
|
|
71
73
|
The CLI does not promise to automatically launch Codex agents. Workers do not need to communicate with each other; the main agent owns final fact-source updates such as PRD, plan, implementation docs, test results and generated overviews.
|
|
72
74
|
|
|
73
75
|
## Stage Task Control
|
|
74
76
|
|
|
75
77
|
Every stage's agent work is plan-controlled. Conversational PRD or design creation, existing document slicing, fact-source-based synthesis, development, review, testing, release preparation and RFC recalibration should create or resume one small `TASK-*` task in `plan.yaml` with a valid `phase`, write the current task's `result_docs` or `implementation_doc`, update indexes/overviews, run `validate-plan`, and remove the task after completion. Phase exit validators reject remaining open tasks.
|
|
76
78
|
|
|
79
|
+
The generic rule is that any workflow promoting a draft task into a formal `TASK-*` in `plan.yaml` must remove the source draft from its draft queue in the same state update. The formal task is then recovered only from `plan.yaml`; completed history lives in implementation docs, git, PR and CI records. The built-in Harness draft queue is currently `plan.draft.yaml.tasks[]`, which means unadopted development drafts only. `/devloop` treats the development queue as exhausted only when both `plan.yaml.tasks[]` and `plan.draft.yaml.tasks[]` have no executable task.
|
|
80
|
+
|
|
81
|
+
Before development starts, `ARCHITECTING` can return to `REQUIREMENT_GATHERING` for PRD edits. The manager uses `python3 tools/transition.py --to REQUIREMENT_GATHERING`, the PM workflow updates the PRD through one `TASK-*`, then `validate-pm` and `python3 tools/transition.py --to ARCHITECTING` return the project to design. Requirement changes after `SPRINTING` still use RFC recalibration.
|
|
82
|
+
|
|
83
|
+
`validate-design` treats semantic slicing as a hard gate. Generated `overview.md` files do not count as deliverables, development draft tasks in `plan.draft.yaml` must reference existing tech plan slices through `docs.tech_plan`, multiple development draft tasks need distinct primary tech plan slices, and explicit AI provider/copilot, external-system, or compliance/permission/audit themes require dedicated architecture slices.
|
|
84
|
+
|
|
85
|
+
## ADR And Memory Boundaries
|
|
86
|
+
|
|
87
|
+
`.docs/05_decisions/` stores ADRs, or Architecture Decision Records. ADRs answer why a key architecture choice was made instead of another option. Architecture and tech plan slices may include local design rationale; create an ADR when a decision has real alternatives, affects multiple modules or stages, is likely to be challenged later, or would be expensive to reverse.
|
|
88
|
+
|
|
89
|
+
`<harnessRoot>/state/memory.md` is only a short cross-stage reminder and navigation surface. It answers what an agent should remember next time and where to find the source. Memory may link to ADRs, PRDs, tech plans or implementation docs; full context, alternatives, tradeoffs and long-term consequences belong in `.docs/05_decisions/` ADRs or other formal `.docs/**` fact sources.
|
|
90
|
+
|
|
77
91
|
## Common Commands
|
|
78
92
|
|
|
79
93
|
```sh
|
|
@@ -31,11 +31,15 @@
|
|
|
31
31
|
## Plan Protocol
|
|
32
32
|
|
|
33
33
|
- `plan.yaml` 是当前和未来任务的短期执行计划事实源。每个阶段的每个 Agent 主任务都应先检查是否过长,必要时拆成大小合适的 open task;open task 直接包含 `phase`、`allowed_paths`、`required_gates`、`acceptance_criteria` 和必要的 `working_notes`。
|
|
34
|
+
- `current_phase` 只保存在 `lifecycle.yaml`;不要在 `plan.yaml`、`plan.draft.yaml` 或 `parallel_execution` 中重复保存当前阶段。
|
|
34
35
|
- 新建任务统一使用 `TASK-*` id,并通过 `phase` 标明属于 `REQUIREMENT_GATHERING`、`ARCHITECTING`、`SPRINTING`、`REVIEWING`、`TESTING`、`RELEASING` 或 `RFC_RECALIBRATION`;历史 `PRD-*`、`DES-*`、`DEV-*` 只作为兼容旧记录和旧提交的 provenance。
|
|
35
36
|
- `next_task_sequence` 记录下一个可分配的 `TASK-*` 序号,避免删除历史 task 后发生 id 冲突。
|
|
36
37
|
- 文档、Review、测试、发布和 RFC 类 task 使用 `result_docs` 指向本 task 产出的 PRD、architecture、tech plan、ADR、review report、test plan、release note、RFC 或 `plan.draft.yaml`;开发 task 使用 `implementation_doc` 指向模块级实现事实。
|
|
37
38
|
- task 完成并写入或更新相关事实源后,从 `plan.yaml` 的 `tasks` 列表移除该 task;不要长期保留 done/cancelled task 摘要。
|
|
38
39
|
- `plan.draft.yaml` 是架构阶段生成的计划草案,不自动覆盖 `plan.yaml`。
|
|
40
|
+
- `plan.draft.yaml` 不保存 `current_phase` 或 `current_task_id`,只保存待采用的 task 草案和必要的 `next_task_sequence`。
|
|
41
|
+
- 通用规则:任何阶段或工作流如果把 draft task promote 成 `plan.yaml` 中的正式 `TASK-*`,必须同次从源 draft queue 删除该 draft;draft queue 永远表示尚未采用的草案,不承担完成历史。
|
|
42
|
+
- 当前内置 draft queue 只有 `plan.draft.yaml.tasks[]`,默认用于 `ARCHITECTING` 产出开发草案、`SPRINTING` 消费开发草案。
|
|
39
43
|
- 不维护 checkpoint 文件;任务现场只存在于 open task 的 plan 条目里。
|
|
40
44
|
- 历史动作记录以 git commit 为准,产物结果以模块、子系统或核心数据流级 implementation doc 为准。
|
|
41
45
|
- `SPRINTING` 阶段每完成一个 task,先在 task 仍位于 `plan.yaml` 时创建 task implementation commit;随后再从 `plan.yaml` 移除该 task 并创建 task completion ledger commit。两段提交 push 成功前不进入下一个 task。
|
|
@@ -126,7 +130,7 @@ Strong success criteria 可以让你 independent loop。Weak criteria,例如
|
|
|
126
130
|
### Harness 补充原则
|
|
127
131
|
|
|
128
132
|
1. 阶段约束优先:除非用户明确要求其它工作流动作,否则使用 `active_skill` 指定的 workflow skill,并服从当前阶段的 allowed paths、required gates 和交付物边界。
|
|
129
|
-
2.
|
|
133
|
+
2. 文档先于实现:产品文档和技术方案未形成前,不写业务代码;尚未进入 `SPRINTING` 且仍在 `ARCHITECTING` 时,可以通过 `transition.py --to REQUIREMENT_GATHERING` 回到 PM/PRD 工作流修改产品事实;进入 `SPRINTING` 后的需求变更必须进入 RFC 工作流。
|
|
130
134
|
3. 验证闭环:多步骤工作先给出简短计划,并为关键步骤绑定验证方式。除非被阻塞,否则持续迭代到对应 `required_gates`、阶段 gate 或明确的人工验收标准满足。
|
|
131
135
|
4. 派生物可再生成:`overview.md`、包内 assets 等 generated artifact 必须由对应命令刷新,不手写局部补丁。
|
|
132
136
|
|
|
@@ -140,7 +144,7 @@ Strong success criteria 可以让你 independent loop。Weak criteria,例如
|
|
|
140
144
|
6. 不要在当前 open task 的 `required_gates` 通过前把任务标记为 `done`。
|
|
141
145
|
7. 代码 gate 通过后,更新相关 implementation doc 和 `.docs/INDEX.md`。
|
|
142
146
|
8. `reviewer` 角色只读,不直接修改源码。
|
|
143
|
-
9.
|
|
147
|
+
9. 进入 `SPRINTING` 后的需求变更必须进入 RFC 工作流;`ARCHITECTING` 阶段发现 PRD 需要修改时,可以先回到 `REQUIREMENT_GATHERING`。
|
|
144
148
|
10. task/release 历史动作记录使用 git commit、tag 或外部 release 系统,不维护 `<harnessRoot>/archive/` 常规归档。
|
|
145
149
|
11. 在 `SPRINTING` 阶段,task 完成闭环必须先创建 task implementation commit,再提交移除该 task 后的 task completion ledger commit;如果没有 remote/upstream、权限或凭证导致无法 push,不要开始下一个 task,先报告 blocker。
|
|
146
150
|
12. 文档 slice 发生变化后,运行 `make docs-overview` 刷新对应 `overview.md`。
|
|
@@ -158,8 +162,8 @@ Strong success criteria 可以让你 independent loop。Weak criteria,例如
|
|
|
158
162
|
- “现在到哪了 / 状态如何” → 等价 `/status`。
|
|
159
163
|
- “继续 / 下一步 / 推进” → 等价 `/next`。
|
|
160
164
|
- “能进入下一阶段吗 / 进入下一步” → 等价 `/advance`。
|
|
161
|
-
- “需求变了 / 这个设计要改” →
|
|
162
|
-
- “完善产品方案 / 写 PRD / 文档切片 / 我提供信息,你帮我完善产品方案” → 等价 `/prd
|
|
165
|
+
- “需求变了 / 这个设计要改” → 如果当前仍在 `ARCHITECTING` 且尚未进入开发,可回到 `REQUIREMENT_GATHERING` 修改 PRD;如果已经进入 `SPRINTING` 或之后,进入 RFC workflow。
|
|
166
|
+
- “完善产品方案 / 写 PRD / 文档切片 / 我提供信息,你帮我完善产品方案” → 在 `REQUIREMENT_GATHERING` 等价 `/prd`;在 `ARCHITECTING` 且尚未进入开发时,先流转回 `REQUIREMENT_GATHERING`,再一次只执行一个 `phase: "REQUIREMENT_GATHERING"` 的 `TASK-*`。
|
|
163
167
|
- “设计技术方案 / 做架构方案 / 根据 PRD 做技术方案 / 切技术方案” → 等价 `/design`,一次只执行一个 `phase: "ARCHITECTING"` 的 `TASK-*`。
|
|
164
168
|
- “开始开发 / 做当前任务 / 做下一个任务” → 等价 `/dev`。
|
|
165
169
|
- “开始循环:写任务,执行任务 / 把开发循环跑完” → 等价 `/devloop`。
|
|
@@ -169,18 +173,18 @@ Strong success criteria 可以让你 independent loop。Weak criteria,例如
|
|
|
169
173
|
- “刷新文档总览 / 同步 overview” → 等价 `/overview`。
|
|
170
174
|
- `/plan` 和 `/goal` 是客户端模式入口,不由 Harness 自动开启;用户可以手动组合,例如 `/plan /prd`、`/plan 完善产品方案`、`/goal /devloop` 或 `/goal 开始循环:写任务,执行任务`。
|
|
171
175
|
|
|
172
|
-
Parallel Execution 是可选协作协议,不是默认模式,也不是 CLI 自动启动 Agent 的承诺。只有用户明确要求多 agent、并行或多 worktree,且当前阶段适合拆分时,Agent 才能在 `plan.yaml` 增加 `parallel_execution`。如果当前 runtime 支持 subagent,由主 Agent 使用 `runtime_managed` 模式编排;否则使用 `user_orchestrated` 模式,主 Agent 生成每个 worker 的可复制 prompt,用户手动打开对话或 worktree 后粘贴执行。worker 之间不要求通信,最终事实源更新、review、merge/cherry-pick 和总 gate 由主 Agent 负责。
|
|
176
|
+
Parallel Execution 是可选协作协议,不是默认模式,也不是 CLI 自动启动 Agent 的承诺。只有用户明确要求多 agent、并行或多 worktree,且当前阶段适合拆分时,Agent 才能在 `plan.yaml` 增加 `parallel_execution`。`parallel_execution` 不保存 `phase` 或 `linked_task_id`;当前阶段读取 `lifecycle.yaml#current_phase`,当前任务读取 `plan.yaml#current_task_id`。如果当前 runtime 支持 subagent,由主 Agent 使用 `runtime_managed` 模式编排;否则使用 `user_orchestrated` 模式,主 Agent 生成每个 worker 的可复制 prompt,用户手动打开对话或 worktree 后粘贴执行。worker 之间不要求通信,最终事实源更新、review、merge/cherry-pick 和总 gate 由主 Agent 负责。
|
|
173
177
|
|
|
174
178
|
如果自然语言意图会改变阶段、创建或删除 task、提交、push 或发布,Agent 先用一句话说明将执行的动作和验证方式,再继续。
|
|
175
179
|
|
|
176
180
|
- `/status`:报告当前阶段、角色、任务、阻塞项和下一步动作。
|
|
177
181
|
- `/next`:运行当前阶段映射的 workflow skill。
|
|
178
|
-
- `/advance`:校验当前阶段出口 gate
|
|
182
|
+
- `/advance`:校验当前阶段出口 gate,通过后才尝试流转;`ARCHITECTING` 的可选返回目标是 `REQUIREMENT_GATHERING`,用于开发前修改 PRD。
|
|
179
183
|
- `/rfc <file>`:挂起当前流程并进入 RFC recalibration。
|
|
180
|
-
- `/prd`:在 `REQUIREMENT_GATHERING` 创建或选择一个最小 `TASK-*` task,澄清需求或切片文档,并只更新当前 task 对应的 PRD、验收标准、open questions、`.docs/INDEX.md` 和 overview
|
|
184
|
+
- `/prd`:在 `REQUIREMENT_GATHERING` 创建或选择一个最小 `TASK-*` task,澄清需求或切片文档,并只更新当前 task 对应的 PRD、验收标准、open questions、`.docs/INDEX.md` 和 overview;如果当前是 `ARCHITECTING` 且尚未进入开发,可先回到 `REQUIREMENT_GATHERING`。
|
|
181
185
|
- `/design`:在 `ARCHITECTING` 创建或选择一个最小 `TASK-*` task,基于 PRD 生成或切分当前 task 对应的 architecture、tech plan 和 `plan.draft.yaml`。
|
|
182
|
-
- `/dev`:在 `SPRINTING` 创建或选择下一个最小 `TASK-*` development task
|
|
183
|
-
- `/devloop`:在 `SPRINTING` 连续运行 `/dev`
|
|
186
|
+
- `/dev`:在 `SPRINTING` 创建或选择下一个最小 `TASK-*` development task;如果从 `plan.draft.yaml.tasks[]` promote draft,必须同次消费并删除该 draft;随后执行一个 task,跑 gate,更新模块级 implementation doc,按两段 commit/push 闭环后停止。
|
|
187
|
+
- `/devloop`:在 `SPRINTING` 连续运行 `/dev` 循环,直到 `plan.yaml.tasks[]` 和 `plan.draft.yaml.tasks[]` 都没有明确可执行任务,或遇到需求、架构、allowed_paths、gate、commit/push blocker。
|
|
184
188
|
- `/syncdocs`:同步 `.docs/INDEX.md` 与当前文档事实源。
|
|
185
189
|
- `/overview`:运行 `make docs-overview`,刷新 `.docs/<stage>/overview.md` 派生视图。
|
|
186
190
|
- `/review`:运行只读 Review 工作流。
|
package/assets/docs/README.md
CHANGED
|
@@ -60,7 +60,7 @@ npx sdlc-harness init --adopt
|
|
|
60
60
|
| 同步 managed workflow 文件 | `npx sdlc-harness sync` | 从包内 canonical assets 物化 `AGENTS.md` managed block、workflow skills、templates、policies、Makefile 片段和 GitHub workflow |
|
|
61
61
|
| 升级已接入项目 | `npx sdlc-harness upgrade` | 执行迁移并自动 `sync`,保留 state、docs、业务代码和本地 override |
|
|
62
62
|
| 接入诊断 | `npx sdlc-harness doctor` | 检查 harness root、版本、schema、关键文件和 managed paths |
|
|
63
|
-
| 阶段 gate | `npx sdlc-harness validate-*`、`make validate-current`、`make validate-harness` |
|
|
63
|
+
| 阶段 gate | `npx sdlc-harness validate-*`、`make validate-current`、`make validate-harness` | 校验需求、设计切片、开发、Review、测试、发布、RFC、Harness 骨架、提示词语言契约和 overview freshness |
|
|
64
64
|
| 生命周期工作流 | `lifecycle.yaml`、`plan.yaml`、`.docs/**` | 固定 REQUIREMENT_GATHERING、ARCHITECTING、SPRINTING、REVIEWING、TESTING、RELEASING、RFC_RECALIBRATION 等阶段事实链 |
|
|
65
65
|
| 阶段小任务管控 | `plan.yaml`、`make validate-plan` | 每个阶段的 Agent 主任务都应拆成足够小的 `TASK-*` open task,并用 `phase` 标明所属阶段 |
|
|
66
66
|
| 自然语言控制 | `AGENTS.md` + workflow skills | 用户可说“继续”“开始开发”“跑测试”“需求变了”等,由 Agent 映射到 `/next`、`/dev`、`/test`、RFC 等动作 |
|
|
@@ -97,6 +97,20 @@ npx sdlc-harness init --adopt
|
|
|
97
97
|
|
|
98
98
|
Agent 会读取 `<harnessRoot>/state/lifecycle.yaml` 和 `<harnessRoot>/state/plan.yaml`,再按当前阶段选择对应 workflow skill、产物和 gate。任何阶段的 Agent 主任务都不是一次性长生成:产品方案、技术方案、文档切片、基于上一阶段事实源生成、Review、测试、发布和 RFC 处理,都应先落成一个最小 `TASK-*` open task,并设置对应 `phase`;当前轮只执行一个 task,写入 `result_docs` 或 `implementation_doc`、更新索引和 overview,运行 `make validate-plan`,任务完成后再从 `plan.yaml` 移除。
|
|
99
99
|
|
|
100
|
+
通用规则是:任何阶段或工作流如果把 draft task promote 成 `plan.yaml` 中的正式 `TASK-*`,必须在同一次状态更新里从源 draft queue 删除该 draft;正式 task 的恢复现场只保存在 `plan.yaml`,完成历史由 implementation docs、git/PR/CI 记录承担。当前 Harness 内置的 draft queue 只有 `plan.draft.yaml.tasks[]`,它表示尚未采用的开发草案;`/devloop` 只有在 `plan.yaml.tasks[]` 和 `plan.draft.yaml.tasks[]` 都没有明确可执行任务时,才把开发队列视为耗尽。
|
|
101
|
+
|
|
102
|
+
技术方案阶段需要产出 `plan.draft.yaml`,是为了解决跨阶段交接和当前执行队列纯净性的冲突。`ARCHITECTING` 必须在进入开发前证明方案可以拆成具体、可验证的开发单元,包括修改范围、gate、implementation doc 和执行顺序;但这些未来开发 task 如果直接进入 `plan.yaml`,会和当前架构阶段 task 混在一起,让阶段 gate 无法区分“架构任务未完成”和“下一阶段任务已预拆”。因此开发任务先作为 draft 暂存,进入 `SPRINTING` 后再逐个 promote 成正式 `TASK-*`。其它阶段默认根据上一阶段已经稳定的事实源即时创建当前阶段 task,只有当某个阶段也需要提前为后续阶段生成具体执行任务时,才应引入同类 draft queue。
|
|
103
|
+
|
|
104
|
+
在尚未进入开发前,`ARCHITECTING` 可以回到 `REQUIREMENT_GATHERING` 修改 PRD:Manager 使用 `python3 tools/transition.py --to REQUIREMENT_GATHERING` 切回 PM/PRD 工作流,完成 PRD task 和 `validate-pm` 后,再用 `python3 tools/transition.py --to ARCHITECTING` 回到设计阶段。进入 `SPRINTING` 后的需求变化仍走 RFC workflow。
|
|
105
|
+
|
|
106
|
+
`validate-design` 会把架构阶段的语义切片作为硬 gate:`overview.md` 不计入 deliverables,`plan.draft.yaml` 中每个开发 draft task 必须通过 `docs.tech_plan` 指向存在的 tech plan slice;多个开发 draft task 默认需要不同 primary tech plan slice。PRD、tech plan 或 draft task 明确出现 AI provider / copilot、外部系统边界、合规 / 权限 / 审计等横切主题时,也需要对应的专门 architecture slice。
|
|
107
|
+
|
|
108
|
+
### ADR 与 Memory 的边界
|
|
109
|
+
|
|
110
|
+
`.docs/05_decisions/` 保存 ADR(Architecture Decision Record)。ADR 是软件工程中常见的架构决策记录实践,用来回答“为什么当时选择这个方案,而不是别的方案”。architecture / tech plan 可以写当前方案里的局部设计理由;如果一个决定有备选方案、影响多个模块或阶段、未来容易被质疑,或修改成本高,就应写成 ADR,记录背景、备选方案、理由、后果和替代关系。
|
|
111
|
+
|
|
112
|
+
`<harnessRoot>/state/memory.md` 只做跨阶段快捷提示和导航,回答“下次进来要先记住什么、去哪里找”。memory 可以链接到 ADR、PRD、tech plan 或 implementation doc;完整背景、备选方案、取舍和长期后果放在 `.docs/05_decisions/` ADR 或其它正式 `.docs/**` 事实源里。
|
|
113
|
+
|
|
100
114
|
### Workflow skill 如何生效
|
|
101
115
|
|
|
102
116
|
`<harnessRoot>/skills/<name>/SKILL.md` 是 Harness 的 workflow skill 事实源,也是稳定的 hard file index。它有两种使用方式:
|
|
@@ -139,6 +153,8 @@ override 文件支持两种写法:普通项目追加片段,或带 `name`/`de
|
|
|
139
153
|
- `runtime_managed`:当前 Agent runtime 支持创建 subagent 时,由主 Agent 分配 worker、等待结果、review、merge/cherry-pick 并跑总 gate。
|
|
140
154
|
- `user_orchestrated`:runtime 不能自动创建 subagent 时,主 Agent 生成每个 worker 的可复制 prompt;用户手动打开多个对话或 worktree 后粘贴执行。
|
|
141
155
|
|
|
156
|
+
`parallel_execution` 不保存当前阶段或当前任务副本;阶段只从 `lifecycle.yaml#current_phase` 读取,当前任务只从 `plan.yaml#current_task_id` 读取。
|
|
157
|
+
|
|
142
158
|
Harness CLI v1 不承诺自动启动 Codex agent,也不要求 worker 之间通信。worker 只处理自己的 `owned_paths` 和 gate,最终 PRD、plan、implementation doc、test result、overview 等事实源由主 Agent 集成。
|
|
143
159
|
|
|
144
160
|
常用快捷入口:
|
|
@@ -147,10 +163,10 @@ Harness CLI v1 不承诺自动启动 Codex agent,也不要求 worker 之间通
|
|
|
147
163
|
|---|---|---|
|
|
148
164
|
| `/status` | 现在到哪一步了 | 读取 lifecycle/plan,报告当前阶段、任务、阻塞项和下一步 |
|
|
149
165
|
| `/next` | 继续推进 | 按当前阶段的 `active_skill` 执行下一步 |
|
|
150
|
-
| `/prd` | 完善产品方案 | 在需求阶段创建或选择一个最小 `TASK-*` task
|
|
166
|
+
| `/prd` | 完善产品方案 | 在需求阶段创建或选择一个最小 `TASK-*` task;如果当前仍在架构阶段且未进入开发,可先回到 `REQUIREMENT_GATHERING` 修改 PRD |
|
|
151
167
|
| `/design` | 设计技术方案 | 在架构阶段创建或选择一个最小 `TASK-*` task,生成或切分当前 architecture / tech plan / `plan.draft.yaml` 产物 |
|
|
152
168
|
| `/dev` | 做下一个任务 | 创建或选择下一个最小 `TASK-*` development task,完成一个 task 闭环后停止 |
|
|
153
|
-
| `/devloop` | 开始循环:写任务,执行任务 | 连续运行 `/dev
|
|
169
|
+
| `/devloop` | 开始循环:写任务,执行任务 | 连续运行 `/dev`,直到 `plan.yaml` 和 `plan.draft.yaml` 都没有明确任务或遇到 blocker |
|
|
154
170
|
| `/test` | 跑一下当前验证 | 运行当前 task 或阶段对应 gate |
|
|
155
171
|
| `/review` | 准备 review | 进入只读 Review workflow |
|
|
156
172
|
|
|
@@ -206,10 +222,12 @@ make docs-overview
|
|
|
206
222
|
|---|---|
|
|
207
223
|
| `<harnessRoot>/state/lifecycle.yaml` | 当前生命周期阶段和 active skill |
|
|
208
224
|
| `<harnessRoot>/state/plan.yaml` | 当前和未来 task 的短期执行计划 |
|
|
225
|
+
| `<harnessRoot>/state/memory.md` | 跨阶段稳定知识的摘要和正式事实源链接 |
|
|
209
226
|
| `.docs/01_product/` | PRD、用户场景、验收标准 |
|
|
210
227
|
| `.docs/02_architecture/` | 架构边界和高层设计 |
|
|
211
228
|
| `.docs/03_tech_plan/` | 技术方案、接口契约、任务拆分 |
|
|
212
229
|
| `.docs/04_implementation/` | 模块、子系统和核心数据流的真实实现事实 |
|
|
230
|
+
| `.docs/05_decisions/` | ADR,长期关键决策及其背景、备选方案、理由和后果 |
|
|
213
231
|
| `.docs/06_review/` | Review 报告 |
|
|
214
232
|
| `.docs/07_test/` | 测试计划和回归记录 |
|
|
215
233
|
| `.docs/08_release/` | 发布记录和回滚方案 |
|
|
@@ -12,7 +12,7 @@ help:
|
|
|
12
12
|
@echo " make validate-plan 校验 plan.yaml task 合同,允许当前 open task"
|
|
13
13
|
@echo " make validate-pm 校验产品需求产物"
|
|
14
14
|
@echo " make validate-design 校验架构设计、技术方案和任务草案"
|
|
15
|
-
@echo " make validate-dev 校验 sprint
|
|
15
|
+
@echo " make validate-dev 校验 sprint 任务状态、draft 消费、路径、代码 gate 和实现文档"
|
|
16
16
|
@echo " make validate-review 校验 Review report"
|
|
17
17
|
@echo " make validate-test 校验 regression/test plan"
|
|
18
18
|
@echo " make validate-release 校验 release note、smoke result 和 rollback plan"
|
|
@@ -49,6 +49,7 @@ validate-design:
|
|
|
49
49
|
|
|
50
50
|
validate-dev:
|
|
51
51
|
$(PYTHON) tools/validate_plan.py
|
|
52
|
+
$(PYTHON) tools/validate_dev_state.py
|
|
52
53
|
$(PYTHON) tools/validate_allowed_paths.py
|
|
53
54
|
$(MAKE) lint
|
|
54
55
|
$(MAKE) test-current-domain
|
|
@@ -41,13 +41,16 @@ phases:
|
|
|
41
41
|
gates:
|
|
42
42
|
- "make validate-design"
|
|
43
43
|
next: "SPRINTING"
|
|
44
|
+
returns:
|
|
45
|
+
- "REQUIREMENT_GATHERING"
|
|
44
46
|
|
|
45
47
|
SPRINTING:
|
|
46
|
-
goal: "
|
|
48
|
+
goal: "按任务状态执行开发、消费已采用草案、测试和实现文档沉淀"
|
|
47
49
|
role: "developer"
|
|
48
50
|
skill: "pjsdlc_dev_sprint"
|
|
49
51
|
inputs:
|
|
50
52
|
- "<harnessRoot>/state/plan.yaml"
|
|
53
|
+
- "<harnessRoot>/state/plan.draft.yaml"
|
|
51
54
|
- "current open task plan contract"
|
|
52
55
|
- ".docs/01_product/"
|
|
53
56
|
- ".docs/03_tech_plan/"
|
|
@@ -55,6 +58,7 @@ phases:
|
|
|
55
58
|
- "src/"
|
|
56
59
|
- "tests/"
|
|
57
60
|
- ".docs/04_implementation/"
|
|
61
|
+
- "<harnessRoot>/state/plan.draft.yaml"
|
|
58
62
|
gates:
|
|
59
63
|
- "make validate-dev"
|
|
60
64
|
next: "REVIEWING"
|
|
@@ -17,8 +17,12 @@ description: Use during ARCHITECTING to create architecture docs, technical plan
|
|
|
17
17
|
|
|
18
18
|
架构产物应区分稳定边界和实现计划:architecture slice 记录领域边界、子系统、关键风险和长期约束;tech plan slice 记录接口契约、数据模型、模块方案、任务拆分和 gate。不要把重大架构变化藏在 task 描述里。
|
|
19
19
|
|
|
20
|
+
ADR 用来解决“后来的人只看到结果,看不到当年取舍”的问题。architecture / tech plan 可以记录当前方案的局部设计理由;如果一个决定有明确备选方案、影响多个模块或阶段、未来容易被质疑、修改成本高,或需要保留 supersede 关系,就写入 `.docs/05_decisions/`。`<harnessRoot>/state/memory.md` 只保留这类决策的简短提示和链接,不承载完整背景、备选方案、取舍和后果。
|
|
21
|
+
|
|
20
22
|
架构和技术方案产出本身也是 workflow task,而不是一次性长文档生成。无论来源是对话式设计、既有完整技术方案切片,还是根据 PRD/architecture 事实源生成新方案,都要先在 `<harnessRoot>/state/plan.yaml` 创建或选择一个足够小的 `TASK-*` open task,并设置 `phase: "ARCHITECTING"`,只完成当前 `current_task_id` 对应的一片 architecture / tech plan / ADR / `plan.draft.yaml` 产物。不要在一个任务里连续创建大量设计文件;如果需要多个 slices,先拆出 pending tasks,当前轮只执行一个 task。
|
|
21
23
|
|
|
24
|
+
如果在 `ARCHITECTING` 中发现 PRD 缺失、验收标准不清或产品边界需要调整,且项目尚未进入 `SPRINTING`,不要用架构文档替代产品事实。先收尾或移除当前 open design task,再请 Manager 使用 `python3 tools/transition.py --to REQUIREMENT_GATHERING` 回到 PM/PRD 工作流修改 `.docs/01_product/**`。进入 `SPRINTING` 后的需求变化走 RFC workflow。
|
|
25
|
+
|
|
22
26
|
## 输入
|
|
23
27
|
|
|
24
28
|
- `.docs/INDEX.md`
|
|
@@ -43,7 +47,11 @@ description: Use during ARCHITECTING to create architecture docs, technical plan
|
|
|
43
47
|
- `.docs/02_architecture/` 按领域边界、子系统、跨模块架构问题或关键技术风险切片。
|
|
44
48
|
- `.docs/03_tech_plan/` 按可实现范围、接口契约、数据模型、模块方案或任务组切片。
|
|
45
49
|
- `.docs/05_decisions/` 按单个架构决策切片,即一份 ADR 只记录一个 durable decision。
|
|
50
|
+
- 写 ADR 的判断标准:存在备选方案、影响多个产物或阶段、未来容易被质疑、修改成本高、或需要说明 `Supersedes / Superseded by` 时,写 ADR;只影响当前模块内部实现细节、且理由已能在 architecture / tech plan 中局部说明时,不单独写 ADR。
|
|
46
51
|
- 如果一个技术方案跨越多个独立模块,应拆成多个 tech plan slice,并在 `plan.draft.yaml` 中分别引用。
|
|
52
|
+
- `plan.draft.yaml` 中每个开发 draft task 必须在 `docs.tech_plan` 引用已有 `.docs/03_tech_plan/` slice;多个开发 draft task 默认应引用不同的 primary tech plan slice,不能用一个总纲 tech plan 覆盖全部模块任务。
|
|
53
|
+
- `overview.md` 是 generated artifact,不算 architecture / tech plan deliverable,也不能作为 `docs.tech_plan` 引用。
|
|
54
|
+
- 如果 PRD、tech plan 或 draft task 明确出现 AI provider / AI copilot、外部系统边界、合规 / 权限 / 审计等横切主题,应各自有专门的 architecture slice;不要把多个横切架构问题都塞进一个总览文档。
|
|
47
55
|
- 如果实现计划改变了已有模块边界,应更新相关 architecture slice,而不是只在 task 描述里补一句。
|
|
48
56
|
- 如果用户明确要求把既有完整技术方案文件切成多个 `.docs/03_tech_plan/` slices,先确认 replacement slices 覆盖原文件中仍有效的接口契约、数据模型、模块方案、任务组和 gate;切片完成并更新 `plan.draft.yaml` 引用、`.docs/INDEX.md`、刷新 `overview.md` 后,删除被替代的完整 tech plan file,避免同一事实由完整文件和 slices 双重保留。
|
|
49
57
|
- 每次新增、拆分、合并或废弃 slice 后,都要更新 `.docs/INDEX.md`。
|
|
@@ -59,15 +67,17 @@ description: Use during ARCHITECTING to create architecture docs, technical plan
|
|
|
59
67
|
5. 执行当前 task 时只编辑 `allowed_paths` 中的文件,完成后更新 `.docs/INDEX.md`、运行 `make docs-overview`,并至少运行 `make validate-plan`;阶段出口前再运行 `make validate-design`。
|
|
60
68
|
6. task 完成后,从 `plan.yaml.tasks` 移除该 task;如果仍有 pending `TASK-*` design task,下一轮 `/design` 或 `/next` 再继续。
|
|
61
69
|
7. 如果网络或上下文中断,新会话先读取 `current_task_id` 和当前 open task,按 `working_notes` 恢复,而不是重新生成全量技术方案。
|
|
70
|
+
8. `make validate-design` 会硬性检查 `plan.draft.yaml` 的 task shape、`docs.tech_plan` 引用、tech plan primary slice 去重,以及横切 architecture slice;不要把这些要求只写成自然语言建议。
|
|
62
71
|
|
|
63
72
|
## 规则
|
|
64
73
|
|
|
65
74
|
1. 技术方案必须引用 PRD 路径和 requirement IDs。
|
|
66
75
|
2. 每个 open task 必须包含 `id`、`phase`、`title`、`status`、`summary`、`docs`、`allowed_paths`、`required_gates`、`acceptance_criteria` 和 `result_docs`;开发阶段 task 继续使用 `implementation_doc`。
|
|
67
76
|
3. `plan.draft.yaml` 不得自动覆盖 `plan.yaml`。
|
|
68
|
-
4.
|
|
69
|
-
5.
|
|
70
|
-
6. `
|
|
77
|
+
4. `plan.draft.yaml` 中开发 draft task 的 `docs.tech_plan` 必须指向存在的 tech plan slice;如果 task 数量超过一个,不能全部指向同一个 primary tech plan 文件。
|
|
78
|
+
5. 风险或不清晰的问题按 `<harnessRoot>/pjsdlc_managed/policies/risk_matrix.yaml` 标记。
|
|
79
|
+
6. 任务边界应足够小,能在一次设计执行内闭环;`result_docs` 应指向将被更新或新增的 architecture、tech plan、ADR 或 `plan.draft.yaml` 文件。
|
|
80
|
+
7. `make validate-design` 是阶段出口 gate;如果还有 open `TASK-*` design task,不要请求进入 `SPRINTING`。
|
|
71
81
|
|
|
72
82
|
## 完成检查
|
|
73
83
|
|
|
@@ -76,6 +86,7 @@ description: Use during ARCHITECTING to create architecture docs, technical plan
|
|
|
76
86
|
- [ ] 当前设计产出或切片工作已绑定 `plan.yaml` 中一个最小 `TASK-*` task,并设置 `phase: "ARCHITECTING"`。
|
|
77
87
|
- [ ] 当前 task 已从 `plan.yaml` 移除,或因中断/blocker 保留为可恢复 open task。
|
|
78
88
|
- [ ] 已判断 architecture / tech plan / ADR 的语义切片边界。
|
|
89
|
+
- [ ] `plan.draft.yaml` 中每个开发 draft task 已通过 `docs.tech_plan` 绑定到对应 tech plan slice。
|
|
79
90
|
- [ ] 如果用户要求把完整技术方案切成 tech plan slices,已删除被替代的完整 tech plan file,并同步 `plan.draft.yaml` 引用。
|
|
80
91
|
- [ ] task draft 字段完整且范围清晰。
|
|
81
92
|
- [ ] `.docs/INDEX.md` 已链接新增产物。
|
|
@@ -15,7 +15,7 @@ description: Use during SPRINTING to execute one task from plan.yaml, respecting
|
|
|
15
15
|
|
|
16
16
|
开始编码前,先确认当前 open task 是否完整,修改范围是否覆盖必要文件,验收标准是否能被测试或 gate 验证。如果发现任务边界、产品行为或技术方案不清晰,要停下来说明 blocker、给出可能解释和推荐下一步,而不是扩大范围继续写。
|
|
17
17
|
|
|
18
|
-
`/dev` 和 `/devloop` 是开发阶段的两个入口。`/dev` 创建或选择下一个最小 `TASK-*` development task,设置 `phase: "SPRINTING"`,并只完成一个 task
|
|
18
|
+
`/dev` 和 `/devloop` 是开发阶段的两个入口。`/dev` 创建或选择下一个最小 `TASK-*` development task,设置 `phase: "SPRINTING"`,并只完成一个 task 闭环后停止。通用规则是从任何 draft queue promote 正式 `TASK-*` 时都必须同次消费源 draft;当前开发阶段的内置 draft queue 是 `plan.draft.yaml.tasks[]`,因此如果这个 task 来自 `plan.draft.yaml.tasks[]`,promote 时必须同次删除源 draft,避免已采用草案继续显示为 `pending`。`/devloop` 连续运行 `/dev`,直到 `plan.yaml.tasks[]` 和 `plan.draft.yaml.tasks[]` 都没有明确可创建/执行的任务,或遇到需求、架构、allowed_paths、gate、commit/push blocker。
|
|
19
19
|
|
|
20
20
|
实现时遵循小步闭环:先检查 `git status`,确认工作区没有未归属到当前 task 的脏变更;再定位相关代码和测试,做必要修改,运行 gate,修复失败,写入或更新相关 implementation doc 并刷新文档派生视图。此时先不要从 `plan.yaml` 移除当前 task,要在当前 task 仍位于 `plan.yaml` 时创建 task implementation commit;随后再移除 task,创建 task completion ledger commit,并 push 两个 commit。不要顺手重构、重排格式或处理无关问题;如果发现无关风险,只记录或报告。
|
|
21
21
|
|
|
@@ -25,6 +25,7 @@ description: Use during SPRINTING to execute one task from plan.yaml, respecting
|
|
|
25
25
|
|
|
26
26
|
- `<harnessRoot>/state/lifecycle.yaml`
|
|
27
27
|
- `<harnessRoot>/state/plan.yaml`
|
|
28
|
+
- `<harnessRoot>/state/plan.draft.yaml`
|
|
28
29
|
- 当前任务关联的 PRD 和技术方案
|
|
29
30
|
- 当前源码和测试文件
|
|
30
31
|
|
|
@@ -35,6 +36,7 @@ description: Use during SPRINTING to execute one task from plan.yaml, respecting
|
|
|
35
36
|
- `.docs/04_implementation/` 下相关模块、子系统或核心数据流的 implementation doc
|
|
36
37
|
- 当前 task `working_notes` 或 implementation doc `Verification` 中的 gate evidence
|
|
37
38
|
- 更新后的 `<harnessRoot>/state/plan.yaml`
|
|
39
|
+
- 如果本轮 promote draft,更新后的 `<harnessRoot>/state/plan.draft.yaml`
|
|
38
40
|
- 更新后的 `.docs/INDEX.md`
|
|
39
41
|
- 当前 task 移除前创建的 task implementation commit
|
|
40
42
|
- 从 `plan.yaml` 移除当前 task 后的 task completion ledger commit
|
|
@@ -51,9 +53,9 @@ description: Use during SPRINTING to execute one task from plan.yaml, respecting
|
|
|
51
53
|
- 本 Skill 不直接重切 PRD 或 tech plan;如果发现上游语义边界错误,进入 `BLOCKED`、创建 RFC,或请求回到 `ARCHITECTING`。
|
|
52
54
|
- gate 通过后调用 `pjsdlc_implementation_doc`,由该 Skill 按真实实现更新或新增 `.docs/04_implementation/` 模块级 slice。
|
|
53
55
|
- 如果一个任务实际变成多个独立实现边界,应停止扩大范围,拆分后续任务或回到任务规划。
|
|
54
|
-
- `/dev` 是单任务执行入口:没有 open task 时,先根据 PRD、architecture、tech plan 和 `plan.draft.yaml` 创建一个最小 `TASK-*` open task
|
|
55
|
-
- `/devloop` 是连续执行入口:每完成一个 task 并 push 两段提交后,重新读取 lifecycle
|
|
56
|
-
- Parallel Execution 是当前 task 的可选协作方式,不替代 task completion protocol;`SPRINTING`
|
|
56
|
+
- `/dev` 是单任务执行入口:没有 open task 时,先根据 PRD、architecture、tech plan 和 `plan.draft.yaml` 创建一个最小 `TASK-*` open task;如果从 `plan.draft.yaml.tasks[]` 采用 draft,必须同次从 draft 列表删除源项;已有 open task 时,直接执行该 task;完成后停止。
|
|
57
|
+
- `/devloop` 是连续执行入口:每完成一个 task 并 push 两段提交后,重新读取 lifecycle、`plan.yaml`、`plan.draft.yaml`、PRD、architecture 和 tech plan,再决定是否创建/执行下一个最小 task;没有 open task 且没有未采用 draft,或出现 blocker 时停止并报告。
|
|
58
|
+
- Parallel Execution 是当前 task 的可选协作方式,不替代 task completion protocol;`SPRINTING` 并行从 lifecycle 的 `current_phase` 和 plan 的 `current_task_id` 推断上下文,不在 `parallel_execution` 内重复保存。
|
|
57
59
|
|
|
58
60
|
## Plan Protocol
|
|
59
61
|
|
|
@@ -61,10 +63,11 @@ description: Use during SPRINTING to execute one task from plan.yaml, respecting
|
|
|
61
63
|
|
|
62
64
|
1. `current_task_id` 指向正在执行的 open task。
|
|
63
65
|
2. open task 直接声明 `phase: "SPRINTING"`、`docs`、`allowed_paths`、`required_gates`、`acceptance_criteria` 和 `implementation_doc`。
|
|
64
|
-
3.
|
|
65
|
-
4.
|
|
66
|
-
5. implementation
|
|
67
|
-
6.
|
|
66
|
+
3. 如果 open task 是由 `plan.draft.yaml.tasks[]` promote 而来,创建正式 `TASK-*` 和删除源 draft 必须发生在同一次状态更新中;正式 task 的恢复现场只保存在 `plan.yaml`。
|
|
67
|
+
4. 任务执行中只保留恢复所需的简短 `working_notes`。
|
|
68
|
+
5. gate、implementation doc、`.docs/INDEX.md` 和 `overview.md` 完成后,在当前 task 仍位于 `plan.yaml` 时创建 task implementation commit。
|
|
69
|
+
6. implementation commit 完成后,再把该 task 从 `plan.yaml` 的 `tasks` 列表移除,并保留/递增 `next_task_sequence`。
|
|
70
|
+
7. 将移除当前 task 后的 `plan.yaml` 提交为 task completion ledger commit,并 `git push` 两个 commit 到当前 upstream branch。
|
|
68
71
|
|
|
69
72
|
done task 的执行流水不在当前 `plan.yaml` 长期保留,也不是默认上下文。修 bug、补功能和继续开发时,优先读取当前代码、测试、PRD、技术方案和模块级 implementation doc;历史 task 查询主要看“做了什么、为什么做、影响哪个模块、验证了什么”,task id 和 commit 作为 implementation doc 的 provenance。`allowed_paths`、`required_gates`、临时 `working_notes` 是执行期约束,不作为历史查询 API。只有用户明确要求 forensic/audit/regression 追溯时,才临时使用 git、PR 或 CI 记录。
|
|
70
73
|
|
|
@@ -72,7 +75,7 @@ done task 的执行流水不在当前 `plan.yaml` 长期保留,也不是默认
|
|
|
72
75
|
|
|
73
76
|
1. 一次只执行一个任务。
|
|
74
77
|
2. 开始修改前检查 `git status`;如果存在不属于当前 task 的未提交变更,先完成对应 task 的 commit/push,或报告 blocker,不要混入当前 task。
|
|
75
|
-
3. 只编辑当前 task 的 `allowed_paths` 允许的文件,以及 `SPRINTING` 阶段允许的 Harness
|
|
78
|
+
3. 只编辑当前 task 的 `allowed_paths` 允许的文件,以及 `SPRINTING` 阶段允许的 Harness 记账文件;如果本轮 promote draft,允许同步编辑 `<harnessRoot>/state/plan.draft.yaml` 来消费源 draft。
|
|
76
79
|
4. 必须运行当前 task 的 `required_gates`。
|
|
77
80
|
5. 如果 gate 因代码或测试逻辑失败,在任务范围内修复。
|
|
78
81
|
6. 如果 gate 因基础设施、凭证缺失、产品行为不清或高风险架构变化失败,进入 `BLOCKED`。
|
|
@@ -83,7 +86,7 @@ done task 的执行流水不在当前 `plan.yaml` 长期保留,也不是默认
|
|
|
83
86
|
11. implementation commit 完成后,从当前 `plan.yaml` 移除该 task,并创建 task completion ledger commit。
|
|
84
87
|
12. 默认不追溯已完成 task 的执行流水;只有显式 forensic/audit/regression 任务才临时查询 git、PR 或 CI 记录。
|
|
85
88
|
13. 两个 commit 后必须 `git push` 到当前 upstream branch;如果没有 remote/upstream、权限或凭证导致无法 push,停止推进并报告 blocker。
|
|
86
|
-
14. `/devloop` 每轮都必须重新读取当前状态,不得在一次上下文中假设 plan、代码或远端状态未变化。
|
|
89
|
+
14. `/devloop` 每轮都必须重新读取当前状态,不得在一次上下文中假设 plan、draft、代码或远端状态未变化。
|
|
87
90
|
15. 只有用户明确要求并行、多 agent 或多 worktree 时,才允许创建 `parallel_execution`;否则不得默认并行。
|
|
88
91
|
16. `runtime_managed` 只在当前 runtime 支持 subagent 时使用;没有该能力时,输出 `user_orchestrated` worker prompt,由用户手动打开对话或 worktree 后粘贴。
|
|
89
92
|
17. worker 不更新主事实源;主 Agent 才能更新 `plan.yaml`、`.docs/INDEX.md`、overview、implementation doc 和最终 gate 证据。
|
|
@@ -94,6 +97,7 @@ done task 的执行流水不在当前 `plan.yaml` 长期保留,也不是默认
|
|
|
94
97
|
- [ ] 当前 task `required_gates` 已通过,或 blocker 已记录。
|
|
95
98
|
- [ ] open task 在 `plan.yaml` 中包含完整执行合同。
|
|
96
99
|
- [ ] 当前任务仍然是单一清晰的执行单元。
|
|
100
|
+
- [ ] 如果当前 task 来自 `plan.draft.yaml.tasks[]`,源 draft 已在 promote 时从 draft 列表删除。
|
|
97
101
|
- [ ] implementation doc 已生成或更新,并反映相关模块的真实代码。
|
|
98
102
|
- [ ] 如果启用了 `parallel_execution`,worker owned paths、forbidden paths、required gates 和主 Agent 集成结果已记录。
|
|
99
103
|
- [ ] gate 结果已写入 implementation doc `Verification`,必要时当前 task `working_notes` 也记录了恢复现场所需的 gate evidence。
|
|
@@ -44,11 +44,11 @@ Parallel Execution 是显式 opt-in:只有用户明确提出“并行”“多
|
|
|
44
44
|
11. 用户自然语言询问状态时,等价执行 `/status`。
|
|
45
45
|
12. 用户自然语言要求继续、推进或下一步时,等价执行 `/next`。
|
|
46
46
|
13. 用户自然语言要求进入下一阶段或检查是否可进入下一阶段时,等价执行 `/advance`。
|
|
47
|
-
14.
|
|
48
|
-
15. 用户输入 `/prd`,或自然语言要求“完善产品方案”“写 PRD”“文档切片”“我提供信息,你帮我完善产品方案”时,如果 `current_phase` 是 `REQUIREMENT_GATHERING
|
|
47
|
+
14. 用户自然语言表达需求或设计变化时,先判断阶段:如果当前是 `ARCHITECTING` 且尚未进入开发,可以说明将回到 `REQUIREMENT_GATHERING` 并用 `python3 tools/transition.py --to REQUIREMENT_GATHERING` 切回 PM/PRD 工作流;如果当前是 `SPRINTING` 或之后,进入 RFC workflow。
|
|
48
|
+
15. 用户输入 `/prd`,或自然语言要求“完善产品方案”“写 PRD”“文档切片”“我提供信息,你帮我完善产品方案”时,如果 `current_phase` 是 `REQUIREMENT_GATHERING`,调用产品方案工作流;如果 `current_phase` 是 `ARCHITECTING`,先确认没有 open design task 需要收尾,说明将开发前回退到 `REQUIREMENT_GATHERING`,再用 `python3 tools/transition.py --to REQUIREMENT_GATHERING` 切换到 PM/PRD 工作流;该工作流必须先创建或选择一个最小 `TASK-*` open task,并设置 `phase: "REQUIREMENT_GATHERING"`,再执行一个 PRD 生成或切片 task;否则说明当前阶段冲突和推荐路径。
|
|
49
49
|
16. 用户输入 `/design`,或自然语言要求“设计技术方案”“做架构方案”“根据 PRD 做技术方案”“切技术方案”时,如果 `current_phase` 是 `ARCHITECTING`,调用架构和技术方案工作流;该工作流必须先创建或选择一个最小 `TASK-*` open task,并设置 `phase: "ARCHITECTING"`,再执行一个 architecture / tech plan / `plan.draft.yaml` 生成或切片 task;否则说明当前阶段冲突和推荐路径。
|
|
50
|
-
17. 用户输入 `/dev`,或自然语言要求“开始开发”“做当前任务”“做下一个任务”“继续开发下一个任务”时,如果 `current_phase` 是 `SPRINTING`,创建或选择一个最小 `TASK-*` development task 并执行一个 task
|
|
51
|
-
18. 用户输入 `/devloop`,或自然语言要求“开始循环:写任务,执行任务”“把开发循环跑完”“连续开发”时,如果 `current_phase` 是 `SPRINTING`,连续运行 `/dev`
|
|
50
|
+
17. 用户输入 `/dev`,或自然语言要求“开始开发”“做当前任务”“做下一个任务”“继续开发下一个任务”时,如果 `current_phase` 是 `SPRINTING`,创建或选择一个最小 `TASK-*` development task 并执行一个 task 闭环;如果 task 来自 `plan.draft.yaml.tasks[]`,promote 时必须同次删除源 draft;否则说明当前阶段冲突和推荐路径。
|
|
51
|
+
18. 用户输入 `/devloop`,或自然语言要求“开始循环:写任务,执行任务”“把开发循环跑完”“连续开发”时,如果 `current_phase` 是 `SPRINTING`,连续运行 `/dev` 循环,直到 `plan.yaml.tasks[]` 和 `plan.draft.yaml.tasks[]` 都没有明确可做任务或遇到 blocker;否则说明当前阶段冲突和推荐路径。
|
|
52
52
|
19. 用户自然语言要求跑测试或验证时,运行当前 task 或当前阶段的对应 gate。
|
|
53
53
|
20. 用户明确要求并行、多 agent 或多 worktree 时,先判断当前阶段是否是 `REQUIREMENT_GATHERING`、`SPRINTING` 或 `TESTING`;如果是,生成或使用 `parallel_execution.trigger: "user_requested"` 合同;否则说明当前阶段不支持并行合同。
|
|
54
54
|
21. `runtime_managed` 模式只在当前 Agent runtime 真实具备 subagent 能力时使用;否则使用 `user_orchestrated` 并输出每个 worker 的可复制 prompt。
|
|
@@ -59,11 +59,11 @@ Parallel Execution 是显式 opt-in:只有用户明确提出“并行”“多
|
|
|
59
59
|
|
|
60
60
|
## Plan Protocol
|
|
61
61
|
|
|
62
|
-
每个 open task 都必须在 `plan.yaml` 中包含 `id`、`phase`、`docs`、`allowed_paths`、`required_gates` 和 `acceptance_criteria`;新 task 统一使用 `TASK-*` id,历史 `DEV-*`、`PRD-*`、`DES-*` task 只作为兼容输入保留。文档和流程产物 task 使用 `result_docs` 指向本 task 产出的 PRD、architecture、tech plan、ADR、review、test、release、RFC 或 `plan.draft.yaml`,开发 task 使用 `implementation_doc`
|
|
62
|
+
每个 open task 都必须在 `plan.yaml` 中包含 `id`、`phase`、`docs`、`allowed_paths`、`required_gates` 和 `acceptance_criteria`;新 task 统一使用 `TASK-*` id,历史 `DEV-*`、`PRD-*`、`DES-*` task 只作为兼容输入保留。文档和流程产物 task 使用 `result_docs` 指向本 task 产出的 PRD、architecture、tech plan、ADR、review、test、release、RFC 或 `plan.draft.yaml`,开发 task 使用 `implementation_doc` 指向模块级实现事实。任何阶段如果从 draft queue promote 正式 `TASK-*`,必须同次消费并删除源 draft;当前内置 draft queue 是 `plan.draft.yaml.tasks[]`,用于保存尚未采用的开发草案。done/cancelled task 不长期留在当前 `plan.yaml`。完成后的产物事实以对应 `.docs/**` slice 或模块级 implementation doc 为准,动作历史以 git/PR/CI/release 系统作为 cold archive,`next_task_sequence` 负责继续分配后续 task id。
|
|
63
63
|
|
|
64
64
|
`/prd`、`/design`、`/dev`、`/review`、`/test`、`/release` 和 `/rfc` 都是单 task 推进:默认只完成一个 `TASK-*`。`validate-plan` 用于检查当前 open task 合同是否完整;阶段出口 gate `validate-pm`、`validate-design`、`validate-dev`、`validate-review`、`validate-test`、`validate-release` 和 `validate-rfc` 都要求没有 open task 残留。
|
|
65
65
|
|
|
66
|
-
`parallel_execution` 是可选顶层合同,缺省表示串行。启用后必须声明 `enabled`、`trigger`、`mode`、`
|
|
66
|
+
`parallel_execution` 是可选顶层合同,缺省表示串行。启用后必须声明 `enabled`、`trigger`、`mode`、`coordinator`、`workers` 和 `integration`;不要在合同内重复保存 `phase` 或 `linked_task_id`,当前阶段来自 lifecycle 的 `current_phase`,当前任务来自 plan 的 `current_task_id`。
|
|
67
67
|
|
|
68
68
|
`lifecycle.yaml` 和 `plan.yaml` 只用于当前可执行状态。默认不要读取过去 phase/task/gate 执行流水;只有用户明确要求 forensic/audit/regression 追溯时,才临时查询 git、PR、CI 或 release 记录。
|
|
69
69
|
|
|
@@ -19,6 +19,8 @@ description: Use during REQUIREMENT_GATHERING to turn raw input into PRD slices
|
|
|
19
19
|
|
|
20
20
|
PRD 产出本身是 workflow task,而不是一次性长文档生成。无论来源是对话式需求澄清、既有完整文档切片,还是根据 `.docs/00_raw/` 等事实源合成产品方案,都要先在 `<harnessRoot>/state/plan.yaml` 创建或选择一个足够小的 `TASK-*` open task,并设置 `phase: "REQUIREMENT_GATHERING"`,只完成当前 `current_task_id` 对应的一片产物。不要在一个任务里连续创建大量 PRD 文件;如果需要多个 slices,先把后续 slices 拆成 pending tasks,当前轮只执行一个 task,方便网络中断后按 `plan.yaml` 恢复。
|
|
21
21
|
|
|
22
|
+
如果项目已经进入 `ARCHITECTING` 但尚未进入 `SPRINTING`,用户发现 PRD 需要补充或调整时,Manager 可以先通过 `python3 tools/transition.py --to REQUIREMENT_GATHERING` 回到本 Skill。此时按正常 PRD task protocol 修改 `.docs/01_product/**`,完成后再通过 `validate-pm` 回到 `ARCHITECTING`;进入 `SPRINTING` 后的需求变化仍走 RFC workflow。
|
|
23
|
+
|
|
22
24
|
如果用户在需求阶段明确要求并行、多 agent 或多 worktree,Parallel Execution 只能用于调研、草稿、场景拆解、风险列表或 open questions 收集。worker 不直接写最终 PRD;主 Agent 必须合成最终 `.docs/01_product/**`,并把假设、分歧和未决项写入 PRD。没有用户显式要求时,不要启用 `parallel_execution`。
|
|
23
25
|
|
|
24
26
|
## 输入
|
|
@@ -2,18 +2,44 @@
|
|
|
2
2
|
|
|
3
3
|
## Status(状态)
|
|
4
4
|
|
|
5
|
-
Proposed
|
|
5
|
+
Proposed / Accepted / Superseded
|
|
6
6
|
|
|
7
7
|
## Context(背景)
|
|
8
8
|
|
|
9
|
-
-
|
|
9
|
+
- 这个决策要解决什么问题?
|
|
10
|
+
- 哪些需求、架构边界、技术方案或约束让这个决策必须长期保留?
|
|
11
|
+
|
|
12
|
+
## Options(备选方案)
|
|
13
|
+
|
|
14
|
+
- Option A:
|
|
15
|
+
- 优点:
|
|
16
|
+
- 代价/风险:
|
|
17
|
+
- Option B:
|
|
18
|
+
- 优点:
|
|
19
|
+
- 代价/风险:
|
|
10
20
|
|
|
11
21
|
## Decision(决策)
|
|
12
22
|
|
|
13
23
|
-
|
|
14
24
|
|
|
25
|
+
## Rationale(理由)
|
|
26
|
+
|
|
27
|
+
- 为什么选择这个方案,而不是其它备选方案?
|
|
28
|
+
- 这个选择依赖哪些前提?哪些变化会触发重新评估?
|
|
29
|
+
|
|
15
30
|
## Consequences(影响)
|
|
16
31
|
|
|
17
32
|
- 正向影响(Positive):
|
|
18
33
|
- 负向影响(Negative):
|
|
19
34
|
- 后续动作(Follow-up):
|
|
35
|
+
|
|
36
|
+
## Supersedes / Superseded By(替代关系)
|
|
37
|
+
|
|
38
|
+
- Supersedes:
|
|
39
|
+
- Superseded by:
|
|
40
|
+
|
|
41
|
+
## Links(链接)
|
|
42
|
+
|
|
43
|
+
- Related PRD:
|
|
44
|
+
- Related architecture / tech plan:
|
|
45
|
+
- Related implementation:
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
current_phase: "REQUIREMENT_GATHERING"
|
|
2
1
|
current_task_id: "TASK-001"
|
|
3
2
|
next_task_sequence: 2
|
|
4
3
|
# Optional. Omit this block for normal serial workflow. Only add it when the
|
|
@@ -7,9 +6,7 @@ next_task_sequence: 2
|
|
|
7
6
|
# enabled: true
|
|
8
7
|
# trigger: "user_requested"
|
|
9
8
|
# mode: "user_orchestrated" # or "runtime_managed" when the current agent runtime can spawn subagents
|
|
10
|
-
# phase: "SPRINTING"
|
|
11
9
|
# coordinator: "main_agent"
|
|
12
|
-
# linked_task_id: "TASK-001"
|
|
13
10
|
# workers:
|
|
14
11
|
# - id: "worker-feature"
|
|
15
12
|
# writes_repo: true
|
|
@@ -43,6 +40,8 @@ tasks:
|
|
|
43
40
|
- ".docs/00_raw/example.md"
|
|
44
41
|
product:
|
|
45
42
|
- ".docs/01_product/example.md"
|
|
43
|
+
architecture: []
|
|
44
|
+
tech_plan: []
|
|
46
45
|
rfc: []
|
|
47
46
|
allowed_paths:
|
|
48
47
|
- ".docs/00_raw/**"
|
package/dist/lib/init.js
CHANGED
|
@@ -53,9 +53,12 @@ async function createProjectState(projectRoot, root, report) {
|
|
|
53
53
|
harnessPath(root, "state", "lifecycle.yaml"),
|
|
54
54
|
`project_name: "Project"\nversion: "v0.1"\ncurrent_phase: "SPRINTING"\nactive_role: "developer"\nactive_skill: "pjsdlc_dev_sprint"\ncurrent_milestone: "MVP"\nblocked_reason: ""\nsuspended_phase: ""\nallowed_next_phases:\n - "REVIEWING"\n`
|
|
55
55
|
],
|
|
56
|
-
[harnessPath(root, "state", "plan.yaml"), `
|
|
57
|
-
[harnessPath(root, "state", "plan.draft.yaml"), `
|
|
58
|
-
[
|
|
56
|
+
[harnessPath(root, "state", "plan.yaml"), `current_task_id: ""\nnext_task_sequence: 1\ntasks: []\n`],
|
|
57
|
+
[harnessPath(root, "state", "plan.draft.yaml"), `next_task_sequence: 1\ntasks: []\n`],
|
|
58
|
+
[
|
|
59
|
+
harnessPath(root, "state", "memory.md"),
|
|
60
|
+
"# Project Memory\n\n短期执行计划写入 plan.yaml;长期稳定知识只在这里记录简短摘要和链接。完整决策背景、备选方案、取舍和后果写入 `.docs/05_decisions/` ADR 或其它 `.docs/**` 正式事实源。\n"
|
|
61
|
+
]
|
|
59
62
|
];
|
|
60
63
|
for (const [relative, content] of files) {
|
|
61
64
|
if (await writeTextIfChanged(path.join(projectRoot, relative), content)) {
|
package/dist/lib/migrations.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { readdir, rename, rm } from "node:fs/promises";
|
|
3
|
-
import { readConfig } from "./config.js";
|
|
3
|
+
import { defaultConfig, readConfig } from "./config.js";
|
|
4
4
|
import { ensureDir, listFiles, pathExists, readText, writeTextIfChanged } from "./fs.js";
|
|
5
5
|
import { harnessConfigPath, harnessPath, harnessRoot } from "./harness-root.js";
|
|
6
6
|
import { parseYaml, stringifyYaml } from "./yaml.js";
|
|
@@ -23,8 +23,8 @@ export async function runMigrations(projectRoot) {
|
|
|
23
23
|
await migrateConfig(projectRoot, root, report);
|
|
24
24
|
await migrateLegacyManagedPaths(projectRoot, root, report);
|
|
25
25
|
await migrateLifecycle(projectRoot, root, report);
|
|
26
|
-
await migratePlan(projectRoot, root, report, "plan.yaml", "tasks.yaml");
|
|
27
|
-
await migratePlan(projectRoot, root, report, "plan.draft.yaml", "tasks.draft.yaml");
|
|
26
|
+
await migratePlan(projectRoot, root, report, "plan.yaml", "tasks.yaml", { activePlan: true });
|
|
27
|
+
await migratePlan(projectRoot, root, report, "plan.draft.yaml", "tasks.draft.yaml", { activePlan: false });
|
|
28
28
|
await removeLegacyGateResults(projectRoot, root, report);
|
|
29
29
|
await removeLegacyCheckpoints(projectRoot, root, report);
|
|
30
30
|
await ensureMemory(projectRoot, root, report);
|
|
@@ -38,6 +38,9 @@ async function migrateConfig(projectRoot, root, report) {
|
|
|
38
38
|
return;
|
|
39
39
|
}
|
|
40
40
|
const config = await readConfig(projectRoot);
|
|
41
|
+
const currentCore = defaultConfig(root).core;
|
|
42
|
+
config.core.package = currentCore.package;
|
|
43
|
+
config.core.version = currentCore.version;
|
|
41
44
|
config.core.schema_version = CURRENT_SCHEMA_VERSION;
|
|
42
45
|
config.managed_files = migrateManagedFiles(config.managed_files, root);
|
|
43
46
|
config.local_overrides = config.local_overrides.map((item) => migrateLocalOverride(item, root));
|
|
@@ -197,7 +200,7 @@ async function migrateLifecycle(projectRoot, root, report) {
|
|
|
197
200
|
report.skipped.push(relativeLifecyclePath);
|
|
198
201
|
}
|
|
199
202
|
}
|
|
200
|
-
async function migratePlan(projectRoot, root, report, planFileName, legacyFileName) {
|
|
203
|
+
async function migratePlan(projectRoot, root, report, planFileName, legacyFileName, options) {
|
|
201
204
|
const relativePlanPath = harnessPath(root, "state", planFileName);
|
|
202
205
|
const planPath = path.join(projectRoot, relativePlanPath);
|
|
203
206
|
const legacyTasksPath = path.join(projectRoot, harnessPath(root, "state", legacyFileName));
|
|
@@ -208,18 +211,32 @@ async function migratePlan(projectRoot, root, report, planFileName, legacyFileNa
|
|
|
208
211
|
}
|
|
209
212
|
const data = (parseYaml(await readText(sourcePath)) ?? {});
|
|
210
213
|
let changed = false;
|
|
211
|
-
if (
|
|
212
|
-
data.current_phase
|
|
214
|
+
if ("current_phase" in data) {
|
|
215
|
+
delete data.current_phase;
|
|
213
216
|
changed = true;
|
|
214
217
|
}
|
|
215
|
-
if (!("current_task_id" in data)) {
|
|
218
|
+
if (options.activePlan && !("current_task_id" in data)) {
|
|
216
219
|
data.current_task_id = "";
|
|
217
220
|
changed = true;
|
|
218
221
|
}
|
|
222
|
+
if (!options.activePlan && "current_task_id" in data) {
|
|
223
|
+
delete data.current_task_id;
|
|
224
|
+
changed = true;
|
|
225
|
+
}
|
|
219
226
|
if (!Array.isArray(data.tasks)) {
|
|
220
227
|
data.tasks = [];
|
|
221
228
|
changed = true;
|
|
222
229
|
}
|
|
230
|
+
if (isRecord(data.parallel_execution)) {
|
|
231
|
+
if ("phase" in data.parallel_execution) {
|
|
232
|
+
delete data.parallel_execution.phase;
|
|
233
|
+
changed = true;
|
|
234
|
+
}
|
|
235
|
+
if ("linked_task_id" in data.parallel_execution) {
|
|
236
|
+
delete data.parallel_execution.linked_task_id;
|
|
237
|
+
changed = true;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
223
240
|
if (Array.isArray(data.tasks)) {
|
|
224
241
|
const retainedTasks = [];
|
|
225
242
|
let maxTaskSequence = 0;
|
|
@@ -266,10 +283,12 @@ async function migratePlan(projectRoot, root, report, planFileName, legacyFileNa
|
|
|
266
283
|
changed = true;
|
|
267
284
|
}
|
|
268
285
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
data
|
|
272
|
-
|
|
286
|
+
if (options.activePlan) {
|
|
287
|
+
const currentTaskId = String(data.current_task_id ?? "");
|
|
288
|
+
if (currentTaskId && !taskById(data, currentTaskId)) {
|
|
289
|
+
data.current_task_id = "";
|
|
290
|
+
changed = true;
|
|
291
|
+
}
|
|
273
292
|
}
|
|
274
293
|
if (changed || sourcePath !== planPath) {
|
|
275
294
|
if (await writeTextIfChanged(planPath, stringifyYaml(data))) {
|
|
@@ -337,7 +356,7 @@ async function ensureMemory(projectRoot, root, report) {
|
|
|
337
356
|
report.skipped.push(relativeMemoryPath);
|
|
338
357
|
return;
|
|
339
358
|
}
|
|
340
|
-
const content = "# Project Memory\n\n
|
|
359
|
+
const content = "# Project Memory\n\n记录跨阶段长期有效知识的简短摘要和链接。完整决策背景、备选方案、取舍和后果写入 `.docs/05_decisions/` ADR 或其它 `.docs/**` 正式事实源。\n";
|
|
341
360
|
if (await writeTextIfChanged(memoryPath, content)) {
|
|
342
361
|
report.changed.push(relativeMemoryPath);
|
|
343
362
|
}
|
package/dist/lib/validators.js
CHANGED
|
@@ -6,8 +6,27 @@ import { listFiles, pathExists, readText } from "./fs.js";
|
|
|
6
6
|
import { parseYaml } from "./yaml.js";
|
|
7
7
|
const execFileAsync = promisify(execFile);
|
|
8
8
|
const PARALLEL_MODES = new Set(["runtime_managed", "user_orchestrated"]);
|
|
9
|
-
const PARALLEL_PHASES = new Set(["REQUIREMENT_GATHERING", "SPRINTING", "TESTING"]);
|
|
10
9
|
const TASK_PHASES = new Set(["REQUIREMENT_GATHERING", "ARCHITECTING", "SPRINTING", "REVIEWING", "TESTING", "RELEASING", "RFC_RECALIBRATION"]);
|
|
10
|
+
const PARALLEL_ALLOWED_PHASES = new Set(["REQUIREMENT_GATHERING", "SPRINTING", "TESTING"]);
|
|
11
|
+
const TASK_STATUSES = new Set(["pending", "in_progress", "done", "blocked", "pending_revision", "cancelled"]);
|
|
12
|
+
const OPEN_TASK_STATUSES = new Set(["pending", "in_progress", "blocked", "pending_revision"]);
|
|
13
|
+
const DESIGN_CATEGORIES = [
|
|
14
|
+
{
|
|
15
|
+
label: "AI copilot/provider",
|
|
16
|
+
triggerTerms: ["ai provider", "ai output", "aioutput", "llm", "copilot", "副驾驶"],
|
|
17
|
+
architectureTerms: ["ai provider", "ai output", "llm", "copilot", "副驾驶", "模型", "智能", "prompt"]
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
label: "external system boundary",
|
|
21
|
+
triggerTerms: ["external system", "external integration", "webhook", "外部系统", "第三方", "微信", "工商", "税务", "社保", "公积金", "金蝶", "对象存储"],
|
|
22
|
+
architectureTerms: ["external system", "external integration", "webhook", "adapter", "适配", "边界", "外部系统", "第三方", "微信", "工商", "税务", "社保", "公积金", "金蝶", "对象存储"]
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
label: "compliance/permission/audit",
|
|
26
|
+
triggerTerms: ["compliance", "authorization", "audit log", "audit trail", "合规", "授权", "客户确认", "回执归档", "权限模型", "权限控制", "权限架构", "审计架构", "审计日志"],
|
|
27
|
+
architectureTerms: ["compliance", "permission", "authorization", "audit", "合规", "权限", "审计", "授权", "客户确认", "回执归档"]
|
|
28
|
+
}
|
|
29
|
+
];
|
|
11
30
|
const validators = {
|
|
12
31
|
"validate-harness": validateHarness,
|
|
13
32
|
"validate-current": validateCurrent,
|
|
@@ -81,9 +100,12 @@ async function validatePm(projectRoot) {
|
|
|
81
100
|
return { info: [`validate-pm checked ${docs.length} file(s)`], errors };
|
|
82
101
|
}
|
|
83
102
|
async function validateDesign(projectRoot) {
|
|
84
|
-
const
|
|
103
|
+
const root = await harnessRoot(projectRoot);
|
|
104
|
+
const lifecycle = await readYamlObject(path.join(projectRoot, root, "state", "lifecycle.yaml"));
|
|
105
|
+
const plan = await validatePlanState(projectRoot, String(lifecycle.current_phase ?? "") !== "ARCHITECTING");
|
|
85
106
|
const architecture = await markdownFiles(path.join(projectRoot, ".docs/02_architecture"));
|
|
86
107
|
const techPlan = await markdownFiles(path.join(projectRoot, ".docs/03_tech_plan"));
|
|
108
|
+
const product = await markdownFiles(path.join(projectRoot, ".docs/01_product"));
|
|
87
109
|
const text = await combinedText([...architecture, ...techPlan]);
|
|
88
110
|
const errors = [...plan.errors];
|
|
89
111
|
if (architecture.length === 0)
|
|
@@ -96,6 +118,9 @@ async function validateDesign(projectRoot) {
|
|
|
96
118
|
errors.push("Design must describe interfaces or contracts");
|
|
97
119
|
if (!containsAny(text, ["task", "任务", "breakdown"]))
|
|
98
120
|
errors.push("Design must include task breakdown");
|
|
121
|
+
const draft = await validateDesignDraft(projectRoot, root, techPlan);
|
|
122
|
+
errors.push(...draft.errors);
|
|
123
|
+
errors.push(...(await validateCrossCuttingArchitecture(projectRoot, product, techPlan, architecture, draft.tasks)));
|
|
99
124
|
return { info: [`validate-design checked ${architecture.length + techPlan.length} file(s)`], errors };
|
|
100
125
|
}
|
|
101
126
|
async function validatePlan(projectRoot) {
|
|
@@ -103,9 +128,160 @@ async function validatePlan(projectRoot) {
|
|
|
103
128
|
const pathErrors = await validateChangedPaths(projectRoot, plan.plan, true);
|
|
104
129
|
return { info: [`validate-plan checked ${plan.taskCount} task(s)`], errors: [...plan.errors, ...pathErrors] };
|
|
105
130
|
}
|
|
131
|
+
async function validateDesignDraft(projectRoot, root, techPlanFiles) {
|
|
132
|
+
const errors = [];
|
|
133
|
+
const draft = await readYamlObject(path.join(projectRoot, root, "state", "plan.draft.yaml"));
|
|
134
|
+
if ("current_phase" in draft) {
|
|
135
|
+
errors.push("plan.draft.yaml must not define current_phase; lifecycle.yaml is the single source for current_phase");
|
|
136
|
+
}
|
|
137
|
+
if ("current_task_id" in draft) {
|
|
138
|
+
errors.push("plan.draft.yaml must not define current_task_id because drafts are not active task state");
|
|
139
|
+
}
|
|
140
|
+
const rawTasks = draft.tasks;
|
|
141
|
+
if (!Array.isArray(rawTasks) || rawTasks.length === 0) {
|
|
142
|
+
errors.push("plan.draft.yaml must contain at least one task before leaving ARCHITECTING");
|
|
143
|
+
return { errors, tasks: [] };
|
|
144
|
+
}
|
|
145
|
+
const tasks = rawTasks.filter(isRecord);
|
|
146
|
+
const availableTechPlans = new Set(techPlanFiles.map((file) => repoRelative(projectRoot, file)));
|
|
147
|
+
const developmentTasks = [];
|
|
148
|
+
const primaryRefs = [];
|
|
149
|
+
rawTasks.forEach((rawTask, index) => {
|
|
150
|
+
if (!isRecord(rawTask)) {
|
|
151
|
+
errors.push(`Task draft #${index + 1} must be a mapping`);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
validateDraftTaskShape(rawTask, index, errors);
|
|
155
|
+
if (rawTask.status !== "pending") {
|
|
156
|
+
errors.push(`Draft task ${String(rawTask.id ?? "")} should start as pending`);
|
|
157
|
+
}
|
|
158
|
+
if (!isDevelopmentDraft(rawTask))
|
|
159
|
+
return;
|
|
160
|
+
developmentTasks.push(rawTask);
|
|
161
|
+
if (!isRecord(rawTask.docs)) {
|
|
162
|
+
errors.push(`Draft task ${String(rawTask.id ?? "")} docs must be a mapping`);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const techRefs = asStringList(rawTask.docs.tech_plan);
|
|
166
|
+
if (techRefs.length === 0) {
|
|
167
|
+
errors.push(`Draft task ${String(rawTask.id ?? "")} must reference at least one tech plan slice in docs.tech_plan`);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const normalizedRefs = techRefs.map(normalizeDocRef);
|
|
171
|
+
for (const ref of normalizedRefs) {
|
|
172
|
+
if (!ref.startsWith(".docs/03_tech_plan/")) {
|
|
173
|
+
errors.push(`Draft task ${String(rawTask.id ?? "")} docs.tech_plan must point into .docs/03_tech_plan/: ${ref}`);
|
|
174
|
+
}
|
|
175
|
+
else if (!availableTechPlans.has(ref)) {
|
|
176
|
+
errors.push(`Draft task ${String(rawTask.id ?? "")} references missing or generated tech plan slice: ${ref}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
primaryRefs.push(normalizedRefs[0]);
|
|
180
|
+
});
|
|
181
|
+
if (developmentTasks.length === 0) {
|
|
182
|
+
errors.push("plan.draft.yaml must contain at least one development task with implementation_doc");
|
|
183
|
+
}
|
|
184
|
+
if (developmentTasks.length > 1 && new Set(primaryRefs).size !== primaryRefs.length) {
|
|
185
|
+
errors.push("Draft development tasks must reference distinct primary tech plan slices in docs.tech_plan");
|
|
186
|
+
}
|
|
187
|
+
return { errors, tasks };
|
|
188
|
+
}
|
|
189
|
+
function validateDraftTaskShape(task, index, errors) {
|
|
190
|
+
const prefix = `Task #${index + 1}`;
|
|
191
|
+
for (const field of ["id", "title", "status", "summary"]) {
|
|
192
|
+
if (!task[field])
|
|
193
|
+
errors.push(`${prefix} missing field: ${field}`);
|
|
194
|
+
}
|
|
195
|
+
const taskId = String(task.id ?? "");
|
|
196
|
+
if (!/^[A-Z]+-\d+$/.test(taskId)) {
|
|
197
|
+
errors.push(`${taskId || prefix} id must match PREFIX-###`);
|
|
198
|
+
}
|
|
199
|
+
if (taskId.startsWith("TASK-") && !TASK_PHASES.has(String(task.phase ?? ""))) {
|
|
200
|
+
errors.push(`${taskId} must define valid phase`);
|
|
201
|
+
}
|
|
202
|
+
else if (task.phase !== undefined && !TASK_PHASES.has(String(task.phase))) {
|
|
203
|
+
errors.push(`${taskId} has invalid phase: ${String(task.phase)}`);
|
|
204
|
+
}
|
|
205
|
+
if (!TASK_STATUSES.has(String(task.status))) {
|
|
206
|
+
errors.push(`${String(task.id ?? prefix)} has invalid status: ${String(task.status)}`);
|
|
207
|
+
}
|
|
208
|
+
if (typeof task.summary !== "string" || !task.summary.trim()) {
|
|
209
|
+
errors.push(`${String(task.id ?? prefix)} must define summary`);
|
|
210
|
+
}
|
|
211
|
+
const hasImplementationDoc = typeof task.implementation_doc === "string" && task.implementation_doc.trim().length > 0;
|
|
212
|
+
const hasResultDocs = Array.isArray(task.result_docs) && task.result_docs.length > 0;
|
|
213
|
+
if (!hasImplementationDoc && !hasResultDocs) {
|
|
214
|
+
errors.push(`${String(task.id ?? prefix)} must define implementation_doc or result_docs`);
|
|
215
|
+
}
|
|
216
|
+
if (OPEN_TASK_STATUSES.has(String(task.status))) {
|
|
217
|
+
if ("gate_result" in task)
|
|
218
|
+
errors.push(`${String(task.id ?? prefix)} open task must not define gate_result`);
|
|
219
|
+
for (const field of ["docs", "allowed_paths", "required_gates", "acceptance_criteria"]) {
|
|
220
|
+
if (!task[field])
|
|
221
|
+
errors.push(`${String(task.id ?? prefix)} open task missing field: ${field}`);
|
|
222
|
+
}
|
|
223
|
+
if (!isRecord(task.docs))
|
|
224
|
+
errors.push(`${String(task.id ?? prefix)} docs must be a mapping`);
|
|
225
|
+
if (!Array.isArray(task.allowed_paths) || task.allowed_paths.length === 0) {
|
|
226
|
+
errors.push(`${String(task.id ?? prefix)} must define allowed_paths`);
|
|
227
|
+
}
|
|
228
|
+
if (!Array.isArray(task.required_gates) || task.required_gates.length === 0) {
|
|
229
|
+
errors.push(`${String(task.id ?? prefix)} must define required_gates`);
|
|
230
|
+
}
|
|
231
|
+
if (!Array.isArray(task.acceptance_criteria) || task.acceptance_criteria.length === 0) {
|
|
232
|
+
errors.push(`${String(task.id ?? prefix)} must define acceptance_criteria`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
for (const field of ["docs", "allowed_paths", "required_gates", "acceptance_criteria", "working_notes", "gate_result", "result_docs"]) {
|
|
237
|
+
if (field in task)
|
|
238
|
+
errors.push(`${String(task.id ?? prefix)} closed task must not retain ${field}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
async function validateCrossCuttingArchitecture(projectRoot, productFiles, techPlanFiles, architectureFiles, draftTasks) {
|
|
243
|
+
const errors = [];
|
|
244
|
+
const sourceText = [
|
|
245
|
+
await combinedText(productFiles),
|
|
246
|
+
await combinedText(techPlanFiles),
|
|
247
|
+
draftTasks.map(taskText).join("\n")
|
|
248
|
+
].join("\n");
|
|
249
|
+
const architectureTexts = await Promise.all(architectureFiles.map(async (file) => ({ file, text: await readText(file) })));
|
|
250
|
+
const assigned = new Set();
|
|
251
|
+
for (const category of DESIGN_CATEGORIES) {
|
|
252
|
+
if (!containsAny(sourceText, category.triggerTerms))
|
|
253
|
+
continue;
|
|
254
|
+
const match = architectureTexts.find((doc) => !assigned.has(repoRelative(projectRoot, doc.file)) && containsAny(doc.text, category.architectureTerms));
|
|
255
|
+
if (!match) {
|
|
256
|
+
errors.push(`Design requires a dedicated ${category.label} architecture slice`);
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
assigned.add(repoRelative(projectRoot, match.file));
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return errors;
|
|
263
|
+
}
|
|
106
264
|
async function validateDev(projectRoot) {
|
|
265
|
+
const root = await harnessRoot(projectRoot);
|
|
107
266
|
const plan = await validatePlanState(projectRoot, false);
|
|
108
|
-
|
|
267
|
+
const draftErrors = await validateDevDraftConsumed(projectRoot, root);
|
|
268
|
+
return { info: [`validate-dev checked ${plan.taskCount} task(s)`], errors: [...plan.errors, ...draftErrors] };
|
|
269
|
+
}
|
|
270
|
+
async function validateDevDraftConsumed(projectRoot, root) {
|
|
271
|
+
const errors = [];
|
|
272
|
+
const draft = await readYamlObject(path.join(projectRoot, root, "state", "plan.draft.yaml"));
|
|
273
|
+
if ("current_phase" in draft) {
|
|
274
|
+
errors.push("plan.draft.yaml must not define current_phase; lifecycle.yaml is the single source for current_phase");
|
|
275
|
+
}
|
|
276
|
+
if ("current_task_id" in draft) {
|
|
277
|
+
errors.push("plan.draft.yaml must not define current_task_id because drafts are not active task state");
|
|
278
|
+
}
|
|
279
|
+
const tasks = Array.isArray(draft.tasks) ? draft.tasks : [];
|
|
280
|
+
if (tasks.length > 0) {
|
|
281
|
+
const ids = tasks.map((task) => (isRecord(task) ? String(task.id ?? "<missing id>") : "<missing id>")).join(", ");
|
|
282
|
+
errors.push(`Unconsumed draft tasks remain in plan.draft.yaml: ${ids}. Promote the next draft into plan.yaml or remove already-consumed drafts before validate-dev.`);
|
|
283
|
+
}
|
|
284
|
+
return errors;
|
|
109
285
|
}
|
|
110
286
|
async function validateReview(projectRoot) {
|
|
111
287
|
const plan = await validatePlanState(projectRoot, false);
|
|
@@ -175,7 +351,12 @@ async function validatePlanState(projectRoot, allowOpen) {
|
|
|
175
351
|
const errors = [];
|
|
176
352
|
const root = await harnessRoot(projectRoot);
|
|
177
353
|
const tasksData = await readYamlObject(path.join(projectRoot, root, "state", "plan.yaml"));
|
|
178
|
-
|
|
354
|
+
const lifecycle = await readYamlObject(path.join(projectRoot, root, "state", "lifecycle.yaml"));
|
|
355
|
+
const currentPhase = String(lifecycle.current_phase ?? "");
|
|
356
|
+
if ("current_phase" in tasksData) {
|
|
357
|
+
errors.push("plan.yaml must not define current_phase; lifecycle.yaml is the single source for current_phase");
|
|
358
|
+
}
|
|
359
|
+
validateParallelExecutionContract(tasksData, currentPhase, errors);
|
|
179
360
|
const tasks = Array.isArray(tasksData.tasks) ? tasksData.tasks : [];
|
|
180
361
|
const nextTaskSequence = tasksData.next_task_sequence;
|
|
181
362
|
if (!Number.isInteger(nextTaskSequence) || Number(nextTaskSequence) <= 0) {
|
|
@@ -254,7 +435,7 @@ async function validatePlanState(projectRoot, allowOpen) {
|
|
|
254
435
|
}
|
|
255
436
|
return { taskCount: tasks.length, errors, plan: tasksData };
|
|
256
437
|
}
|
|
257
|
-
function validateParallelExecutionContract(plan, errors) {
|
|
438
|
+
function validateParallelExecutionContract(plan, currentPhase, errors) {
|
|
258
439
|
const contract = plan.parallel_execution;
|
|
259
440
|
if (contract === undefined || contract === null)
|
|
260
441
|
return;
|
|
@@ -269,17 +450,19 @@ function validateParallelExecutionContract(plan, errors) {
|
|
|
269
450
|
if (!PARALLEL_MODES.has(String(contract.mode ?? ""))) {
|
|
270
451
|
errors.push("parallel_execution.mode must be runtime_managed or user_orchestrated");
|
|
271
452
|
}
|
|
272
|
-
if (
|
|
273
|
-
errors.push("parallel_execution
|
|
453
|
+
if ("phase" in contract) {
|
|
454
|
+
errors.push("parallel_execution must not define phase; lifecycle.yaml is the single source for current_phase");
|
|
455
|
+
}
|
|
456
|
+
if ("linked_task_id" in contract) {
|
|
457
|
+
errors.push("parallel_execution must not define linked_task_id; use plan.yaml current_task_id");
|
|
458
|
+
}
|
|
459
|
+
if (!PARALLEL_ALLOWED_PHASES.has(currentPhase)) {
|
|
460
|
+
errors.push("parallel_execution is only supported during REQUIREMENT_GATHERING, SPRINTING, or TESTING");
|
|
274
461
|
}
|
|
275
462
|
if (contract.coordinator !== "main_agent")
|
|
276
463
|
errors.push('parallel_execution.coordinator must be "main_agent"');
|
|
277
|
-
if (
|
|
278
|
-
|
|
279
|
-
errors.push("SPRINTING parallel_execution must define linked_task_id");
|
|
280
|
-
if (contract.linked_task_id !== plan.current_task_id) {
|
|
281
|
-
errors.push("SPRINTING parallel_execution.linked_task_id must match current_task_id");
|
|
282
|
-
}
|
|
464
|
+
if (currentPhase === "SPRINTING" && !plan.current_task_id) {
|
|
465
|
+
errors.push("SPRINTING parallel_execution requires plan.yaml current_task_id");
|
|
283
466
|
}
|
|
284
467
|
const workers = contract.workers;
|
|
285
468
|
if (!Array.isArray(workers) || workers.length === 0) {
|
|
@@ -383,14 +566,46 @@ async function readYamlObject(filePath) {
|
|
|
383
566
|
}
|
|
384
567
|
async function markdownFiles(root) {
|
|
385
568
|
const files = await listFiles(root);
|
|
386
|
-
return files.filter((file) =>
|
|
569
|
+
return files.filter((file) => {
|
|
570
|
+
const name = path.basename(file).toLowerCase();
|
|
571
|
+
return file.endsWith(".md") && name !== "overview.md" && name !== "readme.md";
|
|
572
|
+
});
|
|
387
573
|
}
|
|
388
574
|
async function combinedText(files) {
|
|
389
575
|
const parts = await Promise.all(files.map((file) => readText(file)));
|
|
390
576
|
return parts.join("\n").toLowerCase();
|
|
391
577
|
}
|
|
392
578
|
function containsAny(text, needles) {
|
|
393
|
-
|
|
579
|
+
const lowered = text.toLowerCase();
|
|
580
|
+
return needles.some((needle) => lowered.includes(needle.toLowerCase()));
|
|
581
|
+
}
|
|
582
|
+
function isDevelopmentDraft(task) {
|
|
583
|
+
const taskId = String(task.id ?? "");
|
|
584
|
+
return Boolean(task.implementation_doc) || task.phase === "SPRINTING" || taskId.startsWith("DEV-");
|
|
585
|
+
}
|
|
586
|
+
function asStringList(value) {
|
|
587
|
+
if (Array.isArray(value)) {
|
|
588
|
+
return value.map((item) => String(item).trim()).filter(Boolean);
|
|
589
|
+
}
|
|
590
|
+
if (typeof value === "string" && value.trim())
|
|
591
|
+
return [value.trim()];
|
|
592
|
+
return [];
|
|
593
|
+
}
|
|
594
|
+
function normalizeDocRef(value) {
|
|
595
|
+
const normalized = value.replace(/\\/g, "/");
|
|
596
|
+
return normalized.startsWith("./") ? normalized.slice(2) : normalized;
|
|
597
|
+
}
|
|
598
|
+
function repoRelative(projectRoot, file) {
|
|
599
|
+
return path.relative(projectRoot, file).split(path.sep).join("/");
|
|
600
|
+
}
|
|
601
|
+
function taskText(task) {
|
|
602
|
+
const parts = ["id", "title", "summary", "phase"].map((key) => String(task[key] ?? "")).filter(Boolean);
|
|
603
|
+
if (isRecord(task.docs)) {
|
|
604
|
+
for (const value of Object.values(task.docs)) {
|
|
605
|
+
parts.push(...asStringList(value));
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
return parts.join("\n");
|
|
394
609
|
}
|
|
395
610
|
export async function changedFiles(projectRoot) {
|
|
396
611
|
try {
|