dic-workflow-kit 1.0.0 → 1.1.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/.codex-plugin/plugin.json +1 -1
- package/README.md +56 -11
- package/README.zh-CN.md +53 -8
- package/agents/code-repairer.md +1 -1
- package/agents/contract-oracle.md +1 -1
- package/agents/final-reviewer.md +1 -1
- package/agents/flow-auditor.md +1 -1
- package/agents/impl-inspector.md +1 -1
- package/agents/profile-builder.md +1 -1
- package/agents/repair-planner.md +1 -1
- package/agents/spec-reader.md +1 -1
- package/agents/validation-runner.md +1 -1
- package/dist/dic-workflow-kit.mjs +10 -0
- package/package.json +12 -13
- package/skills/knowledge-bdd/SKILL.md +1 -1
- package/skills/knowledge-openspec/SKILL.md +1 -1
- package/skills/knowledge-repair-distill/SKILL.md +1 -1
- package/skills/workflow-core/SKILL.md +1 -1
- package/skills/workflow-intake/SKILL.md +1 -1
- package/skills/workflow-profile/SKILL.md +1 -1
- package/skills/workflow-repair/SKILL.md +1 -1
- package/bin/dic-workflow-kit.mjs +0 -150
- package/scripts/dic_workflow.py +0 -507
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Design-Implementation Consistency Workflow Kit
|
|
2
2
|
|
|
3
|
-
Version: `1.
|
|
3
|
+
Version: `1.1.0`
|
|
4
4
|
|
|
5
5
|
Design-Implementation Consistency Workflow Kit, or DIC Workflow Kit, is a portable agentic workflow package for checking and repairing whether implementation matches design intent.
|
|
6
6
|
|
|
@@ -10,31 +10,76 @@ The kit is not tied to one contest, one repository, one language stack, or one a
|
|
|
10
10
|
|
|
11
11
|
> The differentiator is not the number of agents. DIC Workflow Kit uses design intent, typed handoffs, project profiles, validation evidence, and residual-risk reporting to prove that implementation remains aligned with its design contract.
|
|
12
12
|
|
|
13
|
-
## Install
|
|
13
|
+
## Install with npm
|
|
14
14
|
|
|
15
|
-
The npm package contains all seven Skills and all nine Subagents
|
|
15
|
+
The npm package contains all seven Skills and all nine Subagents and installs them with host-native directory layouts for Codex, Claude Code, OpenCode, and MiMo Code.
|
|
16
|
+
|
|
17
|
+
Codex remains the default, so the original command is backward compatible:
|
|
16
18
|
|
|
17
19
|
```text
|
|
18
|
-
npx dic-workflow-kit install
|
|
20
|
+
npx @tonyclaw/dic-workflow-kit@1.1.0 install
|
|
19
21
|
```
|
|
20
22
|
|
|
21
|
-
|
|
23
|
+
Select another host explicitly:
|
|
22
24
|
|
|
23
25
|
```text
|
|
24
|
-
|
|
25
|
-
dic-workflow-kit install
|
|
26
|
+
npx @tonyclaw/dic-workflow-kit@1.1.0 install --host claude-code
|
|
27
|
+
npx @tonyclaw/dic-workflow-kit@1.1.0 install --host opencode
|
|
28
|
+
npx @tonyclaw/dic-workflow-kit@1.1.0 install --host mimo-code
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
User scope is the default. To install into the current repository instead, use `--scope project`:
|
|
32
|
+
|
|
33
|
+
```text
|
|
34
|
+
npx @tonyclaw/dic-workflow-kit@1.1.0 install --host claude-code --scope project
|
|
35
|
+
npx @tonyclaw/dic-workflow-kit@1.1.0 install --host opencode --scope project
|
|
36
|
+
npx @tonyclaw/dic-workflow-kit@1.1.0 install --host mimo-code --scope project
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The host mappings are:
|
|
40
|
+
|
|
41
|
+
| Host | User scope | Project scope |
|
|
42
|
+
| --- | --- | --- |
|
|
43
|
+
| Codex | `~/.agents/plugins/plugins/dic-workflow-kit` plus marketplace registration | Not applicable to this plugin installer |
|
|
44
|
+
| Claude Code | `~/.claude/{skills,agents}` | `.claude/{skills,agents}` |
|
|
45
|
+
| OpenCode | `~/.config/opencode/{skills,agents}` | `.opencode/{skills,agents}` |
|
|
46
|
+
| MiMo Code | platform config root under `mimocode/{skills,agents}` | `.mimocode/{skills,agents}` |
|
|
47
|
+
|
|
48
|
+
On Windows, MiMo Code's user root resolves to `%LOCALAPPDATA%\mimocode`; `MIMOCODE_HOME` and `XDG_CONFIG_HOME` are honored where applicable. The installer adapts Subagent frontmatter per host while keeping `agents/` in the package as the single source of truth.
|
|
49
|
+
|
|
50
|
+
You can also install the CLI globally:
|
|
51
|
+
|
|
52
|
+
```text
|
|
53
|
+
npm install --global @tonyclaw/dic-workflow-kit@1.1.0
|
|
54
|
+
dic-workflow-kit install --host opencode
|
|
26
55
|
```
|
|
27
56
|
|
|
28
|
-
|
|
57
|
+
The same CLI can also be installed directly from the repository:
|
|
29
58
|
|
|
30
59
|
```text
|
|
31
60
|
npm install --global git+ssh://git@gitcode.com/TonyClaw/DICWorkflowKit.git
|
|
32
61
|
dic-workflow-kit install
|
|
33
62
|
```
|
|
34
63
|
|
|
35
|
-
|
|
64
|
+
Use `--dry-run` to preview the resolved destination, `--install-root <path>` for an isolated host root, or the backward-compatible Codex-only `--marketplace-root <path>`. The Codex installer preserves unrelated marketplace entries and replaces only the entry with the same plugin name. Restart or reload the target coding-agent session after its first Skills/agents directory is created.
|
|
65
|
+
|
|
66
|
+
### Dual npm release
|
|
67
|
+
|
|
68
|
+
Every release publishes the same version and payload under both npm names:
|
|
69
|
+
|
|
70
|
+
- `@tonyclaw/dic-workflow-kit`
|
|
71
|
+
- `dic-workflow-kit`
|
|
72
|
+
|
|
73
|
+
`package.json` is the canonical version source. Set a new version once; the helper propagates it to the Codex plugin manifest, workflow runtime, all Skills, all Subagents, and both READMEs:
|
|
74
|
+
|
|
75
|
+
```text
|
|
76
|
+
npm run release:set-version -- 1.1.0
|
|
77
|
+
npm run release:plan
|
|
78
|
+
npm run release:dual:dry-run
|
|
79
|
+
npm run release:dual
|
|
80
|
+
```
|
|
36
81
|
|
|
37
|
-
|
|
82
|
+
npm cannot provide an atomic transaction across two package names. The coordinator therefore uses a convergent release: it completes all local checks first, publishes both packages from temporary staging directories, skips a package when the exact version already exists, and can be safely rerun to publish only the missing side after a partial failure.
|
|
38
83
|
|
|
39
84
|
For a defense-oriented walkthrough of the project claims and their code evidence, see [答辩引导:项目特点与代码证据](docs/答辩引导-项目特点与代码证据.md).
|
|
40
85
|
|
|
@@ -157,7 +202,7 @@ For OpenSpec projects, the kit treats these as first-class design sources:
|
|
|
157
202
|
|
|
158
203
|
The kit checks whether proposals, specs, scenarios, and tasks were actually reflected in implementation and tests.
|
|
159
204
|
|
|
160
|
-
|
|
205
|
+
The source repository also contains a development helper for generating these profiles directly. It is intentionally excluded from the npm runtime package. From a source checkout, scope intake to one OpenSpec Change with:
|
|
161
206
|
|
|
162
207
|
```text
|
|
163
208
|
python scripts/dic_workflow.py \
|
package/README.zh-CN.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 设计—实现一致性 Workflow Kit
|
|
2
2
|
|
|
3
|
-
版本:`1.
|
|
3
|
+
版本:`1.1.0`
|
|
4
4
|
|
|
5
5
|
Design-Implementation Consistency Workflow Kit,简称 DIC Workflow Kit,是一套可移植的智能体工作流包,用于检查和修复“代码实现是否符合设计意图”。
|
|
6
6
|
|
|
@@ -8,29 +8,74 @@ Design-Implementation Consistency Workflow Kit,简称 DIC Workflow Kit,是
|
|
|
8
8
|
|
|
9
9
|
> 项目的差异不在 Agent 数量。DIC Workflow Kit 通过设计意图、类型化交接、项目画像、验证证据和残余风险报告,证明实现仍然符合设计契约。
|
|
10
10
|
|
|
11
|
-
## 通过 npm
|
|
11
|
+
## 通过 npm 安装
|
|
12
12
|
|
|
13
|
-
npm 包完整包含 7 个 Skills 和 9 个 Subagents
|
|
13
|
+
npm 包完整包含 7 个 Skills 和 9 个 Subagents,并支持 Codex、Claude Code、OpenCode 和 MiMo Code 的原生目录结构。
|
|
14
|
+
|
|
15
|
+
Codex 仍是默认宿主,原命令保持兼容:
|
|
16
|
+
|
|
17
|
+
```text
|
|
18
|
+
npx @tonyclaw/dic-workflow-kit@1.1.0 install
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
安装到其他宿主时显式指定 `--host`:
|
|
14
22
|
|
|
15
23
|
```text
|
|
16
|
-
npx dic-workflow-kit install
|
|
24
|
+
npx @tonyclaw/dic-workflow-kit@1.1.0 install --host claude-code
|
|
25
|
+
npx @tonyclaw/dic-workflow-kit@1.1.0 install --host opencode
|
|
26
|
+
npx @tonyclaw/dic-workflow-kit@1.1.0 install --host mimo-code
|
|
17
27
|
```
|
|
18
28
|
|
|
29
|
+
默认是用户级安装;若只希望当前仓库使用,则增加 `--scope project`:
|
|
30
|
+
|
|
31
|
+
```text
|
|
32
|
+
npx @tonyclaw/dic-workflow-kit@1.1.0 install --host claude-code --scope project
|
|
33
|
+
npx @tonyclaw/dic-workflow-kit@1.1.0 install --host opencode --scope project
|
|
34
|
+
npx @tonyclaw/dic-workflow-kit@1.1.0 install --host mimo-code --scope project
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
| 宿主 | 用户级目录 | 项目级目录 |
|
|
38
|
+
| --- | --- | --- |
|
|
39
|
+
| Codex | `~/.agents/plugins/plugins/dic-workflow-kit`,并注册 marketplace | 当前插件安装器不提供项目级模式 |
|
|
40
|
+
| Claude Code | `~/.claude/{skills,agents}` | `.claude/{skills,agents}` |
|
|
41
|
+
| OpenCode | `~/.config/opencode/{skills,agents}` | `.opencode/{skills,agents}` |
|
|
42
|
+
| MiMo Code | 平台配置根目录下的 `mimocode/{skills,agents}` | `.mimocode/{skills,agents}` |
|
|
43
|
+
|
|
44
|
+
Windows 上 MiMo Code 的用户级根目录为 `%LOCALAPPDATA%\mimocode`;适用时也会读取 `MIMOCODE_HOME` 和 `XDG_CONFIG_HOME`。安装器以仓库中的 `agents/` 为唯一来源,并按宿主转换 Subagent frontmatter,避免维护四套重复内容。
|
|
45
|
+
|
|
19
46
|
也可以先全局安装 CLI:
|
|
20
47
|
|
|
21
48
|
```text
|
|
22
|
-
npm install --global dic-workflow-kit
|
|
23
|
-
dic-workflow-kit install
|
|
49
|
+
npm install --global @tonyclaw/dic-workflow-kit@1.1.0
|
|
50
|
+
dic-workflow-kit install --host opencode
|
|
24
51
|
```
|
|
25
52
|
|
|
26
|
-
|
|
53
|
+
也可以直接从仓库安装同一个 CLI:
|
|
27
54
|
|
|
28
55
|
```text
|
|
29
56
|
npm install --global git+ssh://git@gitcode.com/TonyClaw/DICWorkflowKit.git
|
|
30
57
|
dic-workflow-kit install
|
|
31
58
|
```
|
|
32
59
|
|
|
33
|
-
|
|
60
|
+
使用 `--dry-run` 预演目标目录,使用 `--install-root <path>` 指定隔离的宿主根目录;Codex 仍兼容 `--marketplace-root <path>`。首次创建 Skills 或 agents 目录后,请重启或重新加载对应 Coding Agent 会话。
|
|
61
|
+
|
|
62
|
+
### 双 npm 包发布
|
|
63
|
+
|
|
64
|
+
每次发布必须以相同版本、相同内容发布两个包名:
|
|
65
|
+
|
|
66
|
+
- `@tonyclaw/dic-workflow-kit`
|
|
67
|
+
- `dic-workflow-kit`
|
|
68
|
+
|
|
69
|
+
`package.json` 是唯一版本源。版本脚本会同步 Codex 插件清单、工作流脚本、全部 Skills、全部 Subagents 和中英文 README:
|
|
70
|
+
|
|
71
|
+
```text
|
|
72
|
+
npm run release:set-version -- 1.1.0
|
|
73
|
+
npm run release:plan
|
|
74
|
+
npm run release:dual:dry-run
|
|
75
|
+
npm run release:dual
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
npm 不支持两个包名之间的原子事务,因此发布协调器采用可收敛策略:先完成全部本地检查,再从临时 staging 分别发布两个包;某一侧已经存在相同版本时自动跳过,部分失败后重复运行只会补发缺失的一侧。
|
|
34
79
|
|
|
35
80
|
答辩时如何讲解项目特点并定位代码证据,请参考[《答辩引导:项目特点与代码证据》](docs/答辩引导-项目特点与代码证据.md)。
|
|
36
81
|
|
package/agents/code-repairer.md
CHANGED
package/agents/final-reviewer.md
CHANGED
package/agents/flow-auditor.md
CHANGED
package/agents/impl-inspector.md
CHANGED
package/agents/repair-planner.md
CHANGED
package/agents/spec-reader.md
CHANGED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import{cp as A,mkdir as w,readFile as y,readdir as g,writeFile as E}from"node:fs/promises";import{homedir as f}from"node:os";import{dirname as x,join as n,resolve as m}from"node:path";import{fileURLToPath as N}from"node:url";var h="dic-workflow-kit",p=m(x(N(import.meta.url)),".."),j=new Map([["codex","codex"],["claude","claude-code"],["claude-code","claude-code"],["opencode","opencode"],["mimo","mimo-code"],["mimocode","mimo-code"],["mimo-code","mimo-code"]]),L=[".codex-plugin","adapters","agents","schemas","skills","INSTRUCTION.md"];function u(e,o,t){let r=e[o+1];if(!r)throw new Error(`${t} requires a value`);return r}function C(e){let o=e[0]??"help",t="codex",r="user",c,a,s=process.cwd(),d=!1;for(let i=1;i<e.length;i+=1){let l=e[i];if(l==="--host"){let k=u(e,i,l).toLowerCase();if(t=j.get(k),!t)throw new Error(`unsupported host: ${k}; expected codex, claude-code, opencode, or mimo-code`);i+=1}else if(l==="--scope"){if(r=u(e,i,l).toLowerCase(),!["user","project"].includes(r))throw new Error("--scope must be user or project");i+=1}else if(l==="--install-root")c=m(u(e,i,l)),i+=1;else if(l==="--project-root")s=m(u(e,i,l)),i+=1;else if(l==="--marketplace-root")a=m(u(e,i,l)),i+=1;else if(l==="--dry-run")d=!0;else throw new Error(`unknown argument: ${l}`)}if(t!=="codex"&&a)throw new Error("--marketplace-root is only valid for --host codex");if(t==="codex"&&r==="project")throw new Error("Codex plugin installation supports user scope only");return{command:o,host:t,scope:r,installRoot:c,marketplaceRoot:a,projectRoot:s,dryRun:d}}function O({host:e,scope:o,projectRoot:t}){if(o==="project"){let r={"claude-code":".claude",opencode:".opencode","mimo-code":".mimocode"}[e];return n(t,r)}return e==="claude-code"?n(f(),".claude"):e==="opencode"?n(process.env.XDG_CONFIG_HOME||n(f(),".config"),"opencode"):e==="mimo-code"?process.env.MIMOCODE_HOME?m(process.env.MIMOCODE_HOME):process.platform==="win32"&&process.env.LOCALAPPDATA?n(process.env.LOCALAPPDATA,"mimocode"):n(process.env.XDG_CONFIG_HOME||n(f(),".config"),"mimocode"):n(f(),".agents","plugins")}async function v(e){try{let o=JSON.parse(await y(e,"utf8"));if(!o||typeof o!="object"||Array.isArray(o))throw new Error("marketplace.json must contain an object");if(!Array.isArray(o.plugins))throw new Error("marketplace.json must contain a plugins array");return o}catch(o){if(o?.code!=="ENOENT")throw o;return{name:"personal",interface:{displayName:"Personal"},plugins:[]}}}function S(){return{name:h,source:{source:"local",path:`./plugins/${h}`},policy:{installation:"AVAILABLE",authentication:"ON_INSTALL"},category:"Developer Tools"}}async function T(e){let o=e.marketplaceRoot||e.installRoot||O(e),t=n(o,"marketplace.json"),r=n(o,"plugins",h),c=await v(t),a=S(),s=c.plugins.findIndex(d=>d?.name===h);if(s>=0?c.plugins[s]=a:c.plugins.push(a),!e.dryRun){await w(r,{recursive:!0});for(let d of L)await A(n(p,d),n(r,d),{recursive:!0,force:!0});await w(o,{recursive:!0}),await E(t,`${JSON.stringify(c,null,2)}
|
|
3
|
+
`,"utf8")}R({...e,root:r,skillsRoot:n(r,"skills"),agentsRoot:n(r,"agents"),extra:`MARKETPLACE: ${t}`})}function I(e,o){let t=e.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/);if(!t)return e;let r=o==="claude-code"?new Set(["name","description","tools","disallowedTools","model","permissionMode","skills","memory","background","isolation"]):new Set(["name","description","mode","model","temperature","top_p","tools","permission","steps","hidden","color"]);return`---
|
|
4
|
+
${t[1].split(/\r?\n/).filter(a=>{let s=a.match(/^([A-Za-z][A-Za-z0-9_-]*):/)?.[1];return!s||r.has(s)}).join(`
|
|
5
|
+
`)}
|
|
6
|
+
---
|
|
7
|
+
${e.slice(t[0].length)}`}async function $(e){let o=e.installRoot||O(e),t=n(o,"skills"),r=n(o,"agents"),c=await g(n(p,"skills")),a=(await g(n(p,"agents"))).filter(s=>s.endsWith(".md"));if(!e.dryRun){await w(t,{recursive:!0});for(let s of c)await A(n(p,"skills",s),n(t,s),{recursive:!0,force:!0});await w(r,{recursive:!0});for(let s of a){let d=await y(n(p,"agents",s),"utf8");await E(n(r,s),I(d,e.host),"utf8")}}R({...e,root:o,skillsRoot:t,agentsRoot:r})}function R({host:e,scope:o,root:t,skillsRoot:r,agentsRoot:c,dryRun:a,extra:s}){process.stdout.write([a?"DRY_RUN: true":"INSTALLED: true",`HOST: ${e}`,`SCOPE: ${o}`,`ROOT: ${t}`,`SKILLS: ${r}`,`SUBAGENTS: ${c}`,s,""].filter(d=>d!==void 0).join(`
|
|
8
|
+
`))}async function P(e){e.host==="codex"?await T(e):await $(e)}function _(){process.stdout.write(["DIC Workflow Kit multi-host installer","","Usage:"," dic-workflow-kit install [--host <host>] [--scope user|project]"," [--project-root <path>] [--install-root <path>]"," [--marketplace-root <path>] [--dry-run]","","Hosts:"," codex (default), claude-code, opencode, mimo-code","","Examples:"," dic-workflow-kit install"," dic-workflow-kit install --host claude-code"," dic-workflow-kit install --host opencode --scope project"," dic-workflow-kit install --host mimo-code --scope project","","--marketplace-root is a backward-compatible Codex-only option.","--install-root overrides the resolved host configuration directory.",""].join(`
|
|
9
|
+
`))}try{let e=C(process.argv.slice(2));if(e.command==="install")await P(e);else if(e.command==="help"||e.command==="--help")_();else throw new Error(`unknown command: ${e.command}`)}catch(e){process.stderr.write(`ERROR: ${e.message}
|
|
10
|
+
`),process.exitCode=1}
|
package/package.json
CHANGED
|
@@ -1,31 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dic-workflow-kit",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Install DIC Workflow Kit Skills and Subagents
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Install DIC Workflow Kit Skills and Subagents for Codex, Claude Code, OpenCode, and MiMo Code.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"dic-workflow-kit": "
|
|
8
|
-
},
|
|
9
|
-
"scripts": {
|
|
10
|
-
"test": "python -m unittest discover -s tests -v && node --test tests/test_npm_installer.mjs",
|
|
11
|
-
"pack:check": "npm pack --dry-run"
|
|
7
|
+
"dic-workflow-kit": "dist/dic-workflow-kit.mjs"
|
|
12
8
|
},
|
|
13
9
|
"files": [
|
|
14
10
|
".codex-plugin/",
|
|
15
11
|
"adapters/",
|
|
16
12
|
"agents/",
|
|
17
|
-
"
|
|
13
|
+
"dist/",
|
|
18
14
|
"schemas/",
|
|
19
|
-
"scripts/",
|
|
20
|
-
"!scripts/__pycache__/",
|
|
21
|
-
"!**/*.pyc",
|
|
22
15
|
"skills/",
|
|
23
16
|
"INSTRUCTION.md",
|
|
24
|
-
"README.md"
|
|
25
|
-
"README.zh-CN.md"
|
|
17
|
+
"README.md"
|
|
26
18
|
],
|
|
27
19
|
"keywords": [
|
|
28
20
|
"codex",
|
|
21
|
+
"claude-code",
|
|
22
|
+
"opencode",
|
|
23
|
+
"mimo-code",
|
|
29
24
|
"plugin",
|
|
30
25
|
"skills",
|
|
31
26
|
"subagents",
|
|
@@ -39,6 +34,10 @@
|
|
|
39
34
|
"url": "git+ssh://git@gitcode.com/TonyClaw/DICWorkflowKit.git"
|
|
40
35
|
},
|
|
41
36
|
"homepage": "https://gitcode.com/TonyClaw/DICWorkflowKit",
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public",
|
|
39
|
+
"registry": "https://registry.npmjs.org/"
|
|
40
|
+
},
|
|
42
41
|
"engines": {
|
|
43
42
|
"node": ">=20"
|
|
44
43
|
}
|
package/bin/dic-workflow-kit.mjs
DELETED
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { cp, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
4
|
-
import { homedir } from "node:os";
|
|
5
|
-
import { dirname, join, resolve } from "node:path";
|
|
6
|
-
import { fileURLToPath } from "node:url";
|
|
7
|
-
|
|
8
|
-
const PLUGIN_NAME = "dic-workflow-kit";
|
|
9
|
-
const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
10
|
-
const PAYLOAD = [
|
|
11
|
-
".codex-plugin",
|
|
12
|
-
"adapters",
|
|
13
|
-
"agents",
|
|
14
|
-
"schemas",
|
|
15
|
-
"scripts",
|
|
16
|
-
"skills",
|
|
17
|
-
"INSTRUCTION.md",
|
|
18
|
-
"README.md",
|
|
19
|
-
"README.zh-CN.md",
|
|
20
|
-
];
|
|
21
|
-
|
|
22
|
-
function parseArgs(argv) {
|
|
23
|
-
const command = argv[0] ?? "help";
|
|
24
|
-
let marketplaceRoot = join(homedir(), ".agents", "plugins");
|
|
25
|
-
let dryRun = false;
|
|
26
|
-
for (let index = 1; index < argv.length; index += 1) {
|
|
27
|
-
const value = argv[index];
|
|
28
|
-
if (value === "--marketplace-root") {
|
|
29
|
-
const next = argv[index + 1];
|
|
30
|
-
if (!next) {
|
|
31
|
-
throw new Error("--marketplace-root requires a directory");
|
|
32
|
-
}
|
|
33
|
-
marketplaceRoot = resolve(next);
|
|
34
|
-
index += 1;
|
|
35
|
-
} else if (value === "--dry-run") {
|
|
36
|
-
dryRun = true;
|
|
37
|
-
} else {
|
|
38
|
-
throw new Error(`unknown argument: ${value}`);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
return { command, marketplaceRoot: resolve(marketplaceRoot), dryRun };
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
async function loadMarketplace(path) {
|
|
45
|
-
try {
|
|
46
|
-
const value = JSON.parse(await readFile(path, "utf8"));
|
|
47
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
48
|
-
throw new Error("marketplace.json must contain an object");
|
|
49
|
-
}
|
|
50
|
-
if (!Array.isArray(value.plugins)) {
|
|
51
|
-
throw new Error("marketplace.json must contain a plugins array");
|
|
52
|
-
}
|
|
53
|
-
return value;
|
|
54
|
-
} catch (error) {
|
|
55
|
-
if (error?.code !== "ENOENT") {
|
|
56
|
-
throw error;
|
|
57
|
-
}
|
|
58
|
-
return {
|
|
59
|
-
name: "personal",
|
|
60
|
-
interface: { displayName: "Personal" },
|
|
61
|
-
plugins: [],
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function pluginEntry() {
|
|
67
|
-
return {
|
|
68
|
-
name: PLUGIN_NAME,
|
|
69
|
-
source: {
|
|
70
|
-
source: "local",
|
|
71
|
-
path: `./plugins/${PLUGIN_NAME}`,
|
|
72
|
-
},
|
|
73
|
-
policy: {
|
|
74
|
-
installation: "AVAILABLE",
|
|
75
|
-
authentication: "ON_INSTALL",
|
|
76
|
-
},
|
|
77
|
-
category: "Developer Tools",
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
async function install({ marketplaceRoot, dryRun }) {
|
|
82
|
-
const marketplacePath = join(marketplaceRoot, "marketplace.json");
|
|
83
|
-
const pluginRoot = join(marketplaceRoot, "plugins", PLUGIN_NAME);
|
|
84
|
-
const marketplace = await loadMarketplace(marketplacePath);
|
|
85
|
-
const entry = pluginEntry();
|
|
86
|
-
const existingIndex = marketplace.plugins.findIndex(
|
|
87
|
-
(candidate) => candidate?.name === PLUGIN_NAME,
|
|
88
|
-
);
|
|
89
|
-
if (existingIndex >= 0) {
|
|
90
|
-
marketplace.plugins[existingIndex] = entry;
|
|
91
|
-
} else {
|
|
92
|
-
marketplace.plugins.push(entry);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (!dryRun) {
|
|
96
|
-
await mkdir(pluginRoot, { recursive: true });
|
|
97
|
-
for (const relativePath of PAYLOAD) {
|
|
98
|
-
await cp(join(PACKAGE_ROOT, relativePath), join(pluginRoot, relativePath), {
|
|
99
|
-
recursive: true,
|
|
100
|
-
force: true,
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
await mkdir(marketplaceRoot, { recursive: true });
|
|
104
|
-
await writeFile(
|
|
105
|
-
marketplacePath,
|
|
106
|
-
`${JSON.stringify(marketplace, null, 2)}\n`,
|
|
107
|
-
"utf8",
|
|
108
|
-
);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
process.stdout.write(
|
|
112
|
-
[
|
|
113
|
-
dryRun ? "DRY_RUN: true" : "INSTALLED: true",
|
|
114
|
-
`PLUGIN_ROOT: ${pluginRoot}`,
|
|
115
|
-
`MARKETPLACE: ${marketplacePath}`,
|
|
116
|
-
"SKILLS: skills/",
|
|
117
|
-
"SUBAGENTS: agents/",
|
|
118
|
-
"",
|
|
119
|
-
].join("\n"),
|
|
120
|
-
);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function printHelp() {
|
|
124
|
-
process.stdout.write(
|
|
125
|
-
[
|
|
126
|
-
"DIC Workflow Kit Codex installer",
|
|
127
|
-
"",
|
|
128
|
-
"Usage:",
|
|
129
|
-
" dic-workflow-kit install [--marketplace-root <path>] [--dry-run]",
|
|
130
|
-
"",
|
|
131
|
-
"Default destination:",
|
|
132
|
-
" ~/.agents/plugins/plugins/dic-workflow-kit",
|
|
133
|
-
"",
|
|
134
|
-
].join("\n"),
|
|
135
|
-
);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
try {
|
|
139
|
-
const options = parseArgs(process.argv.slice(2));
|
|
140
|
-
if (options.command === "install") {
|
|
141
|
-
await install(options);
|
|
142
|
-
} else if (options.command === "help" || options.command === "--help") {
|
|
143
|
-
printHelp();
|
|
144
|
-
} else {
|
|
145
|
-
throw new Error(`unknown command: ${options.command}`);
|
|
146
|
-
}
|
|
147
|
-
} catch (error) {
|
|
148
|
-
process.stderr.write(`ERROR: ${error.message}\n`);
|
|
149
|
-
process.exitCode = 1;
|
|
150
|
-
}
|
package/scripts/dic_workflow.py
DELETED
|
@@ -1,507 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Portable helper skeleton for DIC Workflow Kit.
|
|
3
|
-
|
|
4
|
-
This script intentionally uses only the Python standard library.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from __future__ import annotations
|
|
8
|
-
|
|
9
|
-
import argparse
|
|
10
|
-
import hashlib
|
|
11
|
-
import json
|
|
12
|
-
import os
|
|
13
|
-
import re
|
|
14
|
-
import shlex
|
|
15
|
-
from pathlib import Path
|
|
16
|
-
from typing import Any
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
VERSION = "1.0.0"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def write_text(path: Path, text: str) -> None:
|
|
23
|
-
path.parent.mkdir(parents=True, exist_ok=True)
|
|
24
|
-
path.write_text(text, encoding="utf-8", newline="\n")
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def find_workflow_root(start: Path) -> Path:
|
|
28
|
-
current = start.resolve()
|
|
29
|
-
if current.is_file():
|
|
30
|
-
current = current.parent
|
|
31
|
-
for candidate in [current, *current.parents]:
|
|
32
|
-
if (candidate / "INSTRUCTION.md").exists():
|
|
33
|
-
return candidate
|
|
34
|
-
return current
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def rel(path: Path, root: Path) -> str:
|
|
38
|
-
try:
|
|
39
|
-
return path.resolve().relative_to(root.resolve()).as_posix()
|
|
40
|
-
except ValueError:
|
|
41
|
-
return path.as_posix()
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def extract_validation_commands(text: str, source: str) -> list[dict[str, Any]]:
|
|
45
|
-
commands = []
|
|
46
|
-
seen = set()
|
|
47
|
-
for value in re.findall(r"`([^`\r\n]+)`", text):
|
|
48
|
-
if not re.match(
|
|
49
|
-
r"^(?:npm\s+(?:run|test|exec)\b|npx\s+|pnpm\s+|yarn\s+|"
|
|
50
|
-
r"openspec\s+validate\b|pytest(?:\s|$)|python\s+|mvn\s+)",
|
|
51
|
-
value,
|
|
52
|
-
):
|
|
53
|
-
continue
|
|
54
|
-
try:
|
|
55
|
-
command = shlex.split(value)
|
|
56
|
-
except ValueError:
|
|
57
|
-
continue
|
|
58
|
-
key = tuple(command)
|
|
59
|
-
if key in seen:
|
|
60
|
-
continue
|
|
61
|
-
seen.add(key)
|
|
62
|
-
commands.append(
|
|
63
|
-
{
|
|
64
|
-
"name": value,
|
|
65
|
-
"command": command,
|
|
66
|
-
"cwd": ".",
|
|
67
|
-
"source": source,
|
|
68
|
-
"kind": "suggested",
|
|
69
|
-
}
|
|
70
|
-
)
|
|
71
|
-
return commands
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def obligation_id(capability_id: str, kind: str, title: str) -> str:
|
|
75
|
-
digest = hashlib.sha256(
|
|
76
|
-
f"{capability_id}\0{kind}\0{title}".encode("utf-8")
|
|
77
|
-
).hexdigest()[:12]
|
|
78
|
-
return f"obl-{digest}"
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
def extract_openspec_obligations(
|
|
82
|
-
spec_path: Path,
|
|
83
|
-
project_root: Path,
|
|
84
|
-
capability_id: str,
|
|
85
|
-
authority: str,
|
|
86
|
-
) -> list[dict[str, Any]]:
|
|
87
|
-
lines = spec_path.read_text(encoding="utf-8").splitlines()
|
|
88
|
-
obligations = []
|
|
89
|
-
current_requirement = None
|
|
90
|
-
index = 0
|
|
91
|
-
while index < len(lines):
|
|
92
|
-
line = lines[index]
|
|
93
|
-
requirement_match = re.match(r"^###\s+Requirement:\s*(.+?)\s*$", line)
|
|
94
|
-
scenario_match = re.match(r"^####\s+Scenario:\s*(.+?)\s*$", line)
|
|
95
|
-
if requirement_match:
|
|
96
|
-
title = requirement_match.group(1)
|
|
97
|
-
statement_lines = []
|
|
98
|
-
cursor = index + 1
|
|
99
|
-
while cursor < len(lines) and not re.match(
|
|
100
|
-
r"^#{3,4}\s+(?:Requirement|Scenario):", lines[cursor]
|
|
101
|
-
):
|
|
102
|
-
value = lines[cursor].strip()
|
|
103
|
-
if value and not value.startswith("#"):
|
|
104
|
-
statement_lines.append(value)
|
|
105
|
-
cursor += 1
|
|
106
|
-
current_requirement = obligation_id(capability_id, "requirement", title)
|
|
107
|
-
obligations.append(
|
|
108
|
-
{
|
|
109
|
-
"id": current_requirement,
|
|
110
|
-
"capabilityId": capability_id,
|
|
111
|
-
"kind": "requirement",
|
|
112
|
-
"title": title,
|
|
113
|
-
"statement": " ".join(statement_lines),
|
|
114
|
-
"source": rel(spec_path, project_root),
|
|
115
|
-
"line": index + 1,
|
|
116
|
-
"authority": authority,
|
|
117
|
-
"parentId": None,
|
|
118
|
-
"acceptanceClauses": [],
|
|
119
|
-
}
|
|
120
|
-
)
|
|
121
|
-
elif scenario_match:
|
|
122
|
-
title = scenario_match.group(1)
|
|
123
|
-
clauses = []
|
|
124
|
-
cursor = index + 1
|
|
125
|
-
while cursor < len(lines) and not re.match(
|
|
126
|
-
r"^#{3,4}\s+(?:Requirement|Scenario):", lines[cursor]
|
|
127
|
-
):
|
|
128
|
-
clause_match = re.match(
|
|
129
|
-
r"^\s*-\s+\*\*(GIVEN|WHEN|THEN|AND|BUT)\*\*\s*(.+?)\s*$",
|
|
130
|
-
lines[cursor],
|
|
131
|
-
flags=re.IGNORECASE,
|
|
132
|
-
)
|
|
133
|
-
if clause_match:
|
|
134
|
-
clauses.append(
|
|
135
|
-
{
|
|
136
|
-
"keyword": clause_match.group(1).upper(),
|
|
137
|
-
"text": clause_match.group(2),
|
|
138
|
-
"line": cursor + 1,
|
|
139
|
-
}
|
|
140
|
-
)
|
|
141
|
-
cursor += 1
|
|
142
|
-
obligations.append(
|
|
143
|
-
{
|
|
144
|
-
"id": obligation_id(capability_id, "scenario", title),
|
|
145
|
-
"capabilityId": capability_id,
|
|
146
|
-
"kind": "scenario",
|
|
147
|
-
"title": title,
|
|
148
|
-
"statement": " ".join(
|
|
149
|
-
f"{item['keyword']} {item['text']}" for item in clauses
|
|
150
|
-
),
|
|
151
|
-
"source": rel(spec_path, project_root),
|
|
152
|
-
"line": index + 1,
|
|
153
|
-
"authority": authority,
|
|
154
|
-
"parentId": current_requirement,
|
|
155
|
-
"acceptanceClauses": clauses,
|
|
156
|
-
}
|
|
157
|
-
)
|
|
158
|
-
index += 1
|
|
159
|
-
return obligations
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
def build_obligation_catalog(
|
|
163
|
-
project_root: Path, design_entry: dict[str, Any]
|
|
164
|
-
) -> dict[str, Any]:
|
|
165
|
-
use_baseline = design_entry["lifecycle"] == "archived" and design_entry["baselineSpecs"]
|
|
166
|
-
selected_specs = (
|
|
167
|
-
design_entry["baselineSpecs"] if use_baseline else design_entry["deltaSpecs"]
|
|
168
|
-
)
|
|
169
|
-
authority = "current-baseline" if use_baseline else "active-change"
|
|
170
|
-
obligations = []
|
|
171
|
-
for spec in selected_specs:
|
|
172
|
-
spec_path = project_root / spec
|
|
173
|
-
capability_id = spec_path.parent.name
|
|
174
|
-
obligations.extend(
|
|
175
|
-
extract_openspec_obligations(
|
|
176
|
-
spec_path, project_root, capability_id, authority
|
|
177
|
-
)
|
|
178
|
-
)
|
|
179
|
-
return {
|
|
180
|
-
"schema": "dic.contract-obligations.v1",
|
|
181
|
-
"version": VERSION,
|
|
182
|
-
"changeId": design_entry["changeId"],
|
|
183
|
-
"changeLifecycle": design_entry["lifecycle"],
|
|
184
|
-
"authorityPolicy": (
|
|
185
|
-
"Archived changes use promoted current baseline specs."
|
|
186
|
-
if use_baseline
|
|
187
|
-
else "Active changes use their delta specs."
|
|
188
|
-
),
|
|
189
|
-
"sources": selected_specs,
|
|
190
|
-
"counts": {
|
|
191
|
-
"requirements": sum(item["kind"] == "requirement" for item in obligations),
|
|
192
|
-
"scenarios": sum(item["kind"] == "scenario" for item in obligations),
|
|
193
|
-
"total": len(obligations),
|
|
194
|
-
},
|
|
195
|
-
"obligations": obligations,
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
def render_obligation_catalog(catalog: dict[str, Any]) -> str:
|
|
200
|
-
lines = [
|
|
201
|
-
"# Contract Obligations",
|
|
202
|
-
"",
|
|
203
|
-
f"- Change: `{catalog['changeId']}`",
|
|
204
|
-
f"- Lifecycle: {catalog['changeLifecycle']}",
|
|
205
|
-
f"- Authority: {catalog['authorityPolicy']}",
|
|
206
|
-
f"- Requirements: {catalog['counts']['requirements']}",
|
|
207
|
-
f"- Scenarios: {catalog['counts']['scenarios']}",
|
|
208
|
-
"",
|
|
209
|
-
]
|
|
210
|
-
for item in catalog["obligations"]:
|
|
211
|
-
lines.extend(
|
|
212
|
-
[
|
|
213
|
-
f"## {item['id']} — {item['title']}",
|
|
214
|
-
"",
|
|
215
|
-
f"- Kind: {item['kind']}",
|
|
216
|
-
f"- Source: `{item['source']}:{item['line']}`",
|
|
217
|
-
f"- Authority: {item['authority']}",
|
|
218
|
-
]
|
|
219
|
-
)
|
|
220
|
-
if item["parentId"]:
|
|
221
|
-
lines.append(f"- Parent: `{item['parentId']}`")
|
|
222
|
-
lines.extend(["", item["statement"] or "_No normative body extracted._", ""])
|
|
223
|
-
return "\n".join(lines)
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
def resolve_design_map(project_root: Path, capability_ids: list[str]) -> dict[str, Any] | None:
|
|
227
|
-
map_path = project_root / "openspec" / "designs" / "spec-to-design-map.md"
|
|
228
|
-
if not map_path.exists():
|
|
229
|
-
return None
|
|
230
|
-
text = map_path.read_text(encoding="utf-8")
|
|
231
|
-
for line in text.splitlines():
|
|
232
|
-
if not line.lstrip().startswith("|"):
|
|
233
|
-
continue
|
|
234
|
-
cells = [cell.strip() for cell in line.strip().strip("|").split("|")]
|
|
235
|
-
if len(cells) < 5:
|
|
236
|
-
continue
|
|
237
|
-
capability = cells[0].strip("` ")
|
|
238
|
-
if capability not in capability_ids:
|
|
239
|
-
continue
|
|
240
|
-
linked_paths = []
|
|
241
|
-
for cell in cells[1:4]:
|
|
242
|
-
linked_paths.extend(re.findall(r"`(openspec/[^`]+\.md)`", cell))
|
|
243
|
-
linked_paths = list(dict.fromkeys(linked_paths))
|
|
244
|
-
return {
|
|
245
|
-
"source": rel(map_path, project_root),
|
|
246
|
-
"capabilityId": capability,
|
|
247
|
-
"architectureDesigns": [
|
|
248
|
-
path for path in linked_paths if "/designs/architecture/" in path
|
|
249
|
-
],
|
|
250
|
-
"moduleDesigns": [
|
|
251
|
-
path for path in linked_paths if "/designs/modules/" in path
|
|
252
|
-
],
|
|
253
|
-
"adrs": [path for path in linked_paths if "/designs/adr/" in path],
|
|
254
|
-
"validationSummary": cells[4],
|
|
255
|
-
"validationCommands": extract_validation_commands(cells[4], rel(map_path, project_root)),
|
|
256
|
-
}
|
|
257
|
-
return None
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
def resolve_openspec_change(project_root: Path, change_id: str) -> dict[str, Any]:
|
|
261
|
-
candidates = [
|
|
262
|
-
("active", project_root / "openspec" / "changes" / change_id),
|
|
263
|
-
("archived", project_root / "openspec" / "changes" / "archive" / change_id),
|
|
264
|
-
]
|
|
265
|
-
for lifecycle, change_root in candidates:
|
|
266
|
-
if not change_root.is_dir():
|
|
267
|
-
continue
|
|
268
|
-
delta_specs = sorted(change_root.glob("specs/**/spec.md"))
|
|
269
|
-
capability_ids = [
|
|
270
|
-
path.relative_to(change_root / "specs").parts[0] for path in delta_specs
|
|
271
|
-
]
|
|
272
|
-
baseline_specs = []
|
|
273
|
-
for delta_spec in delta_specs:
|
|
274
|
-
relative_spec = delta_spec.relative_to(change_root / "specs")
|
|
275
|
-
baseline = project_root / "openspec" / "specs" / relative_spec
|
|
276
|
-
if baseline.exists():
|
|
277
|
-
baseline_specs.append(baseline)
|
|
278
|
-
documents = [
|
|
279
|
-
path
|
|
280
|
-
for path in [
|
|
281
|
-
change_root / "proposal.md",
|
|
282
|
-
change_root / "design.md",
|
|
283
|
-
change_root / "tasks.md",
|
|
284
|
-
]
|
|
285
|
-
if path.exists()
|
|
286
|
-
]
|
|
287
|
-
result = {
|
|
288
|
-
"type": "openspec-change",
|
|
289
|
-
"changeId": change_id,
|
|
290
|
-
"lifecycle": lifecycle,
|
|
291
|
-
"changeRoot": rel(change_root, project_root),
|
|
292
|
-
"documents": [rel(path, project_root) for path in documents],
|
|
293
|
-
"deltaSpecs": [rel(path, project_root) for path in delta_specs],
|
|
294
|
-
"baselineSpecs": [rel(path, project_root) for path in baseline_specs],
|
|
295
|
-
}
|
|
296
|
-
design_map = resolve_design_map(project_root, capability_ids)
|
|
297
|
-
if design_map:
|
|
298
|
-
result["designMap"] = design_map
|
|
299
|
-
return result
|
|
300
|
-
raise SystemExit(
|
|
301
|
-
f"OpenSpec change not found: {change_id} "
|
|
302
|
-
f"(checked active and archived changes under {project_root})"
|
|
303
|
-
)
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
def detect_project(root: Path, change_id: str = "") -> dict[str, Any]:
|
|
307
|
-
project_root = root
|
|
308
|
-
openspec = list(project_root.glob("**/openspec/specs/**/spec.md")) + list(
|
|
309
|
-
project_root.glob("**/openspec/changes/**/proposal.md")
|
|
310
|
-
)
|
|
311
|
-
design_entry = resolve_openspec_change(project_root, change_id) if change_id else None
|
|
312
|
-
if design_entry:
|
|
313
|
-
selected = (
|
|
314
|
-
design_entry["documents"]
|
|
315
|
-
+ design_entry["deltaSpecs"]
|
|
316
|
-
+ design_entry["baselineSpecs"]
|
|
317
|
-
)
|
|
318
|
-
design_map = design_entry.get("designMap")
|
|
319
|
-
if design_map:
|
|
320
|
-
selected += [
|
|
321
|
-
design_map["source"],
|
|
322
|
-
*design_map["architectureDesigns"],
|
|
323
|
-
*design_map["moduleDesigns"],
|
|
324
|
-
*design_map["adrs"],
|
|
325
|
-
]
|
|
326
|
-
design_sources = [project_root / path for path in selected]
|
|
327
|
-
else:
|
|
328
|
-
design_sources = []
|
|
329
|
-
for pattern in ["README*", "docs/**/*.md", "design/**/*.md", "spec/**/*.md", "api/**/*.md", "schemas/**/*"]:
|
|
330
|
-
design_sources.extend(project_root.glob(pattern))
|
|
331
|
-
for path in openspec:
|
|
332
|
-
if path not in design_sources:
|
|
333
|
-
design_sources.append(path)
|
|
334
|
-
|
|
335
|
-
impl_roots = [p for p in [project_root / "src", project_root / "app", project_root / "code"] if p.exists()]
|
|
336
|
-
test_roots = [p for p in [project_root / "test", project_root / "tests", project_root / "test-cases"] if p.exists()]
|
|
337
|
-
project_types = []
|
|
338
|
-
adapters = ["generic"]
|
|
339
|
-
if openspec:
|
|
340
|
-
project_types.append("openspec")
|
|
341
|
-
adapters.append("openspec")
|
|
342
|
-
if (project_root / "pom.xml").exists() or list(project_root.glob("**/pom.xml")):
|
|
343
|
-
project_types.append("maven")
|
|
344
|
-
if (project_root / "package.json").exists() or list(project_root.glob("**/package.json")):
|
|
345
|
-
project_types.append("node")
|
|
346
|
-
|
|
347
|
-
validation_commands = []
|
|
348
|
-
if design_entry:
|
|
349
|
-
for document in design_entry["documents"]:
|
|
350
|
-
path = project_root / document
|
|
351
|
-
validation_commands.extend(
|
|
352
|
-
extract_validation_commands(path.read_text(encoding="utf-8"), document)
|
|
353
|
-
)
|
|
354
|
-
if design_entry.get("designMap"):
|
|
355
|
-
validation_commands.extend(design_entry["designMap"]["validationCommands"])
|
|
356
|
-
if design_entry["lifecycle"] == "archived":
|
|
357
|
-
validation_commands = [
|
|
358
|
-
item
|
|
359
|
-
for item in validation_commands
|
|
360
|
-
if not (
|
|
361
|
-
item["command"][:2] == ["openspec", "validate"]
|
|
362
|
-
and "--all" not in item["command"]
|
|
363
|
-
)
|
|
364
|
-
]
|
|
365
|
-
unique_commands = {}
|
|
366
|
-
for item in validation_commands:
|
|
367
|
-
unique_commands.setdefault(tuple(item["command"]), item)
|
|
368
|
-
validation_commands = list(unique_commands.values())
|
|
369
|
-
|
|
370
|
-
profile = {
|
|
371
|
-
"schema": "dic.project-profile.v1",
|
|
372
|
-
"version": VERSION,
|
|
373
|
-
"projectRoot": project_root.as_posix(),
|
|
374
|
-
"projectTypes": sorted(set(project_types)) or ["generic"],
|
|
375
|
-
"adapters": adapters,
|
|
376
|
-
"designSources": [rel(p, project_root) for p in sorted(set(design_sources))],
|
|
377
|
-
"implementationRoots": [rel(p, project_root) for p in impl_roots],
|
|
378
|
-
"testRoots": [rel(p, project_root) for p in test_roots],
|
|
379
|
-
"protectedPaths": [rel(p, project_root) for p in sorted(set(design_sources))],
|
|
380
|
-
"validationCommands": validation_commands,
|
|
381
|
-
"riskFamilies": ["requirements", "api-contract", "flow-completeness", "validation"],
|
|
382
|
-
"outputContract": {
|
|
383
|
-
"finalResultJson": "reports/FINAL_RESULT.json",
|
|
384
|
-
"finalOutputMd": "result/output.md",
|
|
385
|
-
"contractObligationsJson": "reports/contract-obligations.json",
|
|
386
|
-
"contractObligationsMd": "reports/contract-obligations.md",
|
|
387
|
-
"traceDir": "logs/trace"
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
if design_entry:
|
|
391
|
-
profile["designEntry"] = design_entry
|
|
392
|
-
profile["deltaSpecs"] = design_entry["deltaSpecs"]
|
|
393
|
-
profile["baselineSpecs"] = design_entry["baselineSpecs"]
|
|
394
|
-
return profile
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
def command_init(args: argparse.Namespace) -> int:
|
|
398
|
-
root = Path(args.root or os.getcwd()).resolve()
|
|
399
|
-
output_root = Path(args.output_root).resolve() if args.output_root else root
|
|
400
|
-
for directory in ["reports", "result", "logs/trace"]:
|
|
401
|
-
(output_root / directory).mkdir(parents=True, exist_ok=True)
|
|
402
|
-
profile = detect_project(root, args.change)
|
|
403
|
-
profile_path = output_root / "reports" / "project-profile.json"
|
|
404
|
-
write_text(profile_path, json.dumps(profile, indent=2, ensure_ascii=False))
|
|
405
|
-
if profile.get("designEntry"):
|
|
406
|
-
catalog = build_obligation_catalog(root, profile["designEntry"])
|
|
407
|
-
write_text(
|
|
408
|
-
output_root / "reports" / "contract-obligations.json",
|
|
409
|
-
json.dumps(catalog, indent=2, ensure_ascii=False),
|
|
410
|
-
)
|
|
411
|
-
write_text(
|
|
412
|
-
output_root / "reports" / "contract-obligations.md",
|
|
413
|
-
render_obligation_catalog(catalog),
|
|
414
|
-
)
|
|
415
|
-
output = [
|
|
416
|
-
"# DIC Workflow Init",
|
|
417
|
-
"",
|
|
418
|
-
f"- Version: {VERSION}",
|
|
419
|
-
f"- Project root: `{root.as_posix()}`",
|
|
420
|
-
f"- Output root: `{output_root.as_posix()}`",
|
|
421
|
-
f"- Project profile: `{profile_path.as_posix()}`",
|
|
422
|
-
f"- Adapters: {', '.join(profile['adapters'])}",
|
|
423
|
-
]
|
|
424
|
-
if profile.get("designEntry"):
|
|
425
|
-
entry = profile["designEntry"]
|
|
426
|
-
output.extend(
|
|
427
|
-
[
|
|
428
|
-
f"- Design entry: OpenSpec Change `{entry['changeId']}`",
|
|
429
|
-
f"- Change lifecycle: {entry['lifecycle']}",
|
|
430
|
-
]
|
|
431
|
-
)
|
|
432
|
-
output.append("")
|
|
433
|
-
write_text(output_root / "logs" / "trace" / "init.md", "\n".join(output))
|
|
434
|
-
print(f"DIC_WORKFLOW_VERSION: {VERSION}")
|
|
435
|
-
print(f"PROJECT_ROOT: {root}")
|
|
436
|
-
print(f"OUTPUT_ROOT: {output_root}")
|
|
437
|
-
print(f"PROJECT_PROFILE: {profile_path}")
|
|
438
|
-
return 0
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
def command_finalize(args: argparse.Namespace) -> int:
|
|
442
|
-
root = find_workflow_root(Path(args.root or os.getcwd()))
|
|
443
|
-
status = args.status.upper()
|
|
444
|
-
if status not in {"PASS", "PARTIAL", "BLOCKED", "FAIL"}:
|
|
445
|
-
raise SystemExit(f"invalid status: {args.status}")
|
|
446
|
-
result = {
|
|
447
|
-
"schema": "dic.final-result.v1",
|
|
448
|
-
"status": status,
|
|
449
|
-
"final": True,
|
|
450
|
-
"summary": args.summary,
|
|
451
|
-
"artifacts": [
|
|
452
|
-
"reports/project-profile.json",
|
|
453
|
-
"reports/contract-obligations.json",
|
|
454
|
-
"reports/contract-obligations.md",
|
|
455
|
-
"reports/final-consistency-report.md",
|
|
456
|
-
"reports/FINAL_RESULT.json",
|
|
457
|
-
"result/output.md",
|
|
458
|
-
"logs/trace/"
|
|
459
|
-
],
|
|
460
|
-
"validation": [],
|
|
461
|
-
"changedFiles": [],
|
|
462
|
-
"residualRisks": []
|
|
463
|
-
}
|
|
464
|
-
write_text(root / "reports" / "FINAL_RESULT.json", json.dumps(result, indent=2, ensure_ascii=False))
|
|
465
|
-
lines = [
|
|
466
|
-
"# DIC Final Result",
|
|
467
|
-
"",
|
|
468
|
-
f"FINAL_EXECUTION_RESULT: {status}",
|
|
469
|
-
"FINAL_RESULT_FINAL: true",
|
|
470
|
-
"",
|
|
471
|
-
args.summary,
|
|
472
|
-
"",
|
|
473
|
-
"## Artifacts",
|
|
474
|
-
"",
|
|
475
|
-
"- `reports/FINAL_RESULT.json`",
|
|
476
|
-
"- `reports/contract-obligations.json`",
|
|
477
|
-
"- `reports/contract-obligations.md`",
|
|
478
|
-
"- `result/output.md`",
|
|
479
|
-
"- `logs/trace/`",
|
|
480
|
-
"",
|
|
481
|
-
]
|
|
482
|
-
write_text(root / "result" / "output.md", "\n".join(lines))
|
|
483
|
-
print(f"FINAL_EXECUTION_RESULT: {status}")
|
|
484
|
-
return 0
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
def main() -> int:
|
|
488
|
-
parser = argparse.ArgumentParser(prog="dic_workflow")
|
|
489
|
-
parser.add_argument("--root", default="")
|
|
490
|
-
parser.add_argument("--output-root", default="")
|
|
491
|
-
sub = parser.add_subparsers(dest="command", required=True)
|
|
492
|
-
|
|
493
|
-
p = sub.add_parser("init")
|
|
494
|
-
p.add_argument("--change", default="")
|
|
495
|
-
p.set_defaults(func=command_init)
|
|
496
|
-
|
|
497
|
-
p = sub.add_parser("finalize")
|
|
498
|
-
p.add_argument("--status", default="PARTIAL")
|
|
499
|
-
p.add_argument("--summary", default="DIC workflow finished with recorded evidence.")
|
|
500
|
-
p.set_defaults(func=command_finalize)
|
|
501
|
-
|
|
502
|
-
args = parser.parse_args()
|
|
503
|
-
return int(args.func(args))
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
if __name__ == "__main__":
|
|
507
|
-
raise SystemExit(main())
|