agent-project-sdlc 0.1.7 → 0.1.8
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 +11 -1
- package/assets/agents/AGENTS_CORE.md +3 -0
- package/assets/skills/pjsdlc_dev_sprint/SKILL.md +7 -0
- package/assets/skills/pjsdlc_manager/SKILL.md +10 -4
- package/assets/skills/pjsdlc_pm_prd/SKILL.md +5 -1
- package/assets/skills/pjsdlc_tester/SKILL.md +5 -1
- package/assets/templates/PLAN_TEMPLATE.yaml +31 -0
- package/dist/lib/init.js +1 -1
- package/dist/lib/sync-engine.js +70 -2
- package/dist/lib/validators.js +95 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -28,6 +28,7 @@ npx sdlc-harness init --adopt
|
|
|
28
28
|
| Validators | `npx sdlc-harness validate-*`, `make validate-current`, `make validate-harness` | Checks phase deliverables, 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
|
| 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. |
|
|
31
|
+
| Optional parallel execution contract | `plan.yaml#parallel_execution` | Enabled only when users explicitly request multi-agent, parallel or multi-worktree execution; supports runtime-managed subagents or user-orchestrated worker prompts. |
|
|
31
32
|
| Workflow skills | `<harnessRoot>/skills/pjsdlc_*/SKILL.md` | Provides role prompts for product, architecture, development, implementation docs, review, testing, release and RFC recalibration. |
|
|
32
33
|
| Project-local skill overrides | `<harnessRoot>/pjsdlc_managed/override_skills/<skill_name>.md` + `npx sdlc-harness sync` | Appends project-specific role instructions to generated Skill output without editing managed Skill files. |
|
|
33
34
|
| Local policy overrides | `<harnessRoot>/pjsdlc_managed/policies/*.local.yaml` | Preserves project-specific policy additions separately from package defaults. |
|
|
@@ -56,7 +57,16 @@ Then run:
|
|
|
56
57
|
npx sdlc-harness sync
|
|
57
58
|
```
|
|
58
59
|
|
|
59
|
-
The sync output is the package base Skill plus one appended `Local Override` block. Unknown skill names block sync so misspellings do not silently fail.
|
|
60
|
+
The sync output is the package base Skill plus one appended `Local Override` block. Override files support either a plain project-local snippet or a complete `SKILL.md` with `name` and `description` frontmatter. Complete Skill overrides are appended, not replaced: `sync` validates the override `name`, merges the override `description` into the generated top-level metadata, strips the override frontmatter, and appends the full body. After sync, users or their agents should review the merged Skill for semantic conflicts in phase boundaries, `allowed_paths`, `required_gates`, commit/release rules and completion checks. Unknown skill names block sync so misspellings do not silently fail.
|
|
61
|
+
|
|
62
|
+
## Optional Parallel Execution
|
|
63
|
+
|
|
64
|
+
The default workflow is serial. Agents should only create `parallel_execution` in `plan.yaml` when the user explicitly asks for multi-agent, parallel or multi-worktree execution.
|
|
65
|
+
|
|
66
|
+
- `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.
|
|
67
|
+
- `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.
|
|
68
|
+
|
|
69
|
+
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.
|
|
60
70
|
|
|
61
71
|
## Common Commands
|
|
62
72
|
|
|
@@ -162,10 +162,13 @@ Strong success criteria 可以让你 independent loop。Weak criteria,例如
|
|
|
162
162
|
- “开始开发 / 做当前任务 / 做下一个任务” → 等价 `/dev`。
|
|
163
163
|
- “开始循环:写任务,执行任务 / 把开发循环跑完” → 等价 `/devloop`。
|
|
164
164
|
- “跑测试 / 验证一下” → 运行当前 task 或阶段对应 gate。
|
|
165
|
+
- “并行 / 多 agent / 多 worktree / parallel” → 只有用户显式提出时,才创建或使用 `parallel_execution` 合同;默认 workflow 不启用并行。
|
|
165
166
|
- “准备 review / 帮我 review” → 进入只读 Review workflow。
|
|
166
167
|
- “刷新文档总览 / 同步 overview” → 等价 `/overview`。
|
|
167
168
|
- `/plan` 和 `/goal` 是客户端模式入口,不由 Harness 自动开启;用户可以手动组合,例如 `/plan /prd`、`/plan 完善产品方案`、`/goal /devloop` 或 `/goal 开始循环:写任务,执行任务`。
|
|
168
169
|
|
|
170
|
+
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 负责。
|
|
171
|
+
|
|
169
172
|
如果自然语言意图会改变阶段、创建或删除 task、提交、push 或发布,Agent 先用一句话说明将执行的动作和验证方式,再继续。
|
|
170
173
|
|
|
171
174
|
- `/status`:报告当前阶段、角色、任务、阻塞项和下一步动作。
|
|
@@ -19,6 +19,8 @@ description: Use during SPRINTING to execute one task from plan.yaml, respecting
|
|
|
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
|
|
|
22
|
+
如果用户明确要求并行、多 agent 或多 worktree,开发阶段可以启用可选 `parallel_execution`。主 Agent 先创建合同,声明每个 worker 的 `branch`、`worktree`、`owned_paths`、`forbidden_paths`、`expected_output` 和 `required_gates`。worker 可以在各自 owned paths 内实现,但不得直接修改 `plan.yaml`、`lifecycle.yaml`、`.docs/INDEX.md`、overview 或最终 implementation doc。主 Agent 负责 review、merge/cherry-pick、运行总 gate、更新事实源和完成两段提交。没有用户显式要求时,继续使用串行 `/dev` 或 `/devloop`。
|
|
23
|
+
|
|
22
24
|
## 输入
|
|
23
25
|
|
|
24
26
|
- `<harnessRoot>/state/lifecycle.yaml`
|
|
@@ -51,6 +53,7 @@ description: Use during SPRINTING to execute one task from plan.yaml, respecting
|
|
|
51
53
|
- 如果一个任务实际变成多个独立实现边界,应停止扩大范围,拆分后续任务或回到任务规划。
|
|
52
54
|
- `/dev` 是单任务执行入口:没有 open task 时,先根据 PRD、architecture、tech plan 和 `plan.draft.yaml` 创建一个最小 open task;已有 open task 时,直接执行该 task;完成后停止。
|
|
53
55
|
- `/devloop` 是连续执行入口:每完成一个 task 并 push 两段提交后,重新读取 lifecycle、plan、PRD、architecture 和 tech plan,再决定是否创建/执行下一个最小 task;没有明确任务或出现 blocker 时停止并报告。
|
|
56
|
+
- Parallel Execution 是当前 task 的可选协作方式,不替代 task completion protocol;`SPRINTING` 并行必须用 `parallel_execution.linked_task_id` 绑定当前 `current_task_id`。
|
|
54
57
|
|
|
55
58
|
## Plan Protocol
|
|
56
59
|
|
|
@@ -81,6 +84,9 @@ done task 的执行流水不在当前 `plan.yaml` 长期保留,也不是默认
|
|
|
81
84
|
12. 默认不追溯已完成 task 的执行流水;只有显式 forensic/audit/regression 任务才临时查询 git、PR 或 CI 记录。
|
|
82
85
|
13. 两个 commit 后必须 `git push` 到当前 upstream branch;如果没有 remote/upstream、权限或凭证导致无法 push,停止推进并报告 blocker。
|
|
83
86
|
14. `/devloop` 每轮都必须重新读取当前状态,不得在一次上下文中假设 plan、代码或远端状态未变化。
|
|
87
|
+
15. 只有用户明确要求并行、多 agent 或多 worktree 时,才允许创建 `parallel_execution`;否则不得默认并行。
|
|
88
|
+
16. `runtime_managed` 只在当前 runtime 支持 subagent 时使用;没有该能力时,输出 `user_orchestrated` worker prompt,由用户手动打开对话或 worktree 后粘贴。
|
|
89
|
+
17. worker 不更新主事实源;主 Agent 才能更新 `plan.yaml`、`.docs/INDEX.md`、overview、implementation doc 和最终 gate 证据。
|
|
84
90
|
|
|
85
91
|
## 完成检查
|
|
86
92
|
|
|
@@ -89,6 +95,7 @@ done task 的执行流水不在当前 `plan.yaml` 长期保留,也不是默认
|
|
|
89
95
|
- [ ] open task 在 `plan.yaml` 中包含完整执行合同。
|
|
90
96
|
- [ ] 当前任务仍然是单一清晰的执行单元。
|
|
91
97
|
- [ ] implementation doc 已生成或更新,并反映相关模块的真实代码。
|
|
98
|
+
- [ ] 如果启用了 `parallel_execution`,worker owned paths、forbidden paths、required gates 和主 Agent 集成结果已记录。
|
|
92
99
|
- [ ] gate 结果已写入 implementation doc `Verification`,必要时当前 task `working_notes` 也记录了恢复现场所需的 gate evidence。
|
|
93
100
|
- [ ] task implementation commit 已在 task 移除前创建。
|
|
94
101
|
- [ ] done task 已在 implementation commit 之后从当前 `plan.yaml` 移除。
|
|
@@ -20,6 +20,8 @@ Skill、执行出口 gate,并记录 blocker。
|
|
|
20
20
|
|
|
21
21
|
自然语言和宏指令必须进入同一组 workflow action;区别在于 `/xxx` 入口携带更稳定的细节约束,简单自然语言入口更低成本,但需要你根据当前阶段、plan 和文档上下文补足细节。
|
|
22
22
|
|
|
23
|
+
Parallel Execution 是显式 opt-in:只有用户明确提出“并行”“多 agent”“多 worktree”或等价意图时,才允许创建或使用 `parallel_execution`。不要因为任务看起来可拆就默认启用。当前 runtime 有 subagent 能力时,主 Agent 可使用 `runtime_managed` 编排;没有该能力时,使用 `user_orchestrated`,输出 worker prompts 让用户手动多开对话或 worktree。无论哪种模式,主 Agent 都是 coordinator 和 integration owner。
|
|
24
|
+
|
|
23
25
|
## 输入
|
|
24
26
|
|
|
25
27
|
- `<harnessRoot>/state/lifecycle.yaml`
|
|
@@ -48,15 +50,19 @@ Skill、执行出口 gate,并记录 blocker。
|
|
|
48
50
|
17. 用户输入 `/dev`,或自然语言要求“开始开发”“做当前任务”“做下一个任务”“继续开发下一个任务”时,如果 `current_phase` 是 `SPRINTING`,创建或选择一个最小 DEV task 并执行一个 task 闭环;否则说明当前阶段冲突和推荐路径。
|
|
49
51
|
18. 用户输入 `/devloop`,或自然语言要求“开始循环:写任务,执行任务”“把开发循环跑完”“连续开发”时,如果 `current_phase` 是 `SPRINTING`,连续运行 `/dev` 循环,直到没有明确可做任务或遇到 blocker;否则说明当前阶段冲突和推荐路径。
|
|
50
52
|
19. 用户自然语言要求跑测试或验证时,运行当前 task 或当前阶段的对应 gate。
|
|
51
|
-
20.
|
|
52
|
-
21.
|
|
53
|
-
22.
|
|
54
|
-
23.
|
|
53
|
+
20. 用户明确要求并行、多 agent 或多 worktree 时,先判断当前阶段是否是 `REQUIREMENT_GATHERING`、`SPRINTING` 或 `TESTING`;如果是,生成或使用 `parallel_execution.trigger: "user_requested"` 合同;否则说明当前阶段不支持并行合同。
|
|
54
|
+
21. `runtime_managed` 模式只在当前 Agent runtime 真实具备 subagent 能力时使用;否则使用 `user_orchestrated` 并输出每个 worker 的可复制 prompt。
|
|
55
|
+
22. 用户自然语言要求 review 时,进入只读 Review workflow,不直接改源码。
|
|
56
|
+
23. 用户自然语言要求刷新文档总览时,运行 `make docs-overview`。
|
|
57
|
+
24. `/plan` 和 `/goal` 是客户端模式入口,不由 Harness 自动开启;如果用户手动组合 `/plan` 或 `/goal` 与自然语言或宏指令,应按对应 workflow action 继续执行。
|
|
58
|
+
25. 如果动作会改变阶段、创建或删除 task、提交、push 或发布,先用一句话说明即将执行的动作和验证方式,再继续。
|
|
55
59
|
|
|
56
60
|
## Plan Protocol
|
|
57
61
|
|
|
58
62
|
每个 open task 都必须在 `plan.yaml` 中包含 `docs`、`allowed_paths`、`required_gates` 和 `acceptance_criteria`;done/cancelled task 不长期留在当前 `plan.yaml`。完成后的产物事实以模块、子系统或核心数据流级 implementation doc 为准,动作历史以 git/PR/CI/release 系统作为 cold archive,`next_task_sequence` 负责继续分配后续 task id。
|
|
59
63
|
|
|
64
|
+
`parallel_execution` 是可选顶层合同,缺省表示串行。启用后必须声明 `enabled`、`trigger`、`mode`、`phase`、`coordinator`、`workers` 和 `integration`;`SPRINTING` 并行还必须通过 `linked_task_id` 绑定当前 `current_task_id`。
|
|
65
|
+
|
|
60
66
|
`lifecycle.yaml` 和 `plan.yaml` 只用于当前可执行状态。默认不要读取过去 phase/task/gate 执行流水;只有用户明确要求 forensic/audit/regression 追溯时,才临时查询 git、PR、CI 或 release 记录。
|
|
61
67
|
|
|
62
68
|
## 完成检查
|
|
@@ -17,6 +17,8 @@ description: Use during REQUIREMENT_GATHERING to turn raw input into PRD slices
|
|
|
17
17
|
|
|
18
18
|
产出 PRD 时,优先让后续架构和测试能直接使用:每条需求应有清晰 requirement ID、验收条件、Out of Scope、风险或依赖。对话中出现新范围时,要判断是更新当前 slice、拆出新 slice,还是进入 RFC。
|
|
19
19
|
|
|
20
|
+
如果用户在需求阶段明确要求并行、多 agent 或多 worktree,Parallel Execution 只能用于调研、草稿、场景拆解、风险列表或 open questions 收集。worker 不直接写最终 PRD;主 Agent 必须合成最终 `.docs/01_product/**`,并把假设、分歧和未决项写入 PRD。没有用户显式要求时,不要启用 `parallel_execution`。
|
|
21
|
+
|
|
20
22
|
## 输入
|
|
21
23
|
|
|
22
24
|
- 用户需求或原始记录
|
|
@@ -45,7 +47,8 @@ description: Use during REQUIREMENT_GATHERING to turn raw input into PRD slices
|
|
|
45
47
|
2. 每个 PRD 必须包含目标、用户场景、功能需求、验收标准、Out of Scope 和 Open Questions。
|
|
46
48
|
3. 不确定内容必须写入 `Open Questions`,不要静默假设。
|
|
47
49
|
4. 如果需求与既有架构或已接受决策冲突,先写冲突说明,不要直接编写技术方案。
|
|
48
|
-
5.
|
|
50
|
+
5. 需求阶段并行必须使用 `parallel_execution.trigger: "user_requested"`;`runtime_managed` 只在当前 runtime 支持 subagent 时使用,否则输出 `user_orchestrated` worker prompt。
|
|
51
|
+
6. 本 Skill 不直接进入开发;PRD 完成后请求 `manager` 运行阶段出口 gate。
|
|
49
52
|
|
|
50
53
|
## 完成检查
|
|
51
54
|
|
|
@@ -54,6 +57,7 @@ description: Use during REQUIREMENT_GATHERING to turn raw input into PRD slices
|
|
|
54
57
|
- [ ] Out of Scope 明确。
|
|
55
58
|
- [ ] Open Questions 有 owner/status。
|
|
56
59
|
- [ ] 已判断是否需要新增、拆分、合并或废弃 PRD slice。
|
|
60
|
+
- [ ] 如果用户要求并行,worker output 已由主 Agent 合成,最终 PRD 不由 worker 直接写入。
|
|
57
61
|
- [ ] `.docs/INDEX.md` 已链接新增产物。
|
|
58
62
|
- [ ] 已运行 `make docs-overview` 刷新 `.docs/<stage>/overview.md`。
|
|
59
63
|
- [ ] `make validate-pm` 准备通过。
|
|
@@ -17,6 +17,8 @@ description: Use during TESTING to produce a test matrix, run regression, and do
|
|
|
17
17
|
|
|
18
18
|
执行回归时,优先选择能证明阶段出口的 gate。测试无法运行、环境缺失或数据不可得时,不要宣布通过,应记录 blocker、已完成检查和恢复条件。
|
|
19
19
|
|
|
20
|
+
如果用户明确要求并行、多 agent 或多 worktree,测试阶段可以启用 `parallel_execution`,让 worker 分别执行互不依赖的回归片区、smoke、兼容性或风险验证。worker 只提交证据和必要的 scoped test changes;最终 `.docs/07_test/**`、coverage gaps、PASS/BLOCKED 决策和阶段 gate 由主 Agent 汇总。没有用户显式要求时,测试 workflow 保持串行。
|
|
21
|
+
|
|
20
22
|
## 输入
|
|
21
23
|
|
|
22
24
|
- `.docs/01_product/`
|
|
@@ -46,7 +48,8 @@ description: Use during TESTING to produce a test matrix, run regression, and do
|
|
|
46
48
|
1. 测试用例必须追溯到 PRD acceptance criteria 或 Review findings。
|
|
47
49
|
2. 根据风险补充边界、负向、回归和集成测试。
|
|
48
50
|
3. 如果有意延后覆盖,必须记录风险和 follow-up。
|
|
49
|
-
4.
|
|
51
|
+
4. 并行测试必须使用 `parallel_execution.trigger: "user_requested"`;`runtime_managed` 只在当前 runtime 支持 subagent 时使用,否则输出 `user_orchestrated` worker prompt。
|
|
52
|
+
5. 宣布阶段完成前运行 `make test-all`。
|
|
50
53
|
|
|
51
54
|
## 完成检查
|
|
52
55
|
|
|
@@ -54,6 +57,7 @@ description: Use during TESTING to produce a test matrix, run regression, and do
|
|
|
54
57
|
- [ ] Regression checklist 已完成。
|
|
55
58
|
- [ ] 已判断 test plan / test matrix 的语义切片边界。
|
|
56
59
|
- [ ] Coverage gaps 已明确。
|
|
60
|
+
- [ ] 如果启用了并行测试,worker evidence 已由主 Agent 汇总到测试产物。
|
|
57
61
|
- [ ] 已运行 `make docs-overview` 刷新 `.docs/<stage>/overview.md`。
|
|
58
62
|
- [ ] Final decision 是 `PASS` 或 `BLOCKED`。
|
|
59
63
|
- [ ] `make validate-test` 准备通过。
|
|
@@ -1,6 +1,37 @@
|
|
|
1
1
|
current_phase: "SPRINTING"
|
|
2
2
|
current_task_id: "DEV-001"
|
|
3
3
|
next_task_sequence: 2
|
|
4
|
+
# Optional. Omit this block for normal serial workflow. Only add it when the
|
|
5
|
+
# user explicitly asks for parallel / multi-agent / multi-worktree execution.
|
|
6
|
+
# parallel_execution:
|
|
7
|
+
# enabled: true
|
|
8
|
+
# trigger: "user_requested"
|
|
9
|
+
# mode: "user_orchestrated" # or "runtime_managed" when the current agent runtime can spawn subagents
|
|
10
|
+
# phase: "SPRINTING"
|
|
11
|
+
# coordinator: "main_agent"
|
|
12
|
+
# linked_task_id: "DEV-001"
|
|
13
|
+
# workers:
|
|
14
|
+
# - id: "worker-feature"
|
|
15
|
+
# writes_repo: true
|
|
16
|
+
# branch: "agent/feature"
|
|
17
|
+
# worktree: "../project-feature"
|
|
18
|
+
# owned_paths:
|
|
19
|
+
# - "src/feature/**"
|
|
20
|
+
# - "tests/feature/**"
|
|
21
|
+
# forbidden_paths:
|
|
22
|
+
# - "<harnessRoot>/state/**"
|
|
23
|
+
# - ".docs/INDEX.md"
|
|
24
|
+
# expected_output:
|
|
25
|
+
# - "implementation branch and focused gate evidence"
|
|
26
|
+
# required_gates:
|
|
27
|
+
# - "npm test -- tests/feature"
|
|
28
|
+
# integration:
|
|
29
|
+
# owner: "main_agent"
|
|
30
|
+
# merge_strategy: "main agent reviews worker output, merges or cherry-picks, then runs total gates"
|
|
31
|
+
# required_gates:
|
|
32
|
+
# - "make validate-current"
|
|
33
|
+
# fact_source_updates:
|
|
34
|
+
# - ".docs/04_implementation/"
|
|
4
35
|
tasks:
|
|
5
36
|
- id: "DEV-001"
|
|
6
37
|
title: "实现示例任务"
|
package/dist/lib/init.js
CHANGED
|
@@ -51,7 +51,7 @@ async function createProjectState(projectRoot, root, report) {
|
|
|
51
51
|
const files = [
|
|
52
52
|
[
|
|
53
53
|
harnessPath(root, "state", "lifecycle.yaml"),
|
|
54
|
-
`project_name: "Project"\nversion: "v0.1"\ncurrent_phase: "
|
|
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
56
|
[harnessPath(root, "state", "plan.yaml"), `current_phase: "SPRINTING"\ncurrent_task_id: ""\nnext_task_sequence: 1\ntasks: []\n`],
|
|
57
57
|
[harnessPath(root, "state", "plan.draft.yaml"), `current_phase: "SPRINTING"\ncurrent_task_id: ""\nnext_task_sequence: 1\ntasks: []\n`],
|
package/dist/lib/sync-engine.js
CHANGED
|
@@ -4,6 +4,7 @@ import { harnessRoot } from "./harness-root.js";
|
|
|
4
4
|
import { copyTree, listFiles, pathExists, readText, writeTextIfChanged } from "./fs.js";
|
|
5
5
|
import { AGENTS_BLOCK_MARKERS, MAKEFILE_BLOCK_END, MAKEFILE_BLOCK_MARKERS, MAKEFILE_BLOCK_START, MANAGED_BLOCK_END, MANAGED_BLOCK_START } from "./managed-file.js";
|
|
6
6
|
import { packageAssetPath } from "./paths.js";
|
|
7
|
+
import { parseYaml, stringifyYaml } from "./yaml.js";
|
|
7
8
|
export function emptySyncReport() {
|
|
8
9
|
return {
|
|
9
10
|
changed: [],
|
|
@@ -251,8 +252,23 @@ async function readSkillOverrides(projectRoot, root, knownSkills, report) {
|
|
|
251
252
|
}
|
|
252
253
|
const content = await readText(file);
|
|
253
254
|
if (content.trim()) {
|
|
255
|
+
const fullSkill = parseFullSkillOverride(content);
|
|
256
|
+
if (fullSkill) {
|
|
257
|
+
if (fullSkill.name !== match[1]) {
|
|
258
|
+
report.blocked.push(`skill override name mismatch: ${path.join(root, "pjsdlc_managed", "override_skills", relativePath)} declares name ${fullSkill.name}`);
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
overrides.set(match[1], {
|
|
262
|
+
relativePath: path.join(root, "pjsdlc_managed", "override_skills", relativePath),
|
|
263
|
+
mode: "full-skill",
|
|
264
|
+
content: fullSkill.body,
|
|
265
|
+
description: fullSkill.description
|
|
266
|
+
});
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
254
269
|
overrides.set(match[1], {
|
|
255
270
|
relativePath: path.join(root, "pjsdlc_managed", "override_skills", relativePath),
|
|
271
|
+
mode: "snippet",
|
|
256
272
|
content
|
|
257
273
|
});
|
|
258
274
|
}
|
|
@@ -271,6 +287,10 @@ function renderSkillWithOverride(baseContent, override) {
|
|
|
271
287
|
if (!override) {
|
|
272
288
|
return baseContent;
|
|
273
289
|
}
|
|
290
|
+
const renderedBase = override.mode === "full-skill" ? mergeSkillDescription(baseContent, override.description) : baseContent;
|
|
291
|
+
const guidance = override.mode === "full-skill"
|
|
292
|
+
? "The following project-local full Skill extension is appended by `sdlc-harness sync`. Its frontmatter has been merged into the generated Skill metadata; the body below remains the project-local extension."
|
|
293
|
+
: "The following project-local snippet is appended by `sdlc-harness sync`.";
|
|
274
294
|
const header = [
|
|
275
295
|
"",
|
|
276
296
|
"",
|
|
@@ -278,10 +298,58 @@ function renderSkillWithOverride(baseContent, override) {
|
|
|
278
298
|
"",
|
|
279
299
|
`Source: \`${override.relativePath.split(path.sep).join("/")}\``,
|
|
280
300
|
"",
|
|
281
|
-
|
|
301
|
+
`${guidance} Keep package-managed Skill files unchanged; edit the override source instead.`,
|
|
302
|
+
"",
|
|
303
|
+
"After sync, review the merged Skill for semantic conflicts between the package base and local override, especially phase boundaries, `allowed_paths`, `required_gates`, commit/release rules and completion checks.",
|
|
282
304
|
""
|
|
283
305
|
].join("\n");
|
|
284
|
-
return `${
|
|
306
|
+
return `${renderedBase.trimEnd()}${header}\n${override.content.trim()}\n`;
|
|
307
|
+
}
|
|
308
|
+
function parseFullSkillOverride(content) {
|
|
309
|
+
const parsed = parseFrontmatter(content);
|
|
310
|
+
if (!parsed) {
|
|
311
|
+
return undefined;
|
|
312
|
+
}
|
|
313
|
+
const name = parsed.metadata.name;
|
|
314
|
+
const description = parsed.metadata.description;
|
|
315
|
+
if (typeof name !== "string" || typeof description !== "string") {
|
|
316
|
+
return undefined;
|
|
317
|
+
}
|
|
318
|
+
return { name, description, body: parsed.body };
|
|
319
|
+
}
|
|
320
|
+
function mergeSkillDescription(baseContent, overrideDescription) {
|
|
321
|
+
const parsed = parseFrontmatter(baseContent);
|
|
322
|
+
if (!parsed || typeof parsed.metadata.name !== "string" || typeof parsed.metadata.description !== "string") {
|
|
323
|
+
return baseContent;
|
|
324
|
+
}
|
|
325
|
+
const metadata = {
|
|
326
|
+
...parsed.metadata,
|
|
327
|
+
name: parsed.metadata.name,
|
|
328
|
+
description: `${parsed.metadata.description} Project override: ${overrideDescription}`
|
|
329
|
+
};
|
|
330
|
+
return `---\n${stringifyYaml(metadata).trimEnd()}\n---\n${parsed.body.trimStart()}`;
|
|
331
|
+
}
|
|
332
|
+
function parseFrontmatter(content) {
|
|
333
|
+
if (!content.startsWith("---\n")) {
|
|
334
|
+
return undefined;
|
|
335
|
+
}
|
|
336
|
+
const endIndex = content.indexOf("\n---", 4);
|
|
337
|
+
if (endIndex < 0) {
|
|
338
|
+
return undefined;
|
|
339
|
+
}
|
|
340
|
+
let metadata;
|
|
341
|
+
try {
|
|
342
|
+
metadata = parseYaml(content.slice(4, endIndex));
|
|
343
|
+
}
|
|
344
|
+
catch {
|
|
345
|
+
return undefined;
|
|
346
|
+
}
|
|
347
|
+
if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
|
|
348
|
+
return undefined;
|
|
349
|
+
}
|
|
350
|
+
const bodyStart = content.indexOf("\n", endIndex + 4);
|
|
351
|
+
const body = bodyStart < 0 ? "" : content.slice(bodyStart + 1);
|
|
352
|
+
return { metadata: metadata, body };
|
|
285
353
|
}
|
|
286
354
|
async function syncFile(source, destination, report, missingMode) {
|
|
287
355
|
if (!(await pathExists(source))) {
|
package/dist/lib/validators.js
CHANGED
|
@@ -5,6 +5,8 @@ import { harnessPath, harnessRoot } from "./harness-root.js";
|
|
|
5
5
|
import { listFiles, pathExists, readText } from "./fs.js";
|
|
6
6
|
import { parseYaml } from "./yaml.js";
|
|
7
7
|
const execFileAsync = promisify(execFile);
|
|
8
|
+
const PARALLEL_MODES = new Set(["runtime_managed", "user_orchestrated"]);
|
|
9
|
+
const PARALLEL_PHASES = new Set(["REQUIREMENT_GATHERING", "SPRINTING", "TESTING"]);
|
|
8
10
|
const validators = {
|
|
9
11
|
"validate-harness": validateHarness,
|
|
10
12
|
"validate-current": validateCurrent,
|
|
@@ -88,6 +90,7 @@ async function validateDev(projectRoot) {
|
|
|
88
90
|
const errors = [];
|
|
89
91
|
const root = await harnessRoot(projectRoot);
|
|
90
92
|
const tasksData = await readYamlObject(path.join(projectRoot, root, "state", "plan.yaml"));
|
|
93
|
+
validateParallelExecutionContract(tasksData, errors);
|
|
91
94
|
const tasks = Array.isArray(tasksData.tasks) ? tasksData.tasks : [];
|
|
92
95
|
const nextTaskSequence = tasksData.next_task_sequence;
|
|
93
96
|
if (!Number.isInteger(nextTaskSequence) || Number(nextTaskSequence) <= 0) {
|
|
@@ -135,6 +138,98 @@ async function validateDev(projectRoot) {
|
|
|
135
138
|
}
|
|
136
139
|
return { info: [`validate-dev checked ${tasks.length} task(s)`], errors };
|
|
137
140
|
}
|
|
141
|
+
function validateParallelExecutionContract(plan, errors) {
|
|
142
|
+
const contract = plan.parallel_execution;
|
|
143
|
+
if (contract === undefined || contract === null)
|
|
144
|
+
return;
|
|
145
|
+
if (!isRecord(contract)) {
|
|
146
|
+
errors.push("parallel_execution must be a mapping");
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (contract.enabled !== true)
|
|
150
|
+
errors.push("parallel_execution.enabled must be true when present");
|
|
151
|
+
if (contract.trigger !== "user_requested")
|
|
152
|
+
errors.push('parallel_execution.trigger must be "user_requested"');
|
|
153
|
+
if (!PARALLEL_MODES.has(String(contract.mode ?? ""))) {
|
|
154
|
+
errors.push("parallel_execution.mode must be runtime_managed or user_orchestrated");
|
|
155
|
+
}
|
|
156
|
+
if (!PARALLEL_PHASES.has(String(contract.phase ?? ""))) {
|
|
157
|
+
errors.push("parallel_execution.phase must be REQUIREMENT_GATHERING, SPRINTING, or TESTING");
|
|
158
|
+
}
|
|
159
|
+
if (contract.coordinator !== "main_agent")
|
|
160
|
+
errors.push('parallel_execution.coordinator must be "main_agent"');
|
|
161
|
+
if (contract.phase === "SPRINTING") {
|
|
162
|
+
if (!contract.linked_task_id)
|
|
163
|
+
errors.push("SPRINTING parallel_execution must define linked_task_id");
|
|
164
|
+
if (contract.linked_task_id !== plan.current_task_id) {
|
|
165
|
+
errors.push("SPRINTING parallel_execution.linked_task_id must match current_task_id");
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const workers = contract.workers;
|
|
169
|
+
if (!Array.isArray(workers) || workers.length === 0) {
|
|
170
|
+
errors.push("parallel_execution.workers must be a non-empty list");
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
const seen = new Set();
|
|
174
|
+
workers.forEach((worker, index) => {
|
|
175
|
+
const prefix = `parallel_execution.workers[${index}]`;
|
|
176
|
+
if (!isRecord(worker)) {
|
|
177
|
+
errors.push(`${prefix} must be a mapping`);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const workerId = String(worker.id ?? "");
|
|
181
|
+
if (!workerId.trim()) {
|
|
182
|
+
errors.push(`${prefix}.id must be a non-empty string`);
|
|
183
|
+
}
|
|
184
|
+
else if (seen.has(workerId)) {
|
|
185
|
+
errors.push(`parallel_execution worker id must be unique: ${workerId}`);
|
|
186
|
+
}
|
|
187
|
+
seen.add(workerId);
|
|
188
|
+
if (typeof worker.writes_repo !== "boolean")
|
|
189
|
+
errors.push(`${prefix}.writes_repo must be a boolean`);
|
|
190
|
+
for (const field of ["owned_paths", "forbidden_paths", "expected_output", "required_gates"]) {
|
|
191
|
+
if (!Array.isArray(worker[field]))
|
|
192
|
+
errors.push(`${prefix}.${field} must be a list`);
|
|
193
|
+
}
|
|
194
|
+
if (Array.isArray(worker.expected_output) && worker.expected_output.length === 0) {
|
|
195
|
+
errors.push(`${prefix}.expected_output must not be empty`);
|
|
196
|
+
}
|
|
197
|
+
if (Array.isArray(worker.required_gates) && worker.required_gates.length === 0) {
|
|
198
|
+
errors.push(`${prefix}.required_gates must not be empty`);
|
|
199
|
+
}
|
|
200
|
+
if (worker.writes_repo === true) {
|
|
201
|
+
if (typeof worker.branch !== "string" || !worker.branch.trim()) {
|
|
202
|
+
errors.push(`${prefix}.branch is required when writes_repo is true`);
|
|
203
|
+
}
|
|
204
|
+
if (typeof worker.worktree !== "string" || !worker.worktree.trim()) {
|
|
205
|
+
errors.push(`${prefix}.worktree is required when writes_repo is true`);
|
|
206
|
+
}
|
|
207
|
+
if (!Array.isArray(worker.owned_paths) || worker.owned_paths.length === 0) {
|
|
208
|
+
errors.push(`${prefix}.owned_paths must not be empty when writes_repo is true`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
const integration = contract.integration;
|
|
214
|
+
if (!isRecord(integration)) {
|
|
215
|
+
errors.push("parallel_execution.integration must be a mapping");
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (integration.owner !== "main_agent")
|
|
219
|
+
errors.push('parallel_execution.integration.owner must be "main_agent"');
|
|
220
|
+
if (typeof integration.merge_strategy !== "string" || !integration.merge_strategy.trim()) {
|
|
221
|
+
errors.push("parallel_execution.integration.merge_strategy must be a non-empty string");
|
|
222
|
+
}
|
|
223
|
+
if (!Array.isArray(integration.required_gates) || integration.required_gates.length === 0) {
|
|
224
|
+
errors.push("parallel_execution.integration.required_gates must be a non-empty list");
|
|
225
|
+
}
|
|
226
|
+
if (!Array.isArray(integration.fact_source_updates) || integration.fact_source_updates.length === 0) {
|
|
227
|
+
errors.push("parallel_execution.integration.fact_source_updates must be a non-empty list");
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function isRecord(value) {
|
|
231
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
232
|
+
}
|
|
138
233
|
async function readYamlObject(filePath) {
|
|
139
234
|
if (!(await pathExists(filePath)))
|
|
140
235
|
return {};
|