ai-delivery-workflow 0.2.2

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 (211) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +112 -0
  3. package/bin/ai-delivery.mjs +138 -0
  4. package/bin/validate-skills.mjs +12 -0
  5. package/docs/DUAL-REPOSITORY-WORKSPACE.zh-CN.md +125 -0
  6. package/docs/EVOLUTION-MAINTENANCE.zh-CN.md +75 -0
  7. package/docs/EVOLUTION-USER-GUIDE.zh-CN.md +160 -0
  8. package/docs/FILE-REFERENCE.zh-CN.md +503 -0
  9. package/docs/PROJECT-MANUAL.zh-CN.md +931 -0
  10. package/docs/STATE-CLI-MAINTENANCE.zh-CN.md +135 -0
  11. package/docs/STATE-CLI-USER-GUIDE.zh-CN.md +186 -0
  12. package/docs/VIEWER-MAINTENANCE.zh-CN.md +373 -0
  13. package/docs/VIEWER-USER-GUIDE.zh-CN.md +235 -0
  14. package/lib/delivery-state.mjs +1220 -0
  15. package/lib/evolution.mjs +971 -0
  16. package/lib/project-bootstrap.mjs +927 -0
  17. package/lib/project-installer.mjs +1184 -0
  18. package/lib/skill-validator.mjs +154 -0
  19. package/lib/toml-hooks.mjs +97 -0
  20. package/lib/workspace.mjs +302 -0
  21. package/lib/yaml-runtime.mjs +18 -0
  22. package/package.json +39 -0
  23. package/skills/ai-delivery-assemble-release/SKILL.md +27 -0
  24. package/skills/ai-delivery-assemble-release/agents/openai.yaml +4 -0
  25. package/skills/ai-delivery-assemble-release/assets/release-candidate-manifest.yaml +22 -0
  26. package/skills/ai-delivery-assemble-release/assets/release-git-plan.yaml +9 -0
  27. package/skills/ai-delivery-assemble-release/assets/release-material-index.csv +2 -0
  28. package/skills/ai-delivery-assemble-release/assets/version-inclusion.csv +2 -0
  29. package/skills/ai-delivery-assemble-release/references/release-assembly-contract.md +7 -0
  30. package/skills/ai-delivery-assemble-release/scripts/plan-release.mjs +298 -0
  31. package/skills/ai-delivery-bootstrap/SKILL.md +59 -0
  32. package/skills/ai-delivery-bootstrap/agents/openai.yaml +4 -0
  33. package/skills/ai-delivery-bootstrap/references/bootstrap-contract.md +70 -0
  34. package/skills/ai-delivery-checkpoint-task/SKILL.md +63 -0
  35. package/skills/ai-delivery-checkpoint-task/agents/openai.yaml +4 -0
  36. package/skills/ai-delivery-checkpoint-task/assets/codex-hook-config.toml +92 -0
  37. package/skills/ai-delivery-checkpoint-task/assets/runtime-template/resume.md +15 -0
  38. package/skills/ai-delivery-checkpoint-task/assets/runtime-template/task.json +30 -0
  39. package/skills/ai-delivery-checkpoint-task/assets/runtime-template/version-archive.json +10 -0
  40. package/skills/ai-delivery-checkpoint-task/references/checkpoint-contract.md +95 -0
  41. package/skills/ai-delivery-checkpoint-task/references/checkpoint-recovery.zh-CN.md +111 -0
  42. package/skills/ai-delivery-checkpoint-task/scripts/hook-event.mjs +131 -0
  43. package/skills/ai-delivery-checkpoint-task/scripts/task-state.mjs +1073 -0
  44. package/skills/ai-delivery-close-version/SKILL.md +33 -0
  45. package/skills/ai-delivery-close-version/agents/openai.yaml +4 -0
  46. package/skills/ai-delivery-close-version/assets/line-advance-record.yaml +12 -0
  47. package/skills/ai-delivery-close-version/assets/version-lineage.yaml +12 -0
  48. package/skills/ai-delivery-close-version/assets/version-manifest.yaml +20 -0
  49. package/skills/ai-delivery-close-version/references/version-closeout-contract.md +20 -0
  50. package/skills/ai-delivery-define-product/SKILL.md +76 -0
  51. package/skills/ai-delivery-define-product/agents/openai.yaml +4 -0
  52. package/skills/ai-delivery-define-product/assets/product-template/acceptance-criteria.csv +1 -0
  53. package/skills/ai-delivery-define-product/assets/product-template/discovery-baseline.md +19 -0
  54. package/skills/ai-delivery-define-product/assets/product-template/feature-detail.md +19 -0
  55. package/skills/ai-delivery-define-product/assets/product-template/feature-terminals.csv +1 -0
  56. package/skills/ai-delivery-define-product/assets/product-template/features.csv +1 -0
  57. package/skills/ai-delivery-define-product/assets/product-template/product-baseline.yaml +8 -0
  58. package/skills/ai-delivery-define-product/assets/product-template/product-manual.md +24 -0
  59. package/skills/ai-delivery-define-product/assets/product-template/terminals.csv +1 -0
  60. package/skills/ai-delivery-define-product/assets/product-template/user-stories.csv +1 -0
  61. package/skills/ai-delivery-define-product/references/product-contract.md +57 -0
  62. package/skills/ai-delivery-deploy-production/SKILL.md +45 -0
  63. package/skills/ai-delivery-deploy-production/agents/openai.yaml +4 -0
  64. package/skills/ai-delivery-deploy-production/references/deployment-contract.md +7 -0
  65. package/skills/ai-delivery-design-architecture/SKILL.md +67 -0
  66. package/skills/ai-delivery-design-architecture/agents/openai.yaml +4 -0
  67. package/skills/ai-delivery-design-architecture/assets/architecture-template/architecture-baseline.md +25 -0
  68. package/skills/ai-delivery-design-architecture/assets/architecture-template/architecture-gate.yaml +15 -0
  69. package/skills/ai-delivery-design-architecture/assets/architecture-template/prototype-architecture-validation.yaml +58 -0
  70. package/skills/ai-delivery-design-architecture/references/architecture-contract.md +32 -0
  71. package/skills/ai-delivery-design-experience/SKILL.md +71 -0
  72. package/skills/ai-delivery-design-experience/agents/openai.yaml +4 -0
  73. package/skills/ai-delivery-design-experience/assets/experience-template/canvas-catalog.csv +1 -0
  74. package/skills/ai-delivery-design-experience/assets/experience-template/canvas-pages.csv +1 -0
  75. package/skills/ai-delivery-design-experience/assets/experience-template/component-state-matrix.csv +1 -0
  76. package/skills/ai-delivery-design-experience/assets/experience-template/design-tokens.json +19 -0
  77. package/skills/ai-delivery-design-experience/assets/experience-template/experience-baseline.yaml +17 -0
  78. package/skills/ai-delivery-design-experience/assets/experience-template/experience-change-domains.csv +1 -0
  79. package/skills/ai-delivery-design-experience/assets/experience-template/experience-change-set.csv +1 -0
  80. package/skills/ai-delivery-design-experience/assets/experience-template/feature-screen-coverage.csv +1 -0
  81. package/skills/ai-delivery-design-experience/assets/experience-template/formal-ui-confirmation.yaml +33 -0
  82. package/skills/ai-delivery-design-experience/assets/experience-template/interaction-contract.csv +1 -0
  83. package/skills/ai-delivery-design-experience/assets/experience-template/low-fidelity-confirmation.yaml +32 -0
  84. package/skills/ai-delivery-design-experience/assets/experience-template/page-catalog.csv +1 -0
  85. package/skills/ai-delivery-design-experience/assets/experience-template/page-component-map.csv +1 -0
  86. package/skills/ai-delivery-design-experience/assets/experience-template/product-prototype-reconciliation.yaml +28 -0
  87. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-adjustment-log.csv +1 -0
  88. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-confirmation.yaml +99 -0
  89. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-file-terminals.csv +1 -0
  90. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-files.csv +1 -0
  91. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-impact-assessment.yaml +25 -0
  92. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-manifest.yaml +74 -0
  93. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-product-coverage.csv +1 -0
  94. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-review-comments.csv +1 -0
  95. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-review-decision.yaml +29 -0
  96. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-review-sessions.csv +1 -0
  97. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-set.yaml +22 -0
  98. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-tool-candidates.csv +1 -0
  99. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-tool-selection.yaml +22 -0
  100. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-traceability.csv +1 -0
  101. package/skills/ai-delivery-design-experience/assets/experience-template/requirement-change-set.csv +1 -0
  102. package/skills/ai-delivery-design-experience/assets/experience-template/screen-states.csv +1 -0
  103. package/skills/ai-delivery-design-experience/assets/experience-template/ui-framework-candidates.csv +1 -0
  104. package/skills/ai-delivery-design-experience/assets/experience-template/ui-framework-selection.yaml +18 -0
  105. package/skills/ai-delivery-design-experience/assets/experience-template/ui-handoff.yaml +44 -0
  106. package/skills/ai-delivery-design-experience/assets/experience-template/visual-direction.yaml +16 -0
  107. package/skills/ai-delivery-design-experience/assets/experience-template/visual-system-confirmation.yaml +31 -0
  108. package/skills/ai-delivery-design-experience/references/experience-contract.md +82 -0
  109. package/skills/ai-delivery-design-experience/references/prototype-management-contract.md +68 -0
  110. package/skills/ai-delivery-design-tests/SKILL.md +56 -0
  111. package/skills/ai-delivery-design-tests/agents/openai.yaml +4 -0
  112. package/skills/ai-delivery-design-tests/assets/test-design-template/iteration-test-contract.md +19 -0
  113. package/skills/ai-delivery-design-tests/assets/test-design-template/prototype-test-scope.csv +1 -0
  114. package/skills/ai-delivery-design-tests/assets/test-design-template/prototype-test-validation.yaml +73 -0
  115. package/skills/ai-delivery-design-tests/references/test-design-contract.md +31 -0
  116. package/skills/ai-delivery-develop-iteration/SKILL.md +66 -0
  117. package/skills/ai-delivery-develop-iteration/agents/openai.yaml +4 -0
  118. package/skills/ai-delivery-develop-iteration/assets/development-template/task-experience-change-traceability.csv +1 -0
  119. package/skills/ai-delivery-develop-iteration/assets/development-template/task-prototype-traceability.csv +1 -0
  120. package/skills/ai-delivery-develop-iteration/references/development-contract.md +39 -0
  121. package/skills/ai-delivery-evolve-workflow/SKILL.md +57 -0
  122. package/skills/ai-delivery-evolve-workflow/agents/openai.yaml +4 -0
  123. package/skills/ai-delivery-evolve-workflow/references/evolution-contract.md +42 -0
  124. package/skills/ai-delivery-execute-test/SKILL.md +34 -0
  125. package/skills/ai-delivery-execute-test/agents/openai.yaml +4 -0
  126. package/skills/ai-delivery-execute-test/references/test-execution-contract.md +7 -0
  127. package/skills/ai-delivery-execute-work-package/SKILL.md +53 -0
  128. package/skills/ai-delivery-execute-work-package/agents/openai.yaml +4 -0
  129. package/skills/ai-delivery-execute-work-package/references/work-package-contract.md +20 -0
  130. package/skills/ai-delivery-manage-git/SKILL.md +60 -0
  131. package/skills/ai-delivery-manage-git/agents/openai.yaml +4 -0
  132. package/skills/ai-delivery-manage-git/references/git-policy.md +42 -0
  133. package/skills/ai-delivery-manage-standards/SKILL.md +44 -0
  134. package/skills/ai-delivery-manage-standards/agents/openai.yaml +4 -0
  135. package/skills/ai-delivery-manage-standards/assets/standards-template/standard.md +25 -0
  136. package/skills/ai-delivery-manage-standards/references/standards-contract.md +20 -0
  137. package/skills/ai-delivery-orchestrate/SKILL.md +105 -0
  138. package/skills/ai-delivery-orchestrate/agents/openai.yaml +4 -0
  139. package/skills/ai-delivery-orchestrate/assets/project-template/.codex/config.toml +92 -0
  140. package/skills/ai-delivery-orchestrate/assets/project-template/.gitattributes +6 -0
  141. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/delivery/artifact-registry.yaml +3 -0
  142. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/delivery/project.yaml +6 -0
  143. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/delivery/standards-baseline.yaml +8 -0
  144. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/delivery/workflow-state.yaml +34 -0
  145. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/evolution/decisions/.gitkeep +0 -0
  146. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/evolution/experiments/.gitkeep +0 -0
  147. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/evolution/feedback/.gitkeep +0 -0
  148. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/evolution/index.yaml +7 -0
  149. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/evolution/promotion/.gitkeep +0 -0
  150. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/evolution/proposals/.gitkeep +0 -0
  151. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/extensions/.gitkeep +0 -0
  152. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/standards/STD-ARTIFACT-PATH-001.md +47 -0
  153. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/standards/STD-DOC-LANG-001.md +62 -0
  154. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/standards/STD-TEST-LOCATION-001.md +23 -0
  155. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/standards/index.yaml +36 -0
  156. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/bootstrap/bootstrap.mjs +33 -0
  157. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/evolution/evolve.mjs +15 -0
  158. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/hooks/hook-event.mjs +131 -0
  159. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/state/state.mjs +21 -0
  160. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/viewer/public/app.js +546 -0
  161. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/viewer/public/index.html +199 -0
  162. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/viewer/public/styles.css +529 -0
  163. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/viewer/server.mjs +780 -0
  164. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/viewer/start.cmd +4 -0
  165. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/viewer/start.ps1 +2 -0
  166. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/viewer/start.sh +4 -0
  167. package/skills/ai-delivery-orchestrate/assets/project-template/AGENTS.md +44 -0
  168. package/skills/ai-delivery-orchestrate/references/formal-state-contract.md +64 -0
  169. package/skills/ai-delivery-orchestrate/references/workflow-model.md +29 -0
  170. package/skills/ai-delivery-orchestrate-release/SKILL.md +33 -0
  171. package/skills/ai-delivery-orchestrate-release/agents/openai.yaml +4 -0
  172. package/skills/ai-delivery-orchestrate-release/assets/release-request.yaml +9 -0
  173. package/skills/ai-delivery-orchestrate-release/assets/version-selection.yaml +16 -0
  174. package/skills/ai-delivery-orchestrate-release/references/release-workflow-contract.md +11 -0
  175. package/skills/ai-delivery-plan-iteration/SKILL.md +57 -0
  176. package/skills/ai-delivery-plan-iteration/agents/openai.yaml +4 -0
  177. package/skills/ai-delivery-plan-iteration/assets/planning-template/backlog.csv +1 -0
  178. package/skills/ai-delivery-plan-iteration/assets/planning-template/slice-dependencies.csv +1 -0
  179. package/skills/ai-delivery-plan-iteration/assets/planning-template/slice-experience-change-traceability.csv +1 -0
  180. package/skills/ai-delivery-plan-iteration/assets/planning-template/slice-prototype-traceability.csv +1 -0
  181. package/skills/ai-delivery-plan-iteration/assets/planning-template/slice-sources.csv +1 -0
  182. package/skills/ai-delivery-plan-iteration/assets/planning-template/vertical-slices.csv +1 -0
  183. package/skills/ai-delivery-plan-iteration/references/planning-contract.md +40 -0
  184. package/skills/ai-delivery-prepare-platform/SKILL.md +49 -0
  185. package/skills/ai-delivery-prepare-platform/agents/openai.yaml +4 -0
  186. package/skills/ai-delivery-prepare-platform/assets/platform-template/environment-ledger.csv +1 -0
  187. package/skills/ai-delivery-prepare-platform/references/platform-contract.md +18 -0
  188. package/skills/ai-delivery-prepare-release/SKILL.md +47 -0
  189. package/skills/ai-delivery-prepare-release/agents/openai.yaml +4 -0
  190. package/skills/ai-delivery-prepare-release/assets/release-template/delivery-items.csv +1 -0
  191. package/skills/ai-delivery-prepare-release/assets/release-template/release-manifest.yaml +18 -0
  192. package/skills/ai-delivery-prepare-release/references/release-contract.md +21 -0
  193. package/skills/ai-delivery-review-change/SKILL.md +40 -0
  194. package/skills/ai-delivery-review-change/agents/openai.yaml +4 -0
  195. package/skills/ai-delivery-review-change/references/review-contract.md +20 -0
  196. package/skills/ai-delivery-triage-failure/SKILL.md +45 -0
  197. package/skills/ai-delivery-triage-failure/agents/openai.yaml +4 -0
  198. package/skills/ai-delivery-triage-failure/references/failure-routing-contract.md +13 -0
  199. package/skills/ai-delivery-validate-artifacts/SKILL.md +37 -0
  200. package/skills/ai-delivery-validate-artifacts/agents/openai.yaml +4 -0
  201. package/skills/ai-delivery-validate-artifacts/references/artifact-contract.md +44 -0
  202. package/skills/ai-delivery-verify-candidate/SKILL.md +56 -0
  203. package/skills/ai-delivery-verify-candidate/agents/openai.yaml +4 -0
  204. package/skills/ai-delivery-verify-candidate/assets/verification-template/defect-artifacts.csv +1 -0
  205. package/skills/ai-delivery-verify-candidate/assets/verification-template/defects.csv +1 -0
  206. package/skills/ai-delivery-verify-candidate/assets/verification-template/test-work-packages.csv +1 -0
  207. package/skills/ai-delivery-verify-candidate/references/candidate-verification-contract.md +23 -0
  208. package/skills/ai-delivery-verify-production/SKILL.md +50 -0
  209. package/skills/ai-delivery-verify-production/agents/openai.yaml +4 -0
  210. package/skills/ai-delivery-verify-production/assets/production-verification-template/production-verification.md +15 -0
  211. package/skills/ai-delivery-verify-production/references/production-verification-contract.md +14 -0
