ai-delivery-workflow 0.5.1 → 0.6.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.
Files changed (46) hide show
  1. package/docs/FILE-REFERENCE.zh-CN.md +1 -1
  2. package/docs/MAINTENANCE-VERIFICATION.zh-CN.md +2 -2
  3. package/docs/PROJECT-MANUAL.zh-CN.md +1 -1
  4. package/docs/agents/workflow-manager-current-state.md +1 -1
  5. package/lib/communication-confirmation.mjs +177 -0
  6. package/lib/project-bootstrap.mjs +267 -3
  7. package/lib/project-installer.mjs +25 -1
  8. package/package.json +1 -1
  9. package/skills/ai-delivery-bootstrap/SKILL.md +4 -0
  10. package/skills/ai-delivery-checkpoint-task/SKILL.md +2 -0
  11. package/skills/ai-delivery-checkpoint-task/references/checkpoint-contract.md +2 -0
  12. package/skills/ai-delivery-close-version/SKILL.md +13 -10
  13. package/skills/ai-delivery-close-version/references/version-closeout-contract.md +2 -1
  14. package/skills/ai-delivery-define-product/SKILL.md +6 -3
  15. package/skills/ai-delivery-design-architecture/SKILL.md +10 -5
  16. package/skills/ai-delivery-design-architecture/assets/architecture-template/architecture-gate.yaml +2 -0
  17. package/skills/ai-delivery-design-architecture/assets/architecture-template/prototype-architecture-validation.yaml +2 -0
  18. package/skills/ai-delivery-design-architecture/references/architecture-contract.md +4 -0
  19. package/skills/ai-delivery-design-experience/SKILL.md +5 -3
  20. package/skills/ai-delivery-design-experience/assets/experience-template/product-prototype-reconciliation.yaml +2 -0
  21. package/skills/ai-delivery-design-experience/assets/experience-template/ui-handoff.yaml +3 -0
  22. package/skills/ai-delivery-design-experience/references/experience-contract.md +2 -2
  23. package/skills/ai-delivery-design-tests/SKILL.md +4 -0
  24. package/skills/ai-delivery-develop-iteration/SKILL.md +6 -1
  25. package/skills/ai-delivery-develop-iteration/assets/development-template/candidate-manifest.yaml +2 -0
  26. package/skills/ai-delivery-execute-work-package/SKILL.md +4 -2
  27. package/skills/ai-delivery-orchestrate/SKILL.md +2 -0
  28. package/skills/ai-delivery-orchestrate/assets/codebase-template/AGENTS.md +14 -0
  29. package/skills/ai-delivery-orchestrate/assets/codebase-template/CODEBASE_GUIDE.md +124 -0
  30. package/skills/ai-delivery-orchestrate/assets/codebase-template/docs/CODEBASE_INDEX.md +87 -0
  31. package/skills/ai-delivery-orchestrate/assets/communication-template/confirmation-examples.yaml +67 -0
  32. package/skills/ai-delivery-orchestrate/assets/communication-template/confirmation-summary.yaml +42 -0
  33. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/iterations/ITER-0001/02-solution-design/product-impact-assessment.yaml +42 -0
  34. package/skills/ai-delivery-orchestrate/references/codebase-document-contract.md +91 -0
  35. package/skills/ai-delivery-orchestrate/references/communication-confirmation-contract.md +52 -0
  36. package/skills/ai-delivery-orchestrate/references/product-impact-assessment-contract.md +87 -0
  37. package/skills/ai-delivery-orchestrate/scripts/confirmation-state.mjs +30 -0
  38. package/skills/ai-delivery-orchestrate-release/SKILL.md +3 -1
  39. package/skills/ai-delivery-plan-iteration/SKILL.md +6 -1
  40. package/skills/ai-delivery-prepare-platform/SKILL.md +1 -1
  41. package/skills/ai-delivery-prepare-release/SKILL.md +2 -0
  42. package/skills/ai-delivery-review-change/SKILL.md +3 -1
  43. package/skills/ai-delivery-validate-artifacts/SKILL.md +10 -7
  44. package/skills/ai-delivery-validate-artifacts/references/artifact-contract.md +2 -1
  45. package/skills/ai-delivery-verify-candidate/SKILL.md +5 -2
  46. package/skills/ai-delivery-verify-candidate/references/candidate-verification-contract.md +1 -1
@@ -3,7 +3,7 @@
3
3
  > 当前双仓目录、初始化规则、状态分层和 Git 边界见 [DUAL-REPOSITORY-WORKSPACE.zh-CN.md](DUAL-REPOSITORY-WORKSPACE.zh-CN.md)。
4
4
 
5
5
  > 适用目录:工作流发行仓库
6
- > 适用包版本:`ai-delivery-workflow@0.5.1`
6
+ > 适用包版本:`ai-delivery-workflow@0.6.0`
7
7
  > 使用方法和完整流程:[PROJECT-MANUAL.zh-CN.md](PROJECT-MANUAL.zh-CN.md)
8
8
  > CLI 安装参数:[CLI-PARAMETER-REFERENCE.zh-CN.md](CLI-PARAMETER-REFERENCE.zh-CN.md)
9
9
  > CodeGraph 可选集成:[CODEGRAPH-INTEGRATION.zh-CN.md](CODEGRAPH-INTEGRATION.zh-CN.md)
@@ -5,8 +5,8 @@
5
5
  ## 1. npm 发布预检
6
6
 
7
7
  ```powershell
8
- npm run release:check -- prepare --version 0.5.1
9
- npm run release:check -- verify --version 0.5.1 --commit <40位提交ID>
8
+ npm run release:check -- prepare --version 0.6.0
9
+ npm run release:check -- verify --version 0.6.0 --commit <40位提交ID>
10
10
  ```
11
11
 
