agent-project-sdlc 0.1.4 → 0.1.5
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/assets/agents/AGENTS_CORE.md +25 -20
- package/assets/skills/pjsdlc_dev_sprint/SKILL.md +2 -2
- package/assets/skills/pjsdlc_manager/SKILL.md +13 -9
- package/dist/commands/index.js +1 -1
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.js +57 -5
- package/dist/lib/migrations.js +50 -5
- package/dist/lib/package-source.js +16 -1
- package/dist/lib/sync-engine.js +6 -4
- package/dist/lib/types.d.ts +1 -0
- package/package.json +1 -1
- package/source-mappings.yaml +6 -4
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# AI SDLC Harness Protocol
|
|
2
2
|
|
|
3
|
-
本仓库使用 AI SDLC Harness 工作流。开始任何工作前,把 `.
|
|
3
|
+
本仓库使用 AI SDLC Harness 工作流。开始任何工作前,把 `.codex/` 和 `.docs/`
|
|
4
4
|
视为项目事实源。
|
|
5
5
|
|
|
6
6
|
## 事实源
|
|
7
7
|
|
|
8
|
-
- 生命周期状态:`.
|
|
9
|
-
- 执行计划:`.
|
|
10
|
-
- 计划草案:`.
|
|
11
|
-
- 项目长期记忆:`.
|
|
8
|
+
- 生命周期状态:`.codex/state/lifecycle.yaml`
|
|
9
|
+
- 执行计划:`.codex/state/plan.yaml`
|
|
10
|
+
- 计划草案:`.codex/state/plan.draft.yaml`
|
|
11
|
+
- 项目长期记忆:`.codex/state/memory.md`
|
|
12
12
|
- 产品文档:`.docs/01_product/`
|
|
13
13
|
- 架构文档:`.docs/02_architecture/`
|
|
14
14
|
- 技术方案:`.docs/03_tech_plan/`
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
- 发布文档:`.docs/08_release/`
|
|
19
19
|
- RFC 文档:`.docs/rfc/`
|
|
20
20
|
- 全局文档索引:`.docs/INDEX.md`
|
|
21
|
+
- Harness authoring skills(如果存在):`.codex/skills/authoring/`,只在维护 Harness/workflow/npm package 源码或本仓库自举规则时读取,不作为用户项目默认分发内容
|
|
21
22
|
|
|
22
23
|
## 派生视图
|
|
23
24
|
|
|
@@ -39,20 +40,20 @@
|
|
|
39
40
|
- 历史 task 查询默认面向产物结果和变更意图,读取模块级 implementation doc、RFC、PRD、tech plan 和代码;task id 和 commit 只作为 provenance,`allowed_paths`、`required_gates`、临时 `working_notes` 是执行期约束,不作为历史查询 API。
|
|
40
41
|
- 过去 phase/task/gate 执行流水不是默认上下文;除非用户明确要求 forensic/audit/regression 追溯,否则不要读取或恢复历史执行信息。
|
|
41
42
|
|
|
42
|
-
##
|
|
43
|
+
## Skill Language Contract
|
|
43
44
|
|
|
44
45
|
- 面向人阅读的说明、规则、SOP、检查清单使用中文。
|
|
45
46
|
- 机器契约保持英文,包括字段名、路径、命令、阶段枚举、状态枚举、脚本参数。
|
|
46
|
-
- 不翻译 `.
|
|
47
|
+
- 不翻译 `.codex/state/*.yaml`、`.codex/pjsdlc_managed/policies/*.yaml` 中的 key。
|
|
47
48
|
- 不翻译 `current_phase`、`active_skill`、`allowed_paths`、`required_gates`、`implementation_doc` 等字段名。
|
|
48
49
|
- 不翻译 `REQUIREMENT_GATHERING`、`ARCHITECTING`、`SPRINTING`、`REVIEWING`、`TESTING`、`RELEASING`、`RFC_RECALIBRATION`、`BLOCKED` 等阶段枚举。
|
|
49
50
|
- 不翻译 `pending`、`in_progress`、`done`、`blocked`、`pending_revision`、`cancelled` 等任务状态。
|
|
50
|
-
- 不翻译 `make validate-*`、`python3 tools/transition.py --to <PHASE>`、`.docs/01_product/`、`.
|
|
51
|
-
- 后续更新 `.
|
|
51
|
+
- 不翻译 `make validate-*`、`python3 tools/transition.py --to <PHASE>`、`.docs/01_product/`、`.codex/state/plan.yaml` 等命令和路径。
|
|
52
|
+
- 后续更新 `.codex/skills/*/SKILL.md`、`.codex/skills/authoring/*/SKILL.md` 或 `.codex/pjsdlc_managed/templates/*.md` 时,遵循“中文解释 + 英文精确标识符”。Harness 根目录由 `package.json#sdlcHarness.harnessFolderName` 或 `sdlc-harness.config.json#harnessFolderName` 决定;本仓库显式配置为 `.codex`。
|
|
52
53
|
|
|
53
54
|
## 通用执行原则
|
|
54
55
|
|
|
55
|
-
以下原则完整迁移自 `multica-ai/andrej-karpathy-skills` 的 MIT guideline,并按本仓库的
|
|
56
|
+
以下原则完整迁移自 `multica-ai/andrej-karpathy-skills` 的 MIT guideline,并按本仓库的 Skill Language Contract 转为“中文说明 + 英文关键词”形式,与阶段化 Harness 协议合并执行。
|
|
56
57
|
|
|
57
58
|
### Karpathy Guidelines(MIT 完整本地化)
|
|
58
59
|
|
|
@@ -122,15 +123,15 @@ Strong success criteria 可以让你 independent loop。Weak criteria,例如
|
|
|
122
123
|
|
|
123
124
|
### Harness 补充原则
|
|
124
125
|
|
|
125
|
-
1. 阶段约束优先:除非用户明确要求其它工作流动作,否则使用 `active_skill` 指定的
|
|
126
|
+
1. 阶段约束优先:除非用户明确要求其它工作流动作,否则使用 `active_skill` 指定的 workflow skill,并服从当前阶段的 allowed paths、required gates 和交付物边界。
|
|
126
127
|
2. 文档先于实现:产品文档和技术方案未形成前,不写业务代码;需求变更必须进入 RFC 工作流。
|
|
127
128
|
3. 验证闭环:多步骤工作先给出简短计划,并为关键步骤绑定验证方式。除非被阻塞,否则持续迭代到对应 `required_gates`、阶段 gate 或明确的人工验收标准满足。
|
|
128
129
|
4. 派生物可再生成:`overview.md`、包内 assets 等 generated artifact 必须由对应命令刷新,不手写局部补丁。
|
|
129
130
|
|
|
130
131
|
## 工作规则
|
|
131
132
|
|
|
132
|
-
1. 选择任何角色或
|
|
133
|
-
2. 除非用户明确要求其它工作流动作,否则使用 `active_skill` 指定的
|
|
133
|
+
1. 选择任何角色或 skill 前,先读取 `.codex/state/lifecycle.yaml`。
|
|
134
|
+
2. 除非用户明确要求其它工作流动作,否则使用 `active_skill` 指定的 workflow skill。
|
|
134
135
|
3. 产品文档和技术方案未形成前,不写业务代码。
|
|
135
136
|
4. 在 `SPRINTING` 阶段,一次只执行一个任务。
|
|
136
137
|
5. 在 `SPRINTING` 阶段,只编辑当前 open task 的 `allowed_paths` 允许的文件。
|
|
@@ -138,7 +139,7 @@ Strong success criteria 可以让你 independent loop。Weak criteria,例如
|
|
|
138
139
|
7. 代码 gate 通过后,更新相关 implementation doc 和 `.docs/INDEX.md`。
|
|
139
140
|
8. `reviewer` 角色只读,不直接修改源码。
|
|
140
141
|
9. 需求变更必须进入 RFC 工作流。
|
|
141
|
-
10. task/release 历史动作记录使用 git commit、tag 或外部 release 系统,不维护
|
|
142
|
+
10. task/release 历史动作记录使用 git commit、tag 或外部 release 系统,不维护 `<harnessRoot>/archive/` 常规归档。
|
|
142
143
|
11. 在 `SPRINTING` 阶段,task 完成闭环必须先创建 task implementation commit,再提交移除该 task 后的 task completion ledger commit;如果没有 remote/upstream、权限或凭证导致无法 push,不要开始下一个 task,先报告 blocker。
|
|
143
144
|
12. 文档 slice 发生变化后,运行 `make docs-overview` 刷新对应 `overview.md`。
|
|
144
145
|
13. open task 必须在 `plan.yaml` 中包含完整执行合同,再继续推进或交接。
|
|
@@ -148,27 +149,31 @@ Strong success criteria 可以让你 independent loop。Weak criteria,例如
|
|
|
148
149
|
|
|
149
150
|
## 自然语言与宏指令
|
|
150
151
|
|
|
151
|
-
用户不需要记忆或使用宏指令。自然语言意图和约定宏指令是两层入口:自然语言是默认控制方式,`/xxx`
|
|
152
|
+
用户不需要记忆或使用宏指令。自然语言意图和约定宏指令是两层入口:自然语言是默认控制方式,`/xxx` 是更完整、更细节的提示词别名,也可作为调试入口或自动化入口。两者应映射到同一组 workflow action;自然语言入口成本更低,但细节约束更依赖 Agent 根据上下文判断。
|
|
152
153
|
|
|
153
|
-
当用户用自然语言表达意图时,Agent 应先读取 `.
|
|
154
|
+
当用户用自然语言表达意图时,Agent 应先读取 `.codex/state/lifecycle.yaml` 和必要的 `.codex/state/plan.yaml`,再映射到对应 workflow action:
|
|
154
155
|
|
|
155
156
|
- “现在到哪了 / 状态如何” → 等价 `/status`。
|
|
156
157
|
- “继续 / 下一步 / 推进” → 等价 `/next`。
|
|
157
158
|
- “能进入下一阶段吗 / 进入下一步” → 等价 `/advance`。
|
|
158
159
|
- “需求变了 / 这个设计要改” → 进入 RFC workflow。
|
|
160
|
+
- “完善产品方案 / 写 PRD / 我提供信息,你帮我完善产品方案” → 等价 `/prd`。
|
|
161
|
+
- “设计技术方案 / 做架构方案 / 根据 PRD 做技术方案” → 等价 `/design`。
|
|
159
162
|
- “开始开发 / 做当前任务 / 做下一个任务” → 等价 `/dev`。
|
|
160
163
|
- “开始循环:写任务,执行任务 / 把开发循环跑完” → 等价 `/devloop`。
|
|
161
164
|
- “跑测试 / 验证一下” → 运行当前 task 或阶段对应 gate。
|
|
162
165
|
- “准备 review / 帮我 review” → 进入只读 Review workflow。
|
|
163
166
|
- “刷新文档总览 / 同步 overview” → 等价 `/overview`。
|
|
164
|
-
- `/plan` 和 `/goal` 是客户端模式入口,不由 Harness 自动开启;用户可以手动组合,例如 `/plan 完善产品方案`、`/goal /devloop` 或 `/goal 开始循环:写任务,执行任务`。
|
|
167
|
+
- `/plan` 和 `/goal` 是客户端模式入口,不由 Harness 自动开启;用户可以手动组合,例如 `/plan /prd`、`/plan 完善产品方案`、`/goal /devloop` 或 `/goal 开始循环:写任务,执行任务`。
|
|
165
168
|
|
|
166
169
|
如果自然语言意图会改变阶段、创建或删除 task、提交、push 或发布,Agent 先用一句话说明将执行的动作和验证方式,再继续。
|
|
167
170
|
|
|
168
171
|
- `/status`:报告当前阶段、角色、任务、阻塞项和下一步动作。
|
|
169
|
-
- `/next`:运行当前阶段映射的
|
|
172
|
+
- `/next`:运行当前阶段映射的 workflow skill。
|
|
170
173
|
- `/advance`:校验当前阶段出口 gate,通过后才尝试流转。
|
|
171
174
|
- `/rfc <file>`:挂起当前流程并进入 RFC recalibration。
|
|
175
|
+
- `/prd`:在 `REQUIREMENT_GATHERING` 调用产品方案工作流,澄清需求并更新 PRD、验收标准、open questions、`.docs/INDEX.md` 和 overview。
|
|
176
|
+
- `/design`:在 `ARCHITECTING` 调用架构和技术方案工作流,基于 PRD 更新 architecture、tech plan 和 `plan.draft.yaml`。
|
|
172
177
|
- `/dev`:在 `SPRINTING` 创建或选择下一个最小 DEV task,执行一个 task,跑 gate,更新模块级 implementation doc,按两段 commit/push 闭环后停止。
|
|
173
178
|
- `/devloop`:在 `SPRINTING` 连续运行 `/dev` 循环,直到没有明确可创建/执行的任务,或遇到需求、架构、allowed_paths、gate、commit/push blocker。
|
|
174
179
|
- `/syncdocs`:同步 `.docs/INDEX.md` 与当前文档事实源。
|
|
@@ -178,11 +183,11 @@ Strong success criteria 可以让你 independent loop。Weak criteria,例如
|
|
|
178
183
|
|
|
179
184
|
## 阶段流转
|
|
180
185
|
|
|
181
|
-
正常阶段流转不要手动编辑 `.
|
|
186
|
+
正常阶段流转不要手动编辑 `.codex/state/lifecycle.yaml`。使用:
|
|
182
187
|
|
|
183
188
|
```sh
|
|
184
189
|
python3 tools/transition.py --to <PHASE>
|
|
185
190
|
```
|
|
186
191
|
|
|
187
192
|
流转前先运行阶段 gate,通常使用 `make validate-current`,或使用
|
|
188
|
-
`.
|
|
193
|
+
`.codex/pjsdlc_managed/policies/phase_contracts.yaml` 中列出的具体 `make validate-*` 目标。
|
|
@@ -47,7 +47,7 @@ description: Use during SPRINTING to execute one task from plan.yaml, respecting
|
|
|
47
47
|
- task completion ledger commit 发生在 implementation commit 之后,只负责将该 task 从当前 `plan.yaml` 移除。
|
|
48
48
|
- 一个开发 task 默认对应一个主要 implementation commit 和一个轻量 completion ledger commit。implementation commit message 应包含 task id,例如 `DEV-003: implement login rate limit`;push 成功前,不进入下一个 task。
|
|
49
49
|
- 本 Skill 不直接重切 PRD 或 tech plan;如果发现上游语义边界错误,进入 `BLOCKED`、创建 RFC,或请求回到 `ARCHITECTING`。
|
|
50
|
-
- gate 通过后调用 `
|
|
50
|
+
- gate 通过后调用 `pjsdlc_implementation_doc`,由该 Skill 按真实实现更新或新增 `.docs/04_implementation/` 模块级 slice。
|
|
51
51
|
- 如果一个任务实际变成多个独立实现边界,应停止扩大范围,拆分后续任务或回到任务规划。
|
|
52
52
|
- `/dev` 是单任务执行入口:没有 open task 时,先根据 PRD、architecture、tech plan 和 `plan.draft.yaml` 创建一个最小 open task;已有 open task 时,直接执行该 task;完成后停止。
|
|
53
53
|
- `/devloop` 是连续执行入口:每完成一个 task 并 push 两段提交后,重新读取 lifecycle、plan、PRD、architecture 和 tech plan,再决定是否创建/执行下一个最小 task;没有明确任务或出现 blocker 时停止并报告。
|
|
@@ -73,7 +73,7 @@ done task 的执行流水不在当前 `plan.yaml` 长期保留,也不是默认
|
|
|
73
73
|
4. 必须运行当前 task 的 `required_gates`。
|
|
74
74
|
5. 如果 gate 因代码或测试逻辑失败,在任务范围内修复。
|
|
75
75
|
6. 如果 gate 因基础设施、凭证缺失、产品行为不清或高风险架构变化失败,进入 `BLOCKED`。
|
|
76
|
-
7. gate 通过后调用 `
|
|
76
|
+
7. gate 通过后调用 `pjsdlc_implementation_doc`。
|
|
77
77
|
8. 只有 gate 通过且 implementation doc 校验通过后,才能把任务标记为 `done`。
|
|
78
78
|
9. 任务完成并写入或更新相关 implementation doc、刷新 `overview.md`、记录 gate 后,先创建 task implementation commit;此时不要移除该 task。
|
|
79
79
|
10. task implementation commit 必须发生在 task 移除前;后续默认不要读取其中的执行期字段,历史查询以模块级 implementation doc、RFC、PRD、tech plan 和代码为主。
|
|
@@ -16,7 +16,9 @@ Skill、执行出口 gate,并记录 blocker。
|
|
|
16
16
|
|
|
17
17
|
与用户对话时,先读取 lifecycle 和 plan,再说明当前阶段、active_skill、当前任务、阻塞项和下一步。不要基于猜测切换阶段;如果用户要求的动作与当前阶段冲突,说明冲突、可选路径和推荐处理方式。
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
自然语言是默认控制方式,约定宏指令是更完整、更细节的提示词别名。用户说“状态如何”“继续”“下一步”“完善产品方案”“设计技术方案”“开始开发”“开始循环:写任务,执行任务”“跑测试”“准备 review”“需求变了”等,不应要求用户记忆 `/xxx`;你应先读取 lifecycle 和 plan,再把意图映射到对应 workflow action。执行 `/status`、`/next`、`/advance`、`/rfc`、`/prd`、`/design`、`/dev`、`/devloop` 等宏指令时,输出要短而明确:当前事实是什么、将调用哪个 gate 或 Skill、成功后会进入哪里、失败时如何保持状态安全。
|
|
20
|
+
|
|
21
|
+
自然语言和宏指令必须进入同一组 workflow action;区别在于 `/xxx` 入口携带更稳定的细节约束,简单自然语言入口更低成本,但需要你根据当前阶段、plan 和文档上下文补足细节。
|
|
20
22
|
|
|
21
23
|
## 输入
|
|
22
24
|
|
|
@@ -35,19 +37,21 @@ Skill、执行出口 gate,并记录 blocker。
|
|
|
35
37
|
6. 用户输入 `/status` 时,运行 `make status`。
|
|
36
38
|
7. 用户输入 `/next` 时,调用 `active_skill` 映射的 Skill。
|
|
37
39
|
8. 用户输入 `/advance` 时,运行 `make validate-current`,通过后流转到配置的 `next` 阶段。
|
|
38
|
-
9. 用户输入 `/rfc <file>` 时,流转到 `RFC_RECALIBRATION` 并调用 `
|
|
40
|
+
9. 用户输入 `/rfc <file>` 时,流转到 `RFC_RECALIBRATION` 并调用 `pjsdlc_rfc_recalibrate`。
|
|
39
41
|
10. 如果当前 task 处于 `blocked` 或缺少 open task 必需的 plan 字段,不要推进阶段,先要求 `plan.yaml` 完整。
|
|
40
42
|
11. 用户自然语言询问状态时,等价执行 `/status`。
|
|
41
43
|
12. 用户自然语言要求继续、推进或下一步时,等价执行 `/next`。
|
|
42
44
|
13. 用户自然语言要求进入下一阶段或检查是否可进入下一阶段时,等价执行 `/advance`。
|
|
43
45
|
14. 用户自然语言表达需求或设计变化时,进入 RFC workflow。
|
|
44
|
-
15. 用户输入 `/
|
|
45
|
-
16. 用户输入 `/
|
|
46
|
-
17.
|
|
47
|
-
18.
|
|
48
|
-
19.
|
|
49
|
-
20.
|
|
50
|
-
21.
|
|
46
|
+
15. 用户输入 `/prd`,或自然语言要求“完善产品方案”“写 PRD”“我提供信息,你帮我完善产品方案”时,如果 `current_phase` 是 `REQUIREMENT_GATHERING`,调用产品方案工作流并更新 PRD、验收标准和 open questions;否则说明当前阶段冲突和推荐路径。
|
|
47
|
+
16. 用户输入 `/design`,或自然语言要求“设计技术方案”“做架构方案”“根据 PRD 做技术方案”时,如果 `current_phase` 是 `ARCHITECTING`,调用架构和技术方案工作流并更新 architecture、tech plan 和 `plan.draft.yaml`;否则说明当前阶段冲突和推荐路径。
|
|
48
|
+
17. 用户输入 `/dev`,或自然语言要求“开始开发”“做当前任务”“做下一个任务”“继续开发下一个任务”时,如果 `current_phase` 是 `SPRINTING`,创建或选择一个最小 DEV task 并执行一个 task 闭环;否则说明当前阶段冲突和推荐路径。
|
|
49
|
+
18. 用户输入 `/devloop`,或自然语言要求“开始循环:写任务,执行任务”“把开发循环跑完”“连续开发”时,如果 `current_phase` 是 `SPRINTING`,连续运行 `/dev` 循环,直到没有明确可做任务或遇到 blocker;否则说明当前阶段冲突和推荐路径。
|
|
50
|
+
19. 用户自然语言要求跑测试或验证时,运行当前 task 或当前阶段的对应 gate。
|
|
51
|
+
20. 用户自然语言要求 review 时,进入只读 Review workflow,不直接改源码。
|
|
52
|
+
21. 用户自然语言要求刷新文档总览时,运行 `make docs-overview`。
|
|
53
|
+
22. `/plan` 和 `/goal` 是客户端模式入口,不由 Harness 自动开启;如果用户手动组合 `/plan` 或 `/goal` 与自然语言或宏指令,应按对应 workflow action 继续执行。
|
|
54
|
+
23. 如果动作会改变阶段、创建或删除 task、提交、push 或发布,先用一句话说明即将执行的动作和验证方式,再继续。
|
|
51
55
|
|
|
52
56
|
## Plan Protocol
|
|
53
57
|
|
package/dist/commands/index.js
CHANGED
|
@@ -21,7 +21,7 @@ export const commands = {
|
|
|
21
21
|
export function help() {
|
|
22
22
|
console.log(`sdlc-harness commands:
|
|
23
23
|
init [--adopt] [--harness-folder <path>]
|
|
24
|
-
Initialize a
|
|
24
|
+
Initialize/adopt a project; without --harness-folder, choose target agent first
|
|
25
25
|
sync Materialize canonical assets into the workspace
|
|
26
26
|
upgrade Run migrations and then sync
|
|
27
27
|
doctor Diagnose project configuration and drift
|
package/dist/commands/init.d.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
export declare function init(args: string[]): Promise<void>;
|
|
2
2
|
export declare function resolveInitHarnessRoot(projectRoot: string, args: string[]): Promise<string | undefined>;
|
|
3
|
+
export declare function resolveAgentHarnessFolderName(agentAnswer: string, customFolderAnswer?: string): string;
|
package/dist/commands/init.js
CHANGED
|
@@ -4,6 +4,15 @@ import { runInit } from "../lib/init.js";
|
|
|
4
4
|
import { normalizeHarnessFolderName, readHarnessRootConfig } from "../lib/harness-root.js";
|
|
5
5
|
import { writePackageHarnessRoot } from "../lib/package-json-config.js";
|
|
6
6
|
import { DEFAULT_HARNESS_ROOT } from "../lib/paths.js";
|
|
7
|
+
const AGENT_HARNESS_OPTIONS = [
|
|
8
|
+
{ key: "codex", label: "Codex", harnessFolderName: ".codex" },
|
|
9
|
+
{ key: "claude", label: "Claude Code", harnessFolderName: ".claude" },
|
|
10
|
+
{ key: "cursor", label: "Cursor", harnessFolderName: ".cursor" },
|
|
11
|
+
{ key: "cline", label: "Cline", harnessFolderName: ".cline" },
|
|
12
|
+
{ key: "roo", label: "Roo Code", harnessFolderName: ".roo" },
|
|
13
|
+
{ key: "gemini", label: "Gemini CLI", harnessFolderName: ".gemini" },
|
|
14
|
+
{ key: "other", label: "Other" }
|
|
15
|
+
];
|
|
7
16
|
export async function init(args) {
|
|
8
17
|
const adopt = args.includes("--adopt");
|
|
9
18
|
const force = args.includes("--force");
|
|
@@ -27,21 +36,64 @@ export async function resolveInitHarnessRoot(projectRoot, args) {
|
|
|
27
36
|
if (current.source !== "default") {
|
|
28
37
|
return undefined;
|
|
29
38
|
}
|
|
30
|
-
return
|
|
39
|
+
return promptAgentHarnessRoot();
|
|
31
40
|
}
|
|
32
|
-
async function
|
|
41
|
+
async function promptAgentHarnessRoot() {
|
|
33
42
|
if (!input.isTTY || !output.isTTY) {
|
|
34
|
-
return
|
|
43
|
+
return resolveAgentHarnessFolderName("");
|
|
35
44
|
}
|
|
36
45
|
const rl = readline.createInterface({ input, output });
|
|
37
46
|
try {
|
|
38
|
-
const
|
|
39
|
-
|
|
47
|
+
const agent = await questionUntilValid(rl, `${formatAgentChoices()}\nTarget agent (default 1 Codex): `);
|
|
48
|
+
const option = agentOptionForAnswer(agent);
|
|
49
|
+
if (option?.harnessFolderName) {
|
|
50
|
+
return normalizeHarnessFolderName(option.harnessFolderName);
|
|
51
|
+
}
|
|
52
|
+
const folder = await rl.question(`Harness folder name (default ${DEFAULT_HARNESS_ROOT}; press Enter to use default): `);
|
|
53
|
+
return normalizeHarnessFolderName(folder.trim() || DEFAULT_HARNESS_ROOT);
|
|
40
54
|
}
|
|
41
55
|
finally {
|
|
42
56
|
rl.close();
|
|
43
57
|
}
|
|
44
58
|
}
|
|
59
|
+
async function questionUntilValid(rl, query) {
|
|
60
|
+
while (true) {
|
|
61
|
+
const answer = await rl.question(query);
|
|
62
|
+
if (agentOptionForAnswer(answer)) {
|
|
63
|
+
return answer;
|
|
64
|
+
}
|
|
65
|
+
console.log("Unknown agent choice. Enter a number, agent name, or Other.");
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function formatAgentChoices() {
|
|
69
|
+
const lines = ["Choose target agent:"];
|
|
70
|
+
AGENT_HARNESS_OPTIONS.forEach((option, index) => {
|
|
71
|
+
const folder = option.harnessFolderName ? ` -> ${option.harnessFolderName}` : "";
|
|
72
|
+
lines.push(`${index + 1}) ${option.label}${folder}`);
|
|
73
|
+
});
|
|
74
|
+
return lines.join("\n");
|
|
75
|
+
}
|
|
76
|
+
export function resolveAgentHarnessFolderName(agentAnswer, customFolderAnswer = "") {
|
|
77
|
+
const option = agentOptionForAnswer(agentAnswer);
|
|
78
|
+
if (!option) {
|
|
79
|
+
throw new Error("Unknown agent choice");
|
|
80
|
+
}
|
|
81
|
+
return normalizeHarnessFolderName(option.harnessFolderName ?? (customFolderAnswer.trim() || DEFAULT_HARNESS_ROOT));
|
|
82
|
+
}
|
|
83
|
+
function agentOptionForAnswer(answer) {
|
|
84
|
+
const normalized = answer.trim().toLowerCase();
|
|
85
|
+
if (!normalized) {
|
|
86
|
+
return AGENT_HARNESS_OPTIONS[0];
|
|
87
|
+
}
|
|
88
|
+
const index = Number.parseInt(normalized, 10);
|
|
89
|
+
if (Number.isInteger(index) && String(index) === normalized) {
|
|
90
|
+
return AGENT_HARNESS_OPTIONS[index - 1];
|
|
91
|
+
}
|
|
92
|
+
return AGENT_HARNESS_OPTIONS.find((option) => {
|
|
93
|
+
const label = option.label.toLowerCase();
|
|
94
|
+
return option.key === normalized || label === normalized || label.replace(/\s+/g, "-") === normalized;
|
|
95
|
+
});
|
|
96
|
+
}
|
|
45
97
|
function valueForArg(args, name) {
|
|
46
98
|
const prefix = `${name}=`;
|
|
47
99
|
const inline = args.find((arg) => arg.startsWith(prefix));
|
package/dist/lib/migrations.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import { rename, rm } from "node:fs/promises";
|
|
2
|
+
import { readdir, rename, rm } from "node:fs/promises";
|
|
3
3
|
import { readConfig } from "./config.js";
|
|
4
|
-
import { ensureDir, pathExists, readText, writeTextIfChanged } from "./fs.js";
|
|
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";
|
|
7
7
|
export const CURRENT_SCHEMA_VERSION = "1";
|
|
@@ -49,12 +49,15 @@ async function migrateConfig(projectRoot, root, report) {
|
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
async function migrateLegacyManagedPaths(projectRoot, root, report) {
|
|
52
|
+
await movePromptTreeToSkillsIfDestinationMissing(projectRoot, harnessPath(root, "prompts"), harnessPath(root, "skills"), report);
|
|
53
|
+
await moveIfDestinationMissing(projectRoot, ".agents/skills", harnessPath(root, "skills"), report);
|
|
54
|
+
await moveIfDestinationMissing(projectRoot, ".harness/agents/skills", harnessPath(root, "skills"), report);
|
|
52
55
|
await moveIfDestinationMissing(projectRoot, harnessPath(root, "managed"), harnessPath(root, "pjsdlc_managed"), report);
|
|
53
56
|
await moveIfDestinationMissing(projectRoot, harnessPath(root, "templates"), harnessPath(root, "pjsdlc_managed", "templates"), report);
|
|
54
57
|
await moveIfDestinationMissing(projectRoot, harnessPath(root, "policies"), harnessPath(root, "pjsdlc_managed", "policies"), report);
|
|
55
58
|
await moveIfDestinationMissing(projectRoot, harnessPath(root, "make"), harnessPath(root, "pjsdlc_managed", "make"), report);
|
|
56
59
|
}
|
|
57
|
-
async function
|
|
60
|
+
async function movePromptTreeToSkillsIfDestinationMissing(projectRoot, legacyRelativePath, nextRelativePath, report) {
|
|
58
61
|
const legacyPath = path.join(projectRoot, legacyRelativePath);
|
|
59
62
|
if (!(await pathExists(legacyPath))) {
|
|
60
63
|
report.skipped.push(legacyRelativePath);
|
|
@@ -67,6 +70,37 @@ async function moveIfDestinationMissing(projectRoot, legacyRelativePath, nextRel
|
|
|
67
70
|
}
|
|
68
71
|
await ensureDir(path.dirname(nextPath));
|
|
69
72
|
await rename(legacyPath, nextPath);
|
|
73
|
+
const workflowPath = path.join(nextPath, "workflow");
|
|
74
|
+
if (await pathExists(workflowPath)) {
|
|
75
|
+
for (const entry of await readdir(workflowPath)) {
|
|
76
|
+
await rename(path.join(workflowPath, entry), path.join(nextPath, entry));
|
|
77
|
+
}
|
|
78
|
+
await rm(workflowPath, { recursive: true, force: true });
|
|
79
|
+
}
|
|
80
|
+
for (const file of await listFiles(nextPath)) {
|
|
81
|
+
if (path.basename(file) === "PROMPT.md") {
|
|
82
|
+
await rename(file, path.join(path.dirname(file), "SKILL.md"));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
report.changed.push(`${legacyRelativePath} -> ${nextRelativePath}`);
|
|
86
|
+
}
|
|
87
|
+
async function moveIfDestinationMissing(projectRoot, legacyRelativePath, nextRelativePath, report) {
|
|
88
|
+
const legacyPath = path.join(projectRoot, legacyRelativePath);
|
|
89
|
+
const nextPath = path.join(projectRoot, nextRelativePath);
|
|
90
|
+
if (legacyPath === nextPath) {
|
|
91
|
+
report.skipped.push(legacyRelativePath);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (!(await pathExists(legacyPath))) {
|
|
95
|
+
report.skipped.push(legacyRelativePath);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (await pathExists(nextPath)) {
|
|
99
|
+
report.skipped.push(`${legacyRelativePath} -> ${nextRelativePath}`);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
await ensureDir(path.dirname(nextPath));
|
|
103
|
+
await rename(legacyPath, nextPath);
|
|
70
104
|
report.changed.push(`${legacyRelativePath} -> ${nextRelativePath}`);
|
|
71
105
|
}
|
|
72
106
|
function migrateLocalOverride(item, root) {
|
|
@@ -87,7 +121,10 @@ function migrateManagedFiles(managedFiles, root) {
|
|
|
87
121
|
migrated.push(item);
|
|
88
122
|
};
|
|
89
123
|
for (const item of managedFiles) {
|
|
90
|
-
if (item.path === ".agents/skills" ||
|
|
124
|
+
if (item.path === ".agents/skills" ||
|
|
125
|
+
item.path === ".harness/agents/skills" ||
|
|
126
|
+
item.path === harnessPath(root, "skills") ||
|
|
127
|
+
item.path === harnessPath(root, "prompts")) {
|
|
91
128
|
push({ path: harnessPath(root, "skills"), strategy: "managed" });
|
|
92
129
|
continue;
|
|
93
130
|
}
|
|
@@ -126,11 +163,19 @@ async function migrateLifecycle(projectRoot, root, report) {
|
|
|
126
163
|
}
|
|
127
164
|
const data = (parseYaml(await readText(lifecyclePath)) ?? {});
|
|
128
165
|
let changed = false;
|
|
129
|
-
const activeSkill = String(data.active_skill ?? "");
|
|
166
|
+
const activeSkill = String(data.active_skill ?? data.active_prompt ?? "");
|
|
130
167
|
if (SKILL_RENAMES[activeSkill]) {
|
|
131
168
|
data.active_skill = SKILL_RENAMES[activeSkill];
|
|
132
169
|
changed = true;
|
|
133
170
|
}
|
|
171
|
+
else if (activeSkill && !data.active_skill) {
|
|
172
|
+
data.active_skill = activeSkill;
|
|
173
|
+
changed = true;
|
|
174
|
+
}
|
|
175
|
+
if ("active_prompt" in data) {
|
|
176
|
+
delete data.active_prompt;
|
|
177
|
+
changed = true;
|
|
178
|
+
}
|
|
134
179
|
if ("history" in data) {
|
|
135
180
|
delete data.history;
|
|
136
181
|
changed = true;
|
|
@@ -81,7 +81,11 @@ async function renderMapping(projectRoot, mapping) {
|
|
|
81
81
|
if (path.basename(file) === ".gitkeep") {
|
|
82
82
|
continue;
|
|
83
83
|
}
|
|
84
|
-
|
|
84
|
+
const relative = path.relative(source, file);
|
|
85
|
+
if (isExcluded(relative, mapping.exclude ?? [])) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
rendered.push({ relative, content: await readText(file) });
|
|
85
89
|
}
|
|
86
90
|
return rendered;
|
|
87
91
|
}
|
|
@@ -101,6 +105,17 @@ async function renderMapping(projectRoot, mapping) {
|
|
|
101
105
|
}
|
|
102
106
|
throw new Error(`Unsupported source mapping mode: ${mapping.mode}`);
|
|
103
107
|
}
|
|
108
|
+
function isExcluded(relativePath, patterns) {
|
|
109
|
+
const normalized = relativePath.split(path.sep).join("/");
|
|
110
|
+
return patterns.some((pattern) => {
|
|
111
|
+
const normalizedPattern = pattern.replace(/\\/g, "/");
|
|
112
|
+
if (normalizedPattern.endsWith("/**")) {
|
|
113
|
+
const prefix = normalizedPattern.slice(0, -3);
|
|
114
|
+
return normalized === prefix || normalized.startsWith(`${prefix}/`);
|
|
115
|
+
}
|
|
116
|
+
return normalized === normalizedPattern;
|
|
117
|
+
});
|
|
118
|
+
}
|
|
104
119
|
function normalize(content) {
|
|
105
120
|
return content.replace(/\r\n/g, "\n").trimEnd();
|
|
106
121
|
}
|
package/dist/lib/sync-engine.js
CHANGED
|
@@ -30,12 +30,14 @@ async function syncManagedFile(projectRoot, root, managedFile, report) {
|
|
|
30
30
|
await syncMakefileInclude(destination, root, report);
|
|
31
31
|
return;
|
|
32
32
|
}
|
|
33
|
-
if (managedFile.path === path.join(root, "skills")
|
|
33
|
+
if (managedFile.path === path.join(root, "skills")) {
|
|
34
34
|
await syncTree(packageAssetPath("skills"), destination, report);
|
|
35
35
|
return;
|
|
36
36
|
}
|
|
37
|
-
if (managedFile.path ===
|
|
38
|
-
|
|
37
|
+
if (managedFile.path === path.join(root, "skills") ||
|
|
38
|
+
managedFile.path === ".harness/agents/skills" ||
|
|
39
|
+
(managedFile.path === ".agents/skills" && root !== ".agents")) {
|
|
40
|
+
await syncTree(packageAssetPath("skills"), path.join(projectRoot, root, "skills"), report);
|
|
39
41
|
return;
|
|
40
42
|
}
|
|
41
43
|
if (managedFile.path === path.join(root, "pjsdlc_managed", "templates")) {
|
|
@@ -88,7 +90,7 @@ async function syncAgentsBlock(destination, root, report) {
|
|
|
88
90
|
}
|
|
89
91
|
}
|
|
90
92
|
function renderAgentsCore(content, root) {
|
|
91
|
-
return content.replaceAll(".agent", root);
|
|
93
|
+
return content.replaceAll(".agent", root).replaceAll(".codex", root);
|
|
92
94
|
}
|
|
93
95
|
async function syncMakefileInclude(destination, root, report) {
|
|
94
96
|
const existing = (await pathExists(destination)) ? await readText(destination) : "";
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ export interface ManagedFile {
|
|
|
15
15
|
export interface SourceMapping {
|
|
16
16
|
source: string;
|
|
17
17
|
target: string;
|
|
18
|
+
exclude?: string[];
|
|
18
19
|
mode: "extract-managed-block" | "copy-tree" | "copy-file" | "extract-harness-targets";
|
|
19
20
|
}
|
|
20
21
|
export interface SourceMappingsFile {
|
package/package.json
CHANGED
package/source-mappings.yaml
CHANGED
|
@@ -2,16 +2,18 @@ source_mappings:
|
|
|
2
2
|
- source: "AGENTS.md"
|
|
3
3
|
target: "packages/sdlc-harness/assets/agents/AGENTS_CORE.md"
|
|
4
4
|
mode: "extract-managed-block"
|
|
5
|
-
- source: ".
|
|
5
|
+
- source: ".codex/skills"
|
|
6
6
|
target: "packages/sdlc-harness/assets/skills"
|
|
7
7
|
mode: "copy-tree"
|
|
8
|
-
|
|
8
|
+
exclude:
|
|
9
|
+
- "authoring/**"
|
|
10
|
+
- source: ".codex/pjsdlc_managed/templates"
|
|
9
11
|
target: "packages/sdlc-harness/assets/templates"
|
|
10
12
|
mode: "copy-tree"
|
|
11
|
-
- source: ".
|
|
13
|
+
- source: ".codex/pjsdlc_managed/policies"
|
|
12
14
|
target: "packages/sdlc-harness/assets/policies"
|
|
13
15
|
mode: "copy-tree"
|
|
14
|
-
- source: ".
|
|
16
|
+
- source: ".codex/pjsdlc_managed/make/sdlc-harness.mk"
|
|
15
17
|
target: "packages/sdlc-harness/assets/make/sdlc-harness.mk"
|
|
16
18
|
mode: "copy-file"
|
|
17
19
|
- source: ".github/workflows/harness.yml"
|