Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ This workspace now includes the first MVP slices:
- Safe-corpus gating before semantic closure, indexes, and grounding
- CLI commands for project init, validation, ID listing, reference checks,
grounding, indexing, and demo
- Package-manager-neutral Agent bootstrap with managed Codex Skills,
repository instructions, updates, and diagnostics
- OpenSpec `affects_domain` grounding
- Explicit `required`, `not_required`, and `unclassified` grounding decisions
- Advisory and enforced Grounding Assurance for Codex and CI
Expand All @@ -64,17 +66,37 @@ Install the CLI from npm:

```bash
npm install -g @echopath-labs/opendomain
opendomain init
opendomain init --tools codex
opendomain doctor
opendomain validate
```

The global npm installation is a CLI distribution channel. OpenDomain does not
create or modify the host project's `package.json`, lockfile, dependency list,
or npm scripts. `init --tools codex` adds the canonical `opendomain/` workspace,
generated `.codex/skills/opendomain-*` adapters, and one managed OpenDomain
block in `AGENTS.md`. Existing instructions outside that block remain owned by
the project.

After initialization, users can ask Codex to explore or model the domain,
review a Candidate, or implement a change. The generated Skills and managed
instructions select the appropriate CLI operations; direct commands remain
available for CI and diagnostics.

If workspace configuration later deselects an adapter, `doctor` reports any
remaining generated Skills and `update` removes only files that still carry
OpenDomain generation ownership metadata.

Or try it from a source checkout:

Common commands:

```bash
npm run opendomain -- help
npm run opendomain -- init
npm run opendomain -- init --tools codex
npm run opendomain -- update
npm run opendomain -- doctor
npm run opendomain -- validate
npm run prepare:demo
(cd examples/erp && node ../../bin/opendomain.mjs assure openspec/changes/order-cancellation/spec.md)
Expand Down
32 changes: 27 additions & 5 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,9 @@ OpenDomain 适合:
- 安全 parser 和 Draft 2020-12 Runtime Schema 校验;
- 在 Semantic Closure、index 和 grounding 前执行安全语料门禁;
- CLI 命令:init、validate、ids list、refs check、prepare、assure、integrations、
index、demo;
index、update、doctor、demo;
- 不要求宿主 `package.json` 的 Agent bootstrap,以及受管 Codex Skills 与
`AGENTS.md` 指令区块;
- OpenSpec `affects_domain` grounding;
- 显式 `required` / `not_required` / `unclassified` Grounding Request;
- 面向 Codex 与 CI 的 advisory / enforced Grounding Assurance;
Expand Down Expand Up @@ -136,10 +138,23 @@ OpenDomain 的 npm 包名是 `@echopath-labs/opendomain`,CLI 命令是 `opendo

```bash
npm install -g @echopath-labs/opendomain
opendomain init
opendomain init --tools codex
opendomain doctor
opendomain validate
```

npm 在这里仅是 CLI 的全局分发渠道。OpenDomain 不会在宿主项目中创建或修改
`package.json`、lockfile、依赖声明或 npm scripts。`init --tools codex` 会创建
canonical `opendomain/`、生成 `.codex/skills/opendomain-*`,并在 `AGENTS.md`
中维护一个有明确边界的 OpenDomain 区块;区块之外的项目指令保持原样。

初始化后,用户可以直接要求 Codex 浏览或建模业务、审查 Candidate,或者实现一项
变更。生成的 Skills 和托管指令负责选择 CLI 操作;直接命令主要保留给 CI、诊断和
高级使用。

如果 workspace config 后续取消选择某个 adapter,`doctor` 会报告残留的 generated
Skills,`update` 只移除仍带 OpenDomain generation ownership metadata 的文件。

也可以从源码运行。

克隆仓库:
Expand All @@ -159,6 +174,9 @@ npm run opendomain -- help

```bash
npm run opendomain -- init
npm run opendomain -- init --tools codex
npm run opendomain -- update
npm run opendomain -- doctor
```

运行测试:
Expand Down Expand Up @@ -250,7 +268,9 @@ OpenSpec 描述这次变更,OpenDomain 描述长期语义。

### 3. Codex 先 grounding 再实现

在实现非平凡 Feature 前,Codex 默认执行只读 Assurance:
在执行 `opendomain init --tools codex` 后,repository-local Codex Skills 会分别处理
只读领域探索、Candidate-first 建模和 Candidate 审查;`AGENTS.md` 托管区块要求
Codex 在实现非平凡 Feature 前默认执行只读 Assurance:

