ai-delivery-workflow 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/README.md +5 -6
  2. package/bin/ai-delivery.mjs +2 -24
  3. package/docs/CLI-PARAMETER-REFERENCE.zh-CN.md +6 -67
  4. package/docs/DUAL-REPOSITORY-WORKSPACE.zh-CN.md +1 -1
  5. package/docs/FILE-REFERENCE.zh-CN.md +6 -28
  6. package/docs/MAINTENANCE-VERIFICATION.zh-CN.md +2 -2
  7. package/docs/PROJECT-MANUAL.zh-CN.md +23 -38
  8. package/docs/STATE-CLI-MAINTENANCE.zh-CN.md +8 -10
  9. package/docs/STATE-CLI-USER-GUIDE.zh-CN.md +6 -16
  10. package/docs/agents/workflow-manager-acceptance-matrix.md +9 -15
  11. package/docs/agents/workflow-manager-current-state.md +12 -15
  12. package/lib/communication-confirmation.mjs +177 -0
  13. package/lib/delivery-state.mjs +59 -910
  14. package/lib/fs-utils.mjs +0 -16
  15. package/lib/project-bootstrap.mjs +288 -61
  16. package/lib/project-installer.mjs +49 -181
  17. package/lib/project-repair.mjs +217 -0
  18. package/lib/toml-hooks.mjs +2 -4
  19. package/lib/workflow-manager-governance.mjs +3 -47
  20. package/lib/workspace.mjs +2 -3
  21. package/package.json +2 -2
  22. package/skills/ai-delivery-bootstrap/SKILL.md +5 -1
  23. package/skills/ai-delivery-bootstrap/references/bootstrap-contract.md +1 -1
  24. package/skills/ai-delivery-checkpoint-task/SKILL.md +2 -4
  25. package/skills/ai-delivery-checkpoint-task/references/checkpoint-contract.md +4 -2
  26. package/skills/ai-delivery-checkpoint-task/scripts/hook-event.mjs +2 -8
  27. package/skills/ai-delivery-checkpoint-task/scripts/task-state.mjs +26 -46
  28. package/skills/ai-delivery-close-version/SKILL.md +13 -10
  29. package/skills/ai-delivery-close-version/references/version-closeout-contract.md +2 -1
  30. package/skills/ai-delivery-define-product/SKILL.md +6 -3
  31. package/skills/ai-delivery-design-architecture/SKILL.md +10 -5
  32. package/skills/ai-delivery-design-architecture/assets/architecture-template/architecture-gate.yaml +2 -0
  33. package/skills/ai-delivery-design-architecture/assets/architecture-template/prototype-architecture-validation.yaml +2 -0
  34. package/skills/ai-delivery-design-architecture/references/architecture-contract.md +4 -0
  35. package/skills/ai-delivery-design-experience/SKILL.md +8 -6
  36. package/skills/ai-delivery-design-experience/assets/experience-template/product-prototype-reconciliation.yaml +2 -0
  37. package/skills/ai-delivery-design-experience/assets/experience-template/ui-handoff.yaml +3 -0
  38. package/skills/ai-delivery-design-experience/references/experience-contract.md +3 -13
  39. package/skills/ai-delivery-design-experience/references/prototype-management-contract.md +1 -1
  40. package/skills/ai-delivery-design-tests/SKILL.md +4 -0
  41. package/skills/ai-delivery-develop-iteration/SKILL.md +6 -1
  42. package/skills/ai-delivery-develop-iteration/assets/development-template/candidate-manifest.yaml +2 -0
  43. package/skills/ai-delivery-execute-work-package/SKILL.md +4 -2
  44. package/skills/ai-delivery-orchestrate/SKILL.md +7 -7
  45. package/skills/ai-delivery-orchestrate/assets/codebase-template/AGENTS.md +14 -0
  46. package/skills/ai-delivery-orchestrate/assets/codebase-template/CODEBASE_GUIDE.md +124 -0
  47. package/skills/ai-delivery-orchestrate/assets/codebase-template/docs/CODEBASE_INDEX.md +87 -0
  48. package/skills/ai-delivery-orchestrate/assets/communication-template/confirmation-examples.yaml +67 -0
  49. package/skills/ai-delivery-orchestrate/assets/communication-template/confirmation-summary.yaml +42 -0
  50. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/iterations/ITER-0001/02-solution-design/product-impact-assessment.yaml +42 -0
  51. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/hooks/hook-event.mjs +2 -8
  52. package/skills/ai-delivery-orchestrate/assets/project-template/AGENTS.md +4 -5
  53. package/skills/ai-delivery-orchestrate/references/codebase-document-contract.md +91 -0
  54. package/skills/ai-delivery-orchestrate/references/communication-confirmation-contract.md +52 -0
  55. package/skills/ai-delivery-orchestrate/references/formal-state-contract.md +11 -20
  56. package/skills/ai-delivery-orchestrate/references/product-impact-assessment-contract.md +87 -0
  57. package/skills/ai-delivery-orchestrate/references/workflow-model.md +1 -3
  58. package/skills/ai-delivery-orchestrate/scripts/confirmation-state.mjs +30 -0
  59. package/skills/ai-delivery-orchestrate-release/SKILL.md +3 -1
  60. package/skills/ai-delivery-plan-iteration/SKILL.md +6 -1
  61. package/skills/ai-delivery-prepare-platform/SKILL.md +1 -1
  62. package/skills/ai-delivery-prepare-release/SKILL.md +2 -0
  63. package/skills/ai-delivery-prepare-release/assets/release-template/release-manifest.yaml +0 -2
  64. package/skills/ai-delivery-review-change/SKILL.md +3 -1
  65. package/skills/ai-delivery-validate-artifacts/SKILL.md +10 -7
  66. package/skills/ai-delivery-validate-artifacts/references/artifact-contract.md +2 -1
  67. package/skills/ai-delivery-verify-candidate/SKILL.md +5 -2
  68. package/skills/ai-delivery-verify-candidate/references/candidate-verification-contract.md +1 -1
  69. package/docs/PROTOTYPE-MIGRATION.zh-CN.md +0 -164
  70. package/docs/VIEWER-MAINTENANCE.zh-CN.md +0 -150
  71. package/docs/VIEWER-USER-GUIDE.zh-CN.md +0 -113
  72. package/lib/project-migration.mjs +0 -1193
  73. package/lib/prototype-migration.mjs +0 -713
  74. package/skills/ai-delivery-design-experience/assets/experience-template/LEGACY-COMPATIBILITY.md +0 -16
  75. package/skills/ai-delivery-design-experience/assets/experience-template/canvas-catalog.csv +0 -1
  76. package/skills/ai-delivery-design-experience/assets/experience-template/canvas-pages.csv +0 -1
  77. package/skills/ai-delivery-design-experience/assets/experience-template/page-catalog.csv +0 -1
  78. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-file-terminals.csv +0 -1
  79. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-files.csv +0 -1
  80. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-manifest.yaml +0 -76
  81. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-set.yaml +0 -24