@@ -0,0 +1,971 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { spawnSync } from "node:child_process";
5
+
6
+ const SCHEMA_VERSION = 1;
7
+ const RISK_ORDER = Object.freeze({ low: 0, medium: 1, high: 2 });
8
+ const STATES = new Set(["observed", "proposed", "trial", "active", "rejected", "disabled", "rolled-back", "superseded", "expired"]);
9
+ const SOURCE_KINDS = new Set(["user", "objective", "ai-observation"]);
10
+ const METRIC_RESULTS = new Set(["improved", "neutral", "worsened", "failure"]);
11
+ const SAFE_POINTS = new Set(["task", "version", "review"]);
12
+ const HOOK_EVENTS = new Set([
13
+ ...["task", "node", "gate", "artifact", "version", "release"]
14
+ .flatMap((scope) => ["before", "after", "failed"].map((phase) => `${scope}.${phase}`)),
15
+ "feedback-recorded",
16
+ "evaluation-due",
17
+ "extension-state-changed",
18
+ ]);
19
+ const HIGH_CAPABILITIES = new Set([
20
+ "block", "script", "git", "deployment", "security", "permission", "environment", "external-system",
21
+ ]);
22
+ const MEDIUM_CAPABILITIES = new Set([
23
+ "routing", "template", "work-package", "artifact-generation", "validation",
24
+ ]);
25
+ const LOW_CAPABILITIES = new Set(["advisory", "checklist", "display", "declarative-check"]);
26
+ const SECRET_KEY = /^(authorization|cookie|set-cookie|password|passwd|secret|access[_-]?token|refresh[_-]?token|api[_-]?key|private[_-]?key)$/i;
27
+ const BANNED_IMPORT = /(?:node:)?(?:child_process|worker_threads|cluster|net|http|https|http2|dgram|tls|vm)\b|\brequire\s*\(/;
28
+ const MAX_TEXT = 8_192;
29
+ const MAX_EVENT = 64 * 1024;
30
+ const ACTIVE_TASK = path.join(".workflow", "delivery", "runtime", "active-task.json");
31
+
32
+ function now() {
33
+ return new Date().toISOString();
34
+ }
35
+
36
+ function sha256(value) {
37
+ return crypto.createHash("sha256").update(value).digest("hex");
38
+ }
39
+
40
+ function canonical(value) {
41
+ if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
42
+ if (value && typeof value === "object") {
43
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`).join(",")}}`;
44
+ }
45
+ return JSON.stringify(value);
46
+ }
47
+
48
+ function checksum(value) {
49
+ const copy = structuredClone(value);
50
+ delete copy.checksum;
51
+ return sha256(canonical(copy));
52
+ }
53
+
54
+ function redact(value, key = "") {
55
+ if (SECRET_KEY.test(key)) return "[REDACTED]";
56
+ if (typeof value === "string") {
57
+ return value
58
+ .replace(/Bearer\s+[^\s]+/gi, "Bearer [REDACTED]")
59
+ .replace(/\b(?:sk|ghp|glpat)-[A-Za-z0-9_-]{8,}\b/g, "[REDACTED_TOKEN]")
60
+ .replace(/\b(authorization|password|passwd|secret|access[_-]?token|refresh[_-]?token|api[_-]?key|private[_-]?key)\s*[:=]\s*[^\s,;]+/gi, "$1=[REDACTED]")
61
+ .slice(0, MAX_TEXT);
62
+ }
63
+ if (Array.isArray(value)) return value.slice(0, 100).map((item) => redact(item));
64
+ if (value && typeof value === "object") {
65
+ return Object.fromEntries(Object.entries(value).slice(0, 200).map(([name, item]) => [name, redact(item, name)]));
66
+ }
67
+ return value;
68
+ }
69
+
70
+ function atomicWrite(file, content) {
71
+ fs.mkdirSync(path.dirname(file), { recursive: true });
72
+ const temporary = `${file}.${process.pid}.${Date.now()}.tmp`;
73
+ fs.writeFileSync(temporary, content, "utf8");
74
+ fs.renameSync(temporary, file);
75
+ }
76
+
77
+ function isInside(candidate, root) {
78
+ const relative = path.relative(path.resolve(root), path.resolve(candidate));
79
+ return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
80
+ }
81
+
82
+ function safeId(value, label) {
83
+ const text = String(value || "").trim();
84
+ if (!text || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(text)) {
85
+ throw new Error(`${label} must use letters, numbers, dot, underscore, or hyphen.`);
86
+ }
87
+ return text;
88
+ }
89
+
90
+ function requireText(value, label) {
91
+ const text = String(value || "").trim();
92
+ if (!text) throw new Error(`${label} is required.`);
93
+ return text;
94
+ }
95
+
96
+ function parseArgs(argv) {
97
+ const positional = [];
98
+ const options = new Map();
99
+ const flags = new Set();
100
+ for (let index = 0; index < argv.length; index += 1) {
101
+ const value = argv[index];
102
+ if (!value.startsWith("--")) {
103
+ positional.push(value);
104
+ continue;
105
+ }
106
+ const next = argv[index + 1];
107
+ if (!next || next.startsWith("--")) flags.add(value);
108
+ else {
109
+ options.set(value, next);
110
+ index += 1;
111
+ }
112
+ }
113
+ return { positional, options, flags };
114
+ }
115
+
116
+ function loadStructured(file, yaml) {
117
+ if (!fs.existsSync(file)) throw new Error(`Input does not exist: ${file}`);
118
+ const text = fs.readFileSync(file, "utf8");
119
+ return path.extname(file).toLowerCase() === ".json" ? JSON.parse(text) : yaml.parse(text);
120
+ }
121
+
122
+ function relativeProjectPath(projectRoot, absolute) {
123
+ if (!isInside(absolute, projectRoot)) throw new Error(`Path escapes project: ${absolute}`);
124
+ return path.relative(projectRoot, absolute).replaceAll("\\", "/");
125
+ }
126
+
127
+ function evolutionPaths(projectRoot) {
128
+ const workflow = path.join(projectRoot, ".workflow");
129
+ return {
130
+ workflow,
131
+ root: path.join(workflow, "evolution"),
132
+ index: path.join(workflow, "evolution", "index.yaml"),
133
+ extensions: path.join(workflow, "extensions"),
134
+ runtime: path.join(workflow, "delivery", "runtime", "evolution"),
135
+ };
136
+ }
137
+
138
+ function emptyIndex() {
139
+ return {
140
+ schema_version: SCHEMA_VERSION,
141
+ revision: 0,
142
+ feedback: [],
143
+ proposals: [],
144
+ extensions: [],
145
+ decisions: [],
146
+ updated_at: null,
147
+ };
148
+ }
149
+
150
+ function readIndex(paths, yaml) {
151
+ if (!fs.existsSync(paths.index)) return emptyIndex();
152
+ const index = yaml.parse(fs.readFileSync(paths.index, "utf8"));
153
+ if (!index || index.schema_version !== SCHEMA_VERSION) throw new Error("Evolution index schema_version must be 1.");
154
+ for (const key of ["feedback", "proposals", "extensions", "decisions"]) {
155
+ if (!Array.isArray(index[key])) throw new Error(`Evolution index ${key} must be an array.`);
156
+ }
157
+ return index;
158
+ }
159
+
160
+ function writeIndex(paths, index, yaml) {
161
+ index.revision += 1;
162
+ index.updated_at = now();
163
+ atomicWrite(paths.index, yaml.stringify(index, { lineWidth: 0 }));
164
+ }
165
+
166
+ function writeRecord(projectRoot, paths, directory, id, record, yaml) {
167
+ const file = path.join(paths.root, directory, `${id}.yaml`);
168
+ atomicWrite(file, yaml.stringify(record, { lineWidth: 0 }));
169
+ return relativeProjectPath(projectRoot, file);
170
+ }
171
+
172
+ function nextId(prefix, entries, field) {
173
+ const maximum = entries.reduce((current, entry) => {
174
+ const match = String(entry?.[field] || "").match(new RegExp(`^${prefix}-(\\d+)$`));
175
+ return Math.max(current, match ? Number(match[1]) : 0);
176
+ }, 0);
177
+ return `${prefix}-${String(maximum + 1).padStart(4, "0")}`;
178
+ }
179
+
180
+ function decision(projectRoot, paths, index, yaml, kind, subject, from, to, evidence = {}) {
181
+ if (!STATES.has(to) && to !== "approved") throw new Error(`Unsupported decision state: ${to}`);
182
+ const record = {
183
+ decision_id: nextId("DEC", index.decisions, "decision_id"),
184
+ kind,
185
+ subject,
186
+ from: from || null,
187
+ to,
188
+ evidence: redact(evidence),
189
+ decided_at: now(),
190
+ };
191
+ record.checksum = checksum(record);
192
+ record.path = writeRecord(projectRoot, paths, "decisions", record.decision_id, record, yaml);
193
+ index.decisions.push(record);
194
+ if (kind.startsWith("extension-") || kind === "trial-evaluated") {
195
+ appendRuntimeEvent(paths, {
196
+ schema_version: 1,
197
+ event_id: `EVT-${record.decision_id}`,
198
+ idempotency_key: `decision:${record.decision_id}`,
199
+ event_type: "extension-state-changed",
200
+ occurred_at: record.decided_at,
201
+ scope: { subject },
202
+ payload: { from: record.from, to: record.to, decision_id: record.decision_id },
203
+ });
204
+ }
205
+ return record;
206
+ }
207
+
208
+ function appendRuntimeEvent(paths, event) {
209
+ const file = path.join(paths.runtime, "events", "events.jsonl");
210
+ fs.mkdirSync(path.dirname(file), { recursive: true });
211
+ let line = JSON.stringify(redact(event));
212
+ if (Buffer.byteLength(line, "utf8") > MAX_EVENT) {
213
+ line = JSON.stringify({ schema_version: 1, event_id: event.event_id, event_type: event.event_type, truncated: true });
214
+ }
215
+ fs.appendFileSync(file, `${line}\n`, "utf8");
216
+ if (!new Set(["evaluation-due", "extension-state-changed"]).has(event.event_type)) {
217
+ const pendingFile = path.join(paths.runtime, "pending", `${sha256(event.idempotency_key || event.event_id).slice(0, 20)}.json`);
218
+ if (!fs.existsSync(pendingFile)) atomicWrite(pendingFile, `${JSON.stringify(redact(event), null, 2)}\n`);
219
+ }
220
+ }
221
+
222
+ function clearPending(paths) {
223
+ const pending = path.join(paths.runtime, "pending");
224
+ fs.rmSync(pending, { recursive: true, force: true });
225
+ fs.mkdirSync(pending, { recursive: true });
226
+ }
227
+
228
+ function readProcessed(paths) {
229
+ const file = path.join(paths.runtime, "processed.json");
230
+ if (!fs.existsSync(file)) return { schema_version: 1, keys: {} };
231
+ return JSON.parse(fs.readFileSync(file, "utf8"));
232
+ }
233
+
234
+ function rememberProcessed(paths, key, result) {
235
+ const processed = readProcessed(paths);
236
+ processed.keys[key] = { at: now(), result };
237
+ const keys = Object.keys(processed.keys);
238
+ for (const stale of keys.slice(0, Math.max(0, keys.length - 10_000))) delete processed.keys[stale];
239
+ atomicWrite(path.join(paths.runtime, "processed.json"), `${JSON.stringify(processed, null, 2)}\n`);
240
+ }
241
+
242
+ function classifyRisk(manifest, requested = "low") {
243
+ if (!(requested in RISK_ORDER)) throw new Error(`Unsupported requested risk: ${requested}`);
244
+ const capabilities = new Set(Array.isArray(manifest.capabilities) ? manifest.capabilities : []);
245
+ const hooks = Array.isArray(manifest.hooks) ? manifest.hooks : [];
246
+ if (hooks.some((hook) => hook?.decision === "block" || hook?.entrypoint)) capabilities.add("block");
247
+ let derived = "low";
248
+ if ([...capabilities].some((item) =>
249
+ HIGH_CAPABILITIES.has(item) || (!MEDIUM_CAPABILITIES.has(item) && !LOW_CAPABILITIES.has(item)))) derived = "high";
250
+ else if ([...capabilities].some((item) => MEDIUM_CAPABILITIES.has(item))) derived = "medium";
251
+ return RISK_ORDER[requested] > RISK_ORDER[derived] ? requested : derived;
252
+ }
253
+
254
+ function validateManifest(manifest, sourceRoot) {
255
+ if (!manifest || manifest.schema_version !== 1) throw new Error("Extension manifest schema_version must be 1.");
256
+ safeId(manifest.extension_id, "extension_id");
257
+ safeId(manifest.version, "version");
258
+ if (!Array.isArray(manifest.capabilities) || !manifest.capabilities.length) {
259
+ throw new Error("Extension manifest capabilities must be a non-empty array.");
260
+ }
261
+ if (!Array.isArray(manifest.hooks) || !manifest.hooks.length) {
262
+ throw new Error("Extension manifest hooks must be a non-empty array.");
263
+ }
264
+ for (const hook of manifest.hooks) {
265
+ if (!HOOK_EVENTS.has(hook?.event)) throw new Error(`Unsupported Hook event: ${hook?.event}`);
266
+ if (!new Set(["pass", "warn", "block"]).has(hook?.decision || "pass")) {
267
+ throw new Error(`Unsupported Hook decision: ${hook?.decision}`);
268
+ }
269
+ if (hook?.decision === "block" && !hook.event.endsWith(".before")) {
270
+ throw new Error(`Blocking Hooks are only valid for before events: ${hook.event}`);
271
+ }
272
+ if (hook.entrypoint) {
273
+ const entrypoint = path.resolve(sourceRoot, hook.entrypoint);
274
+ if (!isInside(entrypoint, sourceRoot) || !entrypoint.endsWith(".mjs") || !fs.existsSync(entrypoint)) {
275
+ throw new Error(`Hook entrypoint must be an existing project-local .mjs file: ${hook.entrypoint}`);
276
+ }
277
+ const source = fs.readFileSync(entrypoint, "utf8");
278
+ if (BANNED_IMPORT.test(source)) throw new Error(`Hook entrypoint imports a prohibited runtime: ${hook.entrypoint}`);
279
+ }
280
+ }
281
+ for (const file of filesUnder(sourceRoot).filter((item) => item.endsWith(".mjs"))) {
282
+ if (BANNED_IMPORT.test(fs.readFileSync(file, "utf8"))) {
283
+ throw new Error(`Extension module imports a prohibited runtime: ${path.relative(sourceRoot, file)}`);
284
+ }
285
+ }
286
+ }
287
+
288
+ function filesUnder(root) {
289
+ const files = [];
290
+ const visit = (directory) => {
291
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
292
+ const absolute = path.join(directory, entry.name);
293
+ if (entry.isSymbolicLink()) throw new Error(`Extension sources may not contain symbolic links: ${absolute}`);
294
+ if (entry.isDirectory()) visit(absolute);
295
+ else if (entry.isFile()) files.push(absolute);
296
+ }
297
+ };
298
+ visit(root);
299
+ return files.sort();
300
+ }
301
+
302
+ function directoryChecksum(root) {
303
+ const hash = crypto.createHash("sha256");
304
+ for (const file of filesUnder(root)) {
305
+ hash.update(path.relative(root, file).replaceAll("\\", "/"));
306
+ hash.update("\0");
307
+ hash.update(fs.readFileSync(file));
308
+ hash.update("\0");
309
+ }
310
+ return hash.digest("hex");
311
+ }
312
+
313
+ function findExtension(index, id, version) {
314
+ const extension = index.extensions.find((item) => item.extension_id === id && item.version === version);
315
+ if (!extension) throw new Error(`Extension is not registered: ${id}@${version}`);
316
+ return extension;
317
+ }
318
+
319
+ function eligibleFeedback(feedback, allFeedback) {
320
+ if (feedback.source_kind === "ai-observation") return false;
321
+ if (feedback.source_kind === "user" && feedback.explicit_user) return true;
322
+ if (feedback.source_kind === "objective" && feedback.strong_signal) return true;
323
+ const related = allFeedback.filter((item) => item.signal_key === feedback.signal_key);
324
+ const events = new Set(related.map((item) => item.source_event_id));
325
+ const tasks = new Set(related.map((item) => item.task_id).filter(Boolean));
326
+ return events.size >= 3 || tasks.size >= 2;
327
+ }
328
+
329
+ function requireSafePoint(projectRoot, options) {
330
+ const safePoint = options.get("--safe-point");
331
+ if (!SAFE_POINTS.has(safePoint)) {
332
+ throw new Error(`--safe-point must be one of: ${[...SAFE_POINTS].join(", ")}.`);
333
+ }
334
+ if (fs.existsSync(path.join(projectRoot, ACTIVE_TASK))) {
335
+ throw new Error("Evolution state cannot change while an active task marker exists.");
336
+ }
337
+ return safePoint;
338
+ }
339
+
340
+ function normalizeHookOutcome(output) {
341
+ if (!output || typeof output !== "object" || Array.isArray(output)) {
342
+ throw new Error("Hook output must be a JSON object.");
343
+ }
344
+ if (!new Set(["pass", "warn", "block"]).has(output.decision)) {
345
+ throw new Error("Hook output decision is invalid.");
346
+ }
347
+ if (output.messages !== undefined && !Array.isArray(output.messages)) {
348
+ throw new Error("Hook output messages must be an array.");
349
+ }
350
+ if (output.evidence_refs !== undefined && !Array.isArray(output.evidence_refs)) {
351
+ throw new Error("Hook output evidence_refs must be an array.");
352
+ }
353
+ return redact({
354
+ decision: output.decision,
355
+ messages: (output.messages || []).slice(0, 50).map((item) => String(item)),
356
+ evidence_refs: (output.evidence_refs || []).slice(0, 100).map((item) => String(item)),
357
+ });
358
+ }
359
+
360
+ function requireChecksum(extension, supplied) {
361
+ if (!supplied || supplied !== extension.checksum) {
362
+ throw new Error(`Checksum mismatch for ${extension.extension_id}@${extension.version}.`);
363
+ }
364
+ }
365
+
366
+ function hasValidApproval(index, extension) {
367
+ if (!extension.approval || extension.approval.checksum !== extension.checksum) return false;
368
+ const confirmation = index.feedback.find((item) =>
369
+ item.feedback_id === extension.approval.confirmation_feedback_id);
370
+ return Boolean(confirmation
371
+ && confirmation.source_kind === "user"
372
+ && confirmation.explicit_user
373
+ && confirmation.category === "extension-approval"
374
+ && confirmation.confirmation_subject === `${extension.extension_id}@${extension.version}`
375
+ && confirmation.confirmation_checksum === extension.checksum
376
+ && confirmation.source_event_id === extension.approval.source_event_id);
377
+ }
378
+
379
+ function handleFeedback(projectRoot, paths, index, options, flags, yaml) {
380
+ const idempotencyKey = safeId(options.get("--idempotency-key") || options.get("--source-event-id"), "idempotency_key");
381
+ const existing = index.feedback.find((item) => item.idempotency_key === idempotencyKey);
382
+ if (existing) return { feedback_id: existing.feedback_id, path: existing.path, deduplicated: true };
383
+ const sourceKind = options.get("--source-kind") || "user";
384
+ if (!SOURCE_KINDS.has(sourceKind)) throw new Error(`Unsupported source kind: ${sourceKind}`);
385
+ const record = redact({
386
+ feedback_id: nextId("FDB", index.feedback, "feedback_id"),
387
+ status: "observed",
388
+ category: requireText(options.get("--category"), "category"),
389
+ summary: requireText(options.get("--summary"), "summary"),
390
+ impact: requireText(options.get("--impact"), "impact"),
391
+ source_kind: sourceKind,
392
+ source_event_id: safeId(options.get("--source-event-id"), "source_event_id"),
393
+ signal_key: safeId(options.get("--signal-key"), "signal_key"),
394
+ task_id: options.get("--task-id") || null,
395
+ node_id: options.get("--node-id") || null,
396
+ iteration_id: options.get("--iteration-id") || null,
397
+ artifact_id: options.get("--artifact-id") || null,
398
+ excerpt: options.get("--excerpt") || null,
399
+ idempotency_key: idempotencyKey,
400
+ explicit_user: flags.has("--explicit-user"),
401
+ strong_signal: flags.has("--strong-signal"),
402
+ confirmation_subject: options.get("--confirmation-subject") || null,
403
+ confirmation_checksum: options.get("--confirmation-checksum") || null,
404
+ recorded_at: now(),
405
+ });
406
+ if (record.confirmation_checksum && !/^[a-f0-9]{64}$/i.test(record.confirmation_checksum)) {
407
+ throw new Error("confirmation_checksum must be a SHA-256 value.");
408
+ }
409
+ record.checksum = checksum(record);
410
+ record.path = writeRecord(projectRoot, paths, "feedback", record.feedback_id, record, yaml);
411
+ index.feedback.push(record);
412
+ decision(projectRoot, paths, index, yaml, "feedback-recorded", record.feedback_id, null, "observed", {
413
+ source_event_id: record.source_event_id,
414
+ });
415
+ writeIndex(paths, index, yaml);
416
+ appendRuntimeEvent(paths, {
417
+ schema_version: 1,
418
+ event_id: `EVT-${record.feedback_id}`,
419
+ idempotency_key: `feedback:${record.idempotency_key}`,
420
+ event_type: "feedback-recorded",
421
+ occurred_at: record.recorded_at,
422
+ scope: { feedback_id: record.feedback_id },
423
+ payload: { category: record.category, signal_key: record.signal_key },
424
+ });
425
+ return { feedback_id: record.feedback_id, path: record.path, deduplicated: false, checksum: record.checksum };
426
+ }
427
+
428
+ function handlePropose(projectRoot, paths, index, options, yaml) {
429
+ const specFile = path.resolve(options.get("--spec") || "");
430
+ if (!isInside(specFile, projectRoot)) throw new Error("Proposal spec must be inside the project.");
431
+ const spec = redact(loadStructured(specFile, yaml));
432
+ for (const field of ["title", "extension_id", "hypothesis", "baseline"]) requireText(spec[field], field);
433
+ safeId(spec.extension_id, "extension_id");
434
+ if (!Array.isArray(spec.feedback_ids) || !spec.feedback_ids.length) throw new Error("feedback_ids must be a non-empty array.");
435
+ if (!Array.isArray(spec.metrics) || !spec.metrics.length) throw new Error("metrics must be a non-empty array.");
436
+ if (!Array.isArray(spec.scope) || !spec.scope.length) throw new Error("scope must be a non-empty array.");
437
+ if (!Array.isArray(spec.rollback_conditions) || !spec.rollback_conditions.length) {
438
+ throw new Error("rollback_conditions must be a non-empty array.");
439
+ }
440
+ const feedback = spec.feedback_ids.map((id) => {
441
+ const record = index.feedback.find((item) => item.feedback_id === id);
442
+ if (!record) throw new Error(`Unknown feedback: ${id}`);
443
+ return record;
444
+ });
445
+ if (!feedback.some((item) => eligibleFeedback(item, index.feedback))) {
446
+ throw new Error("Proposal evidence has not reached the repeated-signal or strong-signal threshold.");
447
+ }
448
+ const record = {
449
+ proposal_id: nextId("PRP", index.proposals, "proposal_id"),
450
+ status: "proposed",
451
+ title: spec.title,
452
+ feedback_ids: [...new Set(spec.feedback_ids)],
453
+ extension_id: spec.extension_id,
454
+ requested_risk: spec.requested_risk || "low",
455
+ hypothesis: spec.hypothesis,
456
+ baseline: spec.baseline,
457
+ metrics: spec.metrics,
458
+ scope: spec.scope,
459
+ rollback_conditions: spec.rollback_conditions,
460
+ proposed_at: now(),
461
+ };
462
+ if (!(record.requested_risk in RISK_ORDER)) throw new Error(`Unsupported requested risk: ${record.requested_risk}`);
463
+ record.checksum = checksum(record);
464
+ record.path = writeRecord(projectRoot, paths, "proposals", record.proposal_id, record, yaml);
465
+ index.proposals.push(record);
466
+ decision(projectRoot, paths, index, yaml, "proposal-created", record.proposal_id, "observed", "proposed", {
467
+ feedback_ids: record.feedback_ids,
468
+ });
469
+ writeIndex(paths, index, yaml);
470
+ return { proposal_id: record.proposal_id, path: record.path, checksum: record.checksum, status: record.status };
471
+ }
472
+
473
+ function handleRegister(projectRoot, paths, index, options, yaml) {
474
+ const proposalId = safeId(options.get("--proposal-id"), "proposal_id");
475
+ const proposal = index.proposals.find((item) => item.proposal_id === proposalId);
476
+ if (!proposal) throw new Error(`Unknown proposal: ${proposalId}`);
477
+ if (proposal.status !== "proposed") throw new Error(`Proposal ${proposalId} is ${proposal.status}.`);
478
+ const source = path.resolve(options.get("--source") || "");
479
+ if (!isInside(source, projectRoot) || !fs.existsSync(source) || !fs.statSync(source).isDirectory()) {
480
+ throw new Error("Extension source must be an existing directory inside the project.");
481
+ }
482
+ const manifestFile = ["manifest.yaml", "manifest.yml", "manifest.json"]
483
+ .map((name) => path.join(source, name)).find((file) => fs.existsSync(file));
484
+ if (!manifestFile) throw new Error("Extension source is missing manifest.yaml, manifest.yml, or manifest.json.");
485
+ const manifest = loadStructured(manifestFile, yaml);
486
+ validateManifest(manifest, source);
487
+ if (manifest.extension_id !== proposal.extension_id) throw new Error("Extension manifest does not match proposal extension_id.");
488
+ if (index.extensions.some((item) => item.extension_id === manifest.extension_id && item.version === manifest.version)) {
489
+ throw new Error(`Extension version is already registered: ${manifest.extension_id}@${manifest.version}`);
490
+ }
491
+ const effectiveRisk = classifyRisk(manifest, proposal.requested_risk);
492
+ const destination = path.join(paths.extensions, manifest.extension_id, "versions", manifest.version);
493
+ if (fs.existsSync(destination)) throw new Error(`Extension destination already exists: ${destination}`);
494
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
495
+ fs.cpSync(source, destination, { recursive: true, errorOnExist: true });
496
+ const digest = directoryChecksum(destination);
497
+ const record = {
498
+ extension_id: manifest.extension_id,
499
+ version: manifest.version,
500
+ proposal_id: proposalId,
501
+ state: "proposed",
502
+ requested_risk: proposal.requested_risk,
503
+ effective_risk: effectiveRisk,
504
+ manifest_path: relativeProjectPath(projectRoot, path.join(destination, path.basename(manifestFile))),
505
+ root_path: relativeProjectPath(projectRoot, destination),
506
+ checksum: digest,
507
+ approval: null,
508
+ blocking_hook_events: manifest.hooks.filter((hook) => hook.decision === "block").map((hook) => hook.event),
509
+ registered_at: now(),
510
+ updated_at: now(),
511
+ };
512
+ index.extensions.push(record);
513
+ decision(projectRoot, paths, index, yaml, "extension-registered", `${record.extension_id}@${record.version}`, "proposed", "proposed", {
514
+ effective_risk: effectiveRisk,
515
+ checksum: digest,
516
+ });
517
+ writeIndex(paths, index, yaml);
518
+ return { extension_id: record.extension_id, version: record.version, state: record.state, effective_risk: effectiveRisk, checksum: digest };
519
+ }
520
+
521
+ function handleApprove(projectRoot, paths, index, options, yaml) {
522
+ const extension = findExtension(index, safeId(options.get("--extension-id"), "extension_id"), safeId(options.get("--version"), "version"));
523
+ requireChecksum(extension, options.get("--checksum"));
524
+ if (!new Set(["proposed", "trial"]).has(extension.state)) {
525
+ throw new Error(`Extension cannot be approved from state ${extension.state}.`);
526
+ }
527
+ const confirmationId = safeId(options.get("--confirmation-feedback-id"), "confirmation_feedback_id");
528
+ const confirmation = index.feedback.find((item) => item.feedback_id === confirmationId);
529
+ const expectedSubject = `${extension.extension_id}@${extension.version}`;
530
+ if (!confirmation || confirmation.source_kind !== "user" || !confirmation.explicit_user
531
+ || confirmation.category !== "extension-approval"
532
+ || confirmation.confirmation_subject !== expectedSubject
533
+ || confirmation.confirmation_checksum !== extension.checksum) {
534
+ throw new Error("Approval requires explicit-user feedback bound to this extension version and checksum.");
535
+ }
536
+ extension.approval = {
537
+ checksum: extension.checksum,
538
+ source_event_id: confirmation.source_event_id,
539
+ confirmation_feedback_id: confirmationId,
540
+ approved_at: now(),
541
+ };
542
+ extension.updated_at = now();
543
+ const proposal = index.proposals.find((item) => item.proposal_id === extension.proposal_id);
544
+ if (proposal) proposal.approved_at = extension.approval.approved_at;
545
+ decision(projectRoot, paths, index, yaml, "extension-approved", `${extension.extension_id}@${extension.version}`, extension.state, "approved", extension.approval);
546
+ writeIndex(paths, index, yaml);
547
+ return { extension_id: extension.extension_id, version: extension.version, approval: extension.approval };
548
+ }
549
+
550
+ function experimentFile(paths, extension) {
551
+ return path.join(paths.root, "experiments", `${extension.extension_id}-${extension.version}.yaml`);
552
+ }
553
+
554
+ function readExperiment(paths, extension, yaml) {
555
+ const file = experimentFile(paths, extension);
556
+ if (!fs.existsSync(file)) {
557
+ return {
558
+ experiment_id: `EXP-${extension.extension_id}-${extension.version}`,
559
+ extension_id: extension.extension_id,
560
+ version: extension.version,
561
+ state: "trial",
562
+ target_executions: 3,
563
+ target_iterations: 1,
564
+ observations: [],
565
+ started_at: now(),
566
+ };
567
+ }
568
+ return yaml.parse(fs.readFileSync(file, "utf8"));
569
+ }
570
+
571
+ function writeExperiment(paths, experiment, yaml) {
572
+ experiment.checksum = checksum(experiment);
573
+ atomicWrite(experimentFile(paths, experiment), yaml.stringify(experiment, { lineWidth: 0 }));
574
+ }
575
+
576
+ function handleEnable(projectRoot, paths, index, options, yaml) {
577
+ const extension = findExtension(index, safeId(options.get("--extension-id"), "extension_id"), safeId(options.get("--version"), "version"));
578
+ requireChecksum(extension, options.get("--checksum"));
579
+ const safePoint = requireSafePoint(projectRoot, options);
580
+ const targetState = options.get("--state") || "trial";
581
+ if (!new Set(["trial", "active"]).has(targetState)) throw new Error("Enable state must be trial or active.");
582
+ if (targetState === "trial" && extension.state !== "proposed") {
583
+ throw new Error(`Extension can enter trial only from proposed, not ${extension.state}.`);
584
+ }
585
+ if (targetState === "active") {
586
+ if (extension.state !== "trial") throw new Error(`Extension can become active only from trial, not ${extension.state}.`);
587
+ const experiment = readExperiment(paths, extension, yaml);
588
+ if (experiment.state !== "awaiting-approval") {
589
+ throw new Error("Extension can become active only after its trial window passes.");
590
+ }
591
+ }
592
+ if (extension.effective_risk !== "low" && !hasValidApproval(index, extension)) {
593
+ throw new Error(`Extension ${extension.extension_id}@${extension.version} requires checksum-bound human approval.`);
594
+ }
595
+ if (targetState === "active" && !hasValidApproval(index, extension)) {
596
+ throw new Error("Direct activation requires checksum-bound human approval.");
597
+ }
598
+ const { manifest } = loadExtensionManifest(projectRoot, extension, yaml);
599
+ const capabilityKeys = new Set(Array.isArray(manifest.capability_keys) ? manifest.capability_keys : []);
600
+ const declaredConflicts = new Set(Array.isArray(manifest.conflicts_with) ? manifest.conflicts_with : []);
601
+ for (const other of index.extensions.filter((item) => item !== extension && (item.state === "trial" || item.state === "active"))) {
602
+ const { manifest: otherManifest } = loadExtensionManifest(projectRoot, other, yaml);
603
+ const otherKeys = new Set(Array.isArray(otherManifest.capability_keys) ? otherManifest.capability_keys : []);
604
+ const otherConflicts = new Set(Array.isArray(otherManifest.conflicts_with) ? otherManifest.conflicts_with : []);
605
+ const conflicts = declaredConflicts.has(other.extension_id)
606
+ || otherConflicts.has(extension.extension_id)
607
+ || [...capabilityKeys].some((key) => otherKeys.has(key));
608
+ if (!conflicts) continue;
609
+ const priority = extension.approval ? 2 : 1;
610
+ const otherPriority = other.approval ? 2 : 1;
611
+ if (priority === otherPriority && priority === 2) {
612
+ throw new Error(`Approved extensions conflict at equal precedence: ${extension.extension_id} and ${other.extension_id}.`);
613
+ }
614
+ const disabled = priority > otherPriority ? other : extension;
615
+ const winner = disabled === extension ? other : extension;
616
+ disabled.state = "disabled";
617
+ disabled.reason = `conflicts with higher-precedence extension ${winner.extension_id}@${winner.version}`;
618
+ disabled.updated_at = now();
619
+ decision(projectRoot, paths, index, yaml, "extension-conflict", `${disabled.extension_id}@${disabled.version}`, "proposed", "disabled", {
620
+ winner: `${winner.extension_id}@${winner.version}`,
621
+ });
622
+ if (disabled === extension) {
623
+ writeIndex(paths, index, yaml);
624
+ return { extension_id: extension.extension_id, version: extension.version, state: "disabled", conflict_with: winner.extension_id };
625
+ }
626
+ }
627
+ const previous = extension.state;
628
+ extension.state = targetState;
629
+ extension.updated_at = now();
630
+ extension.effective_from = { safe_point: safePoint, boundary: "next-task-or-version", decided_at: extension.updated_at };
631
+ if (targetState === "trial") writeExperiment(paths, readExperiment(paths, extension, yaml), yaml);
632
+ decision(projectRoot, paths, index, yaml, "extension-enabled", `${extension.extension_id}@${extension.version}`, previous, targetState, {
633
+ checksum: extension.checksum,
634
+ automatic: extension.effective_risk === "low" && !extension.approval,
635
+ effective_from: extension.effective_from,
636
+ });
637
+ writeIndex(paths, index, yaml);
638
+ return { extension_id: extension.extension_id, version: extension.version, state: extension.state, effective_risk: extension.effective_risk };
639
+ }
640
+
641
+ function handleEvaluate(projectRoot, paths, index, options, yaml) {
642
+ const safePoint = requireSafePoint(projectRoot, options);
643
+ clearPending(paths);
644
+ if (!options.has("--extension-id")) {
645
+ const eligible = index.feedback.filter((item) => eligibleFeedback(item, index.feedback));
646
+ appendRuntimeEvent(paths, {
647
+ schema_version: 1,
648
+ event_id: `EVT-EVALUATION-${Date.now()}`,
649
+ idempotency_key: `evaluation:${index.revision}:${safePoint}`,
650
+ event_type: "evaluation-due",
651
+ occurred_at: now(),
652
+ scope: { safe_point: safePoint },
653
+ payload: { eligible_feedback_ids: eligible.map((item) => item.feedback_id) },
654
+ });
655
+ return { safe_point: safePoint, eligible_feedback: eligible.map((item) => ({ feedback_id: item.feedback_id, signal_key: item.signal_key })) };
656
+ }
657
+ const extension = findExtension(index, safeId(options.get("--extension-id"), "extension_id"), safeId(options.get("--version"), "version"));
658
+ if (extension.state !== "trial") throw new Error(`Extension is not in trial: ${extension.extension_id}@${extension.version}`);
659
+ const installedRoot = path.join(projectRoot, extension.root_path);
660
+ if (!fs.existsSync(installedRoot) || directoryChecksum(installedRoot) !== extension.checksum) {
661
+ const previous = extension.state;
662
+ extension.state = "disabled";
663
+ extension.reason = "extension checksum verification failed";
664
+ extension.updated_at = now();
665
+ decision(projectRoot, paths, index, yaml, "extension-quarantined", `${extension.extension_id}@${extension.version}`, previous, "disabled", {
666
+ reason: extension.reason,
667
+ });
668
+ writeIndex(paths, index, yaml);
669
+ return { extension_id: extension.extension_id, version: extension.version, state: extension.state, quarantined: true };
670
+ }
671
+ const metricResult = options.get("--metric-result");
672
+ if (!METRIC_RESULTS.has(metricResult)) throw new Error(`Unsupported metric result: ${metricResult}`);
673
+ const eventId = safeId(options.get("--event-id"), "event_id");
674
+ const experiment = readExperiment(paths, extension, yaml);
675
+ if (!experiment.observations.some((item) => item.event_id === eventId)) {
676
+ experiment.observations.push({
677
+ event_id: eventId,
678
+ safe_point: safePoint,
679
+ iteration_id: options.get("--iteration-id") || null,
680
+ result: metricResult,
681
+ observed_at: now(),
682
+ });
683
+ }
684
+ const previous = extension.state;
685
+ if (experiment.observations.some((item) => item.result === "worsened" || item.result === "failure")) {
686
+ experiment.state = "rolled-back";
687
+ extension.state = "rolled-back";
688
+ } else {
689
+ const iterations = new Set(experiment.observations.map((item) => item.iteration_id).filter(Boolean));
690
+ const windowComplete = experiment.observations.length >= experiment.target_executions
691
+ || (safePoint === "version" && iterations.size >= experiment.target_iterations);
692
+ if (windowComplete) {
693
+ experiment.state = extension.effective_risk === "low" ? "active" : "awaiting-approval";
694
+ if (extension.effective_risk === "low") extension.state = "active";
695
+ }
696
+ }
697
+ extension.updated_at = now();
698
+ experiment.completed_at = experiment.state === "trial" ? null : now();
699
+ writeExperiment(paths, experiment, yaml);
700
+ if (extension.state !== previous) {
701
+ decision(projectRoot, paths, index, yaml, "trial-evaluated", `${extension.extension_id}@${extension.version}`, previous, extension.state, {
702
+ observations: experiment.observations.length,
703
+ experiment_state: experiment.state,
704
+ });
705
+ }
706
+ writeIndex(paths, index, yaml);
707
+ return { extension_id: extension.extension_id, version: extension.version, state: extension.state, experiment_state: experiment.state };
708
+ }
709
+
710
+ function handleTerminal(projectRoot, paths, index, options, yaml, command) {
711
+ const extension = findExtension(index, safeId(options.get("--extension-id"), "extension_id"), safeId(options.get("--version"), "version"));
712
+ requireChecksum(extension, options.get("--checksum"));
713
+ const previous = extension.state;
714
+ let target = command === "rollback" ? "rolled-back" : options.get("--disposition") || "disabled";
715
+ if (!new Set(["disabled", "expired", "superseded", "rolled-back"]).has(target)) throw new Error(`Unsupported terminal state: ${target}`);
716
+ if (command === "rollback" && !new Set(["trial", "active"]).has(previous)) {
717
+ throw new Error(`Extension cannot roll back from state ${previous}.`);
718
+ }
719
+ if (command === "disable" && target === "rolled-back") {
720
+ throw new Error("Use rollback to enter rolled-back state.");
721
+ }
722
+ if (target === "disabled" && !new Set(["proposed", "trial", "active"]).has(previous)) {
723
+ throw new Error(`Extension cannot be disabled from state ${previous}.`);
724
+ }
725
+ if (target === "superseded") {
726
+ const replacement = requireText(options.get("--superseded-by"), "superseded_by");
727
+ const match = replacement.match(/^([^@]+)@(.+)$/);
728
+ if (!match || match[1] !== extension.extension_id) {
729
+ throw new Error("superseded_by must reference another registered version of the same extension.");
730
+ }
731
+ const successor = findExtension(index, safeId(match[1], "superseded_extension_id"), safeId(match[2], "superseded_version"));
732
+ if (successor === extension || successor.superseded_by === `${extension.extension_id}@${extension.version}`) {
733
+ throw new Error("superseded_by would create an invalid replacement chain.");
734
+ }
735
+ }
736
+ extension.state = target;
737
+ extension.superseded_by = options.get("--superseded-by") || null;
738
+ extension.reason = requireText(options.get("--reason"), "reason");
739
+ extension.updated_at = now();
740
+ decision(projectRoot, paths, index, yaml, `extension-${command}`, `${extension.extension_id}@${extension.version}`, previous, target, {
741
+ reason: extension.reason,
742
+ superseded_by: extension.superseded_by,
743
+ });
744
+ writeIndex(paths, index, yaml);
745
+ return { extension_id: extension.extension_id, version: extension.version, state: target };
746
+ }
747
+
748
+ function handleReject(projectRoot, paths, index, options, yaml) {
749
+ const proposalId = safeId(options.get("--proposal-id"), "proposal_id");
750
+ const proposal = index.proposals.find((item) => item.proposal_id === proposalId);
751
+ if (!proposal) throw new Error(`Unknown proposal: ${proposalId}`);
752
+ if (proposal.status !== "proposed") throw new Error(`Proposal cannot be rejected from status ${proposal.status}.`);
753
+ if (index.extensions.some((item) => item.proposal_id === proposalId)) {
754
+ throw new Error("A proposal with a registered extension version cannot be rejected.");
755
+ }
756
+ const previous = proposal.status;
757
+ proposal.status = "rejected";
758
+ proposal.rejected_at = now();
759
+ proposal.reason = requireText(options.get("--reason"), "reason");
760
+ decision(projectRoot, paths, index, yaml, "proposal-rejected", proposalId, previous, "rejected", { reason: proposal.reason });
761
+ writeIndex(paths, index, yaml);
762
+ return { proposal_id: proposalId, status: proposal.status };
763
+ }
764
+
765
+ function loadExtensionManifest(projectRoot, extension, yaml) {
766
+ const file = path.join(projectRoot, extension.manifest_path);
767
+ const manifest = loadStructured(file, yaml);
768
+ const root = path.join(projectRoot, extension.root_path);
769
+ validateManifest(manifest, root);
770
+ return { manifest, root };
771
+ }
772
+
773
+ function executeScript(root, hook, event) {
774
+ const entrypoint = path.resolve(root, hook.entrypoint);
775
+ const result = spawnSync(process.execPath, [entrypoint], {
776
+ cwd: root,
777
+ input: JSON.stringify(event),
778
+ encoding: "utf8",
779
+ timeout: Math.min(Number(hook.timeout_seconds || 5), 10) * 1_000,
780
+ windowsHide: true,
781
+ env: {
782
+ PATH: process.env.PATH || "",
783
+ SystemRoot: process.env.SystemRoot || "",
784
+ TEMP: process.env.TEMP || "",
785
+ TMP: process.env.TMP || "",
786
+ AI_DELIVERY_HOOK_EVENT: event.event_type,
787
+ },
788
+ });
789
+ if (result.error || result.status !== 0) throw new Error(result.error?.message || result.stderr || `Hook exited ${result.status}`);
790
+ const output = JSON.parse(result.stdout || "{}");
791
+ return normalizeHookOutcome(output);
792
+ }
793
+
794
+ function dispatchHookEvent(projectRoot, paths, index, rawEvent, yaml) {
795
+ const event = redact(rawEvent);
796
+ if (event.schema_version !== 1) throw new Error("Hook event schema_version must be 1.");
797
+ safeId(event.event_id, "event_id");
798
+ safeId(event.idempotency_key, "idempotency_key");
799
+ if (!HOOK_EVENTS.has(event.event_type)) throw new Error(`Unsupported Hook event: ${event.event_type}`);
800
+ const processed = readProcessed(paths);
801
+ if (processed.keys[event.idempotency_key]) return { ...processed.keys[event.idempotency_key].result, deduplicated: true };
802
+ appendRuntimeEvent(paths, event);
803
+ const outcomes = [];
804
+ let indexChanged = false;
805
+ for (const extension of index.extensions.filter((item) => item.state === "active" || item.state === "trial")) {
806
+ const approved = hasValidApproval(index, extension);
807
+ const failClosed = approved
808
+ && Array.isArray(extension.blocking_hook_events)
809
+ && extension.blocking_hook_events.includes(event.event_type);
810
+ try {
811
+ if (extension.effective_risk !== "low" && !approved) {
812
+ throw new Error("extension approval receipt is missing or invalid");
813
+ }
814
+ if (directoryChecksum(path.join(projectRoot, extension.root_path)) !== extension.checksum) {
815
+ throw new Error("extension checksum mismatch");
816
+ }
817
+ const { manifest, root } = loadExtensionManifest(projectRoot, extension, yaml);
818
+ const matchingHooks = manifest.hooks.filter((item) => item.event === event.event_type);
819
+ for (const hook of matchingHooks) {
820
+ let outcome = hook.entrypoint
821
+ ? executeScript(root, hook, event)
822
+ : normalizeHookOutcome({
823
+ decision: hook.decision || "pass",
824
+ messages: hook.message ? [hook.message] : [],
825
+ evidence_refs: hook.evidence_refs || [],
826
+ });
827
+ if (outcome.decision === "block" && (extension.state === "trial" && extension.effective_risk === "low" || !approved)) {
828
+ outcome = { ...outcome, decision: "warn", messages: [...outcome.messages, "未获人工批准的扩展不能阻断流程。"] };
829
+ }
830
+ if (outcome.decision === "block" && !event.event_type.endsWith(".before")) {
831
+ outcome = { ...outcome, decision: "warn", messages: [...outcome.messages, "仅 before Hook 可以阻断流程。"] };
832
+ }
833
+ outcomes.push(redact({ extension_id: extension.extension_id, version: extension.version, ...outcome }));
834
+ }
835
+ } catch (error) {
836
+ const previous = extension.state;
837
+ extension.state = previous === "trial" ? "rolled-back" : "disabled";
838
+ extension.reason = `Hook failure: ${redact(error.message)}`;
839
+ extension.updated_at = now();
840
+ decision(projectRoot, paths, index, yaml, "extension-hook-failed", `${extension.extension_id}@${extension.version}`, previous, extension.state, {
841
+ event_id: event.event_id,
842
+ event_type: event.event_type,
843
+ reason: extension.reason,
844
+ });
845
+ indexChanged = true;
846
+ outcomes.push(redact({
847
+ extension_id: extension.extension_id,
848
+ version: extension.version,
849
+ decision: failClosed ? "block" : "warn",
850
+ messages: [`Hook 执行失败:${error.message}`],
851
+ evidence_refs: [],
852
+ }));
853
+ }
854
+ }
855
+ const result = {
856
+ schema_version: 1,
857
+ event_id: event.event_id,
858
+ decision: outcomes.some((item) => item.decision === "block") ? "block"
859
+ : outcomes.some((item) => item.decision === "warn") ? "warn" : "pass",
860
+ messages: redact(outcomes.flatMap((item) => item.messages)),
861
+ evidence_refs: redact([...new Set(outcomes.flatMap((item) => item.evidence_refs))]),
862
+ outcomes,
863
+ deduplicated: false,
864
+ };
865
+ if (outcomes.length) {
866
+ const record = {
867
+ schema_version: 1,
868
+ event_id: event.event_id,
869
+ event_type: event.event_type,
870
+ decision: result.decision,
871
+ outcomes,
872
+ recorded_at: now(),
873
+ };
874
+ record.checksum = checksum(record);
875
+ writeRecord(projectRoot, paths, path.join("experiments", "hook-results"), event.event_id, redact(record), yaml);
876
+ }
877
+ if (indexChanged) writeIndex(paths, index, yaml);
878
+ rememberProcessed(paths, event.idempotency_key, result);
879
+ return result;
880
+ }
881
+
882
+ function handleHook(projectRoot, paths, index, options, yaml) {
883
+ const input = options.get("--input");
884
+ const event = input === "-"
885
+ ? JSON.parse(fs.readFileSync(0, "utf8"))
886
+ : loadStructured(path.resolve(input || ""), yaml);
887
+ return dispatchHookEvent(projectRoot, paths, index, event, yaml);
888
+ }
889
+
890
+ function handleVerify(projectRoot, paths, index, yaml) {
891
+ const issues = [];
892
+ let indexChanged = false;
893
+ for (const extension of index.extensions) {
894
+ try {
895
+ const root = path.join(projectRoot, extension.root_path);
896
+ if (!fs.existsSync(root)) throw new Error("extension directory is missing");
897
+ if (directoryChecksum(root) !== extension.checksum) throw new Error("checksum mismatch");
898
+ loadExtensionManifest(projectRoot, extension, yaml);
899
+ if (extension.approval && !hasValidApproval(index, extension)) throw new Error("approval receipt or checksum mismatch");
900
+ } catch (error) {
901
+ issues.push({ extension_id: extension.extension_id, version: extension.version, severity: "warning", message: error.message });
902
+ if (extension.state === "trial" || extension.state === "active") {
903
+ const previous = extension.state;
904
+ extension.state = "disabled";
905
+ extension.reason = `verification failed: ${redact(error.message)}`;
906
+ extension.updated_at = now();
907
+ decision(projectRoot, paths, index, yaml, "extension-quarantined", `${extension.extension_id}@${extension.version}`, previous, "disabled", {
908
+ reason: extension.reason,
909
+ });
910
+ indexChanged = true;
911
+ }
912
+ }
913
+ }
914
+ if (indexChanged) writeIndex(paths, index, yaml);
915
+ return { ok: issues.length === 0, schema_version: index.schema_version, revision: index.revision, issues };
916
+ }
917
+
918
+ function handleExport(projectRoot, paths, index, options, yaml) {
919
+ const extension = findExtension(index, safeId(options.get("--extension-id"), "extension_id"), safeId(options.get("--version"), "version"));
920
+ const bundle = {
921
+ schema_version: 1,
922
+ bundle_id: `PROMOTION-${extension.extension_id}-${extension.version}`,
923
+ extension_id: extension.extension_id,
924
+ version: extension.version,
925
+ effective_risk: extension.effective_risk,
926
+ checksum: extension.checksum,
927
+ state: extension.state,
928
+ proposal_id: extension.proposal_id,
929
+ exported_at: now(),
930
+ contains_executable_code: extension.effective_risk === "high",
931
+ transfer_status: "local-only",
932
+ };
933
+ const file = path.join(paths.root, "promotion", `${bundle.bundle_id}.yaml`);
934
+ atomicWrite(file, yaml.stringify(bundle, { lineWidth: 0 }));
935
+ return { bundle_id: bundle.bundle_id, path: relativeProjectPath(projectRoot, file), checksum: checksum(bundle) };
936
+ }
937
+
938
+ export function executeEvolutionCommand(argv, dependencies) {
939
+ const yaml = dependencies?.yaml;
940
+ if (!yaml?.parse || !yaml?.stringify) throw new Error("Evolution CLI requires YAML parse and stringify functions.");
941
+ const { positional, options, flags } = parseArgs(argv);
942
+ const projectRoot = path.resolve(options.get("--project") || process.cwd());
943
+ const paths = evolutionPaths(projectRoot);
944
+ const index = readIndex(paths, yaml);
945
+ const [command, subcommand] = positional;
946
+ if (command === "feedback" && subcommand === "record") return handleFeedback(projectRoot, paths, index, options, flags, yaml);
947
+ if (command === "propose") return handlePropose(projectRoot, paths, index, options, yaml);
948
+ if (command === "register") return handleRegister(projectRoot, paths, index, options, yaml);
949
+ if (command === "approve") return handleApprove(projectRoot, paths, index, options, yaml);
950
+ if (command === "reject") return handleReject(projectRoot, paths, index, options, yaml);
951
+ if (command === "enable") return handleEnable(projectRoot, paths, index, options, yaml);
952
+ if (command === "disable" || command === "rollback") return handleTerminal(projectRoot, paths, index, options, yaml, command);
953
+ if (command === "evaluate") return handleEvaluate(projectRoot, paths, index, options, yaml);
954
+ if (command === "hook" && subcommand === "dispatch") return handleHook(projectRoot, paths, index, options, yaml);
955
+ if (command === "verify") return handleVerify(projectRoot, paths, index, yaml);
956
+ if (command === "export") return handleExport(projectRoot, paths, index, options, yaml);
957
+ if (command === "inspect") return index;
958
+ throw new Error(`Unknown evolution command: ${positional.join(" ") || "(missing)"}`);
959
+ }
960
+
961
+ export function dispatchEvolutionHookEvent(projectRoot, event, dependencies) {
962
+ const yaml = dependencies?.yaml;
963
+ if (!yaml?.parse || !yaml?.stringify) throw new Error("Evolution Hook dispatch requires YAML parse and stringify functions.");
964
+ const resolvedRoot = path.resolve(projectRoot || process.cwd());
965
+ const paths = evolutionPaths(resolvedRoot);
966
+ const index = readIndex(paths, yaml);
967
+ return dispatchHookEvent(resolvedRoot, paths, index, event, yaml);
968
+ }
969
+
970
+ export const EVOLUTION_HOOK_PROTOCOL_VERSION = SCHEMA_VERSION;
971
+ export const EVOLUTION_HOOK_EVENTS = [...HOOK_EVENTS];