12
12
  `prepare` 默认执行 `full` 检查:清单与锁文件版本、`main`、干净工作树、Tag 可用性、全量测试、25 个 Skill、审计验证、`npm pack --dry-run`、`npm publish --dry-run`、官方 registry、版本未占用、npm 身份和 `auth-and-writes` 2FA。两个 dry-run 还必须返回一致的包名、版本、文件清单、文件数量和制品摘要;本包必须包含 `bin/`、`docs/`、`lib/`、`skills/` 及有效的 `bin.ai-delivery`。任一项失败都会输出 `decision: fail` 并返回非零退出码。
@@ -18,7 +18,7 @@ workspace/code/.git 代码 Git
18
18
  初始化器还会合并根 `.gitattributes`,对 `.workflow/**`、项目级 `ai-delivery-*` Skills、`.codex/config.toml`、`AGENTS.md`、`.gitignore` 和 `.gitattributes` 强制 `eol=lf`。这是 checksum 可移植性约束;不会修改嵌套代码仓的换行策略。已冻结的旧迭代证据必须在根 `.gitattributes` 中使用精确路径,或“固定迭代目录 + 文件名 glob”的窄 `-text` 规则登记后,才能保留 raw checksum;嵌套或全局 attributes 不能自行获得该例外。产品、控制、工具等其他路径和 `text eol=crlf` 覆盖也不享受该例外。缺失或无效规则会使 `doctor` 失败,避免 Windows checkout 后正式状态和物料被误判为篡改。
19
19
 
20
20
  > 文档版本:1.0
21
- > 适用包版本:`ai-delivery-workflow@0.5.1`
21
+ > 适用包版本:`ai-delivery-workflow@0.6.0`
22
22
  > 默认文档语言:简体中文
23
23
  > 文件级索引:[FILE-REFERENCE.zh-CN.md](FILE-REFERENCE.zh-CN.md)
24
24
  > CodeGraph 可选集成:[CODEGRAPH-INTEGRATION.zh-CN.md](CODEGRAPH-INTEGRATION.zh-CN.md)
@@ -7,7 +7,7 @@
7
7
  ## 1. 当前基线
8
8
 
9
9
  - 当前仓库是 `ai-delivery-workflow` 的维护与发行源码仓库,不是安装了工作流的业务项目。
10
- - 当前正式源码候选版本为 `0.5.1`(待完成发布 Gate);上一版 `0.5.0` 的提交、Git tag、npm 包身份及 Workflow Manager 的历史 `0.4.0` 候选身份、校验和和生命周期证据仍记录在维护仓库的 `audit/WORKFLOW-MANAGER-P0-P1-P2-CLOSURE.md`,这些历史记录不可改写。候选包和展开验证工作区保留在被忽略的 `.tmp/`。
10
+ - 当前正式源码候选版本为 `0.6.0`(待完成发布 Gate);上一版 `0.5.1` 的提交、Git tag、npm 包身份及 Workflow Manager 的历史 `0.4.0` 候选身份、校验和和生命周期证据仍记录在维护仓库的 `audit/WORKFLOW-MANAGER-P0-P1-P2-CLOSURE.md`,这些历史记录不可改写。候选包和展开验证工作区保留在被忽略的 `.tmp/`。
11
11
  - 正式维护源码位于 `bin/`、`lib/`、`skills/`、`docs/`、`verification/` 和 `audit/`。
12
12
  - Workflow Manager 设计原型位于 `prototypes/workflow-manager/`,业务项目原型夹具位于 `prototypes/order-ops-business/`。两者是可版本化设计资产,不进入发行包,也不等于正式前端已经实现;运行时依赖、构建产物和一次性验证工作区仍放在 `.tmp/`。
13
13
 