```bash
npm run opendomain -- assure <feature-spec-or-dir>
Expand Down Expand Up @@ -380,8 +400,10 @@ Candidate 不是 accepted truth。它只是待人类审查的提案。

| 目标 | 命令 |
| --- | --- |
| 查看帮助 | `npm run opendomain -- help` |
| 初始化 OpenDomain 目录 | `npm run opendomain -- init` |
| 查看帮助 | `opendomain help` |
| 初始化 OpenDomain 与 Codex | `opendomain init --tools codex` |
| 更新托管 Agent 适配 | `opendomain update` |
| 检查 workspace 与 Agent 适配 | `opendomain doctor` |
| 复制 ERP 示例 | `npm run opendomain -- init --example erp` |
| 验证全部 OpenDomain 文件 | `npm run opendomain -- validate` |
| 验证指定目录 | `npm run opendomain -- validate examples/erp` |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
---
type: domain_candidate
id: candidate-0012-agent-native-project-adoption
status: proposed
proposed_change_type: add_rule
target:
type: business_rule
id: opendomain.project-adoption-must-not-require-host-package-manifest
confidence: high
extracted_by: codex
extracted_at: 2026-08-03
evidence:
- type: human_review
location: README.zh-CN.md
summary: The maintainer requires OpenDomain adoption to avoid forcing npm scripts or a package.json into host projects.
confidence: high
- type: code
location: src/init.mjs
summary: Project initialization creates canonical semantic and Agent integration files without modifying host package metadata.
confidence: high
- type: test
location: tests/agent-workspace.test.mjs
summary: Conformance tests require Codex integration to initialize and update without creating package.json or package-lock.json.
confidence: high
possible_conflicts:
- The OpenDomain CLI is still distributed through npm and requires a user-level Node.js runtime until standalone binaries are released.
- Agent-specific repository files remain necessary even though host language package metadata is not.
- Homebrew and standalone binary delivery are separate changes and have not yet provided external adoption evidence.
review:
state: proposed
suggested_reviewer: opendomain-maintainer
---

# Candidate: Agent-native Project Adoption

## Proposed Rule

Adopting OpenDomain in a project must not require that project to create or
modify a package manifest, package-manager lockfile, or npm script. Runtime
installation belongs to the user's tool environment; the project contains only
canonical OpenDomain sources and explicitly selected repository-local Agent
integration files.

## Agent Workflow Meaning

Humans should be able to state their goal in natural language. Agent adapters
select deterministic OpenDomain CLI operations for exploration, modeling,
Candidate review, and implementation grounding. Direct CLI commands remain
available for CI, debugging, and advanced use, but they are not the primary
human workflow.

## Requested Human Review

Keep this rule proposed until the Codex bootstrap, standalone binary, Homebrew
installation, and at least one external project adoption confirm that a
package-manager-neutral project workspace remains practical across upgrades.
31 changes: 31 additions & 0 deletions schemas/workspace-config.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://opendomain.dev/schemas/workspace-config.schema.json",
"title": "OpenDomain Workspace Integration Configuration v1",
"type": "object",
"required": ["schema_version", "agent_integration"],
"properties": {
"schema_version": {
"const": "1"
},
"agent_integration": {
"type": "object",
"required": ["adapter_version", "tools"],
"properties": {
"adapter_version": {
"type": "string",
"pattern": "^[1-9][0-9]*$"
},
"tools": {
"type": "array",
"uniqueItems": true,
"items": {
"enum": ["codex"]
}
}
},
"additionalProperties": false
}
},
"additionalProperties": false
}
22 changes: 21 additions & 1 deletion scripts/smoke-installed-package.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
access,
mkdtemp,
mkdir,
readFile,
rm,
writeFile
} from "node:fs/promises";
Expand Down Expand Up @@ -49,13 +50,31 @@ try {
"opendomain"
);
const cli = path.join(installedRoot, "bin", "opendomain.mjs");
const hostPackageFile = path.join(consumer, "package.json");
const hostLockFile = path.join(consumer, "package-lock.json");
const hostPackageBefore = await readFile(hostPackageFile, "utf8");
const hostLockBefore = await readFile(hostLockFile, "utf8");
await access(path.join(installedRoot, "schemas", "integration-profile.schema.json"));
await access(path.join(installedRoot, "schemas", "domain-declaration.schema.json"));
await access(path.join(installedRoot, "schemas", "assurance-result.schema.json"));
await access(path.join(installedRoot, "schemas", "workspace-config.schema.json"));
await access(path.join(installedRoot, "scripts", "smoke-installed-package.mjs"));

const init = await runJsonCli(cli, ["init", "--example", "erp", "--json"], consumer);
const init = await runJsonCli(cli, [
"init",
"--tools",
"codex",
"--example",
"erp",
"--json"
], consumer);
assert.deepEqual(init.errors, []);
assert.equal(await readFile(hostPackageFile, "utf8"), hostPackageBefore);
assert.equal(await readFile(hostLockFile, "utf8"), hostLockBefore);
await access(path.join(consumer, ".codex", "skills", "opendomain-explore", "SKILL.md"));
const doctor = await runJsonCli(cli, ["doctor", "--json"], consumer);
assert.equal(doctor.status, "healthy");
assert.deepEqual(doctor.errors, []);

const exampleRoot = path.join(consumer, "examples", "erp");
const inspection = await runJsonCli(
Expand Down Expand Up @@ -102,6 +121,7 @@ try {
`Installed-package smoke passed: ${packPayload[0].filename}, `
+ `${inspection.valid_profile_count} Profile, `
+ `${automatic.read_first.length} grounded sources, `
+ `Agent integration ${doctor.status}, `
+ `Assurance ${assurance.policy.outcome}.\n`
);
} finally {
Expand Down
101 changes: 101 additions & 0 deletions src/agent-resources.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { stringify as stringifyYaml } from "yaml";

export const AGENT_ADAPTER_VERSION = "1";
export const WORKSPACE_CONFIG_SCHEMA_VERSION = "1";
export const SUPPORTED_AGENT_TOOLS = Object.freeze(["codex"]);

const CODEX_SKILLS = Object.freeze([
Object.freeze({
name: "opendomain-explore",
description: "Explore accepted OpenDomain semantics and Candidate boundaries without changing the domain model.",
body: `Explore the project's OpenDomain model without mutating it.