@@ -8,11 +8,10 @@ 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
- import { planPrototypeMigration } from "./prototype-migration.mjs";
13
13
  import { inspectHookConfig, managedHookConfigProjection, mergeHookConfig } from "./toml-hooks.mjs";
14
14
  import {
15
- LEGACY_WORKSPACE_CONFIG,
16
15
  WORKSPACE_CONFIG,
17
16
  gitCommonDirectory,
18
17
  gitTopLevel,
@@ -39,7 +38,6 @@ const GIT_ATTRIBUTE_ENTRIES = [
39
38
  "/.gitattributes text eol=lf",
40
39
  ...RAW_GIT_ATTRIBUTE_TARGETS.map((file) => `/${file} -text`),
41
40
  ];
42
- const OBSOLETE_IGNORE_ENTRIES = new Set([".ai-delivery-task", ".ai-delivery/backups/"]);
43
41
  const INSTALL_MANIFEST = path.join(".workflow", "config", "install-manifest.json");
44
42
  const UPGRADE_RUNTIME = path.join(".workflow", "delivery", "runtime", "upgrades");
45
43
  const PRESERVED_DETAIL_LIMIT = 200;
@@ -48,32 +46,18 @@ const DOCTOR_ATTRIBUTE_BATCH_SIZE = 5_000;
48
46
  const DOCTOR_DETAIL_LIMIT = 200;
49
47
  const FORMAL_STATE_INSPECT_MAX_BUFFER = 16 * 1024 * 1024;
50
48
  const INSTALL_MANIFEST_SCHEMA = 6;
51
- const UI_CONTRACT_VERSION = "prototype-first-ui-v1";
52
- const FORMAL_STATE_YAML = {
53
- parse: parseYaml,
54
- stringify: (value) => stringifyYaml(value, { lineWidth: 0 }),
55
- };
56
49
  const MERGED_MANAGED_SURFACES = Object.freeze([
57
50
  { key: "agents", path: "AGENTS.md" },
58
51
  { key: "hook_config", path: ".codex/config.toml" },
59
52
  { key: "gitignore", path: ".gitignore" },
60
53
  { key: "gitattributes", path: ".gitattributes" },
61
54
  ]);
62
- const ACTIVE_TASK = path.join(".workflow", "delivery", "runtime", "active-task.json");
63
- const LEGACY_ACTIVE_TASK = ".ai-delivery-task";
64
55
  const WORKFLOW_CONTENT_DIRECTORIES = ["product", "architecture", "experience", "iterations", "releases"];
65
56
  const CONTROL_DIRECTORIES = [
66
57
  "tasks", "checkpoints", "artifacts", "gates", "versions", "releases", "events", "archives",
67
- "migrations", "repairs",
58
+ "repairs",
68
59
  ];
69
- const ENSURED_CONTROL_DIRECTORIES = ["migrations", "repairs"];
70
- const RETIRED_MANAGED_ASSETS = Object.freeze([
71
- {
72
- path: ".workflow/tools/viewer",
73
- kind: "retired-directory",
74
- manifest_key: "viewer",
75
- },
76
- ]);
60
+ const ENSURED_CONTROL_DIRECTORIES = ["repairs"];
77
61
  const ESSENTIAL_STARTUP_SKILLS = [
78
62
  "ai-delivery-assemble-release",
79
63
  "ai-delivery-bootstrap",
@@ -138,11 +122,9 @@ const V2_PROTOTYPE_DELIVERY_SURFACE_FILES = Object.freeze([
138
122
  const REQUIRED_PACKAGE_SURFACE_FILES = Object.freeze([
139
123
  "LICENSE",
140
124
  "bin/ai-delivery.mjs",
141
- "docs/PROTOTYPE-MIGRATION.zh-CN.md",
142
125
  "lib/mock-scenario-pack.mjs",
143
126
  "lib/mock-system.mjs",
144
- "lib/project-migration.mjs",
145
- "lib/prototype-migration.mjs",
127
+ "lib/project-repair.mjs",
146
128
  "lib/prototype-page-review.mjs",
147
129
  "lib/prototype-review-target.mjs",
148
130
  "skills/ai-delivery-checkpoint-task/scripts/hook-event.mjs",
@@ -344,8 +326,7 @@ function mergeManagedMarkdown(existing, managedTemplate) {
344
326
  }
345
327
 
346
328
  function mergeGitignore(existing, requiredEntries = []) {
347
- const lines = existing.split(/\r?\n/)
348
- .filter((line) => !OBSOLETE_IGNORE_ENTRIES.has(line.trim()));
329
+ const lines = existing.split(/\r?\n/);
349
330
  const additions = [...IGNORE_ENTRIES, ...requiredEntries]
350
331
  .filter((entry) => !lines.some((line) => line.trim() === entry));
351
332
  const retained = lines.join("\n").trimEnd();
@@ -542,7 +523,7 @@ function bootstrapRuntimeSources(packageRoot, sourceTemplate) {
542
523
  relative: "bootstrap.mjs",
543
524
  },
544
525
  ...[
545
- "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",
546
527
  "history-anchor.mjs", "fs-utils.mjs",
547
528
  ].map((name) => ({
548
529
  source: path.join(packageRoot, "lib", name),
@@ -706,7 +687,7 @@ function mergedSurfaceManagedDigest(key, text, codePath) {
706
687
  if (key === "hook_config") return contentDigest(JSON.stringify(managedHookConfigProjection(text)));
707
688
  const lines = text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
708
689
  if (key === "gitignore") {
709
- const managedEntries = new Set([...IGNORE_ENTRIES, `/${codePath}/`, ...OBSOLETE_IGNORE_ENTRIES]);
690
+ const managedEntries = new Set([...IGNORE_ENTRIES, `/${codePath}/`]);
710
691
  return contentDigest(JSON.stringify(lines.filter((line) => managedEntries.has(line)).sort()));
711
692
  }
712
693
  return contentDigest(JSON.stringify(lines.filter((line) => GIT_ATTRIBUTE_ENTRIES.includes(line)).sort()));
@@ -868,10 +849,11 @@ export function initProject(packageRoot, target, options = {}) {
868
849
  "backups",
869
850
  new Date().toISOString().replaceAll(":", "-"),
870
851
  );
871
- const legacyRoot = path.join(target, ".ai-delivery");
872
- const legacyActiveTask = path.join(target, LEGACY_ACTIVE_TASK);
873
- const hasLegacyRoot = fs.existsSync(legacyRoot);
874
- const hasLegacyActiveTask = fs.existsSync(legacyActiveTask);
852
+ const unsupportedRoot = path.join(target, ".ai-delivery");
853
+ const unsupportedActiveTask = path.join(target, ".ai-delivery-task");
854
+ if (fs.existsSync(unsupportedRoot) || fs.existsSync(unsupportedActiveTask)) {
855
+ throw new Error("Unsupported workflow layout detected; install requires the current .workflow contract.");
856
+ }
875
857
  let previousManifest = null;
876
858
  try {
877
859
  previousManifest = JSON.parse(fs.readFileSync(path.join(target, INSTALL_MANIFEST), "utf8"));
@@ -889,24 +871,9 @@ export function initProject(packageRoot, target, options = {}) {
889
871
  throw new Error(`Refusing package downgrade from ${previousManifest.version} to ${packageVersion}; use --force only after explicit approval.`);
890
872
  }
891
873
  const packageVersionUpgrade = packageVersionComparison > 0;
892
- const reinstallManagedRuntime = force || hasLegacyRoot || hasLegacyActiveTask
893
- || packageVersionUpgrade || (previousManifest && previousManifest.schema_version < 5);
894
- if (!dryRun && hasLegacyRoot) {
895
- const legacyBackup = path.join(backupRoot, "legacy-ai-delivery");
896
- fs.mkdirSync(path.dirname(legacyBackup), { recursive: true });
897
- fs.cpSync(legacyRoot, legacyBackup, { recursive: true });
898
- }
899
- if (!dryRun && hasLegacyActiveTask) {
900
- const taskBackup = path.join(backupRoot, "legacy-active-task.json");
901
- fs.mkdirSync(path.dirname(taskBackup), { recursive: true });
902
- fs.copyFileSync(legacyActiveTask, taskBackup);
903
- }
874
+ const reinstallManagedRuntime = force || packageVersionUpgrade;
904
875
  const currentWorkspace = readWorkspaceConfiguration(target, { optional: true });
905
- const legacyWorkspaceFile = path.join(target, LEGACY_WORKSPACE_CONFIG);
906
- const legacyWorkspace = !currentWorkspace && fs.existsSync(legacyWorkspaceFile)
907
- ? readWorkspaceConfiguration(target, { configurationPath: LEGACY_WORKSPACE_CONFIG })
908
- : null;
909
- const existingWorkspace = (currentWorkspace || legacyWorkspace)?.configuration;
876
+ const existingWorkspace = currentWorkspace?.configuration;
910
877
  const codePath = options.codePath || existingWorkspace?.code_repository?.path || "code";
911
878
  const codeId = options.codeId || existingWorkspace?.code_repository?.repo_id || "app";
912
879
  const sharedRemote = options.sharedRemote || null;
@@ -942,23 +909,23 @@ export function initProject(packageRoot, target, options = {}) {
942
909
  const sourceStandardsIndex = path.join(sourceWorkflow, "standards", "index.yaml");
943
910
  const workflowRoot = path.join(target, ".workflow");
944
911
  const standardsIndexFile = path.join(workflowRoot, "standards", "index.yaml");
945
- const legacyIndicators = [
912
+ const unsupportedIndicators = [
946
913
  path.join(target, "delivery", "project.yaml"),
947
914
  path.join(target, "delivery", "workflow-state.yaml"),
948
915
  path.join(target, "delivery", "artifact-registry.yaml"),
949
916
  path.join(target, "delivery", "standards-baseline.yaml"),
950
917
  path.join(target, "standards", "index.yaml"),
951
918
  ].filter((candidate) => fs.existsSync(candidate));
952
- if (fs.existsSync(workflowRoot) && legacyIndicators.length) {
919
+ if (fs.existsSync(workflowRoot) && unsupportedIndicators.length) {
953
920
  throw new Error(
954
- "Detected both .workflow and legacy delivery/standards directories. "
921
+ "Detected both .workflow and unsupported delivery/standards directories. "
955
922
  + "Resolve the split workflow state before initialization.",
956
923
  );
957
924
  }
958
- if (legacyIndicators.length) {
925
+ if (unsupportedIndicators.length) {
959
926
  throw new Error(
960
- "Legacy workflow directories detected at delivery/ or standards/. "
961
- + "Move them under .workflow/ before upgrading; refusing to create split workflow state.",
927
+ "Unsupported workflow directories detected at delivery/ or standards/. "
928
+ + "Remove them before initialization; refusing to create split workflow state.",
962
929
  );
963
930
  }
964
931
  for (const managedPath of [
@@ -986,8 +953,6 @@ export function initProject(packageRoot, target, options = {}) {
986
953
  const hookConflict = fs.existsSync(hookDestination)
987
954
  && !managedFilesEqual(hookDestination, sourceHook);
988
955
  if (hookConflict && !reinstallManagedRuntime) conflicts.push(path.relative(target, hookDestination));
989
- const retiredViewerDestination = path.join(target, ".workflow", "tools", "viewer");
990
- const retireLegacyViewer = hasLegacyRoot && fs.existsSync(retiredViewerDestination);
991
956
  const designReferenceDestination = path.join(target, "ref", "ui");
992
957
  let designReferencePlan = null;
993
958
  if (!fs.existsSync(designReferenceDestination)) {
@@ -1064,19 +1029,6 @@ export function initProject(packageRoot, target, options = {}) {
1064
1029
  ? fs.readFileSync(gitattributesFile, "utf8")
1065
1030
  : "";
1066
1031
  const gitattributesText = mergeGitattributes(existingGitattributes);
1067
- if (hasLegacyRoot || hasLegacyActiveTask) {
1068
- actions.push("migrate legacy .ai-delivery runtime into .workflow");
1069
- if (!dryRun) {
1070
- if (hasLegacyActiveTask) {
1071
- const activeTask = path.join(target, ACTIVE_TASK);
1072
- if (!fs.existsSync(activeTask)) {
1073
- fs.mkdirSync(path.dirname(activeTask), { recursive: true });
1074
- fs.copyFileSync(legacyActiveTask, activeTask);
1075
- }
1076
- }
1077
- }
1078
- }
1079
-
1080
1032
  for (const plan of skillPlans) {
1081
1033
  if (plan.type === "copy") {
1082
1034
  actions.push(`install .agents/skills/${plan.name}`);
@@ -1098,9 +1050,6 @@ export function initProject(packageRoot, target, options = {}) {
1098
1050
  } else if (hookConflict) {
1099
1051
  backupAndReplace(target, hookDestination, sourceHook, backupRoot, actions, dryRun);
1100
1052
  }
1101
- if (retireLegacyViewer) {
1102
- backupAndRemove(target, retiredViewerDestination, backupRoot, actions, dryRun);
1103
- }
1104
1053
  if (designReferencePlan?.type === "copy") {
1105
1054
  actions.push("install ref/ui Design Reference Library");
1106
1055
  if (!dryRun) {
@@ -1200,21 +1149,6 @@ export function initProject(packageRoot, target, options = {}) {
1200
1149
  if (!dryRun) atomicWrite(manifestFile, manifestText);
1201
1150
  }
1202
1151
 
1203
- if (!dryRun && (hasLegacyRoot || hasLegacyActiveTask)) {
1204
- for (const required of [
1205
- path.join(target, WORKSPACE_CONFIG),
1206
- path.join(target, INSTALL_MANIFEST),
1207
- path.join(target, ".workflow", "tools", "state", "state.mjs"),
1208
- path.join(target, ".workflow", "tools", "hooks", "hook-event.mjs"),
1209
- ]) {
1210
- if (!fs.existsSync(required)) {
1211
- throw new Error(`Legacy migration is incomplete; preserved .ai-delivery for recovery: ${required}`);
1212
- }
1213
- }
1214
- if (hasLegacyRoot) fs.rmSync(legacyRoot, { recursive: true, force: true });
1215
- if (hasLegacyActiveTask) fs.rmSync(legacyActiveTask, { force: true });
1216
- }
1217
-
1218
1152
  return {
1219
1153
  actions: [...workspace.actions, ...actions],
1220
1154
  summary: `${dryRun ? "Dry run" : "Initialized"} ${skillNames.length} workspace-scoped AI delivery skills in ${target}`,
@@ -1456,11 +1390,9 @@ function planUpgrade(packageRoot, target, options) {
1456
1390
  validateManagedPath(target, path.join(target, managedPath));
1457
1391
  }
1458
1392
  const { manifest, manifestFile } = readInstallManifest(target);
1459
- if (!Number.isInteger(manifest.schema_version)
1460
- || manifest.schema_version < 5
1461
- || manifest.schema_version > INSTALL_MANIFEST_SCHEMA) {
1393
+ if (manifest.schema_version !== INSTALL_MANIFEST_SCHEMA) {
1462
1394
  throw new Error(
1463
- `Installation manifest schema ${manifest.schema_version} is incompatible with upgrade; run the compatible migration or repair command first.`,
1395
+ `Installation manifest schema ${manifest.schema_version} is unsupported; expected current schema ${INSTALL_MANIFEST_SCHEMA}.`,
1464
1396
  );
1465
1397
  }
1466
1398
  const comparison = comparePackageVersions(packageVersion, manifest.version);
@@ -1471,10 +1403,6 @@ function planUpgrade(packageRoot, target, options) {
1471
1403
  throw new Error(`Refusing package downgrade from ${manifest.version} to ${packageVersion}; use --force only after explicit approval.`);
1472
1404
  }
1473
1405
  const workspace = readWorkspaceConfiguration(target);
1474
- const contractRetirement = executeDeliveryStateCommand([
1475
- "contract", "retire", "--project", target, "--gate-id", "UX-LF", "--dry-run",
1476
- ], { yaml: FORMAL_STATE_YAML });
1477
- const prototypeMigration = planPrototypeMigration(target);
1478
1406
  const codeRepository = workspace.configuration.code_repository;
1479
1407
  if (["path", "repo_id", "remote", "default_branch"].some((key) =>
1480
1408
  (manifest.code_repository?.[key] ?? null) !== (codeRepository[key] ?? null))) {
@@ -1484,18 +1412,12 @@ function planUpgrade(packageRoot, target, options) {
1484
1412
  }
1485
1413
  const sources = installationSources(packageRoot);
1486
1414
  const exactAssets = managedAssetInventory(sources, manifest);
1487
- const retiredAssets = RETIRED_MANAGED_ASSETS
1488
- .filter((asset) => manifest.managed_assets?.[asset.manifest_key])
1489
- .map((asset) => ({
1490
- ...asset,
1491
- previous_digest: manifest.managed_assets[asset.manifest_key].sha256 || null,
1492
- }));
1493
1415
  const mergedAssets = upgradeMergedAssets(
1494
1416
  target,
1495
1417
  sources,
1496
1418
  { ...manifest, code_repository: codeRepository },
1497
1419
  );
1498
- for (const asset of [...exactAssets, ...retiredAssets, ...mergedAssets]) {
1420
+ for (const asset of [...exactAssets, ...mergedAssets]) {
1499
1421
  validateManagedPath(target, installPath(target, asset.path));
1500
1422
  }
1501
1423
  const additions = [];
@@ -1552,32 +1474,12 @@ function planUpgrade(packageRoot, target, options) {
1552
1474
  replacements.push({ path: asset.path, kind: "merged-file", forced: false });
1553
1475
  }
1554
1476
  }
1555
- for (const asset of retiredAssets) {
1556
- const destination = installPath(target, asset.path);
1557
- asset.destination = destination;
1558
- const currentDigest = pathDigest(destination);
1559
- asset.current_digest = currentDigest;
1560
- if (currentDigest === null) {
1561
- preserved.push({ path: asset.path, reason: "retired managed asset is already absent" });
1562
- } else if (asset.previous_digest && currentDigest === asset.previous_digest) {
1563
- removals.push({ path: asset.path, kind: asset.kind, forced: false });
1564
- } else {
1565
- conflicts.push({
1566
- path: asset.path,
1567
- reason: asset.previous_digest
1568
- ? "retired managed asset differs from the installed manifest digest"
1569
- : "installed manifest does not prove ownership of the retired managed asset",
1570
- });
1571
- if (force) removals.push({ path: asset.path, kind: asset.kind, forced: true });
1572
- }
1573
- }
1574
1477
  const preservedWorkflow = preservedWorkflowFiles(
1575
1478
  target,
1576
1479
  [
1577
1480
  ...exactAssets
1578
1481
  .filter((asset) => asset.kind !== "ensure-directory")
1579
1482
  .map((asset) => asset.path),
1580
- ...retiredAssets.map((asset) => asset.path),
1581
1483
  ...mergedAssets.map((asset) => asset.path),
1582
1484
  ],
1583
1485
  );
@@ -1599,7 +1501,6 @@ function planUpgrade(packageRoot, target, options) {
1599
1501
  codeRepository,
1600
1502
  sources,
1601
1503
  exactAssets,
1602
- retiredAssets,
1603
1504
  mergedAssets,
1604
1505
  additions,
1605
1506
  replacements,
@@ -1609,8 +1510,6 @@ function planUpgrade(packageRoot, target, options) {
1609
1510
  conflicts,
1610
1511
  backups,
1611
1512
  comparison,
1612
- contractRetirement,
1613
- prototypeMigration,
1614
1513
  };
1615
1514
  }
1616
1515
 
@@ -1704,17 +1603,6 @@ function applyUpgradePlan(target, plan, packageVersion) {
1704
1603
  apply: () => atomicWrite(asset.destination, asset.desired),
1705
1604
  });
1706
1605
  }
1707
- for (const asset of plan.retiredAssets) {
1708
- if (!changes.has(asset.path)) continue;
1709
- applyReceiptBackedAsset({
1710
- target,
1711
- upgradeRoot,
1712
- asset,
1713
- operation: "remove",
1714
- receiptAssets,
1715
- apply: () => fs.rmSync(asset.destination, { recursive: true, force: true }),
1716
- });
1717
- }
1718
1606
  const manifestRelative = INSTALL_MANIFEST.replaceAll("\\", "/");
1719
1607
  const manifestBackup = backupUpgradePath(target, upgradeRoot, manifestRelative);
1720
1608
  const nextManifest = buildInstallManifest(
@@ -1742,30 +1630,10 @@ function applyUpgradePlan(target, plan, packageVersion) {
1742
1630
  to_version: packageVersion,
1743
1631
  applied_at: new Date().toISOString(),
1744
1632
  formal_event_tree_fingerprint: formalEventFingerprint,
1745
- contract_retirement: {
1746
- gate_id: plan.contractRetirement.gate_id,
1747
- contract_version: UI_CONTRACT_VERSION,
1748
- affected_iterations: plan.contractRetirement.affected_iterations,
1749
- expected_scope_revisions: plan.contractRetirement.expected_scope_revisions,
1750
- },
1751
1633
  assets: receiptAssets,
1752
1634
  };
1753
1635
  atomicWrite(path.join(upgradeRoot, "receipt.json"), `${JSON.stringify(receipt, null, 2)}\n`);
1754
- const retirementArgs = [
1755
- "contract", "retire", "--project", target,
1756
- "--gate-id", "UX-LF",
1757
- "--actor", "system:package-upgrade",
1758
- "--reason", "prototype-first UI contract",
1759
- "--contract-version", UI_CONTRACT_VERSION,
1760
- ];
1761
- const expectedRevisions = Object.values(plan.contractRetirement.expected_scope_revisions);
1762
- if (expectedRevisions.length === 1) {
1763
- retirementArgs.push("--expected-scope-revision", String(expectedRevisions[0]));
1764
- }
1765
- const contractRetirement = executeDeliveryStateCommand(retirementArgs, {
1766
- yaml: FORMAL_STATE_YAML,
1767
- });
1768
- return { upgradeId, contractRetirement };
1636
+ return { upgradeId };
1769
1637
  } catch (error) {
1770
1638
  restoreUpgradeAssets(target, upgradeRoot, receiptAssets);
1771
1639
  fs.rmSync(upgradeRoot, { recursive: true, force: true });
@@ -1796,7 +1664,6 @@ function rollbackUpgrade(packageRoot, target, upgradeId) {
1796
1664
  const sources = installationSources(packageRoot);
1797
1665
  const allowedPaths = new Set([
1798
1666
  ...managedAssetInventory(sources).map((asset) => asset.path),
1799
- ...RETIRED_MANAGED_ASSETS.map((asset) => asset.path),
1800
1667
  ...MERGED_MANAGED_SURFACES.map((surface) => surface.path),
1801
1668
  INSTALL_MANIFEST.replaceAll("\\", "/"),
1802
1669
  ]);
@@ -1866,26 +1733,10 @@ export function upgradeProject(packageRoot, target, options = {}) {
1866
1733
  conflicts: plan.conflicts,
1867
1734
  backups: plan.backups,
1868
1735
  },
1869
- compatibility: {
1870
- installation_manifest: "compatible",
1871
- package_version: plan.comparison > 0 ? "upgrade" : plan.comparison === 0 ? "same-version repair" : "forced downgrade",
1872
- },
1873
1736
  code_repository: {
1874
1737
  path: plan.codeRepository.path,
1875
1738
  write_planned: false,
1876
1739
  },
1877
- contract_retirement: {
1878
- ...plan.contractRetirement,
1879
- contract_version: UI_CONTRACT_VERSION,
1880
- write_planned: plan.contractRetirement.affected_iterations.length > 0,
1881
- },
1882
- prototype_migration: {
1883
- ...plan.prototypeMigration,
1884
- required_follow_up: {
1885
- command: "ai-delivery migrate",
1886
- write_planned: false,
1887
- },
1888
- },
1889
1740
  };
1890
1741
  if (dryRun) return report;
1891
1742
  const blockingConflicts = plan.conflicts.filter((item) => !force || item.forceable === false);
@@ -1897,12 +1748,6 @@ export function upgradeProject(packageRoot, target, options = {}) {
1897
1748
  }
1898
1749
  const applied = applyUpgradePlan(target, plan, packageVersion);
1899
1750
  report.upgrade_id = applied.upgradeId;
1900
- report.contract_retirement = {
1901
- ...report.contract_retirement,
1902
- ...applied.contractRetirement,
1903
- contract_version: UI_CONTRACT_VERSION,
1904
- write_planned: false,
1905
- };
1906
1751
  report.applied = true;
1907
1752
  return report;
1908
1753
  }
@@ -2074,6 +1919,28 @@ export function doctorProject(target, options = {}) {
2074
1919
 
2075
1920
  const workflowGit = gitTopLevel(target);
2076
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
+ }
2077
1944
  const workflowCommonGit = workflowGit ? gitCommonDirectory(target) : null;
2078
1945
  const codeCommonGit = codeGit && workspace ? gitCommonDirectory(workspace.codeRoot) : null;
2079
1946
  checks.push({
@@ -2231,6 +2098,7 @@ export function doctorProject(target, options = {}) {
2231
2098
  const bootstrapFiles = [
2232
2099
  "bootstrap.mjs",
2233
2100
  "project-bootstrap.mjs",
2101
+ "communication-confirmation.mjs",
2234
2102
  "workspace.mjs",
2235
2103
  "yaml-runtime.mjs",
2236
2104
  "delivery-state.mjs",
@@ -2496,8 +2364,8 @@ export function doctorProject(target, options = {}) {
2496
2364
  });
2497
2365
  }
2498
2366
  checks.push({
2499
- ok: !fs.existsSync(path.join(target, ".ai-delivery")) && !fs.existsSync(path.join(target, LEGACY_ACTIVE_TASK)),
2500
- message: "legacy .ai-delivery paths are absent",
2367
+ ok: !fs.existsSync(path.join(target, ".ai-delivery")) && !fs.existsSync(path.join(target, ".ai-delivery-task")),
2368
+ message: "unsupported workflow paths are absent",
2501
2369
  });
2502
2370
  checks.push({ ok: fs.existsSync(path.join(target, "AGENTS.md")), message: "project AGENTS.md is present" });
2503
2371
  return { checks, ok: checks.filter((check) => check.required !== false).every((check) => check.ok) };
@@ -0,0 +1,217 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { parse as parseYaml } from "yaml";
5
+ import { atomicWrite, filesUnder } from "./fs-utils.mjs";
6
+
7
+ function toPosix(value) {
8
+ return value.replaceAll("\\", "/");
9
+ }
10
+
11
+ function sha256(value) {
12
+ return crypto.createHash("sha256").update(value).digest("hex");
13
+ }
14
+
15
+ function treeDigest(root) {
16
+ const hash = crypto.createHash("sha256");
17
+ for (const file of filesUnder(root)) {
18
+ hash.update(toPosix(path.relative(root, file)));
19
+ hash.update("\0");
20
+ hash.update(fs.readFileSync(file));
21
+ hash.update("\0");
22
+ }
23
+ return hash.digest("hex");
24
+ }
25
+
26
+ function readStructured(file, label) {
27
+ try {
28
+ const text = fs.readFileSync(file, "utf8");
29
+ const value = path.extname(file).toLowerCase() === ".json" ? JSON.parse(text) : parseYaml(text);
30
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("expected a mapping");
31
+ return value;
32
+ } catch (error) {
33
+ throw new Error(`Cannot read ${label} (${toPosix(file)}): ${error.message}`);
34
+ }
35
+ }
36
+
37
+ function stableValue(value) {
38
+ if (Array.isArray(value)) return value.map(stableValue);
39
+ if (!value || typeof value !== "object") return value;
40
+ return Object.fromEntries(Object.keys(value).sort().map((key) => [key, stableValue(value[key])]));
41
+ }
42
+
43
+ function stableJson(value) {
44
+ return JSON.stringify(stableValue(value));
45
+ }
46
+
47
+ function uniqueSorted(values) {
48
+ return [...new Set((Array.isArray(values) ? values : []).map(String))].sort();
49
+ }
50
+
51
+ function unsignedRepairRecord(record) {
52
+ const unsigned = { ...record };
53
+ delete unsigned.record_checksum;
54
+ return unsigned;
55
+ }
56
+
57
+ function readRepairTransactions(project) {
58
+ const directory = path.join(project, ".workflow", "control", "repairs", "transactions");
59
+ const records = [];
60
+ let previousChecksum = null;
61
+ for (const file of filesUnder(directory).filter((item) => item.endsWith(".json"))) {
62
+ const record = readStructured(file, "repair transaction");
63
+ if (record.schema_version !== 1
64
+ || !Number.isInteger(record.sequence)
65
+ || record.sequence !== records.length + 1
66
+ || record.previous_checksum !== previousChecksum
67
+ || record.record_checksum !== sha256(stableJson(unsignedRepairRecord(record)))) {
68
+ throw new Error(`Repair transaction chain is invalid: ${toPosix(path.relative(project, file))}.`);
69
+ }
70
+ records.push(record);
71
+ previousChecksum = record.record_checksum;
72
+ }
73
+ return records;
74
+ }
75
+
76
+ function archiveIterationId(value) {
77
+ const iterationId = String(value || "");
78
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(iterationId)) {
79
+ throw new Error(`Invalid archive iteration ID: ${iterationId || "missing"}.`);
80
+ }
81
+ return iterationId;
82
+ }
83
+
84
+ function verifiedArchiveInvalidations(project, records) {
85
+ for (const record of records.filter((item) => item.transaction_type === "archive-invalidation")) {
86
+ const iterationId = archiveIterationId(record.target?.iteration_id);
87
+ const expectedArchivePath = `.workflow/control/archives/versions/${iterationId}`;
88
+ const expectedManifestPath = `${expectedArchivePath}/version-manifest.json`;
89
+ if (record.target.archive_path !== expectedArchivePath
90
+ || record.target.manifest_path !== expectedManifestPath
91
+ || !/^[a-f0-9]{64}$/.test(record.target.archive_sha256 || "")
92
+ || !/^[a-f0-9]{64}$/.test(record.target.manifest_sha256 || "")) {
93
+ throw new Error(`Archive invalidation target is invalid: ${record.transaction_id}.`);
94
+ }
95
+ const archiveDirectory = path.join(project, ...expectedArchivePath.split("/"));
96
+ const manifestFile = path.join(project, ...expectedManifestPath.split("/"));
97
+ if (!fs.existsSync(manifestFile)
98
+ || treeDigest(archiveDirectory) !== record.target.archive_sha256
99
+ || sha256(fs.readFileSync(manifestFile)) !== record.target.manifest_sha256) {
100
+ throw new Error(`Invalidated archive ${iterationId} bytes changed after ${record.transaction_id}.`);
101
+ }
102
+ }
103
+ }
104
+
105
+ function repairTransactionPath(sequence, transactionId) {
106
+ return `.workflow/control/repairs/transactions/${String(sequence).padStart(6, "0")}-${transactionId}.json`;
107
+ }
108
+
109
+ export function repairProject(project, options = {}) {
110
+ const root = path.resolve(project);
111
+ if (!fs.existsSync(root) || !fs.statSync(root).isDirectory()) {
112
+ throw new Error(`Project directory does not exist: ${root}`);
113
+ }
114
+ const iterationId = archiveIterationId(options.invalidateArchive);
115
+ const reason = String(options.reason || "").trim();
116
+ const actor = String(options.actor || "").trim();
117
+ const evidence = uniqueSorted(options.evidence);
118
+ if (!reason) throw new Error("Archive invalidation requires --reason.");
119
+ if (!actor.startsWith("human:")) {
120
+ throw new Error("Archive invalidation requires an explicit human actor through --actor human:<identity>.");
121
+ }
122
+ if (evidence.length < 2) {
123
+ throw new Error("Archive invalidation requires at least two --evidence values for RCA and recovery evidence.");
124
+ }
125
+ const archiveDirectory = path.join(root, ".workflow", "control", "archives", "versions", iterationId);
126
+ const manifestFile = path.join(archiveDirectory, "version-manifest.json");
127
+ if (!fs.existsSync(manifestFile)) throw new Error(`Version archive not found: ${iterationId}.`);
128
+ const manifest = readStructured(manifestFile, "version archive manifest");
129
+ if (manifest.iteration_id !== iterationId) {
130
+ throw new Error(`Version archive identity mismatch: expected ${iterationId}, found ${manifest.iteration_id || "missing"}.`);
131
+ }
132
+ const target = {
133
+ iteration_id: iterationId,
134
+ archive_path: toPosix(path.relative(root, archiveDirectory)),
135
+ manifest_path: toPosix(path.relative(root, manifestFile)),
136
+ manifest_sha256: sha256(fs.readFileSync(manifestFile)),
137
+ archive_sha256: treeDigest(archiveDirectory),
138
+ original_bytes_preserved: true,
139
+ };
140
+ const identity = { transaction_type: "archive-invalidation", target, correction: { actor, reason, evidence } };
141
+ const idempotencyKey = `archive-invalidation:${iterationId}:${sha256(stableJson(identity))}`;
142
+ const existingTransactions = readRepairTransactions(root);
143
+ verifiedArchiveInvalidations(root, existingTransactions);
144
+ const existing = existingTransactions.find((record) => record.idempotency_key === idempotencyKey);
145
+ const sequence = existing?.sequence || existingTransactions.length + 1;
146
+ const transactionId = existing?.transaction_id || `REPAIR-${sha256(idempotencyKey).slice(0, 16).toUpperCase()}`;
147
+ const transactionPath = repairTransactionPath(sequence, transactionId);
148
+ const report = {
149
+ command: "repair",
150
+ action: "archive-invalidation",
151
+ dry_run: Boolean(options.dryRun),
152
+ applied: false,
153
+ forward_only: true,
154
+ original_bytes_preserved: true,
155
+ target,
156
+ actor,
157
+ reason,
158
+ evidence,
159
+ transaction: {
160
+ transaction_id: transactionId,
161
+ path: transactionPath,
162
+ previous_checksum: existing?.previous_checksum || existingTransactions.at(-1)?.record_checksum || null,
163
+ },
164
+ eligibility: existing
165
+ ? { iteration_id: iterationId, current: "invalidated", source_transaction_id: transactionId }
166
+ : { iteration_id: iterationId, current: "eligible", source_transaction_id: null },
167
+ };
168
+ if (report.dry_run || existing) return report;
169
+
170
+ const repairRoot = path.join(root, ".workflow", "control", "repairs");
171
+ const lockFile = path.join(repairRoot, ".repair.lock");
172
+ fs.mkdirSync(repairRoot, { recursive: true });
173
+ let lock;
174
+ try {
175
+ lock = fs.openSync(lockFile, "wx");
176
+ } catch (error) {
177
+ throw new Error(`Repair transaction is locked by another writer: ${error.message}`);
178
+ }
179
+ try {
180
+ const currentTransactions = readRepairTransactions(root);
181
+ const duplicate = currentTransactions.find((record) => record.idempotency_key === idempotencyKey);
182
+ if (duplicate) {
183
+ report.transaction.transaction_id = duplicate.transaction_id;
184
+ report.transaction.path = repairTransactionPath(duplicate.sequence, duplicate.transaction_id);
185
+ report.eligibility = { iteration_id: iterationId, current: "invalidated", source_transaction_id: duplicate.transaction_id };
186
+ return report;
187
+ }
188
+ if (currentTransactions.length !== existingTransactions.length
189
+ || (currentTransactions.at(-1)?.record_checksum || null) !== (existingTransactions.at(-1)?.record_checksum || null)) {
190
+ throw new Error("Repair transaction revision changed during apply; rerun the command.");
191
+ }
192
+ if (treeDigest(archiveDirectory) !== target.archive_sha256
193
+ || sha256(fs.readFileSync(manifestFile)) !== target.manifest_sha256) {
194
+ throw new Error(`Version archive changed during repair apply: ${iterationId}.`);
195
+ }
196
+ const transaction = {
197
+ schema_version: 1,
198
+ sequence,
199
+ transaction_id: transactionId,
200
+ transaction_type: "archive-invalidation",
201
+ idempotency_key: idempotencyKey,
202
+ occurred_at: new Date().toISOString(),
203
+ target,
204
+ correction: { actor, reason, evidence },
205
+ previous_checksum: existingTransactions.at(-1)?.record_checksum || null,
206
+ };
207
+ transaction.record_checksum = sha256(stableJson(transaction));
208
+ atomicWrite(path.join(root, ...transactionPath.split("/")), `${JSON.stringify(transaction, null, 2)}\n`);
209
+ report.applied = true;
210
+ report.transaction.previous_checksum = transaction.previous_checksum;
211
+ report.eligibility = { iteration_id: iterationId, current: "invalidated", source_transaction_id: transactionId };
212
+ return report;
213
+ } finally {
214
+ if (lock !== undefined) fs.closeSync(lock);
215
+ fs.rmSync(lockFile, { force: true });
216
+ }
217
+ }