@@ -0,0 +1,177 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { parse as parseYaml, stringify as stringifyYaml } from "./yaml-runtime.mjs";
5
+
6
+ export const CONFIRMATION_STATUSES = new Set(["proposed", "confirmed", "rejected", "deferred", "superseded"]);
7
+ export const CONFIRMATION_CATEGORIES = new Set([
8
+ "observed_fact", "business_fact", "business_decision", "domain_term", "business_constraint",
9
+ "architecture_decision", "standard_decision", "risk", "assumption", "open_question",
10
+ "execution_fact", "execution_decision",
11
+ ]);
12
+ export const NEXT_ACTION_OWNERS = new Set(["agent", "user", "external"]);
13
+
14
+ const CLOSED_STATUSES = new Set(["confirmed", "rejected", "superseded"]);
15
+ const ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]*$/;
16
+
17
+ function text(value) {
18
+ return typeof value === "string" && value.trim().length > 0;
19
+ }
20
+
21
+ function list(value) {
22
+ return Array.isArray(value) ? value : [];
23
+ }
24
+
25
+ function addError(errors, location, message) {
26
+ errors.push(`${location}: ${message}`);
27
+ }
28
+
29
+ function stableValue(value) {
30
+ if (Array.isArray(value)) return value.map(stableValue);
31
+ if (value && typeof value === "object") {
32
+ return Object.fromEntries(Object.keys(value).sort().map((key) => [key, stableValue(value[key])]));
33
+ }
34
+ return value;
35
+ }
36
+
37
+ function sameRecord(left, right) {
38
+ return JSON.stringify(stableValue(left)) === JSON.stringify(stableValue(right));
39
+ }
40
+
41
+ function recordIsClosed(record, byId, visiting = new Set()) {
42
+ if (!record) return false;
43
+ if (record.status !== "superseded") return CLOSED_STATUSES.has(record.status);
44
+ if (!record.superseded_by || visiting.has(record.confirmation_id)) return false;
45
+ visiting.add(record.confirmation_id);
46
+ const replacement = byId.get(record.superseded_by);
47
+ return Boolean(replacement) && recordIsClosed(replacement, byId, visiting);
48
+ }
49
+
50
+ function detectCycles(records, byId, errors) {
51
+ const visiting = new Set();
52
+ const visited = new Set();
53
+ const visit = (record) => {
54
+ if (visited.has(record.confirmation_id)) return;
55
+ if (visiting.has(record.confirmation_id)) {
56
+ addError(errors, `records.${record.confirmation_id}`, "dependency cycle detected");
57
+ return;
58
+ }
59
+ visiting.add(record.confirmation_id);
60
+ for (const dependencyId of list(record.depends_on)) {
61
+ const dependency = byId.get(dependencyId);
62
+ if (dependency) visit(dependency);
63
+ }
64
+ visiting.delete(record.confirmation_id);
65
+ visited.add(record.confirmation_id);
66
+ };
67
+ records.forEach(visit);
68
+ }
69
+
70
+ export function validateConfirmationSummary(summary, options = {}) {
71
+ const errors = [];
72
+ const warnings = [];
73
+ if (!summary || typeof summary !== "object" || Array.isArray(summary)) {
74
+ return { valid: false, errors: ["summary: must be a mapping"], warnings: [], frontier: [], blocked_proposals: [] };
75
+ }
76
+ if (summary.schema_version !== 1) addError(errors, "schema_version", "must equal 1");
77
+ for (const field of ["summary_id", "node", "iteration_id", "owner", "source_revision", "updated_at"]) {
78
+ if (!text(summary[field])) addError(errors, field, "must be a non-empty string");
79
+ }
80
+ if (!ID_PATTERN.test(String(summary.summary_id || ""))) addError(errors, "summary_id", "has invalid format");
81
+ if (!Number.isInteger(summary.version) || summary.version < 1) addError(errors, "version", "must be a positive integer");
82
+ if (!Number.isInteger(summary.frontier_round) || summary.frontier_round < 1) addError(errors, "frontier_round", "must be a positive integer");
83
+ if (!Array.isArray(summary.records)) addError(errors, "records", "must be an array");
84
+ const records = list(summary.records);
85
+ const byId = new Map();
86
+ records.forEach((record, index) => {
87
+ const prefix = `records[${index}]`;
88
+ if (!record || typeof record !== "object" || Array.isArray(record)) {
89
+ addError(errors, prefix, "must be a mapping");
90
+ return;
91
+ }
92
+ if (!text(record.confirmation_id) || !ID_PATTERN.test(record.confirmation_id || "")) addError(errors, `${prefix}.confirmation_id`, "must be a stable ID");
93
+ if (byId.has(record.confirmation_id)) addError(errors, `${prefix}.confirmation_id`, "must be unique");
94
+ else byId.set(record.confirmation_id, record);
95
+ if (!text(record.question_id) || !ID_PATTERN.test(record.question_id || "")) addError(errors, `${prefix}.question_id`, "must be a stable ID");
96
+ if (!CONFIRMATION_CATEGORIES.has(record.category)) addError(errors, `${prefix}.category`, "is not an allowed category");
97
+ if (!CONFIRMATION_STATUSES.has(record.status)) addError(errors, `${prefix}.status`, "is not an allowed status");
98
+ if (!Number.isInteger(record.frontier_round) || record.frontier_round < 1) addError(errors, `${prefix}.frontier_round`, "must be a positive integer");
99
+ if (!text(record.question)) addError(errors, `${prefix}.question`, "must be provided");
100
+ if (!text(record.recommendation)) addError(errors, `${prefix}.recommendation`, "must be provided");
101
+ if (!Array.isArray(record.depends_on)) addError(errors, `${prefix}.depends_on`, "must be an array");
102
+ if (!Array.isArray(record.evidence_refs)) addError(errors, `${prefix}.evidence_refs`, "must be an array");
103
+ if (!Array.isArray(record.authority_refs)) addError(errors, `${prefix}.authority_refs`, "must be an array");
104
+ if (!NEXT_ACTION_OWNERS.has(record.next_action_owner)) addError(errors, `${prefix}.next_action_owner`, "must be agent, user, or external");
105
+ if (!text(record.next_action)) addError(errors, `${prefix}.next_action`, "must be explicit and idempotent");
106
+ if (record.status === "proposed" && record.decision !== null && record.decision !== undefined) addError(errors, `${prefix}.decision`, "must be null while proposed");
107
+ if (["confirmed", "rejected"].includes(record.status)) {
108
+ if (!text(record.decision)) addError(errors, `${prefix}.decision`, "is required after a decision");
109
+ if (!text(record.actor)) addError(errors, `${prefix}.actor`, "is required after a decision");
110
+ if (!text(record.confirmed_at)) addError(errors, `${prefix}.confirmed_at`, "is required after a decision");
111
+ if (!list(record.authority_refs).length && !options.allowUnlinkedConfirmed) addError(errors, `${prefix}.authority_refs`, "must point to the owning artifact or handoff");
112
+ }
113
+ if (["observed_fact", "execution_fact"].includes(record.category) && !list(record.evidence_refs).length) addError(errors, `${prefix}.evidence_refs`, "is required for an observed fact");
114
+ if (record.status === "deferred" && record.next_action_owner === "agent") addError(errors, `${prefix}.next_action_owner`, "deferred decisions require user or external ownership");
115
+ if (record.status === "superseded" && !text(record.supersedes) && !text(record.superseded_by)) addError(errors, prefix, "must point to a replacement or predecessor");
116
+ });
117
+
118
+ for (const record of records) {
119
+ for (const dependencyId of list(record.depends_on)) {
120
+ if (!byId.has(dependencyId)) addError(errors, `records.${record.confirmation_id}.depends_on`, `unknown dependency ${dependencyId}`);
121
+ if (dependencyId === record.confirmation_id) addError(errors, `records.${record.confirmation_id}.depends_on`, "cannot depend on itself");
122
+ }
123
+ if (record.supersedes && !byId.has(record.supersedes)) addError(errors, `records.${record.confirmation_id}.supersedes`, `unknown record ${record.supersedes}`);
124
+ if (record.superseded_by && !byId.has(record.superseded_by)) addError(errors, `records.${record.confirmation_id}.superseded_by`, `unknown record ${record.superseded_by}`);
125
+ }
126
+ detectCycles(records, byId, errors);
127
+ const frontier = records.filter((record) => record.status === "proposed"
128
+ && list(record.depends_on).every((dependencyId) => recordIsClosed(byId.get(dependencyId), byId)));
129
+ const blockedProposals = records.filter((record) => record.status === "proposed" && !frontier.includes(record));
130
+ if (summary.shared_understanding === true && frontier.length) addError(errors, "shared_understanding", "cannot be true while frontier is non-empty");
131
+ if (summary.shared_understanding === true && blockedProposals.length) addError(errors, "shared_understanding", "cannot be true while a proposed decision is blocked");
132
+ if (summary.shared_understanding === true) {
133
+ if (!text(summary.shared_understanding_actor)) addError(errors, "shared_understanding_actor", "is required after user confirmation");
134
+ if (!text(summary.shared_understanding_confirmed_at)) addError(errors, "shared_understanding_confirmed_at", "is required after user confirmation");
135
+ }
136
+ return {
137
+ valid: errors.length === 0,
138
+ errors,
139
+ warnings,
140
+ frontier: frontier.map((record) => record.confirmation_id),
141
+ blocked_proposals: blockedProposals.map((record) => record.confirmation_id),
142
+ };
143
+ }
144
+
145
+ export function readConfirmationSummary(file, options = {}) {
146
+ const absolute = path.resolve(file);
147
+ const bytes = fs.readFileSync(absolute);
148
+ const summary = parseYaml(bytes.toString("utf8"));
149
+ return {
150
+ file: absolute,
151
+ checksum: crypto.createHash("sha256").update(bytes).digest("hex"),
152
+ summary,
153
+ validation: validateConfirmationSummary(summary, options),
154
+ };
155
+ }
156
+
157
+ export function writeConfirmationSummary(file, summary) {
158
+ const validation = validateConfirmationSummary(summary);
159
+ if (!validation.valid) throw new Error(`Invalid confirmation summary:\n${validation.errors.join("\n")}`);
160
+ const absolute = path.resolve(file);
161
+ if (fs.existsSync(absolute)) {
162
+ const existing = readConfirmationSummary(absolute, { allowUnlinkedConfirmed: false });
163
+ if (existing.summary.summary_id !== summary.summary_id) throw new Error("summary_id cannot change for an existing confirmation summary");
164
+ if (summary.version < existing.summary.version) throw new Error("confirmation summary version cannot move backwards");
165
+ const incoming = new Map(list(summary.records).map((record) => [record.confirmation_id, record]));
166
+ for (const record of list(existing.summary.records)) {
167
+ if (CLOSED_STATUSES.has(record.status) && incoming.has(record.confirmation_id)
168
+ && !sameRecord(record, incoming.get(record.confirmation_id))) {
169
+ throw new Error(`closed confirmation ${record.confirmation_id} is immutable; append a superseding record`);
170
+ }
171
+ }
172
+ if (summary.version === existing.summary.version && sameRecord(existing.summary, summary)) return validation;
173
+ }
174
+ fs.mkdirSync(path.dirname(absolute), { recursive: true });
175
+ fs.writeFileSync(absolute, stringifyYaml(summary), "utf8");
176
+ return validation;
177
+ }
@@ -8,6 +8,7 @@ import {
8
8
  stableValue,
9
9
  withFormalStateReadLock,
10
10
  } from "./delivery-state.mjs";