1. Run \`opendomain validate\` before relying on workspace sources.
2. Use \`opendomain ids list\` or the semantic index to find the smallest relevant accepted sources.
3. Read accepted concepts, rules, lifecycles, events, and their evidence.
4. Keep every Domain Candidate visibly separate from accepted knowledge.
5. Report gaps or conflicts; do not silently resolve or promote them.
`
}),
Object.freeze({
name: "opendomain-model",
description: "Build or refine an OpenDomain model while keeping inferred knowledge in Candidate form until human review.",
body: `Build or refine the project's long-lived domain model.

1. Separate stable business semantics from delivery intent and implementation details.
2. Read existing accepted sources and evidence before proposing changes.
3. Record uncertain, inferred, or conflicting knowledge as a Domain Candidate first.
4. Run \`opendomain validate\` after changing OpenDomain files.
5. Never accept a Candidate without an explicit human review decision.
`
}),
Object.freeze({
name: "opendomain-review",
description: "Review OpenDomain Candidates with evidence and conflicts while preserving explicit human decision ownership.",
body: `Review a Domain Candidate without treating it as accepted truth.

1. Use \`opendomain candidate show <candidate-id>\` to inspect the proposal.
2. Read its evidence, target, confidence, conflicts, and existing accepted sources.
3. Explain compatibility impact and unresolved uncertainty to the human reviewer.
4. Invoke \`opendomain candidate review\` only after the human explicitly chooses a decision, reviewer, and reason.
5. Run \`opendomain validate\` after a review mutation.
`
})
]);

export function workspaceConfigTemplate(tools = []) {
return stringifyYaml({
schema_version: WORKSPACE_CONFIG_SCHEMA_VERSION,
agent_integration: {
adapter_version: AGENT_ADAPTER_VERSION,
tools
}
}, { lineWidth: 0 });
}

export function managedAgentsTemplate() {
return `<!-- opendomain:managed:start -->
## OpenDomain

This repository uses OpenDomain for long-lived business semantics.

Before implementing a non-trivial change with an applicable Source Unit, run:

\`\`\`bash
opendomain assure <source-unit>
\`\`\`

Read every accepted source listed in \`grounding_pack.read_first\`. Treat
\`grounding_pack.candidate_boundaries\` as proposed knowledge, never accepted
truth. Report the accepted IDs and Candidate boundaries used when completing
the task.

AI-inferred domain knowledge starts as a Domain Candidate. Human reviewers own
acceptance, rejection, risk decisions, and final validation.
<!-- opendomain:managed:end -->
`;
}

export function agentSkillResources(tools = []) {
if (!tools.includes("codex")) {
return [];
}
Comment thread
chasechou007 marked this conversation as resolved.

return allAgentSkillResources();
}

export function allAgentSkillResources() {
return CODEX_SKILLS.map((skill) => ({
path: `.codex/skills/${skill.name}/SKILL.md`,
content: `---
name: ${skill.name}
description: ${skill.description}
compatibility: Requires the opendomain CLI.
metadata:
generatedBy: opendomain
adapterVersion: "${AGENT_ADAPTER_VERSION}"
---

${skill.body}`
}));
}
38 changes: 38 additions & 0 deletions src/atomic-write.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { randomUUID } from "node:crypto";
import { lstat, open, rename, rm } from "node:fs/promises";

export async function atomicWriteUtf8(file, content) {
const temporary = `${file}.opendomain-${process.pid}-${randomUUID()}.tmp`;
const existingMode = await regularFileMode(file);
let handle = null;

try {
handle = await open(temporary, "wx", existingMode ?? 0o666);
await handle.writeFile(content, { encoding: "utf8" });
if (existingMode !== null) {
await handle.chmod(existingMode);
}
await handle.close();
handle = null;
await rename(temporary, file);
} catch (error) {
await handle?.close().catch(() => {});
await rm(temporary, { force: true }).catch(() => {});
throw error;
}
}

async function regularFileMode(file) {
try {
const fileStat = await lstat(file);
if (!fileStat.isFile()) {
throw new Error(`Atomic write target '${file}' is not a regular file.`);
}
return fileStat.mode & 0o7777;
} catch (error) {
if (error.code === "ENOENT") {
return null;
}
throw error;
}
}
Loading
Loading