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.
- package/docs/FILE-REFERENCE.zh-CN.md +1 -1
- package/docs/MAINTENANCE-VERIFICATION.zh-CN.md +2 -2
- package/docs/PROJECT-MANUAL.zh-CN.md +1 -1
- package/docs/agents/workflow-manager-current-state.md +1 -1
- package/lib/communication-confirmation.mjs +177 -0
- package/lib/project-bootstrap.mjs +267 -3
- package/lib/project-installer.mjs +25 -1
- package/package.json +1 -1
- package/skills/ai-delivery-bootstrap/SKILL.md +4 -0
- package/skills/ai-delivery-checkpoint-task/SKILL.md +2 -0
- package/skills/ai-delivery-checkpoint-task/references/checkpoint-contract.md +2 -0
- package/skills/ai-delivery-close-version/SKILL.md +13 -10
- package/skills/ai-delivery-close-version/references/version-closeout-contract.md +2 -1
- package/skills/ai-delivery-define-product/SKILL.md +6 -3
- package/skills/ai-delivery-design-architecture/SKILL.md +10 -5
- package/skills/ai-delivery-design-architecture/assets/architecture-template/architecture-gate.yaml +2 -0
- package/skills/ai-delivery-design-architecture/assets/architecture-template/prototype-architecture-validation.yaml +2 -0
- package/skills/ai-delivery-design-architecture/references/architecture-contract.md +4 -0
- package/skills/ai-delivery-design-experience/SKILL.md +5 -3
- package/skills/ai-delivery-design-experience/assets/experience-template/product-prototype-reconciliation.yaml +2 -0
- package/skills/ai-delivery-design-experience/assets/experience-template/ui-handoff.yaml +3 -0
- package/skills/ai-delivery-design-experience/references/experience-contract.md +2 -2
- package/skills/ai-delivery-design-tests/SKILL.md +4 -0
- package/skills/ai-delivery-develop-iteration/SKILL.md +6 -1
- package/skills/ai-delivery-develop-iteration/assets/development-template/candidate-manifest.yaml +2 -0
- package/skills/ai-delivery-execute-work-package/SKILL.md +4 -2
- package/skills/ai-delivery-orchestrate/SKILL.md +2 -0
- package/skills/ai-delivery-orchestrate/assets/codebase-template/AGENTS.md +14 -0
- package/skills/ai-delivery-orchestrate/assets/codebase-template/CODEBASE_GUIDE.md +124 -0
- package/skills/ai-delivery-orchestrate/assets/codebase-template/docs/CODEBASE_INDEX.md +87 -0
- package/skills/ai-delivery-orchestrate/assets/communication-template/confirmation-examples.yaml +67 -0
- package/skills/ai-delivery-orchestrate/assets/communication-template/confirmation-summary.yaml +42 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/iterations/ITER-0001/02-solution-design/product-impact-assessment.yaml +42 -0
- package/skills/ai-delivery-orchestrate/references/codebase-document-contract.md +91 -0
- package/skills/ai-delivery-orchestrate/references/communication-confirmation-contract.md +52 -0
- package/skills/ai-delivery-orchestrate/references/product-impact-assessment-contract.md +87 -0
- package/skills/ai-delivery-orchestrate/scripts/confirmation-state.mjs +30 -0
- package/skills/ai-delivery-orchestrate-release/SKILL.md +3 -1
- package/skills/ai-delivery-plan-iteration/SKILL.md +6 -1
- package/skills/ai-delivery-prepare-platform/SKILL.md +1 -1
- package/skills/ai-delivery-prepare-release/SKILL.md +2 -0
- package/skills/ai-delivery-review-change/SKILL.md +3 -1
- package/skills/ai-delivery-validate-artifacts/SKILL.md +10 -7
- package/skills/ai-delivery-validate-artifacts/references/artifact-contract.md +2 -1
- package/skills/ai-delivery-verify-candidate/SKILL.md +5 -2
- 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.
|
|
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.
|
|
9
|
-
npm run release:check -- verify --version 0.
|
|
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.
|
|
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.
|
|
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 (
|
|
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
|
@@ -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
|