11
+ import { readConfirmationSummary } from "./communication-confirmation.mjs";
11
12
  import { inspectGitRepository, readWorkspaceConfiguration } from "./workspace.mjs";
12
13
 
13
14
  const IGNORED_DIRECTORIES = new Set([
@@ -32,6 +33,50 @@ const IGNORED_DIRECTORIES = new Set([
32
33
  "target",
33
34
  ]);
34
35
  const VALID_NEXT_ACTION_OWNERS = new Set(["agent", "user", "external"]);
36
+ const CODEBASE_DOCUMENTATION_FILES = [
37
+ { path: "AGENTS.md", kind: "agent-entrypoint" },
38
+ { path: "CODEBASE_GUIDE.md", kind: "maintainer-guide" },
39
+ { path: "docs/CODEBASE_INDEX.md", kind: "navigation-index" },
40
+ ];
41
+ const CODEBASE_DOCUMENTATION_STATUS = new Set(["observed", "approved", "stale"]);
42
+
43
+ export function inspectConfirmationSummaries(root) {
44
+ const base = path.join(root, ".workflow", "iterations");
45
+ const summaries = [];
46
+ const errors = [];
47
+ if (!fs.existsSync(base)) return { status: "absent", summaries, errors };
48
+ const visit = (directory) => {
49
+ let entries;
50
+ try { entries = fs.readdirSync(directory, { withFileTypes: true }); } catch (error) {
51
+ errors.push(`${toPosix(path.relative(root, directory))}: ${error.message}`);
52
+ return;
53
+ }
54
+ for (const entry of entries) {
55
+ const absolute = path.join(directory, entry.name);
56
+ if (entry.isDirectory()) visit(absolute);
57
+ else if (entry.isFile() && entry.name === "confirmation-summary.yaml") {
58
+ try {
59
+ const result = readConfirmationSummary(absolute);
60
+ summaries.push({
61
+ path: toPosix(path.relative(root, absolute)),
62
+ checksum: sha256File(absolute),
63
+ summary_id: result.summary.summary_id,
64
+ version: result.summary.version,
65
+ frontier: result.validation.frontier,
66
+ blocked_proposals: result.validation.blocked_proposals,
67
+ valid: result.validation.valid,
68
+ errors: result.validation.errors,
69
+ });
70
+ if (!result.validation.valid) errors.push(...result.validation.errors.map((item) => `${toPosix(path.relative(root, absolute))}: ${item}`));
71
+ } catch (error) {
72
+ errors.push(`${toPosix(path.relative(root, absolute))}: ${error.message}`);
73
+ }
74
+ }
75
+ }
76
+ };
77
+ visit(base);
78
+ return { status: errors.length ? "invalid" : summaries.length ? "valid" : "absent", summaries, errors };
79
+ }
35
80
 
36
81
  const SOURCE_EXTENSIONS = new Map([
37
82
  [".c", "C"],
@@ -255,6 +300,199 @@ function scanProjectFiles(root, maxFiles = 10_000) {
255
300
  };
256
301
  }
257
302
 
303
+ function hasSensitiveContent(text) {
304
+ return /BEGIN (?:RSA|OPENSSH|EC|DSA) PRIVATE KEY|(?:AKIA|ASIA)[A-Z0-9]{16}|(?:ghp|github_pat)_[A-Za-z0-9_]{20,}/.test(text)
305
+ || /(?:password|secret|token|api[_-]?key)\s*[:=]\s*["'][^"']{8,}["']/i.test(text);
306
+ }
307
+
308
+ function documentationMetadata(text) {
309
+ const status = text.match(/(?:^|\|)\s*(?:状态|项目状态)\s*\|\s*`?([^|\n`]+)`?/m)?.[1]?.trim().toLowerCase() || null;
310
+ const sourceCommit = text.match(/(?:适用代码提交|对应代码提交|source_commit)\s*\|\s*`?([^|\n`]+)`?/m)?.[1]?.trim()
311
+ || text.match(/`source_commit`\s*\|\s*`?([^|\n`]+)`?/m)?.[1]?.trim()
312
+ || null;
313
+ const lastReviewed = text.match(/(?:最后核对|last_reviewed)\s*\|\s*`?([^|\n`]+)`?/m)?.[1]?.trim()
314
+ || text.match(/`last_reviewed`\s*\|\s*`?([^|\n`]+)`?/m)?.[1]?.trim()
315
+ || null;
316
+ return { status, source_commit: sourceCommit, last_reviewed: lastReviewed };
317
+ }
318
+
319
+ function documentationPathEntries(text) {
320
+ const entries = [];
321
+ let header = null;
322
+ for (const [index, line] of text.split(/\r?\n/).entries()) {
323
+ if (!line.trim().startsWith("|")) continue;
324
+ const cells = line.split("|").slice(1, -1).map((cell) => cell.trim());
325
+ if (!cells.length) continue;
326
+ if (cells.some((cell) => /^(路径|文件)$/.test(cell))) {
327
+ header = cells;
328
+ continue;
329
+ }
330
+ if (!header || cells.every((cell) => /^:?-{3,}:?$/.test(cell))) continue;
331
+ const pathIndex = header.findIndex((cell) => /^(路径|文件)$/.test(cell));
332
+ if (pathIndex < 0 || !cells[pathIndex]) continue;
333
+ entries.push({ value: cells[pathIndex].replaceAll("`", "").trim(), line: index + 1 });
334
+ }
335
+ return entries;
336
+ }
337
+
338
+ function documentationLinks(text) {
339
+ return [...text.matchAll(/\[[^\]]*\]\(([^)]+)\)/g)].map((match) => match[1].trim());
340
+ }
341
+
342
+ export function inspectCodebaseDocumentation(root, git = inspectGitRepository(root)) {
343
+ const files = [];
344
+ const errors = [];
345
+ const warnings = [];
346
+ const missing = [];
347
+ const invalid = [];
348
+ const stale = [];
349
+ const statuses = [];
350
+ if (!fs.existsSync(root) || !fs.statSync(root).isDirectory()) {
351
+ return {
352
+ status: "missing",
353
+ files: [],
354
+ missing: CODEBASE_DOCUMENTATION_FILES.map((item) => item.path),
355
+ invalid: [],
356
+ stale: [],
357
+ sensitive_file_count: 0,
358
+ errors: [],
359
+ warnings: ["代码仓库目录不存在,无法发现项目说明书和索引。"],
360
+ };
361
+ }
362
+
363
+ for (const expected of CODEBASE_DOCUMENTATION_FILES) {
364
+ const absolute = path.join(root, expected.path);
365
+ if (!fs.existsSync(absolute)) {
366
+ missing.push(expected.path);
367
+ continue;
368
+ }
369
+ let stat;
370
+ try {
371
+ stat = fs.lstatSync(absolute);
372
+ } catch (error) {
373
+ invalid.push({ path: expected.path, reason: `无法读取文件:${error.code || error.message}` });
374
+ continue;
375
+ }
376
+ if (stat.isSymbolicLink()) {
377
+ invalid.push({ path: expected.path, reason: "说明文件不能是符号链接" });
378
+ continue;
379
+ }
380
+ if (!stat.isFile()) {
381
+ invalid.push({ path: expected.path, reason: "说明路径必须是普通文件" });
382
+ continue;
383
+ }
384
+ let text;
385
+ try {
386
+ text = fs.readFileSync(absolute, "utf8");
387
+ } catch (error) {
388
+ invalid.push({ path: expected.path, reason: `无法读取文件:${error.code || error.message}` });
389
+ continue;
390
+ }
391
+ const metadata = documentationMetadata(text);
392
+ const record = { path: expected.path, kind: expected.kind, ...metadata, bytes: stat.size };
393
+ files.push(record);
394
+ if (expected.kind !== "agent-entrypoint") {
395
+ if (!metadata.status) invalid.push({ path: expected.path, reason: "缺少 status/状态字段" });
396
+ if (!metadata.source_commit) invalid.push({ path: expected.path, reason: "缺少 source_commit/适用代码提交字段" });
397
+ if (!metadata.last_reviewed) invalid.push({ path: expected.path, reason: "缺少 last_reviewed/最后核对字段" });
398
+ }
399
+ if (metadata.status && !CODEBASE_DOCUMENTATION_STATUS.has(metadata.status)) {
400
+ invalid.push({ path: expected.path, reason: `status 无效:${metadata.status}` });
401
+ }
402
+ if (metadata.status) statuses.push(metadata.status);
403
+ if (hasSensitiveContent(text)) {
404
+ invalid.push({ path: expected.path, reason: "疑似包含凭据、私钥或敏感值" });
405
+ }
406
+ if (git?.head && metadata.source_commit && !/^<.*>$/.test(metadata.source_commit)
407
+ && metadata.source_commit !== "-" && metadata.source_commit !== git.head) {
408
+ stale.push({ path: expected.path, reason: `source_commit ${metadata.source_commit} 与当前 HEAD ${git.head} 不一致` });
409
+ }
410
+ if (expected.kind === "maintainer-guide") {
411
+ for (const heading of ["项目概览", "快速开始", "仓库地图", "工程分层", "工程约定", "变更工作流", "风险和保护边界", "文档维护"]) {
412
+ if (!new RegExp(`^## .*${heading}`, "m").test(text)) {
413
+ invalid.push({ path: expected.path, reason: `缺少章节:${heading}` });
414
+ }
415
+ }
416
+ }
417
+ if (expected.kind === "navigation-index") {
418
+ for (const field of ["路径", "职责", "主要入口", "关联测试", "风险", "状态", "source_commit", "last_reviewed"]) {
419
+ if (!text.includes(field)) invalid.push({ path: expected.path, reason: `缺少索引字段:${field}` });
420
+ }
421
+ for (const entry of documentationPathEntries(text)) {
422
+ const value = entry.value;
423
+ if (!value || value === "-" || /^<.*>$/.test(value) || value.includes("*")) continue;
424
+ if (path.isAbsolute(value) || value.split("/").includes("..")) {
425
+ invalid.push({ path: expected.path, reason: `第 ${entry.line} 行路径越界:${value}` });
426
+ continue;
427
+ }
428
+ const target = path.resolve(root, value);
429
+ const relative = path.relative(root, target);
430
+ if (relative.startsWith("..") || path.isAbsolute(relative)) {
431
+ invalid.push({ path: expected.path, reason: `第 ${entry.line} 行路径越界:${value}` });
432
+ continue;
433
+ }
434
+ if (!fs.existsSync(target)) {
435
+ invalid.push({ path: expected.path, reason: `第 ${entry.line} 行引用路径不存在:${value}` });
436
+ continue;
437
+ }
438
+ if (fs.lstatSync(target).isSymbolicLink()) {
439
+ invalid.push({ path: expected.path, reason: `第 ${entry.line} 行引用符号链接:${value}` });
440
+ }
441
+ }
442
+ }
443
+ const seenPaths = new Set();
444
+ for (const entry of documentationPathEntries(text)) {
445
+ if (/^<.*>$/.test(entry.value) || entry.value === "-" || entry.value.includes("*")) continue;
446
+ if (seenPaths.has(entry.value)) {
447
+ invalid.push({ path: expected.path, reason: `第 ${entry.line} 行重复索引路径:${entry.value}` });
448
+ }
449
+ seenPaths.add(entry.value);
450
+ }
451
+ for (const link of documentationLinks(text)) {
452
+ const targetValue = link.split(/[?#]/, 1)[0];
453
+ if (!targetValue || targetValue.startsWith("#") || /^[a-z][a-z0-9+.-]*:/i.test(targetValue)) continue;
454
+ if (path.isAbsolute(targetValue) || targetValue.split("/").includes("..")) {
455
+ invalid.push({ path: expected.path, reason: `Markdown 链接越界:${link}` });
456
+ continue;
457
+ }
458
+ const target = path.resolve(root, path.dirname(expected.path), targetValue);
459
+ const relative = path.relative(root, target);
460
+ if (relative.startsWith("..") || path.isAbsolute(relative) || !fs.existsSync(target)) {
461
+ invalid.push({ path: expected.path, reason: `Markdown 链接路径不存在:${link}` });
462
+ }
463
+ }
464
+ }
465
+
466
+ if (missing.length) warnings.push(`缺少代码仓库说明文件:${missing.join(", ")}`);
467
+ if (invalid.length) errors.push(...invalid.map((item) => `${item.path}: ${item.reason}`));
468
+ if (stale.length) warnings.push(...stale.map((item) => `${item.path}: ${item.reason}`));
469
+ const hasTemplatePlaceholders = files.some((file) => file.source_commit?.startsWith("<") || file.status === null);
470
+ if (new Set(statuses).size > 1) {
471
+ errors.push(`代码仓库说明文档状态冲突:${[...new Set(statuses)].join(", ")}`);
472
+ }
473
+ const status = invalid.length || errors.length
474
+ ? "invalid"
475
+ : stale.length
476
+ ? "stale"
477
+ : missing.length
478
+ ? "missing"
479
+ : statuses.some((value) => value === "stale")
480
+ ? "stale"
481
+ : hasTemplatePlaceholders || statuses.some((value) => value === "observed")
482
+ ? "observed"
483
+ : "approved";
484
+ return {
485
+ status,
486
+ files,
487
+ missing,
488
+ invalid,
489
+ stale,
490
+ sensitive_file_count: invalid.filter((item) => item.reason.includes("敏感值")).length,
491
+ errors,
492
+ warnings,
493
+ };
494
+ }
495
+
258
496
  function gitState(root) {
259
497
  const run = (args) => spawnSync("git", args, { cwd: root, encoding: "utf8", windowsHide: true });
260
498
  const topLevel = run(["rev-parse", "--show-toplevel"]);
@@ -593,8 +831,19 @@ export function inspectProjectState(target) {
593
831
  }
594
832
  }
595
833
 
834
+ const codeGit = inspectGitRepository(codeRoot);
835
+ const documentation = inspectCodebaseDocumentation(codeRoot, codeGit);
836
+ const confirmations = inspectConfirmationSummaries(root);
837
+ if (confirmations.errors.length) errors.push(...confirmations.errors);
838
+ codebase.summary.documentation = documentation;
839
+ codebase.summary.confirmations = confirmations;
840
+ errors.push(...documentation.errors);
841
+ const documentationWarnings = documentation.warnings;
842
+
596
843
  let state;
597
- if (!installation.detected) {
844
+ if (errors.length) {
845
+ state = "workflow-inconsistent";
846
+ } else if (!installation.detected) {
598
847
  state = codebase.summary.has_code ? "codebase-uninitialized" : "empty-uninitialized";
599
848
  } else if (!installation.complete || errors.length) {
600
849
  state = "workflow-inconsistent";
@@ -604,7 +853,6 @@ export function inspectProjectState(target) {
604
853
  state = "workflow-ready";
605
854
  }
606
855
  const recommendation = recommendationFor(state, workflow, runtime);
607
- const codeGit = inspectGitRepository(codeRoot);
608
856
  const workflowGit = inspectGitRepository(root);
609
857
  const syncWarnings = [];
610
858
  if (workflowGit.sync_status === "uncommitted") {
@@ -625,11 +873,12 @@ export function inspectProjectState(target) {
625
873
  workflow,
626
874
  runtime,
627
875
  codebase: codebase.summary,
876
+ confirmations,
628
877
  git: codeGit,
629
878
  workflow_git: workflowGit,
630
879
  recommendation,
631
880
  errors,
632
- warnings: [...codebase.warnings, ...syncWarnings],
881
+ warnings: [...codebase.warnings, ...documentationWarnings, ...syncWarnings],
633
882
  };
634
883
  }
635
884
 
@@ -701,6 +950,10 @@ function adoptionMarkdown(initialState, scan, result) {
701
950
  if (!artifactTypes.some((type) => /architecture.*baseline/.test(type))) gaps.push("缺少经确认的架构基线。");
702
951
  if (scan.summary.has_code && !scan.summary.categories.test) gaps.push("未发现可识别的自动化测试文件,需人工核对测试入口。");
703
952
  if (!result.git.repository) gaps.push("当前目录不是有效 Git 仓库,分支和提交身份不可用。");
953
+ const documentation = result.codebase.documentation;
954
+ if (documentation?.status === "missing") gaps.push("代码仓库说明书或导航索引缺失,需要在维护者确认后补齐。");
955
+ if (documentation?.status === "invalid") gaps.push("代码仓库说明书或导航索引存在结构、路径或敏感内容问题,Bootstrap 已阻止将其视为可信上下文。");
956
+ if (documentation?.status === "stale") gaps.push("代码仓库说明书或导航索引相对当前提交过期,需要更新 source_commit 并重新审阅。");
704
957
  return `# 存量项目接入评估
705
958
 
706
959
  ## 探测结论
@@ -712,6 +965,14 @@ function adoptionMarkdown(initialState, scan, result) {
712
965
  - 检测技术:${scan.summary.technologies.join("、") || "未识别"}
713
966
  - 排除的敏感文件:${scan.summary.sensitive_file_count}
714
967
 
968
+ ## 代码仓库说明书与导航
969
+
970
+ - 状态:\`${documentation?.status || "missing"}\`
971
+ - 已发现:${(documentation?.files || []).filter((item) => item.status !== "missing").map((item) => item.path).join("、") || "无"}
972
+ - 缺失:${(documentation?.missing || []).join("、") || "无"}
973
+ - 失效:${(documentation?.invalid || []).map((item) => item.path).join("、") || "无"}
974
+ - 过期:${(documentation?.stale || []).map((item) => item.path).join("、") || "无"}
975
+
715
976
  ## 上下文边界
716
977
 
717
978
  本报告只描述代码和仓库的可观察现状。代码行为不是已批准产品需求,自动归纳内容均保持 \`draft\`,必须通过产品和架构流程核对后才能成为正式基线。
@@ -777,6 +1038,7 @@ function writeBootstrapArtifacts(root, initialState, result, sourceScan = null)
777
1038
  detected_at: result.detected_at,
778
1039
  repository: result.git,
779
1040
  codebase: scan.summary,
1041
+ documentation: result.codebase.documentation,
780
1042
  command_candidates: packageCommands(root, scan.files),
781
1043
  constraints: [
782
1044
  "Observed code is not an approved product requirement.",
@@ -833,6 +1095,8 @@ function writeBootstrapArtifacts(root, initialState, result, sourceScan = null)
833
1095
  workflow: result.workflow,
834
1096
  runtime: result.runtime,
835
1097
  codebase: result.codebase,
1098
+ documentation: result.codebase.documentation,
1099
+ confirmations: result.confirmations,
836
1100
  git: result.git,
837
1101
  recommendation: result.recommendation,
838
1102
  errors: result.errors,
@@ -8,6 +8,7 @@ import { fileURLToPath } from "node:url";
8
8
  import { isSeq, parse as parseYaml, parseDocument, stringify as stringifyYaml } from "yaml";
9
9
  import { assertPathInside, atomicWrite, filesUnder } from "./fs-utils.mjs";
10
10
  import { inspectCodeGraph } from "./codegraph.mjs";
11
+ import { inspectCodebaseDocumentation, inspectConfirmationSummaries } from "./project-bootstrap.mjs";
11
12
  import { executeDeliveryStateCommand } from "./delivery-state.mjs";
12
13
  import { inspectHookConfig, managedHookConfigProjection, mergeHookConfig } from "./toml-hooks.mjs";
13
14
  import {
@@ -522,7 +523,7 @@ function bootstrapRuntimeSources(packageRoot, sourceTemplate) {
522
523
  relative: "bootstrap.mjs",
523
524
  },
524
525
  ...[
525
- "project-bootstrap.mjs", "workspace.mjs", "yaml-runtime.mjs", "delivery-state.mjs",
526
+ "project-bootstrap.mjs", "communication-confirmation.mjs", "workspace.mjs", "yaml-runtime.mjs", "delivery-state.mjs",
526
527
  "history-anchor.mjs", "fs-utils.mjs",
527
528
  ].map((name) => ({
528
529
  source: path.join(packageRoot, "lib", name),
@@ -1918,6 +1919,28 @@ export function doctorProject(target, options = {}) {
1918
1919
 
1919
1920
  const workflowGit = gitTopLevel(target);
1920
1921
  const codeGit = workspace ? gitTopLevel(workspace.codeRoot) : null;
1922
+ if (workspace) {
1923
+ const documentation = inspectCodebaseDocumentation(workspace.codeRoot);
1924
+ checks.push({
1925
+ ok: documentation.status !== "invalid",
1926
+ required: false,
1927
+ scope: "codebase-documentation",
1928
+ message: documentation.status === "missing"
1929
+ ? "code repository handbook/index are not installed; Bootstrap or maintainers may add them"
1930
+ : `code repository handbook/index status: ${documentation.status}`,
1931
+ });
1932
+ const confirmations = inspectConfirmationSummaries(target);
1933
+ checks.push({
1934
+ ok: confirmations.status !== "invalid",
1935
+ required: false,
1936
+ scope: "communication-confirmation",
1937
+ message: confirmations.status === "absent"
1938
+ ? "no communication confirmation summaries are present"
1939
+ : confirmations.status === "invalid"
1940
+ ? `communication confirmation summaries are invalid: ${confirmations.errors.join("; ")}`
1941
+ : `${confirmations.summaries.length} communication confirmation summary(ies) validate; frontier=${confirmations.summaries.flatMap((item) => item.frontier).join(",") || "empty"}`,
1942
+ });
1943
+ }
1921
1944
  const workflowCommonGit = workflowGit ? gitCommonDirectory(target) : null;
1922
1945
  const codeCommonGit = codeGit && workspace ? gitCommonDirectory(workspace.codeRoot) : null;
1923
1946
  checks.push({
@@ -2075,6 +2098,7 @@ export function doctorProject(target, options = {}) {
2075
2098
  const bootstrapFiles = [
2076
2099
  "bootstrap.mjs",
2077
2100
  "project-bootstrap.mjs",
2101
+ "communication-confirmation.mjs",
2078
2102
  "workspace.mjs",
2079
2103
  "yaml-runtime.mjs",
2080
2104
  "delivery-state.mjs",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-delivery-workflow",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "description": "Project-scoped Codex skills and hooks for artifact-driven AI software delivery",
5
5
  "type": "module",
6
6
  "repository": {
@@ -52,6 +52,10 @@ Treat repository scans as observations, not approved product or architecture tru
52
52
 
53
53
  Do not replace existing product, architecture, test, or release baselines. Keep the project-state report and related observations outside the formal artifact registry while they remain working drafts, preserve every timestamped bootstrap run, and update only the `.workflow/delivery/bootstrap/latest.yaml` pointer.
54
54
 
55
+ ### Codebase Handbook Discovery
56
+
57
+ For the configured nested code repository, inspect `AGENTS.md`, `CODEBASE_GUIDE.md`, and `docs/CODEBASE_INDEX.md` as read-only maintainer documents. The report records `observed`, `approved`, or `stale`; missing files are a warning, while malformed, path-invalid, symlink, or sensitive-content findings fail closed. Compare each declared `source_commit` with the code repository HEAD. Never infer an approved rule from the generated inventory, and never overwrite these files during bootstrap or upgrade.
58
+
55
59
  ## Resume Boundary
56
60
 
57
61
  Codex lifecycle Hooks provide audit and heartbeat data only. Versioned Evolution Hooks may return `pass`, `warn`, or an approved `block`, but they never select or complete tasks and never mutate formal workflow state. A bootstrap report may recommend recovery, but `$ai-delivery-checkpoint-task` remains authoritative for task order, dependency readiness, worktree reconciliation, and idempotent continuation. Route pending feedback and extension evaluation to `$ai-delivery-evolve-workflow` only at a task, version, or explicit review safe point.
@@ -60,6 +60,8 @@ Checkpoint after every externally meaningful milestone, before a long command, a
60
60
 
61
61
  Use shared shards and the newest shared checkpoint as the portable recovery source. If local runtime is absent and `next_action_owner` is `agent`, `recover` restores it from shared state. For `user` or `external`, report the wait state without hydration; after the input arrives, `activate` performs the atomic hydration and owner transfer. If local and shared revisions or semantic checksums differ, stop for reconciliation. Inspect both Git repositories before resuming. Report pending `.workflow/delivery/runtime/evolution/pending/` events, but defer their evaluation until the recovered task reaches a safe point.
62
62
 
63
+ Confirmation waits persist the summary path, question identity, frontier round, and an idempotent next action. `user` and `external` ownership remains strict zero-mutation wait until explicit input transfer.
64
+
63
65
  Generate a compact recovery report from snapshots, checkpoints, Hook events, Git, and workspace state. Return active work and failed tasks in `retryable_task_ids` by default; use `--include-closed` only when completed or abandoned details are required for an explicit audit. For each iteration, report total, completed, closed, remaining, active, retryable, queued, dependency-ready, and waiting tasks plus one deterministic `recommended_task_id`. Prefer an interrupted or active task, then the lowest-sequence failed task requiring `activate --new-attempt --phase repair`, then the lowest-sequence queued task whose dependencies are all `completed`. Resume only idempotent steps. Re-read production and external systems before any deployment recovery; never replay production deployment automatically.
64
66
 
65
67
  ## Hook Boundary