@fooksky/lodestar 0.0.0-stage → 0.2.0
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/LICENSE +21 -0
- package/README.md +104 -2
- package/cordis.patch.yml +6 -0
- package/docs/branch-management.md +60 -0
- package/docs/design.md +342 -0
- package/docs/proposal.md +129 -0
- package/docs/research/agent-presets.md +99 -0
- package/docs/research/development-mode.md +98 -0
- package/docs/research/labwarden-session-cost.md +91 -0
- package/docs/research/workflow-survey.md +136 -0
- package/icon.svg +1 -0
- package/index.js +31 -0
- package/locale/en.json +4 -0
- package/locale/zh.json +4 -0
- package/package.json +45 -4
- package/skills/dev-workflow/SKILL.md +35 -0
- package/skills/dev-workflow/references/delegation.md +23 -0
- package/skills/dev-workflow/references/execute.md +13 -0
- package/skills/dev-workflow/references/intent.md +9 -0
- package/skills/dev-workflow/references/plan.md +11 -0
- package/skills/dev-workflow/references/recover.md +13 -0
- package/skills/dev-workflow/references/spec.md +9 -0
- package/skills/dev-workflow/references/workspace.md +33 -0
- package/src/preset.js +37 -0
- package/src/schema.js +134 -0
- package/src/state.js +148 -0
- package/src/store.js +111 -0
- package/src/tool.js +52 -0
- package/src/workspace.js +93 -0
- package/tests/scenarios.md +60 -0
package/package.json
CHANGED
|
@@ -1,6 +1,47 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fooksky/lodestar",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
}
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"repository": { "type": "git", "url": "git+https://github.com/urzz/lodestar.git" },
|
|
6
|
+
"publishConfig": { "access": "public", "registry": "https://registry.npmjs.org/", "provenance": false },
|
|
7
|
+
"keywords": ["dsh", "cordis", "agent", "development", "workflow", "worktree"],
|
|
8
|
+
"type": "module",
|
|
9
|
+
"description": "Native DSH development workflow skill and structured change state",
|
|
10
|
+
"engines": { "node": ">=22", "dsh": ">=0.2.2-rc.1 <0.3.0" },
|
|
11
|
+
"exports": {
|
|
12
|
+
".": "./index.js",
|
|
13
|
+
"./preset": "./src/preset.js",
|
|
14
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
15
|
+
"./package.json": "./package.json",
|
|
16
|
+
"./locale/*.json": "./locale/*.json"
|
|
17
|
+
},
|
|
18
|
+
"files": ["index.js", "src", "cordis.patch.yml", "skills", "locale", "icon.svg", "README.md", "docs/design.md", "docs/branch-management.md", "docs/proposal.md", "docs/research", "tests/scenarios.md", "LICENSE"],
|
|
19
|
+
"scripts": { "test": "node --test tests/*.test.js", "check": "node --check index.js && node --check src/state.js && node --check src/store.js && node --check src/tool.js && node --check src/schema.js && node --check src/workspace.js && node --check src/preset.js", "prepack": "npm run check && npm test" },
|
|
20
|
+
"icon": "./icon.svg",
|
|
21
|
+
"dsh": { "bundle": { "patch": "./cordis.patch.yml" } },
|
|
22
|
+
"peerDependencies": {
|
|
23
|
+
"@deepseek-ai/cordis": "~4.1.0",
|
|
24
|
+
"@deepseek-ai/dsh-skill": "0.2.2-rc.1",
|
|
25
|
+
"@deepseek-ai/dsh-tools": "0.2.2-rc.1",
|
|
26
|
+
"@deepseek-ai/dsh-fs": "0.2.2-rc.1",
|
|
27
|
+
"@deepseek-ai/dsh-working-directory": "0.2.2-rc.1",
|
|
28
|
+
"@deepseek-ai/dsh-sandbox-policy": "0.2.2-rc.1",
|
|
29
|
+
"@deepseek-ai/dsh-subprocess": "0.2.2-rc.1",
|
|
30
|
+
"@deepseek-ai/dsh-sandbox": "0.2.2-rc.1",
|
|
31
|
+
"@deepseek-ai/dsh-agent-preset-registry": "0.2.2-rc.1",
|
|
32
|
+
"@deepseek-ai/dsh-system-prompt": "0.2.2-rc.1"
|
|
33
|
+
},
|
|
34
|
+
"peerDependenciesMeta": {
|
|
35
|
+
"@deepseek-ai/cordis": { "optional": true },
|
|
36
|
+
"@deepseek-ai/dsh-skill": { "optional": true },
|
|
37
|
+
"@deepseek-ai/dsh-tools": { "optional": true },
|
|
38
|
+
"@deepseek-ai/dsh-fs": { "optional": true },
|
|
39
|
+
"@deepseek-ai/dsh-working-directory": { "optional": true },
|
|
40
|
+
"@deepseek-ai/dsh-sandbox-policy": { "optional": true },
|
|
41
|
+
"@deepseek-ai/dsh-subprocess": { "optional": true },
|
|
42
|
+
"@deepseek-ai/dsh-sandbox": { "optional": true },
|
|
43
|
+
"@deepseek-ai/dsh-agent-preset-registry": { "optional": true },
|
|
44
|
+
"@deepseek-ai/dsh-system-prompt": { "optional": true }
|
|
45
|
+
},
|
|
46
|
+
"dependencies": { "zod": "^4.6.7" }
|
|
47
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# 开发工作流
|
|
2
|
+
|
|
3
|
+
用于开发变更和恢复既有 change。主线:确认真实意图 → 审核具体方案并获得实施授权 → 生成 plan → 原生执行与验证 → 交付。用户当前明确要求优先;调查、解释或仅设计请求不自动升级为实施。
|
|
4
|
+
|
|
5
|
+
受委派成员遵循自己的任务包,向 Lead 返回结果,不重新启动此主流程、不修改主材料或 change 状态。以下流程由 Lead 单 owner 执行。
|
|
6
|
+
|
|
7
|
+
## 开始与材料
|
|
8
|
+
|
|
9
|
+
先确定用户范围,由用户或外部助手准备隔离 worktree,再读取 references/workspace.md 并调用 workspace 核对实际 projectRoot、分支与绑定。默认一个独立需求对应一个 change、一条分支、一个 worktree 和一个 Lead;插件不负责跨需求调度或创建/切换分支。每次 dev_workflow 调用显式传本 worktree 的 projectRoot,不维护全局当前 change。恢复时读 references/recover.md;新工作按需读 references/intent.md。没有状态工具时明确报告能力缺失,不手工伪造 state.json。
|
|
10
|
+
|
|
11
|
+
新 change 先调用 create 创建 schemaVersion: 2 状态,workspace 必填并自动绑定当前命名分支或普通目录,再按阶段写正文并登记;create 拒绝已存在目录,不自动接管或覆盖。材料位于本 worktree 的 .dev-docs/changes/<change-id>/,ID 用 YYYYMMDD-short-slug,冲突才加短后缀。只复用本 worktree 的 .dev-docs/index.md、knowledge/ 和已有项目文档,不读取其他 checkout 的实时材料或共享材料根。intent.md 在澄清时创建,spec.md 在设计时创建,plan.md 在实施授权后创建;不预建空模板树。正文可短,Markdown 是语义权威,state 是状态与关联权威,DSH 是执行事实权威。
|
|
12
|
+
|
|
13
|
+
通过宿主文件工具先写正文,再用 dev_workflow update/register-document 登记;固定正文 SHA-256 仅用于检测变化,不证明语义一致。不得用裸 fs 绕过宿主权限、手改 state、自动覆盖损坏记录或把未经核实的材料当成用户确认。
|
|
14
|
+
|
|
15
|
+
## 授权与阶段
|
|
16
|
+
|
|
17
|
+
澄清时可调查,不写业务实现、脚手架或原型。需求确认后进 spec;具体方案审核且实施授权后进 plan;plan 及验收/步骤关联有效后进 execute。只要求设计时交付 spec 并停止,不创建实施 plan 或启动编码 goal。
|
|
18
|
+
|
|
19
|
+
真实用户表达可同时确认需求、审核方案并授权实施,在一次 update 登记多条决定;已授权范围继续推进,不机械重复询问。“设计没问题,先别做”只表示审核。沉默、跳过、工具审批或 goal 续轮不是用户决定。“继续”解除暂停,不补齐缺失的需求/方案决定。source 保存短摘录和可得 session/event 引用;没有事件定位时标为 Agent 记录并说明上下文,不伪造事件号。
|
|
20
|
+
|
|
21
|
+
读当前阶段对应参考:references/spec.md、references/plan.md、references/execute.md。主材料语义升版必须在同次 update 退回相应阶段,或登记已有明确新授权后通过最终约束;排版修订提供理由、不升语义版本。局部修复与分工变化不改变需求/方案时不重新审批。独立新需求另建 change,用户明确纳入才合并。
|
|
22
|
+
|
|
23
|
+
## 工具使用
|
|
24
|
+
|
|
25
|
+
工具只有 dev_workflow,action 为 workspace/list/get/create/update。workspace 只读观察当前工作区;get/list 返回实时工作区与绑定状态,list 默认分页 20,get 可按 section 读取且不附正文。create 不覆盖;update 携 expectedRevision 和最多 32 个有类型 operations。领域操作是 bind-workspace、register-document、record-decision、set-acceptance、bind-step、set-step-progress、record-verification、invalidate-verification、set-blocker、set-workflow、close。具体字段以工具 schema 为准,不用 JSON Patch。
|
|
26
|
+
|
|
27
|
+
bindingStatus 仅为 matched、mismatch 或 detached。mismatch 先核对工作区和接管依据;分支改名、明确接管或普通目录初始化为仓库后,单独 bind-workspace 并提供 reason,不能夹带业务变更,workspaceHistory 每项的 previous 必填。未知或旧 schema、缺失 workspace 均拒绝,不能用重绑修复。list 不特殊识别未受管目录,缺失或非法 state 显示错误。detached 只读;unborn 命名分支允许 baseCommit 为 null。Git projectRoot 必须与 Agent cwd 属于同一 checkout,普通目录根须位于 cwd 内。写正文前也遵守这些边界,状态绑定不能限制其他文件工具;运行期间不外部切分支或清理工作区。详细规则见 references/workspace.md。
|
|
28
|
+
|
|
29
|
+
冲突后 get,做必要语义合并;回执不明先按稳定记录 ID 和内容核对,不盲重试。正文变化先登记;权限拒绝保留并报告,不换路径绕过。未知 schema 或损坏记录停止写入并报告。不要自动删除、搬移历史、暂存、提交、推送、发布或安装 profile。
|
|
30
|
+
|
|
31
|
+
## 完成
|
|
32
|
+
|
|
33
|
+
验收编号 A1、A2…稳定且不复用给新含义。所有 required 验收有当前有效 passed、无 open blocker,且真实需求满足时才交付;close 要写交付摘要、需求满足依据和知识处理结论。缺环境只能报告“已实现,验证未完成”。已满足需求、规格和必需验收即收敛,不追加无关优化。
|
|
34
|
+
|
|
35
|
+
委派按需读取 references/delegation.md。浏览器与记忆不是必需依赖,但不能以 fixture 或截图生成冒充真实交互验收。长期知识只写稳定事实,可明确“无稳定知识变化”。完成后保留原 change 路径,不自动归档。
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# 按需委派
|
|
2
|
+
|
|
3
|
+
Lead 持有当前 change 的真实需求、授权判定、主材料与状态。独立需求默认独立分支/worktree/Lead,由外部助手准备和分配;插件不承担跨需求调度。同一需求内部按需使用原生 Teams,不固定成员数或评审轮数。角色是职责说明,不是权限隔离;不新建角色 preset、不假设 Team spawn 支持逐成员 preset/model。
|
|
4
|
+
|
|
5
|
+
任务包必须自足:角色与目标、本 worktree 的 projectRoot/changeId、预期分支与工作目录、原生任务 ID、A/P 编号与材料版本、资料路径和关键摘要、允许写范围、禁止触碰的主材料、依赖及并行 owner、验证要求、停止条件、输出格式。fresh 成员不能只收到“按上文”。成员先核对工作目录和预期分支,发现漂移报 Lead;不自行切分支、重绑 change 或读取其他 checkout 的实时材料。跨需求依赖由 Lead 协调明确的 Git 集成,不让成员绕过隔离共享知识根。
|
|
6
|
+
|
|
7
|
+
## implementer
|
|
8
|
+
|
|
9
|
+
完成批准范围内的有界实现和相应检查。返回完成情况、修改路径、相关验收、实际测试结果、未验证项和阻断。方案不成立或需扩大范围时停止受影响工作并报 Lead;不登记用户授权、不改主状态。
|
|
10
|
+
|
|
11
|
+
## reviewer
|
|
12
|
+
|
|
13
|
+
针对具体差异/问题做独立审查,默认不改文件。只报告实质问题:位置、触发条件、影响、依据、必要修正方向及未覆盖边界;没有问题直接说。低风险局部修改不强制独立审查,不为了凑数追加风格建议。
|
|
14
|
+
|
|
15
|
+
## verifier
|
|
16
|
+
|
|
17
|
+
依据验收行为、对象/环境和现有证据实际检查。默认不改产品代码。返回每项观察、方法、产物身份、环境、证据、未覆盖项与复现。缺环境报告无法验证,不生成假证据;最终验收由 Lead 登记。
|
|
18
|
+
|
|
19
|
+
## 原生协作
|
|
20
|
+
|
|
21
|
+
成员按 list → get → claim(当前 revision)→ 工作 → complete 更新任务。依赖就绪不会自动唤醒成员,Lead 显式 send_message。inactive 不等于 completed;任务完成不等于需求满足。write_scopes 仅提示,重叠路径指定唯一 owner 或串行交接。
|
|
22
|
+
|
|
23
|
+
一次性独立工作用实际可用的 subagent,持续协作用 Teams,需要历史才 fork。角色提示不改变宿主权限;模型继续用宿主配置。小任务 Lead 直接完成,无需预建固定组织。必要成员未返回前不宣布集成交付。
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# 执行与交付
|
|
2
|
+
|
|
3
|
+
使用原生工具在已绑定的当前 worktree 执行计划,先按 workspace.md 核对工作区;mismatch 或 detached 先处理相应缺口,不先写正文再等状态拒绝。运行期间不外部切分支、搬移目录或清理 worktree。任务 complete 不是业务验收 passed;inactive 不是任务完成。成员给证据,Lead 登记主状态并判断真实需求满足。
|
|
4
|
+
|
|
5
|
+
验证记录引用 A 编号、当前 specVersion、method、subject、environment、evidenceRefs 和观察摘要。未提交代码不能只用 HEAD 定位,远端页面不能只给 URL;保存可辨认产物及运行环境。passed 必须有依据;not-run/failed 如实记录。not-applicable 不满足 required 项,取消必需项须修订已审核方案索引。
|
|
6
|
+
|
|
7
|
+
需求、相关代码、方法或环境改变时仅将受影响结果标 stale;新 spec 下显式 carriedFrom 沿用仍成立的证据并解释依据。无关文档变化、compact、任务重排不自动全量重验。回执未知先查 job/日志;确实缺证据再补验。连续两次同类失败无新证据则改变诊断方法,不降低标准。
|
|
8
|
+
|
|
9
|
+
视觉检查分有界小包及时记结论;要求全量覆盖时不能静默抽样。缺浏览器等必需环境就报告具体阻断。测试通过但真实需求不足仍不得 close。
|
|
10
|
+
|
|
11
|
+
进入 deliver 需 required 当前有效 passed 且无 open blocker;close 再记录交付说明、需求满足依据、必需验收关联、知识处理结论。稳定知识只更新本 worktree 的现有主题并引用已有 README/架构,不读取其他 checkout 的实时知识、不保存调试流水账。可写“无稳定知识变化”。达到范围即收敛;未完成明确差距,不用“已实现”冒充全部验收通过。
|
|
12
|
+
|
|
13
|
+
PR/MR 地址、源/目标分支、评审与合入结果及证据写入既有 plan/交付材料;若合入是必需交付条件,close 前完成合入与集成验证;若边界是提交 PR,不声称已经合入。PR 反馈按原有阶段退回、版本和局部证据失效规则处理。需要保留的代码、知识与 change 材料随分支提交并经 PR/MR 集成,同一知识文件或 state 冲突需核对语义,文本合并成功不能代替验收。提交及 worktree 清理按明确授权执行,清理前保留未提交材料与证据;数据库、端口、账号及外部资源仍须单独隔离。
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# 意图
|
|
2
|
+
|
|
3
|
+
开发需求先按 workspace.md 核对外部已准备的 worktree 和当前工作区,再 create 绑定该 change;普通解释或调查可以不建 change。一个独立需求默认有独立 change、分支、worktree 和 Lead,不把跨需求分配交给插件。已有材料需搬移时先明确唯一权威,不能复制后双写。
|
|
4
|
+
|
|
5
|
+
保存用户原始诉求或准确引用、真实需求、场景、目标、范围/非目标、约束和待明确问题。不要把猜测写成确认事实。澄清前可以读项目、调查与比较;不实现业务、生成脚手架或原型。
|
|
6
|
+
|
|
7
|
+
只为影响需求、方案或授权的缺口提问。先检查现有表达:范围明确且用户已批准时直接复用,不为了固定轮次重问。登记 intent 后,用 record-decision 引用当前语义版本和真实用户来源;同一表达可以同时覆盖后续已明确的方案与实施授权。
|
|
8
|
+
|
|
9
|
+
恢复已有 change 时读 recover.md,核对当前有效的状态、材料、授权来源与剩余事项。
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# 计划
|
|
2
|
+
|
|
3
|
+
只有当前需求确认、方案审核和实施授权有效时创建 plan。plan basedOn 当前 intent/spec,记录 P 编号步骤、A 编号关联、依赖、写入范围、验证方法及取舍。步骤规模应可实际交付;新项目按可运行功能切片推进,不堆积空骨架。
|
|
4
|
+
|
|
5
|
+
plan 写明当前 worktree 的 projectRoot、changeId、预期分支和工作目录;代码、知识、主材料和需保留的证据留在本 worktree,不共享材料根。跨需求依赖通过明确 Git 集成引入。PR/MR 地址、源/目标分支、待评审/待合入结果及证据沿用 plan/交付材料记录,不另建远端调度状态。明确交付边界是提交 PR 还是完成合入;要求合入时把合入和集成验证列为验收条件。
|
|
6
|
+
|
|
7
|
+
每步明确 direct 或 native。direct 进度通过 set-step-progress 维护;native 绑定原生任务来源,状态仅查询原生任务板,不在 state 保存第二份 status,也不手工重复勾选 plan。
|
|
8
|
+
|
|
9
|
+
小任务 Lead 直接做;有界独立调查可用可用的 subagent;并行模块及持续协作用 Teams。读 delegation.md,明确 owner、写入范围和依赖。write_scopes 只是提示,不是锁。主材料由 Lead 单独拥有。
|
|
10
|
+
|
|
11
|
+
按风险选择静态检查、单测、真实 API/CLI、构建或浏览器检查。替代方法必须证明同一命题,不能用编译代替集成、用 fixture 代替交互。计划不自动触发提交、推送、发布、安装或提高权限。
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# 恢复
|
|
2
|
+
|
|
3
|
+
读取顺序:显式 change/确定候选 → 按 workspace.md 调用 workspace 并 get 核对实时工作区、已存绑定与正文一致性 → 当前 intent/spec 和 plan 相关段 → 必要源代码/产物 → 实际原生任务与成员 → 缺口对应的历史事件。多候选先列标题与状态,无法确定才询问,不全读旧对话和知识库。
|
|
4
|
+
|
|
5
|
+
恢复只接受 schemaVersion: 2 且 workspace 完整的状态;未知或旧 schema、缺失绑定均报告错误,不用 bind-workspace 修复。mismatch 先核对是否进错 worktree,不能以相同 HEAD 猜归属;分支重命名、已有明确接管依据或普通目录初始化为仓库后才显式重绑,workspaceHistory 每项保留必填 previous、原因和时间。detached 只读,unborn 命名分支可写。移动/重新检出到其他目录不因绝对路径变化要求重绑,普通提交/rebase 不因 HEAD 改变误锁。合入后的历史默认只读,不批量改归属。恢复只使用本 worktree 的知识和材料。
|
|
6
|
+
|
|
7
|
+
来源清晰、范围匹配且未撤回的当前版本确认直接复用。用户“继续”可恢复暂停,但不填补从未获得的授权;来源模糊才定向查历史。没有事件定位时标明 Agent 记录,不编造引用。
|
|
8
|
+
|
|
9
|
+
正文已改、登记未完成:核对差异并登记,不回滚用户文件以迎合状态。状态已写、回执丢失:get 核对具体记录 ID 与内容,再决定下一步;CAS 冲突重新读取并语义合并,不盲目重发。损坏/未知 schema 停止写入并报告,不初始化空状态。
|
|
10
|
+
|
|
11
|
+
同会话查询仍在写的成员和原生任务。跨会话旧 Team 不可达时核对成果,仅为剩余部分建新任务,通过 bind-step 为同一 P 编号重新绑定时提供 reason,previousBindings 保留旧引用和替代关系;旧成员写入归属不明时先解决,不能重复抢写相同路径。
|
|
12
|
+
|
|
13
|
+
读取状态不会触发全量重验。证据仍有效则沿用;必需证据丢失才补验。done 后独立新需求另建 change;原交付问题可记录原因退回对应阶段。暂停、等待、阻断保留阶段,不靠切模式或重建 change 绕过缺口。
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# 方案
|
|
2
|
+
|
|
3
|
+
spec 引用已确认 intent,说明预期行为、影响范围、职责/数据流、必要接口或数据变化、关键取舍和验收要求。细节应足以实施,篇幅随规模,不复制背景全文。
|
|
4
|
+
|
|
5
|
+
验收使用稳定 A 编号;删除的编号不复用。覆盖真实用户需求,不能只证明代码符合自己的方案。明确 required 项、实际被测对象与环境;确无适用验收时,在已审核 spec 中说明并登记显式依据,不以空集合绕过验收。
|
|
6
|
+
|
|
7
|
+
正文写入后登记 register-document,spec basedOn 当前 intent;set-acceptance 显式更新当前版本索引。来源明确、范围匹配且未撤回的审核可复用;“先别做”不是实施授权。仅设计请求到此交付并停止。
|
|
8
|
+
|
|
9
|
+
语义改变时先暂停受影响成员,同次状态更新退回 spec 并使旧版本决定不再适用。用户已明确批准差异就引用该表达登记新版本决定,无需再问。排版变化有理由即可,不自动撤回确认;程序摘要不判断语义,Lead 负责分类。
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# 工作区与分支绑定
|
|
2
|
+
|
|
3
|
+
开始新需求或恢复前调用 `dev_workflow` 的 `workspace` action,显式传当前 worktree 内的 `projectRoot`。默认一个独立需求对应一个 change、一条分支、一个 worktree 和一个 Lead。外部助手或用户负责领取 Issue、跨需求分配、创建/选择 worktree 和启动会话;插件只维护当前分支内的流程与状态,不创建/切换/删除分支、不扫描其他工作区、不调度需求、不自动提交或调用 PR API。
|
|
4
|
+
|
|
5
|
+
代码、`.dev-docs/knowledge/`、change 正文、state 和需保留的证据全部位于本 worktree。新工作区继承基准提交的知识快照,之后只读写本地材料;不使用共享材料根、符号链接或主 checkout 的实时知识。材料以当前 worktree 为唯一权威,不能复制后两处继续写;插件不自动搬移或提交。
|
|
6
|
+
|
|
7
|
+
## 当前观察与读取
|
|
8
|
+
|
|
9
|
+
每个 action 都按调用 Agent 的实际 cwd 和显式 projectRoot 观察工作区。Git 项目的 cwd 和 projectRoot 必须属于同一 checkout,项目子目录可以使用;不能从需求 worktree 指向主 checkout。普通目录的 projectRoot 必须存在且位于 cwd 内。Git、宿主服务、权限、取消或仓库损坏错误保持可见,不能当成普通目录;bare 仓库拒绝使用。
|
|
10
|
+
|
|
11
|
+
每个 action 的顶层 workspace 直接返回当前观察(实际项目根、checkout 根、项目相对路径、分支及 HEAD),没有 current/status 嵌套。workspace action 不返回 bindingStatus;get 在顶层返回 bindingStatus 和已存 binding;list 有效行的 workspace 为已存绑定,并由该行 bindingStatus 显示归属状态。bindingStatus 仅为 matched、mismatch 或 detached。分支和 HEAD 来自当前 Git 事实,不用 state 猜测。detached HEAD 可读不可写;unborn 命名分支可写,HEAD 为 null。历史 change 合入目标分支后仍可 get/list,不自动改为目标分支归属。
|
|
12
|
+
|
|
13
|
+
## 创建与重绑
|
|
14
|
+
|
|
15
|
+
状态格式仅接受 schemaVersion: 2,workspace 必填。create 自动创建该状态并绑定当前命名分支或普通目录。Git 绑定为 `{ kind: 'git', branch, baseCommit, projectPath }`,普通目录为 `{ kind: 'directory' }`。baseCommit 是首次绑定或重绑时的 HEAD,可为 null,仅记录来源,不要求后续 HEAD 相等或满足祖先关系。state 不保存机器绝对路径、session owner 或可变当前 HEAD;changeId 保持需求身份,同名分支并不证明跨仓库身份或接管授权。
|
|
16
|
+
|
|
17
|
+
分支改名、明确接管或普通目录初始化为仓库后,核对重绑依据,单独 update,携当前 expectedRevision 和唯一操作:
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{"type":"bind-workspace","reason":"说明本次分支改名、接管或目录初始化为仓库后重绑的实际依据"}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
该操作只接受 reason,绑定由工具观察,不填写分支、路径或提交,也不能夹带业务操作。已有绑定被替换时,通过 CAS 更新,workspaceHistory 每项保留必填的 previous、reason 和 recordedAt。未知或旧 schema、缺失 workspace、损坏状态均报告错误,不能用 bind-workspace 修复。list 不特殊识别未受管目录;缺失或非法 state 的条目显示对应错误,不自动初始化。
|
|
24
|
+
|
|
25
|
+
branch、项目相对路径或工作区种类漂移时,常规 update 拒绝;先核对是否用错工作区。分支重命名、已有明确接管依据、普通目录初始化仓库等场景才按实际授权显式重绑;已有依据足够时不重复询问。相同绑定重试不增加 revision,正常提交/rebase 后重绑同一分支及项目路径也不刷新 baseCommit。不能靠相同 HEAD 推断归属,不能批量重绑已合入的历史记录。
|
|
26
|
+
|
|
27
|
+
绑定保护状态工具自身写入,不是 Git 锁、工作目录锁或 Lead 所有权锁,也不能限制其他文件工具。运行期间不得由外部切分支、搬移目录或清理 worktree;同一 checkout 不安排不同需求并发切换分支。单 change 的多 Lead 交接仍需明确所有权,CAS 不能替代交接。
|
|
28
|
+
|
|
29
|
+
## 集成与清理
|
|
30
|
+
|
|
31
|
+
计划和委派包写明本 worktree 的 projectRoot、changeId、预期分支和工作目录。跨需求依赖通过明确的 Git 集成引入。PR/MR 地址、源/目标分支、待评审/待合入结果与证据写入既有 plan/交付材料,状态机不管理远端生命周期。若合入是必需交付条件,close 前完成合入及集成验证;若边界是提交 PR,close 只陈述已提交的范围。反馈修订沿用原阶段退回、材料版本及证据失效规则。
|
|
32
|
+
|
|
33
|
+
需要保留的代码、知识与 change 材料随分支提交,通过 PR/MR 集成;同一知识文件和 state 的合并冲突必须核对语义,文本合并成功不等于验收通过。提交与清理仍需相应授权,worktree 清理前保留未提交材料和证据。数据库、端口、账号及外部资源须按需求隔离,Git worktree 只隔离文件。
|
package/src/preset.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export const name = 'dev-workflow-preset';
|
|
2
|
+
export const inject = ['tools', 'agentPresets', 'systemPrompt'];
|
|
3
|
+
|
|
4
|
+
// Reuse the deployment's native services and visible tools, without cloning a profile.
|
|
5
|
+
export const requiredTools = [
|
|
6
|
+
'dev_workflow', 'read', 'write', 'edit', 'glob', 'grep', 'bash',
|
|
7
|
+
'job_output', 'job_list', 'job_kill', 'skill', 'ask_user_question',
|
|
8
|
+
'session_search', 'session_event_search', 'session_event_read',
|
|
9
|
+
'team_task_list', 'team_task_get', 'team_task_update', 'team_task_create',
|
|
10
|
+
'spawn_teammate', 'send_message', 'list_agents',
|
|
11
|
+
];
|
|
12
|
+
export const modeText = '本会话采用开发工作流。开发变更先加载 dev-workflow,定位或创建 change,按当前阶段继续。只调查、解释或设计时遵守该范围;选择模式不构成实施、提交、发布或安装授权。受委派成员执行任务包并向 Lead 回报,不重复启动 Lead 需求确认流程。浏览器、记忆和普通 subagent 仅在部署提供且任务需要时使用。';
|
|
13
|
+
export const definition = {
|
|
14
|
+
id: 'dev-workflow', name: '开发工作流',
|
|
15
|
+
description: 'Confirmed intent, reviewed design and evidence-based development using native Teams.',
|
|
16
|
+
plugins: [{ id: 'dev-workflow-entry', name: '@fooksky/lodestar/preset', config: { entry: true } }],
|
|
17
|
+
};
|
|
18
|
+
export async function apply(ctx, config = {}) {
|
|
19
|
+
if (config.entry) {
|
|
20
|
+
if (!ctx.systemPrompt) throw new Error('dev-workflow mode requires systemPrompt');
|
|
21
|
+
const dispose = ctx.systemPrompt.section({
|
|
22
|
+
name: 'dev-workflow-entry', order: 650,
|
|
23
|
+
text({ scope }) {
|
|
24
|
+
const visible = new Set(ctx.tools.definitions(scope).map(tool => tool.name));
|
|
25
|
+
const missing = requiredTools.filter(name => !visible.has(name));
|
|
26
|
+
return modeText + (missing.length
|
|
27
|
+
? `\n当前作用域未提供以下完整开发模式能力:${missing.join(', ')}。Lead 首次开展开发时检查所需能力并报告配置缺口;受委派成员只按任务包判断实际需要的能力,不因缺少创建团队等 Lead 能力重启主流程。`
|
|
28
|
+
: '');
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
ctx.effect(() => dispose);
|
|
32
|
+
} else {
|
|
33
|
+
if (!ctx.agentPresets) throw new Error('dev-workflow preset requires agentPresets');
|
|
34
|
+
const dispose = await ctx.agentPresets.register(definition);
|
|
35
|
+
ctx.effect(() => dispose);
|
|
36
|
+
}
|
|
37
|
+
}
|
package/src/schema.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export class DomainError extends Error {
|
|
4
|
+
constructor(code, message) { super(message); this.name = 'DomainError'; this.code = code; }
|
|
5
|
+
}
|
|
6
|
+
export function parseState(raw) {
|
|
7
|
+
if (raw?.schemaVersion !== 2) throw new DomainError('UNSUPPORTED_SCHEMA', `Unsupported schemaVersion: ${raw?.schemaVersion}`);
|
|
8
|
+
const result = stateSchema.safeParse(raw);
|
|
9
|
+
if (!result.success) throw new DomainError('INVALID_STATE', result.error.message);
|
|
10
|
+
validateRelations(result.data);
|
|
11
|
+
return result.data;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function validateRelations(s, code = 'INVALID_STATE') {
|
|
15
|
+
const check = (ok, message) => { if (!ok) throw new DomainError(code, message); };
|
|
16
|
+
const unique = (rows, key) => check(new Set(rows.map(r => r[key])).size === rows.length, `Duplicate ${key}`);
|
|
17
|
+
unique(s.decisions, 'id'); unique(s.verification, 'id'); unique(s.execution, 'stepId'); unique(s.blockers, 'id');
|
|
18
|
+
const current = refs => Object.entries(refs).every(([k,v]) => s.documents[k]?.version === v);
|
|
19
|
+
const historical = refs => Object.entries(refs).every(([k,v]) => s.documents[k] && v <= s.documents[k].version);
|
|
20
|
+
for (const [kind, doc] of Object.entries(s.documents)) {
|
|
21
|
+
const keys = kind === 'intent' ? [] : kind === 'spec' ? ['intent'] : ['intent','spec'];
|
|
22
|
+
check(keys.length === Object.keys(doc.basedOn).length && keys.every(k => doc.basedOn[k]) && historical(doc.basedOn), 'Invalid document dependencies');
|
|
23
|
+
}
|
|
24
|
+
const decisionIds = new Set();
|
|
25
|
+
for (const d of s.decisions) {
|
|
26
|
+
check(historical(d.subjects), 'Invalid decision subject versions');
|
|
27
|
+
const required = d.kind === 'intent-confirmation' ? ['intent'] : d.kind === 'revocation' ? [] : ['intent', 'spec'];
|
|
28
|
+
check(required.every(k => d.subjects[k]), 'Missing decision subjects');
|
|
29
|
+
check(d.kind === 'revocation' ? !!d.revokes : !d.revokes, 'Invalid revocation');
|
|
30
|
+
for (const key of ['supersedes', 'revokes']) if (d[key]) check(decisionIds.has(d[key]), 'Invalid decision reference');
|
|
31
|
+
decisionIds.add(d.id);
|
|
32
|
+
}
|
|
33
|
+
if (s.acceptance) {
|
|
34
|
+
unique(s.acceptance.items, 'id');
|
|
35
|
+
check(s.documents.spec && s.acceptance.specVersion <= s.documents.spec.version, 'Invalid acceptance spec version');
|
|
36
|
+
}
|
|
37
|
+
const acceptanceIds = new Set(s.acceptance?.items.map(a => a.id));
|
|
38
|
+
for (const step of s.execution) check(new Set(step.acceptanceIds).size === step.acceptanceIds.length && step.acceptanceIds.every(id => acceptanceIds.has(id)), 'Invalid step acceptance references');
|
|
39
|
+
const verificationIds = new Set();
|
|
40
|
+
for (const v of s.verification) {
|
|
41
|
+
check(s.documents.spec && v.specVersion <= s.documents.spec.version, 'Invalid verification spec version');
|
|
42
|
+
if (v.specVersion === s.acceptance?.specVersion) check(v.acceptanceIds.every(id => acceptanceIds.has(id)), 'Invalid verification acceptance references');
|
|
43
|
+
check(new Set(v.acceptanceIds).size === v.acceptanceIds.length, 'Duplicate verification acceptance references');
|
|
44
|
+
check(v.outcome !== 'passed' || v.evidenceRefs.length > 0, 'Passed verification requires evidence');
|
|
45
|
+
for (const key of ['supersedes', 'carriedFrom']) if (v[key]) check(verificationIds.has(v[key]), 'Invalid verification reference');
|
|
46
|
+
check(!v.carriedFrom || !!v.carryReason, 'Missing carry reason');
|
|
47
|
+
verificationIds.add(v.id);
|
|
48
|
+
}
|
|
49
|
+
check(s.blockers.every(b => b.status !== 'resolved' || b.resolution), 'Missing blocker resolution');
|
|
50
|
+
const removed = new Set(s.decisions.flatMap(d => [d.revokes, d.supersedes].filter(Boolean)));
|
|
51
|
+
const decision = (kind, keys) => s.decisions.some(d => d.kind === kind && !removed.has(d.id) && current(d.subjects) && keys.every(k => d.subjects[k] && d.subjects[k] === s.documents[k]?.version));
|
|
52
|
+
const level = ['intent','spec','plan','execute','deliver','done'].indexOf(s.workflow.phase);
|
|
53
|
+
if (level >= 1) check(decision('intent-confirmation', ['intent']), 'Missing current intent confirmation');
|
|
54
|
+
if (level >= 2) check(s.documents.spec && current(s.documents.spec.basedOn) && decision('spec-review', ['intent','spec']) && decision('implementation-authorization', ['intent','spec']), 'Missing current spec approval/authorization');
|
|
55
|
+
if (level >= 3) {
|
|
56
|
+
check(s.documents.plan && current(s.documents.plan.basedOn), 'Invalid current plan');
|
|
57
|
+
check(s.acceptance?.specVersion === s.documents.spec.version, 'Missing current acceptance');
|
|
58
|
+
check(s.acceptance.items.some(a => a.required) || s.acceptance.noApplicableReason, 'Empty required acceptance without rationale');
|
|
59
|
+
check(s.execution.length || s.acceptance.noApplicableReason, 'Missing execution steps');
|
|
60
|
+
check(s.acceptance.items.filter(a => a.required).every(a => s.execution.some(p => p.acceptanceIds.includes(a.id))), 'Missing acceptance step coverage');
|
|
61
|
+
}
|
|
62
|
+
if (level >= 4) {
|
|
63
|
+
check(!s.blockers.some(b => b.status === 'open'), 'Open blockers prevent delivery');
|
|
64
|
+
const superseded = new Set(s.verification.map(v => v.supersedes));
|
|
65
|
+
for (const a of s.acceptance.items.filter(a => a.required)) {
|
|
66
|
+
const records = s.verification.filter(v => !superseded.has(v.id) && v.specVersion === s.documents.spec.version && v.acceptanceIds.includes(a.id));
|
|
67
|
+
check(records.some(v => v.outcome === 'passed') && !records.some(v => v.outcome === 'failed'), 'Missing current passed verification');
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
check(level === 5 ? s.closure && s.workflow.status === 'active' : !s.closure, 'Closure and phase mismatch');
|
|
71
|
+
if (s.closure) check(s.closure.acceptanceIds.every(id => acceptanceIds.has(id)) && s.acceptance.items.filter(a => a.required).every(a => s.closure.acceptanceIds.includes(a.id)), 'Invalid closure acceptance coverage');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const text = z.string().trim().min(1);
|
|
75
|
+
const id = text.regex(/^[A-Za-z0-9][A-Za-z0-9_-]*$/);
|
|
76
|
+
const version = z.number().int().positive();
|
|
77
|
+
const kinds = ['intent', 'spec', 'plan'];
|
|
78
|
+
export const phaseSchema = z.enum([...kinds, 'execute', 'deliver', 'done']);
|
|
79
|
+
const status = z.enum(['active', 'waiting-user', 'blocked', 'paused']);
|
|
80
|
+
const subjects = z.object({ intent: version.optional(), spec: version.optional(), plan: version.optional() }).strict();
|
|
81
|
+
const source = z.object({ quote: text, sessionId: text.optional(), eventSeq: z.number().int().nonnegative().optional(), context: text.optional(), recordedBy: z.enum(['agent', 'user']).default('agent') }).strict().refine(s => s.eventSeq === undefined || s.sessionId !== undefined, 'eventSeq requires sessionId').refine(s => s.eventSeq !== undefined || !!s.context, 'Unlocated source requires context');
|
|
82
|
+
const decision = z.object({ id, kind: z.enum(['intent-confirmation', 'spec-review', 'implementation-authorization', 'revocation']), subjects, scope: text, source, supersedes: id.optional(), revokes: id.optional() }).strict();
|
|
83
|
+
const item = z.object({ id, anchor: text, required: z.boolean() }).strict();
|
|
84
|
+
const step = z.discriminatedUnion('mode', [
|
|
85
|
+
z.object({ stepId: id, acceptanceIds: z.array(id), mode: z.literal('direct'), status: z.enum(['pending', 'in-progress', 'completed']), note: text.optional() }).strict(),
|
|
86
|
+
z.object({ stepId: id, acceptanceIds: z.array(id), mode: z.literal('native'), nativeRef: z.object({ sessionId: text, taskId: text }).strict() }).strict(),
|
|
87
|
+
]);
|
|
88
|
+
const verification = z.object({ id, acceptanceIds: z.array(id).min(1), specVersion: version, outcome: z.enum(['passed', 'failed', 'not-run', 'not-applicable', 'stale']), method: text, subject: text, environment: text, evidenceRefs: z.array(text), summary: text, supersedes: id.optional(), carriedFrom: id.optional(), carryReason: text.optional(), observedAt: z.iso.datetime().optional() }).strict();
|
|
89
|
+
const blocker = z.object({ id, description: text, status: z.enum(['open', 'resolved']), resolution: text.optional() }).strict();
|
|
90
|
+
const workflow = z.object({ phase: phaseSchema, status, reason: text, nextAction: text }).strict();
|
|
91
|
+
const closure = z.object({ summary: text, acceptanceIds: z.array(id), satisfaction: text, knowledge: text, evidenceRefs: z.array(text) }).strict();
|
|
92
|
+
const op = (type, shape) => z.object({ type: z.literal(type), ...shape }).strict();
|
|
93
|
+
export const workspaceSchema = z.discriminatedUnion('kind', [
|
|
94
|
+
z.object({ kind: z.literal('directory') }).strict(),
|
|
95
|
+
z.object({ kind: z.literal('git'), branch: z.string().min(1), baseCommit: z.string().regex(/^[a-f0-9]{40}([a-f0-9]{24})?$/).nullable(), projectPath: z.string() }).strict(),
|
|
96
|
+
]);
|
|
97
|
+
const bindingStatus = z.enum(['matched', 'mismatch', 'detached']);
|
|
98
|
+
const currentWorkspace = z.discriminatedUnion('kind', [
|
|
99
|
+
z.object({ kind: z.literal('directory'), projectRoot: z.string() }).strict(),
|
|
100
|
+
z.object({ kind: z.literal('git'), projectRoot: z.string(), checkoutRoot: z.string(), projectPath: z.string(), branch: z.string().nullable(), headCommit: z.string().nullable() }).strict(),
|
|
101
|
+
]);
|
|
102
|
+
export const operationSchema = z.discriminatedUnion('type', [
|
|
103
|
+
op('bind-workspace', { reason: text }),
|
|
104
|
+
op('register-document', { kind: z.enum(kinds), classification: z.enum(['semantic', 'format']), basedOn: subjects, reason: text }),
|
|
105
|
+
op('record-decision', { decision }),
|
|
106
|
+
op('set-acceptance', { specVersion: version, items: z.array(item), noApplicableReason: text.optional() }),
|
|
107
|
+
op('bind-step', { step, reason: text.optional() }),
|
|
108
|
+
op('set-step-progress', { stepId: id, status: z.enum(['pending', 'in-progress', 'completed']), note: text.optional() }),
|
|
109
|
+
op('record-verification', { verification }),
|
|
110
|
+
op('invalidate-verification', { ids: z.array(id).min(1), reason: text }),
|
|
111
|
+
op('set-blocker', { blocker }),
|
|
112
|
+
op('set-workflow', workflow.shape),
|
|
113
|
+
op('close', closure.shape),
|
|
114
|
+
]);
|
|
115
|
+
export const stateSchema = z.object({
|
|
116
|
+
schemaVersion: z.literal(2), workspace: workspaceSchema,
|
|
117
|
+
workspaceHistory: z.array(z.object({ previous: workspaceSchema, reason: text, recordedAt: z.iso.datetime() }).strict()).optional(),
|
|
118
|
+
id, title: text, revision: version, createdAt: z.iso.datetime(), updatedAt: z.iso.datetime(),
|
|
119
|
+
documents: z.object(Object.fromEntries(kinds.map(k => [k, z.object({ version, path: z.literal(`${k}.md`), digest: z.string().regex(/^[a-f0-9]{64}$/), basedOn: subjects, reason: text }).strict().optional()]))).strict(),
|
|
120
|
+
workflow, decisions: z.array(decision.extend({ recordedAt: z.iso.datetime() })),
|
|
121
|
+
acceptance: z.object({ specVersion: version, items: z.array(item), noApplicableReason: text.optional() }).strict().optional(),
|
|
122
|
+
execution: z.array(z.union(step.options.map(option => option.extend({ previousBindings: z.array(z.object({ step, reason: text, recordedAt: z.iso.datetime() }).strict()).optional() })))), verification: z.array(verification.extend({ recordedAt: z.iso.datetime(), staleReason: text.optional() })),
|
|
123
|
+
blockers: z.array(blocker), closure: closure.extend({ recordedAt: z.iso.datetime() }).optional(),
|
|
124
|
+
}).strict();
|
|
125
|
+
const base = { projectRoot: text };
|
|
126
|
+
export const inputSchema = z.discriminatedUnion('action', [
|
|
127
|
+
z.object({ ...base, action: z.literal('workspace') }).strict(),
|
|
128
|
+
z.object({ ...base, action: z.literal('list'), limit: z.number().int().min(1).max(100).default(20), cursor: text.optional() }).strict(),
|
|
129
|
+
z.object({ ...base, action: z.literal('get'), changeId: id, section: z.enum(['documents', 'workflow', 'decisions', 'acceptance', 'execution', 'verification', 'blockers', 'closure']).optional() }).strict(),
|
|
130
|
+
z.object({ ...base, action: z.literal('create'), changeId: id, title: text }).strict(),
|
|
131
|
+
z.object({ ...base, action: z.literal('update'), changeId: id, expectedRevision: version, operations: z.array(operationSchema).min(1).max(32) }).strict(),
|
|
132
|
+
]);
|
|
133
|
+
// Reads have action-specific sections; the store supplies their canonical payload.
|
|
134
|
+
export const outputSchema = z.object({ action: z.enum(['workspace', 'list', 'get', 'create', 'update']), workspace: currentWorkspace.optional(), binding: workspaceSchema.optional(), bindingStatus: bindingStatus.optional(), id: id.optional(), revision: version.optional(), changed: z.boolean().optional(), summary: z.array(z.string()).optional(), state: stateSchema.optional(), data: z.unknown().optional(), consistency: z.unknown().optional(), blockers: z.array(blocker).optional(), items: z.array(z.unknown()).optional(), cursor: z.string().optional() }).strict();
|
package/src/state.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { isDeepStrictEqual as equal } from 'node:util';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { DomainError, parseState, operationSchema, validateRelations } from './schema.js';
|
|
4
|
+
import { bindWorkspace, workspaceStatus } from './workspace.js';
|
|
5
|
+
export { DomainError } from './schema.js';
|
|
6
|
+
const phases = ['intent', 'spec', 'plan', 'execute', 'deliver', 'done'];
|
|
7
|
+
const fail = (code, message) => { throw new DomainError(code, message); };
|
|
8
|
+
const requireThat = (condition, message, code = 'INVALID_REFERENCE') => { if (!condition) fail(code, message); };
|
|
9
|
+
const timestamp = now => typeof now === 'function' ? now() : now ?? new Date().toISOString();
|
|
10
|
+
export function createState({ id, title }, { now, workspace = { kind: 'directory' } } = {}) {
|
|
11
|
+
const at = timestamp(now);
|
|
12
|
+
return parseState({ schemaVersion: 2, workspace: bindWorkspace(workspace), id, title, revision: 1, createdAt: at, updatedAt: at, documents: {}, workflow: { phase: 'intent', status: 'active', reason: 'Change created', nextAction: 'Clarify and register intent' }, decisions: [], execution: [], verification: [], blockers: [] });
|
|
13
|
+
}
|
|
14
|
+
const currentSubjects = (s, refs) => Object.entries(refs).every(([k, v]) => s.documents[k]?.version === v);
|
|
15
|
+
function refs(s, ids) {
|
|
16
|
+
requireThat(new Set(ids).size === ids.length, 'Duplicate acceptance reference');
|
|
17
|
+
for (const id of ids) requireThat(s.acceptance?.items.some(a => a.id === id), `Unknown acceptance: ${id}`);
|
|
18
|
+
}
|
|
19
|
+
function uniqueRecord(records, record) {
|
|
20
|
+
const previous = records.find(r => r.id === record.id);
|
|
21
|
+
if (previous) {
|
|
22
|
+
const { recordedAt, staleReason, ...content } = previous;
|
|
23
|
+
requireThat(equal(content, record), `Record ID conflict: ${record.id}`, 'INVALID_STATE');
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
function dependencies(s, kind, basedOn) {
|
|
29
|
+
const required = kind === 'intent' ? [] : kind === 'spec' ? ['intent'] : ['intent', 'spec'];
|
|
30
|
+
requireThat(equal(Object.keys(basedOn).sort(), required.sort()) && currentSubjects(s, basedOn), `${kind} must reference current ${required.join('/') || 'no documents'}`);
|
|
31
|
+
}
|
|
32
|
+
export function validateState(s) {
|
|
33
|
+
validateRelations(s, 'INVALID_TRANSITION');
|
|
34
|
+
return s;
|
|
35
|
+
}
|
|
36
|
+
export function applyOperations(state, operations, { expectedRevision, documents = {}, now, workspace } = {}) {
|
|
37
|
+
const original = parseState(state);
|
|
38
|
+
requireThat(original.revision === expectedRevision, 'Revision changed; get state and merge again', 'REVISION_CONFLICT');
|
|
39
|
+
const parsed = z.array(operationSchema).min(1).max(32).safeParse(operations);
|
|
40
|
+
if (!parsed.success) fail('INVALID_STATE', parsed.error.message);
|
|
41
|
+
if (parsed.data.some(op => op.type === 'bind-workspace')) requireThat(parsed.data.length === 1, 'bind-workspace must be a separate update', 'INVALID_STATE');
|
|
42
|
+
const s = structuredClone(original), at = timestamp(now);
|
|
43
|
+
const summary = [];
|
|
44
|
+
let needsConsistency = false;
|
|
45
|
+
for (const op of parsed.data) {
|
|
46
|
+
switch (op.type) {
|
|
47
|
+
case 'bind-workspace': {
|
|
48
|
+
requireThat(!!workspace, 'bind-workspace requires observed workspace facts');
|
|
49
|
+
const binding = bindWorkspace(workspace);
|
|
50
|
+
if (workspaceStatus(workspace, s.workspace) !== 'matched') {
|
|
51
|
+
s.workspaceHistory = [...(s.workspaceHistory ?? []), { previous: s.workspace, reason: op.reason, recordedAt: at }];
|
|
52
|
+
s.workspace = binding;
|
|
53
|
+
}
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
case 'register-document': {
|
|
57
|
+
const doc = documents[op.kind];
|
|
58
|
+
requireThat(!!doc, `Missing document: ${op.kind}`, 'DOCUMENT_CHANGED');
|
|
59
|
+
dependencies(s, op.kind, op.basedOn);
|
|
60
|
+
const old = s.documents[op.kind];
|
|
61
|
+
requireThat(op.classification !== 'format' || !!old, 'First registration must be semantic');
|
|
62
|
+
if (op.classification === 'format') requireThat(equal(old.basedOn, op.basedOn), 'Format update cannot change dependencies');
|
|
63
|
+
s.documents[op.kind] = { version: old ? old.version + (op.classification === 'semantic' ? 1 : 0) : 1, path: `${op.kind}.md`, digest: doc.digest, basedOn: op.basedOn, reason: op.reason };
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
case 'record-decision': {
|
|
67
|
+
needsConsistency = true;
|
|
68
|
+
const d = op.decision;
|
|
69
|
+
if (!uniqueRecord(s.decisions, d)) break;
|
|
70
|
+
requireThat(currentSubjects(s, d.subjects), 'Decision subjects must be current');
|
|
71
|
+
const required = d.kind === 'intent-confirmation' ? ['intent'] : d.kind === 'revocation' ? [] : ['intent', 'spec'];
|
|
72
|
+
requireThat(required.every(k => d.subjects[k] === s.documents[k]?.version && !!s.documents[k]), 'Decision requires current subjects');
|
|
73
|
+
for (const key of ['supersedes', 'revokes']) if (d[key]) requireThat(s.decisions.some(v => v.id === d[key] && v.kind !== 'revocation'), `Unknown decision: ${d[key]}`);
|
|
74
|
+
requireThat(d.kind === 'revocation' ? !!d.revokes : !d.revokes, 'Only revocations may revoke and must name a decision');
|
|
75
|
+
s.decisions.push({ ...d, recordedAt: at }); break;
|
|
76
|
+
}
|
|
77
|
+
case 'set-acceptance': {
|
|
78
|
+
requireThat(op.specVersion === s.documents.spec?.version, 'Acceptance requires current spec');
|
|
79
|
+
requireThat(new Set(op.items.map(a => a.id)).size === op.items.length, 'Duplicate acceptance IDs');
|
|
80
|
+
const { type, ...acceptance } = op; s.acceptance = acceptance; break;
|
|
81
|
+
}
|
|
82
|
+
case 'bind-step': {
|
|
83
|
+
refs(s, op.step.acceptanceIds);
|
|
84
|
+
const index = s.execution.findIndex(step => step.stepId === op.step.stepId);
|
|
85
|
+
if (index >= 0) {
|
|
86
|
+
const { previousBindings = [], ...previous } = s.execution[index];
|
|
87
|
+
if (!equal(previous, op.step)) {
|
|
88
|
+
requireThat(!!op.reason, 'Replacing a step binding requires reason');
|
|
89
|
+
s.execution[index] = { ...op.step, previousBindings: [...previousBindings, { step: previous, reason: op.reason, recordedAt: at }] };
|
|
90
|
+
}
|
|
91
|
+
} else s.execution.push(op.step);
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
case 'set-step-progress': {
|
|
95
|
+
const step = s.execution.find(v => v.stepId === op.stepId);
|
|
96
|
+
requireThat(step?.mode === 'direct', 'Progress can only update a known direct step');
|
|
97
|
+
step.status = op.status; if (op.note) step.note = op.note; break;
|
|
98
|
+
}
|
|
99
|
+
case 'record-verification': {
|
|
100
|
+
const v = op.verification;
|
|
101
|
+
if (!uniqueRecord(s.verification, v)) break;
|
|
102
|
+
refs(s, v.acceptanceIds);
|
|
103
|
+
requireThat(v.specVersion === s.documents.spec?.version && s.acceptance?.specVersion === v.specVersion, 'Verification requires current spec and acceptance');
|
|
104
|
+
requireThat(v.outcome !== 'passed' || v.evidenceRefs.length > 0, 'Passed verification requires evidence');
|
|
105
|
+
for (const key of ['supersedes', 'carriedFrom']) if (v[key]) requireThat(s.verification.some(old => old.id === v[key]), `Unknown verification: ${v[key]}`);
|
|
106
|
+
requireThat(!v.carriedFrom || !!v.carryReason, 'Carried verification requires applicability reason');
|
|
107
|
+
s.verification.push({ ...v, recordedAt: at }); break;
|
|
108
|
+
}
|
|
109
|
+
case 'invalidate-verification':
|
|
110
|
+
for (const id of op.ids) {
|
|
111
|
+
const v = s.verification.find(v => v.id === id); requireThat(!!v, `Unknown verification: ${id}`);
|
|
112
|
+
v.outcome = 'stale'; v.staleReason = op.reason;
|
|
113
|
+
}
|
|
114
|
+
break;
|
|
115
|
+
case 'set-blocker': {
|
|
116
|
+
const b = op.blocker;
|
|
117
|
+
requireThat(b.status !== 'resolved' || !!b.resolution, 'Resolved blocker requires resolution');
|
|
118
|
+
const index = s.blockers.findIndex(v => v.id === b.id);
|
|
119
|
+
if (index < 0) s.blockers.push(b); else s.blockers[index] = b;
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
case 'set-workflow': {
|
|
123
|
+
requireThat(op.phase !== 'done', 'Use close to enter done', 'INVALID_TRANSITION');
|
|
124
|
+
needsConsistency = true;
|
|
125
|
+
const { type, ...workflow } = op; s.workflow = workflow; delete s.closure; break;
|
|
126
|
+
}
|
|
127
|
+
case 'close': {
|
|
128
|
+
needsConsistency = true;
|
|
129
|
+
refs(s, op.acceptanceIds);
|
|
130
|
+
requireThat(s.acceptance?.items.filter(a => a.required).every(a => op.acceptanceIds.includes(a.id)), 'Closure must cover all required acceptance');
|
|
131
|
+
const { type, ...closure } = op;
|
|
132
|
+
s.closure = { ...closure, recordedAt: at };
|
|
133
|
+
s.workflow = { phase: 'done', status: 'active', reason: op.summary, nextAction: 'Delivered' }; break;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
summary.push(op.type);
|
|
137
|
+
}
|
|
138
|
+
if (needsConsistency) for (const [kind, doc] of Object.entries(s.documents)) requireThat(doc.digest === documents[kind]?.digest, `Document changed or missing: ${kind}; register it first`, 'DOCUMENT_CHANGED');
|
|
139
|
+
for (const op of parsed.data.filter(op => op.type === 'register-document' && op.classification === 'semantic' && original.documents[op.kind])) {
|
|
140
|
+
requireThat(phases.indexOf(s.workflow.phase) <= phases.indexOf(op.kind) || parsed.data.some(op => op.type === 'set-workflow' || op.type === 'close'), `Semantic ${op.kind} revision requires explicit phase reassessment`, 'INVALID_TRANSITION');
|
|
141
|
+
}
|
|
142
|
+
validateState(s);
|
|
143
|
+
// Any update must leave the final phase's full prerequisites satisfied.
|
|
144
|
+
parseState(s);
|
|
145
|
+
const changed = !equal(s, original);
|
|
146
|
+
if (changed) { s.revision++; s.updatedAt = at; }
|
|
147
|
+
return { state: s, id: s.id, revision: s.revision, changed, summary: changed ? summary : [] };
|
|
148
|
+
}
|