@open-agent-toolkit/cli 0.2.40 → 0.2.45

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 (213) hide show
  1. package/assets/agents/oat-codebase-mapper.md +15 -3
  2. package/assets/agents/oat-phase-implementer.md +34 -9
  3. package/assets/agents/oat-reviewer.md +2 -2
  4. package/assets/bundle-metadata.json +1 -1
  5. package/assets/docs/cli-utilities/configuration.md +11 -1
  6. package/assets/docs/cli-utilities/tool-packs.md +8 -0
  7. package/assets/docs/contributing/skills.md +67 -14
  8. package/assets/docs/quickstart.md +9 -0
  9. package/assets/docs/reference/cli-reference.md +56 -1
  10. package/assets/docs/reference/file-locations.md +18 -1
  11. package/assets/docs/reference/oat-directory-structure.md +20 -4
  12. package/assets/docs/workflows/projects/artifacts.md +27 -1
  13. package/assets/docs/workflows/projects/implementation-execution.md +22 -0
  14. package/assets/docs/workflows/projects/index.md +6 -0
  15. package/assets/docs/workflows/projects/lifecycle.md +19 -5
  16. package/assets/docs/workflows/projects/picking-up-projects.md +75 -0
  17. package/assets/docs/workflows/projects/pr-flow.md +22 -1
  18. package/assets/docs/workflows/projects/reviewing-oat-prs.md +60 -0
  19. package/assets/public-package-versions.json +4 -4
  20. package/assets/skills/analyze/SKILL.md +13 -5
  21. package/assets/skills/compare/SKILL.md +11 -2
  22. package/assets/skills/oat-brainstorm/SKILL.md +135 -32
  23. package/assets/skills/oat-brainstorm/references/destinations.md +9 -6
  24. package/assets/skills/oat-cursor-cloud-projects/SKILL.md +11 -1
  25. package/assets/skills/oat-dispatch-subagents/SKILL.md +19 -6
  26. package/assets/skills/oat-doctor/SKILL.md +8 -1
  27. package/assets/skills/oat-project-autonomous/SKILL.md +45 -1
  28. package/assets/skills/oat-project-autonomous/references/gate-inventory.md +8 -8
  29. package/assets/skills/oat-project-capture/SKILL.md +29 -9
  30. package/assets/skills/oat-project-complete/SKILL.md +610 -38
  31. package/assets/skills/oat-project-complete/scripts/parse-completion-retry-fields.mjs +265 -0
  32. package/assets/skills/oat-project-complete/scripts/recover-completion-receipts.mjs +764 -0
  33. package/assets/skills/oat-project-complete/scripts/resolve-completion-retry.mjs +134 -0
  34. package/assets/skills/oat-project-complete/scripts/validate-nonarchive-lifecycle-receipt.mjs +160 -0
  35. package/assets/skills/oat-project-design/SKILL.md +33 -9
  36. package/assets/skills/oat-project-discover/SKILL.md +16 -3
  37. package/assets/skills/oat-project-document/SKILL.md +10 -4
  38. package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +8 -8
  39. package/assets/skills/oat-project-implement/SKILL.md +14 -3
  40. package/assets/skills/oat-project-implement/references/completion-and-closeout.md +18 -6
  41. package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +8 -8
  42. package/assets/skills/oat-project-implement/references/phase-execution.md +9 -3
  43. package/assets/skills/oat-project-implement/references/plan-and-resume.md +3 -0
  44. package/assets/skills/oat-project-import-plan/SKILL.md +15 -9
  45. package/assets/skills/oat-project-new/SKILL.md +7 -5
  46. package/assets/skills/oat-project-next/SKILL.md +32 -10
  47. package/assets/skills/oat-project-plan/SKILL.md +18 -3
  48. package/assets/skills/oat-project-pr-final/SKILL.md +23 -2
  49. package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +8 -8
  50. package/assets/skills/oat-project-pr-progress/SKILL.md +23 -2
  51. package/assets/skills/oat-project-progress/SKILL.md +23 -9
  52. package/assets/skills/oat-project-promote-spec-driven/SKILL.md +20 -2
  53. package/assets/skills/oat-project-quick-start/SKILL.md +48 -18
  54. package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +8 -8
  55. package/assets/skills/oat-project-reconcile/SKILL.md +14 -10
  56. package/assets/skills/oat-project-retro/SKILL.md +3 -3
  57. package/assets/skills/oat-project-retro/references/apply-procedure.md +102 -6
  58. package/assets/skills/oat-project-retro-file/SKILL.md +57 -6
  59. package/assets/skills/oat-project-review-provide/SKILL.md +56 -6
  60. package/assets/skills/oat-project-review-receive/SKILL.md +16 -7
  61. package/assets/skills/oat-project-review-receive-remote/SKILL.md +9 -3
  62. package/assets/skills/oat-project-revise/SKILL.md +12 -3
  63. package/assets/skills/oat-project-spec/SKILL.md +16 -3
  64. package/assets/skills/oat-project-summary/SKILL.md +113 -13
  65. package/assets/skills/oat-repo-improve/SKILL.md +18 -7
  66. package/assets/skills/oat-review-provide-remote/SKILL.md +11 -2
  67. package/assets/skills/oat-wave-execute/SKILL.md +34 -2
  68. package/assets/skills/oat-worktree-bootstrap/SKILL.md +13 -1
  69. package/assets/skills/oat-worktree-bootstrap-auto/SKILL.md +13 -1
  70. package/dist/commands/config/index.d.ts +1 -0
  71. package/dist/commands/config/index.d.ts.map +1 -1
  72. package/dist/commands/config/index.js +30 -2
  73. package/dist/commands/doctor/index.d.ts +2 -0
  74. package/dist/commands/doctor/index.d.ts.map +1 -1
  75. package/dist/commands/doctor/index.js +27 -3
  76. package/dist/commands/doctor/synced-projects.d.ts +12 -0
  77. package/dist/commands/doctor/synced-projects.d.ts.map +1 -0
  78. package/dist/commands/doctor/synced-projects.js +208 -0
  79. package/dist/commands/init/gitattributes.d.ts +4 -0
  80. package/dist/commands/init/gitattributes.d.ts.map +1 -0
  81. package/dist/commands/init/gitattributes.js +12 -0
  82. package/dist/commands/init/gitignore.d.ts +8 -1
  83. package/dist/commands/init/gitignore.d.ts.map +1 -1
  84. package/dist/commands/init/gitignore.js +146 -41
  85. package/dist/commands/init/index.d.ts +2 -0
  86. package/dist/commands/init/index.d.ts.map +1 -1
  87. package/dist/commands/init/index.js +3 -0
  88. package/dist/commands/init/managed-block.d.ts +10 -0
  89. package/dist/commands/init/managed-block.d.ts.map +1 -0
  90. package/dist/commands/init/managed-block.js +40 -0
  91. package/dist/commands/init/tools/workflows/install-workflows.js +1 -1
  92. package/dist/commands/local/index.d.ts.map +1 -1
  93. package/dist/commands/local/index.js +5 -1
  94. package/dist/commands/local/sync.d.ts +4 -0
  95. package/dist/commands/local/sync.d.ts.map +1 -1
  96. package/dist/commands/local/sync.js +123 -2
  97. package/dist/commands/pjm/adoption.d.ts +2 -0
  98. package/dist/commands/pjm/adoption.d.ts.map +1 -1
  99. package/dist/commands/pjm/adoption.js +1 -1
  100. package/dist/commands/project/archive/archive-utils.d.ts +24 -0
  101. package/dist/commands/project/archive/archive-utils.d.ts.map +1 -1
  102. package/dist/commands/project/archive/archive-utils.js +349 -22
  103. package/dist/commands/project/archive/index.d.ts.map +1 -1
  104. package/dist/commands/project/archive/index.js +1 -0
  105. package/dist/commands/project/archive/push-runner.d.ts +5 -1
  106. package/dist/commands/project/archive/push-runner.d.ts.map +1 -1
  107. package/dist/commands/project/archive/push-runner.js +33 -4
  108. package/dist/commands/project/complete-state/index.d.ts.map +1 -1
  109. package/dist/commands/project/complete-state/index.js +3 -1
  110. package/dist/commands/project/index.d.ts.map +1 -1
  111. package/dist/commands/project/index.js +12 -0
  112. package/dist/commands/project/links/compute.d.ts +9 -0
  113. package/dist/commands/project/links/compute.d.ts.map +1 -0
  114. package/dist/commands/project/links/compute.js +27 -0
  115. package/dist/commands/project/links/index.d.ts +17 -0
  116. package/dist/commands/project/links/index.d.ts.map +1 -0
  117. package/dist/commands/project/links/index.js +72 -0
  118. package/dist/commands/project/links/refresh.d.ts +26 -0
  119. package/dist/commands/project/links/refresh.d.ts.map +1 -0
  120. package/dist/commands/project/links/refresh.js +92 -0
  121. package/dist/commands/project/links/render.d.ts +24 -0
  122. package/dist/commands/project/links/render.d.ts.map +1 -0
  123. package/dist/commands/project/links/render.js +68 -0
  124. package/dist/commands/project/list.d.ts +5 -0
  125. package/dist/commands/project/list.d.ts.map +1 -1
  126. package/dist/commands/project/list.js +182 -9
  127. package/dist/commands/project/migrate/index.d.ts +22 -0
  128. package/dist/commands/project/migrate/index.d.ts.map +1 -0
  129. package/dist/commands/project/migrate/index.js +79 -0
  130. package/dist/commands/project/new/index.d.ts +2 -0
  131. package/dist/commands/project/new/index.d.ts.map +1 -1
  132. package/dist/commands/project/new/index.js +14 -2
  133. package/dist/commands/project/new/scaffold.d.ts +22 -1
  134. package/dist/commands/project/new/scaffold.d.ts.map +1 -1
  135. package/dist/commands/project/new/scaffold.js +287 -69
  136. package/dist/commands/project/open/index.d.ts +16 -0
  137. package/dist/commands/project/open/index.d.ts.map +1 -1
  138. package/dist/commands/project/open/index.js +151 -19
  139. package/dist/commands/project/pause/index.d.ts +8 -0
  140. package/dist/commands/project/pause/index.d.ts.map +1 -1
  141. package/dist/commands/project/pause/index.js +129 -19
  142. package/dist/commands/project/prune/index.d.ts +21 -0
  143. package/dist/commands/project/prune/index.d.ts.map +1 -0
  144. package/dist/commands/project/prune/index.js +95 -0
  145. package/dist/commands/project/pull/index.d.ts +28 -0
  146. package/dist/commands/project/pull/index.d.ts.map +1 -0
  147. package/dist/commands/project/pull/index.js +157 -0
  148. package/dist/commands/project/push/index.d.ts +22 -0
  149. package/dist/commands/project/push/index.d.ts.map +1 -0
  150. package/dist/commands/project/push/index.js +100 -0
  151. package/dist/commands/project/scope/index.d.ts +17 -0
  152. package/dist/commands/project/scope/index.d.ts.map +1 -0
  153. package/dist/commands/project/scope/index.js +107 -0
  154. package/dist/commands/project/split/run.d.ts +4 -0
  155. package/dist/commands/project/split/run.d.ts.map +1 -1
  156. package/dist/commands/project/split/run.js +48 -10
  157. package/dist/commands/project/sync/git.d.ts +18 -0
  158. package/dist/commands/project/sync/git.d.ts.map +1 -0
  159. package/dist/commands/project/sync/git.js +60 -0
  160. package/dist/commands/project/sync/record.d.ts +22 -0
  161. package/dist/commands/project/sync/record.d.ts.map +1 -0
  162. package/dist/commands/project/sync/record.js +133 -0
  163. package/dist/commands/project/sync/ref-sync.d.ts +120 -0
  164. package/dist/commands/project/sync/ref-sync.d.ts.map +1 -0
  165. package/dist/commands/project/sync/ref-sync.js +962 -0
  166. package/dist/commands/project/sync/resolve-target.d.ts +27 -0
  167. package/dist/commands/project/sync/resolve-target.d.ts.map +1 -0
  168. package/dist/commands/project/sync/resolve-target.js +159 -0
  169. package/dist/commands/shared/oat-paths.d.ts +4 -1
  170. package/dist/commands/shared/oat-paths.d.ts.map +1 -1
  171. package/dist/commands/shared/oat-paths.js +2 -2
  172. package/dist/commands/shared/project-scope.d.ts +23 -0
  173. package/dist/commands/shared/project-scope.d.ts.map +1 -0
  174. package/dist/commands/shared/project-scope.js +106 -0
  175. package/dist/commands/tools/migrate/index.js +1 -1
  176. package/dist/commands/tools/shared/pack-inventory.d.ts.map +1 -1
  177. package/dist/commands/tools/shared/pack-inventory.js +11 -0
  178. package/dist/commands/tools/shared/pack-lifecycle.js +1 -1
  179. package/dist/commands/tools/shared/pack-reconcile.d.ts.map +1 -1
  180. package/dist/commands/tools/shared/pack-reconcile.js +4 -1
  181. package/dist/commands/tools/update/index.d.ts.map +1 -1
  182. package/dist/commands/tools/update/index.js +20 -10
  183. package/dist/commands/tools/update/update-tools.d.ts +2 -0
  184. package/dist/commands/tools/update/update-tools.d.ts.map +1 -1
  185. package/dist/config/oat-config.d.ts +6 -3
  186. package/dist/config/oat-config.d.ts.map +1 -1
  187. package/dist/config/oat-config.js +41 -7
  188. package/dist/config/resolve.d.ts.map +1 -1
  189. package/dist/config/resolve.js +2 -1
  190. package/dist/fs/io.d.ts +2 -1
  191. package/dist/fs/io.d.ts.map +1 -1
  192. package/dist/fs/io.js +5 -2
  193. package/dist/projects/split/finalize.d.ts.map +1 -1
  194. package/dist/projects/split/finalize.js +55 -2
  195. package/dist/projects/split/resume.d.ts +3 -0
  196. package/dist/projects/split/resume.d.ts.map +1 -1
  197. package/dist/projects/split/resume.js +34 -7
  198. package/dist/projects/split/seed-children.d.ts.map +1 -1
  199. package/dist/projects/split/seed-children.js +6 -0
  200. package/dist/projects/split/write-parent.d.ts +7 -0
  201. package/dist/projects/split/write-parent.d.ts.map +1 -1
  202. package/dist/projects/split/write-parent.js +7 -4
  203. package/dist/release/public-package-contract.d.ts.map +1 -1
  204. package/dist/release/public-package-contract.js +1 -0
  205. package/dist/validation/skills.d.ts.map +1 -1
  206. package/dist/validation/skills.js +434 -0
  207. package/package.json +4 -4
  208. package/dist/__tests__/skills/split-flow-fixtures.d.ts +0 -50
  209. package/dist/__tests__/skills/split-flow-fixtures.d.ts.map +0 -1
  210. package/dist/__tests__/skills/split-flow-fixtures.js +0 -161
  211. package/dist/commands/__tests__/helpers.d.ts +0 -12
  212. package/dist/commands/__tests__/helpers.d.ts.map +0 -1
  213. package/dist/commands/__tests__/helpers.js +0 -36
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: oat-project-complete
3
- version: 1.6.2
3
+ version: 1.7.5
4
4
  description: Use when all implementation work is finished and the project is ready to close. Marks the OAT project lifecycle as complete.
5
5
  disable-model-invocation: true
6
6
  user-invocable: true
@@ -42,14 +42,42 @@ if [[ -z "$PROJECT_PATH" ]]; then
42
42
  fi
43
43
 
44
44
  PROJECT_NAME=$(basename "$PROJECT_PATH")
45
+ ACTIVE_PROJECT_PATH="$PROJECT_PATH"
46
+
47
+ # Set SKILL_DIR to the absolute directory containing this loaded SKILL.md.
48
+ COMPLETION_RECEIPT_SCRIPT="$SKILL_DIR/scripts/recover-completion-receipts.mjs"
49
+ COMPLETION_RETRY_SCRIPT="$SKILL_DIR/scripts/resolve-completion-retry.mjs"
50
+ COMPLETION_RETRY_FIELDS_SCRIPT="$SKILL_DIR/scripts/parse-completion-retry-fields.mjs"
51
+ NONARCHIVE_LIFECYCLE_RECEIPT_SCRIPT="$SKILL_DIR/scripts/validate-nonarchive-lifecycle-receipt.mjs"
52
+ test -f "$COMPLETION_RECEIPT_SCRIPT" || {
53
+ echo "oat: completion receipt recovery script is missing" >&2
54
+ exit 1
55
+ }
56
+ test -f "$COMPLETION_RETRY_SCRIPT" || {
57
+ echo "oat: completion retry routing script is missing" >&2
58
+ exit 1
59
+ }
60
+ test -f "$COMPLETION_RETRY_FIELDS_SCRIPT" || {
61
+ echo "oat: completion retry field decoder is missing" >&2
62
+ exit 1
63
+ }
64
+ test -f "$NONARCHIVE_LIFECYCLE_RECEIPT_SCRIPT" || {
65
+ echo "Missing non-archive lifecycle receipt validator: $NONARCHIVE_LIFECYCLE_RECEIPT_SCRIPT" >&2
66
+ exit 1
67
+ }
45
68
 
46
- PROJECTS_ROOT="${OAT_PROJECTS_ROOT:-$(oat config get projects.root 2>/dev/null || echo ".oat/projects/shared")}"
47
- PROJECTS_ROOT="${PROJECTS_ROOT%/}"
48
- IS_SHARED_PROJECT="false"
49
-
50
- case "$PROJECT_PATH" in
51
- "${PROJECTS_ROOT}/"*) IS_SHARED_PROJECT="true" ;;
52
- esac
69
+ PROJECT_SCOPE=$(oat project scope "$PROJECT_PATH" --format value) || { echo "oat: cannot resolve project scope for $PROJECT_PATH; refusing completion" >&2; exit 1; }
70
+ if [[ "$PROJECT_SCOPE" == "synced" ]]; then
71
+ oat project pull "$PROJECT_PATH" || { echo "oat: project pull failed for $PROJECT_PATH; resolve the reported state before continuing" >&2; exit 1; }
72
+ fi
73
+ PROJECT_RETAINED_REF=""
74
+ if [[ "$PROJECT_SCOPE" == "synced" ]]; then
75
+ PROJECT_RETAINED_REF="refs/oat/projects/${PROJECT_NAME}"
76
+ fi
77
+ IS_DURABLE_PROJECT="false"
78
+ if [[ "$PROJECT_SCOPE" == "shared" || "$PROJECT_SCOPE" == "synced" ]]; then
79
+ IS_DURABLE_PROJECT="true"
80
+ fi
53
81
  ```
54
82
 
55
83
  ### Step 2: Upfront User Questions (Batched)
@@ -94,16 +122,102 @@ PR_ON_COMPLETE=$(oat config get workflow.createPrOnComplete 2>/dev/null || true)
94
122
  PROJECT_RECAP_CONFIG=$(oat config get workflow.explainers.projectRecap --json 2>/dev/null || true)
95
123
  ```
96
124
 
97
- - **If `ARCHIVE_PREF` is `true`:** Set `SHOULD_ARCHIVE="true"`. Skip the archive question. Print `Archive on complete: enabled (from workflow.archiveOnComplete).`
98
- - **If `ARCHIVE_PREF` is `false`:** Set `SHOULD_ARCHIVE="false"`. Skip the archive question. Print `Archive on complete: disabled (from workflow.archiveOnComplete).`
99
- - **If unset:** Include the archive question in the batched prompt as normal (backward compatible).
125
+ - **If `IS_DURABLE_PROJECT` is `false`:** Omit the archive question regardless of
126
+ `ARCHIVE_PREF`. The resolver below selects `local-default`; do not assign
127
+ `SHOULD_ARCHIVE` directly.
128
+ - **If `IS_DURABLE_PROJECT` is `true` and `ARCHIVE_PREF` is `true`:** Skip the
129
+ archive question. Print
130
+ `Archive on complete: enabled (from workflow.archiveOnComplete).`
131
+ - **If `IS_DURABLE_PROJECT` is `true` and `ARCHIVE_PREF` is `false`:** Skip the
132
+ archive question. Print
133
+ `Archive on complete: disabled (from workflow.archiveOnComplete).`
134
+ - **If `IS_DURABLE_PROJECT` is `true` and `ARCHIVE_PREF` is unset:** Include
135
+ the archive question in the batched prompt as normal (backward compatible).
100
136
  - **If `PR_ON_COMPLETE` is `true` AND no tracked open PR exists:** Set `SHOULD_OPEN_PR="true"`. Skip the Open PR question. Print `PR on complete: enabled (from workflow.createPrOnComplete).`
101
137
  - **If `PR_ON_COMPLETE` is `false`:** Set `SHOULD_OPEN_PR="false"`. Skip the Open PR question. Print `PR on complete: disabled (from workflow.createPrOnComplete).`
102
138
  - **If `PR_ON_COMPLETE` is unset:** Include the Open PR question in the batched prompt as normal (backward compatible).
103
139
  - The existing tracked-PR skip still applies: if `oat_pr_status` is `open`, do not ask the Open PR question and do not honor `PR_ON_COMPLETE=true` — the PR already exists.
104
140
 
141
+ Select archive prompt participation before assembling the batched prompt:
142
+
143
+ ```bash
144
+ # archive-prompt-selection:start
145
+ ARCHIVE_QUESTION_REQUIRED="false"
146
+ if [[ "$IS_DURABLE_PROJECT" == "true" ]]; then
147
+ if [[ "$ARCHIVE_PREF" == "true" ]]; then
148
+ echo "Archive on complete: enabled (from workflow.archiveOnComplete)."
149
+ elif [[ "$ARCHIVE_PREF" == "false" ]]; then
150
+ echo "Archive on complete: disabled (from workflow.archiveOnComplete)."
151
+ else
152
+ ARCHIVE_QUESTION_REQUIRED="true"
153
+ fi
154
+ fi
155
+ # archive-prompt-selection:end
156
+ ```
157
+
105
158
  The "Ready to mark complete?" confirmation is always asked — it is a meaningful "are you sure" moment, not a preference.
106
159
 
160
+ The configured-decline case is `workflow.archiveOnComplete=false`. When the
161
+ preference is unset and the user declines the batched archive question, the
162
+ interactive archive answer is `false`. Both cases select the same explicit
163
+ non-archive transaction below; declining archive never means skipping synced
164
+ record durability.
165
+
166
+ Route every source through
167
+ `scripts/recover-completion-receipts.mjs#resolveCompletionArchiveDecision`
168
+ before assigning `SHOULD_ARCHIVE`. For a local project, pass the explicit
169
+ `localNonArchive=true` decision without asking the archive question. For a
170
+ durable project, pass `configuredPreference` only when
171
+ `workflow.archiveOnComplete` is set; otherwise pass the accepted batched
172
+ `interactiveAnswer`. Require the result's `source` to be `local-default`,
173
+ `configured`, or `interactive` as appropriate and use its `shouldArchive`
174
+ boolean. An absent, conflicting, or non-boolean decision fails closed instead
175
+ of silently selecting a lifecycle path. The executable completion transaction
176
+ tests must use this same resolver; scenario labels alone are not decision
177
+ coverage.
178
+
179
+ After the configured preference or accepted interactive answer is available,
180
+ invoke the resolver rather than assigning `SHOULD_ARCHIVE` directly:
181
+
182
+ ```bash
183
+ # archive-decision-resolution:start
184
+ ARCHIVE_DECISION_ARGS=()
185
+ EXPECTED_ARCHIVE_DECISION_SOURCE=""
186
+ if [[ "$IS_DURABLE_PROJECT" == "false" ]]; then
187
+ ARCHIVE_DECISION_ARGS+=(--local-nonarchive true)
188
+ EXPECTED_ARCHIVE_DECISION_SOURCE="local-default"
189
+ elif [[ "$ARCHIVE_PREF" == "true" || "$ARCHIVE_PREF" == "false" ]]; then
190
+ ARCHIVE_DECISION_ARGS+=(--archive-preference "$ARCHIVE_PREF")
191
+ EXPECTED_ARCHIVE_DECISION_SOURCE="configured"
192
+ else
193
+ test "$ARCHIVE_INTERACTIVE_ANSWER" = "true" \
194
+ || test "$ARCHIVE_INTERACTIVE_ANSWER" = "false" \
195
+ || exit 1
196
+ ARCHIVE_DECISION_ARGS+=(--interactive-archive "$ARCHIVE_INTERACTIVE_ANSWER")
197
+ EXPECTED_ARCHIVE_DECISION_SOURCE="interactive"
198
+ fi
199
+ ARCHIVE_DECISION_JSON=$(node "$COMPLETION_RECEIPT_SCRIPT" \
200
+ "${ARCHIVE_DECISION_ARGS[@]}") || exit 1
201
+ ARCHIVE_DECISION_FIELDS=$(node -e '
202
+ const value = JSON.parse(process.argv[1]);
203
+ if (typeof value.shouldArchive !== "boolean" || !["local-default", "configured", "interactive"].includes(value.source)) process.exit(1);
204
+ process.stdout.write(`${value.shouldArchive}\t${value.source}`);
205
+ ' "$ARCHIVE_DECISION_JSON") || exit 1
206
+ IFS=$'\t' read -r SHOULD_ARCHIVE ARCHIVE_DECISION_SOURCE \
207
+ <<< "$ARCHIVE_DECISION_FIELDS"
208
+ test "$ARCHIVE_DECISION_SOURCE" = "$EXPECTED_ARCHIVE_DECISION_SOURCE" || exit 1
209
+ if [[ "$ARCHIVE_DECISION_SOURCE" == "local-default" ]]; then
210
+ test "$SHOULD_ARCHIVE" = "false" || exit 1
211
+ fi
212
+ # archive-decision-resolution:end
213
+ ```
214
+
215
+ Require `ARCHIVE_DECISION_SOURCE` to match the source selected above. Local
216
+ completion always resolves to explicit non-archive without asking the archive
217
+ question. Store an unconfigured durable-project batched answer as
218
+ `ARCHIVE_INTERACTIVE_ANSWER` until this resolver returns; only then assign
219
+ `SHOULD_ARCHIVE`.
220
+
107
221
  Resolve `projectRecap` intent before presenting the batched completion prompt.
108
222
  Use the `oat-explainer-kit` lifecycle intent resolver in interactive mode with
109
223
  the current `oat_project_recap` value from the same `state.md` read and the
@@ -142,7 +256,8 @@ implementation ran:
142
256
  **Questions to ask (in a single prompt):**
143
257
 
144
258
  1. **Confirm completion:** "Ready to mark **{PROJECT_NAME}** as complete?"
145
- 2. **Archive** (only if `IS_SHARED_PROJECT` is `true`): "Archive the project after completion?"
259
+ 2. **Archive** (only if `ARCHIVE_QUESTION_REQUIRED` is `true`):
260
+ "Archive the project after completion?"
146
261
  3. **Generate or refresh summary** (only if summary status is `missing` or `stale`): present the status explicitly:
147
262
  - Missing example: "A summary has not been generated yet. Would you like me to generate it now as part of completion?"
148
263
  - Stale example: "The project summary is out of date. Would you like me to refresh it now as part of completion?"
@@ -166,9 +281,11 @@ Ready to complete project **{PROJECT_NAME}**?
166
281
 
167
282
  If the user declines the completion confirmation, exit gracefully.
168
283
 
169
- After the user accepts the completion confirmation, store the answers as
170
- `SHOULD_ARCHIVE`, `SHOULD_GENERATE_SUMMARY`, `SHOULD_GENERATE_RETRO`,
171
- `SHOULD_GENERATE_RECAP`, and `SHOULD_OPEN_PR` for use in later steps. Set
284
+ After the user accepts the completion confirmation, store a prompted archive
285
+ answer as `ARCHIVE_INTERACTIVE_ANSWER`; assign `SHOULD_ARCHIVE` only through
286
+ the decision resolver above. Store the remaining answers as
287
+ `SHOULD_GENERATE_SUMMARY`, `SHOULD_GENERATE_RETRO`, `SHOULD_GENERATE_RECAP`,
288
+ and `SHOULD_OPEN_PR` for use in later steps. Set
172
289
  `SHOULD_GENERATE_RETRO="false"` when the retro already exists or this completion
173
290
  run is non-interactive. Persist a prompted recap answer only after that
174
291
  confirmation is accepted.
@@ -352,7 +469,69 @@ a warning rather than changing project completion status.
352
469
 
353
470
  `project-explainer` runs are active-project working artifacts, not durable post-completion reference products. Do not export, re-attest, or add archive-aware PR or summary reference links for a `project-explainer` run.
354
471
 
355
- For `IS_SHARED_PROJECT="false"`, never export a tracked project recap and never construct or pass `--project-recap-run`. A local-scope recap remains `built-not-durable` unless its manifest already contains independently verified publish evidence. Do not treat local filesystem presence as durability. Completion-bookkeeping durability, relocation re-attestation, and archive-aware recap links are handled by the later durability stage, not by this selection gate.
472
+ For `IS_DURABLE_PROJECT="false"`, never export a tracked project recap and never construct or pass `--project-recap-run`. This is the local scope. A local-scope recap remains `built-not-durable` unless its manifest already contains independently verified publish evidence. Do not treat local filesystem presence as durability. Shared and synced recaps are exported and attested through the later durability stage.
473
+
474
+ ### Step 3.65: Recover a Recognizable Completion Receipt Before Mutation
475
+
476
+ Initialize `PROJECT_LINKS_PIN_COMMIT=""`, `PROJECT_REF_COMMIT=""`,
477
+ `EVIDENCE_COMMIT=""`, `RECOVERED_EVIDENCE_COMMIT=""`,
478
+ `EVIDENCE_PUSH_REQUIRED=""`, `PR_DESCRIPTION_RELATIVE_PATH=""`, and
479
+ `COMPLETION_RECEIPTS_RECOVERED="false"`. For a
480
+ synced non-archive run, invoke the skill-owned retry router before the
481
+ project-log probe, seal append, review moves, `complete-state`, active-pointer,
482
+ or PR-artifact mutation. This is the one executable routing surface; do not
483
+ recreate candidate detection and recovery as separate shell branches:
484
+
485
+ ```bash
486
+ if [[ "$PROJECT_SCOPE" == "synced" && "$SHOULD_ARCHIVE" == "false" ]]; then
487
+ COMPLETION_RETRY_ARGS=(
488
+ --project-path "$ACTIVE_PROJECT_PATH"
489
+ --retained-ref "$PROJECT_RETAINED_REF"
490
+ )
491
+ if [[ -n "$SELECTED_PROJECT_RECAP_RUN" ]]; then
492
+ COMPLETION_RETRY_ARGS+=(
493
+ --evidence-path "$SELECTED_PROJECT_RECAP_RUN/manifest.json"
494
+ --evidence-path "$SELECTED_PROJECT_RECAP_RUN/build-record.json"
495
+ )
496
+ fi
497
+ COMPLETION_RETRY_JSON=$(node "$COMPLETION_RETRY_SCRIPT" \
498
+ "${COMPLETION_RETRY_ARGS[@]}") || exit 1
499
+ COMPLETION_RETRY_FIELDS=$(node "$COMPLETION_RETRY_FIELDS_SCRIPT" \
500
+ "$COMPLETION_RETRY_JSON") || exit 1
501
+ IFS=$'\t' read -r COMPLETION_RETRY_ROUTE _ \
502
+ <<< "$COMPLETION_RETRY_FIELDS"
503
+ if [[ "$COMPLETION_RETRY_ROUTE" == "recovery" ]]; then
504
+ IFS=$'\t' read -r COMPLETION_RETRY_ROUTE PROJECT_LINKS_PIN_COMMIT \
505
+ PROJECT_REF_COMMIT RECOVERED_EVIDENCE_COMMIT EVIDENCE_PUSH_REQUIRED \
506
+ PR_DESCRIPTION_RELATIVE_PATH <<< "$COMPLETION_RETRY_FIELDS"
507
+ if [[ "$RECOVERED_EVIDENCE_COMMIT" != "-" ]]; then
508
+ EVIDENCE_COMMIT="$RECOVERED_EVIDENCE_COMMIT"
509
+ fi
510
+ PR_DESCRIPTION_PATH="$ACTIVE_PROJECT_PATH/$PR_DESCRIPTION_RELATIVE_PATH"
511
+ COMPLETION_RECEIPTS_RECOVERED="true"
512
+ elif [[ "$COMPLETION_RETRY_ROUTE" == "pin-source" ]]; then
513
+ IFS=$'\t' read -r COMPLETION_RETRY_ROUTE PROJECT_LINKS_PIN_COMMIT \
514
+ PR_DESCRIPTION_RELATIVE_PATH <<< "$COMPLETION_RETRY_FIELDS"
515
+ PR_DESCRIPTION_PATH="$ACTIVE_PROJECT_PATH/$PR_DESCRIPTION_RELATIVE_PATH"
516
+ COMPLETION_RECEIPTS_RECOVERED="true"
517
+ elif [[ "$COMPLETION_RETRY_ROUTE" != "normal" || \
518
+ "$COMPLETION_RETRY_FIELDS" != "normal" ]]; then
519
+ exit 1
520
+ fi
521
+ fi
522
+ ```
523
+
524
+ A recognized candidate must be clean and validate completely; dirty,
525
+ malformed, mixed, or contradictory candidates exit before the router returns.
526
+ When the result is `route: "normal"`, follow the ordinary flow without
527
+ changing its existing dirty-worktree guarantees. When it is
528
+ `route: "recovery"`, the executable has already restored the final receipts.
529
+ Jump directly to Step 7.5 and skip every mutation in Steps 3.7 through 7. When
530
+ it is `route: "pin-source"`, the executable has validated the already-published
531
+ pin-source tree and PR artifact; preserve `PROJECT_LINKS_PIN_COMMIT`, jump
532
+ directly to Step 8.6, and skip Steps 3.7 through 7, including a duplicate
533
+ pin-source push. The executable transaction matrix must use this same router
534
+ for all configured and interactive interruption rows.
356
535
 
357
536
  ### Step 3.7: Project Log Completion Gate
358
537
 
@@ -439,13 +618,26 @@ Delegate the canonical `state.md` completion mutation to the CLI:
439
618
 
440
619
  ```bash
441
620
  COMPLETE_STATE_ARGS=("$PROJECT_PATH")
442
- if [[ "$SHOULD_ARCHIVE" == "true" && "$IS_SHARED_PROJECT" == "true" ]]; then
621
+ if [[ "$SHOULD_ARCHIVE" == "true" && "$IS_DURABLE_PROJECT" == "true" ]]; then
443
622
  COMPLETE_STATE_ARGS+=("--archived")
444
623
  fi
445
624
 
446
625
  oat project complete-state "${COMPLETE_STATE_ARGS[@]}"
447
626
  ```
448
627
 
628
+ For `synced`, keep this finalized lifecycle state in the project checkout until
629
+ Step 7.5 publishes it together with every later pre-archive artifact write. Do
630
+ not push here: Step 7 may still create or replace the PR-description artifact,
631
+ and the retained project ref must include that late artifact.
632
+
633
+ When archive is selected, the synced completion order is: finalize → generate
634
+ the PR artifact → project-ref pin-source push → project archive → render
635
+ `oat project links --durable-summary <path>` → update the open PR body. When
636
+ archive is declined, it is: finalize → generate the PR artifact →
637
+ project-ref pin-source push → render the final links → final artifact push →
638
+ exact discovery-record commit → optional active-recap evidence commit and
639
+ project push.
640
+
449
641
  The CLI command owns both the frontmatter completion fields and the canonical markdown body updates for `state.md`.
450
642
  It must set `oat_lifecycle: complete`, completion timestamps, `**Status:** Complete`, `**Last Updated:**`, the canonical `## Current Phase` body, normalized `## Progress`, and `## Next Milestone`.
451
643
 
@@ -481,6 +673,15 @@ fi
481
673
 
482
674
  5. **Write PR description artifact** — write to `{PROJECT_PATH}/pr/project-pr-YYYY-MM-DD.md` following the template and policies from `oat-project-pr-final` Step 4 (frontmatter policy, reference links policy, local path exclusion).
483
675
 
676
+ When no PR description artifact exists, write it before the final synced
677
+ project-ref publication, regardless of archive or recap selection.
678
+
679
+ Retain the one selected or written artifact as `PR_DESCRIPTION_PATH`. Resolve
680
+ `PR_DESCRIPTION_RELATIVE_PATH` from `ACTIVE_PROJECT_PATH`, require it to be a
681
+ normalized project-relative path, and reject zero, multiple, symlinked, or
682
+ outside-project candidates. Step 7.5 passes that exact relative path to the
683
+ receipt recovery surface.
684
+
484
685
  If a PR description artifact already exists at `{PROJECT_PATH}/pr/project-pr-*.md`:
485
686
 
486
687
  - When `SHOULD_ARCHIVE` is `true`, regenerate it (overwrite). The existing artifact was authored by `oat-project-pr-final` before any archive intent existed and links to artifact paths that will be local-only after Step 8. Regenerating ensures Step 11 / Step 11.5 push a body whose links still resolve on the remote.
@@ -509,9 +710,161 @@ When archiving, the project artifacts at `{PROJECT_PATH}/{plan,implementation,di
509
710
 
510
711
  Anti-pattern: do not "rescue" a dropped artifact by linking to its archived path under `.oat/projects/archived/<name>/...`. That path is gitignored on every checkout and never reaches the remote.
511
712
 
713
+ #### Step 7.5: Publish Synced Project Pin Source
714
+
715
+ For `synced`, publish only after Step 7 has finished every pre-archive project
716
+ artifact write. This receipt is the immutable pin source used when Step 8.6
717
+ renders the final links. Keep it separate from the final non-archive artifact
718
+ receipt:
719
+
720
+ Preserve `PROJECT_LINKS_PIN_COMMIT`, `PROJECT_REF_COMMIT`, `EVIDENCE_COMMIT`,
721
+ and `EVIDENCE_PUSH_REQUIRED` when the Step 3.65 router restored them. A
722
+ `pin-source` route skips this step entirely and resumes at Step 8.6. Do not
723
+ initialize over a recovered value or run a second candidate-routing branch at
724
+ this step. The pre-mutation router owns candidate detection, exact PR-artifact
725
+ selection, pin-source retry validation, and `recoverCompletionReceipts`; its
726
+ read-only recovery must have validated the applicable receipt chain before
727
+ returning a receipt:
728
+
729
+ - a clean synced checkout and the retained local ref;
730
+ - the exact final-artifact and optional evidence subjects;
731
+ - exactly the PR-description path in the final-artifact commit and exactly the
732
+ two supplied recap record paths in an evidence commit;
733
+ - single-parent pin-source → final-artifact → optional evidence ordering, with
734
+ the pin source subject equal to the preliminary push message below;
735
+ - canonical `state.md` lifecycle fields in the pin-source tree, including
736
+ `oat_lifecycle: complete` and equal valid UTC completion/update timestamps;
737
+ - when the pin source contains `project-log.md`, its final entry is the
738
+ canonical `oat-project-complete` completion seal; an absent log remains the
739
+ supported inert project-log configuration from Step 3.7;
740
+ - exactly one well-ordered links block pinned to the pin-source parent; and
741
+ - either equal local/remote final-artifact receipts, equal local/remote evidence
742
+ receipts, or the one allowed unpublished-evidence state where checkout HEAD
743
+ is the exact evidence child while both retained refs remain at its exact
744
+ final-artifact parent.
745
+
746
+ Restore the returned `projectLinksPinCommit` as
747
+ `PROJECT_LINKS_PIN_COMMIT`, `projectRefCommit` as `PROJECT_REF_COMMIT`, and a
748
+ non-null `evidenceCommit` as `EVIDENCE_COMMIT`. Any candidate with a malformed
749
+ subject, path set, parent, links block, retained ref, or local/remote relation
750
+ fails closed. Do not fall through to a new pin-source publication after a
751
+ partial or contradictory candidate. This retry recognition is valid whether
752
+ the parent discovery record is still active or already complete.
753
+
754
+ Build the recovery arguments only for post-push revalidation of an unpublished
755
+ evidence receipt:
756
+
757
+ ```bash
758
+ RECOVERY_ARGS=(
759
+ --project-path "$ACTIVE_PROJECT_PATH"
760
+ --retained-ref "$PROJECT_RETAINED_REF"
761
+ --pr-artifact "$PR_DESCRIPTION_RELATIVE_PATH"
762
+ )
763
+ if [[ -n "$SELECTED_PROJECT_RECAP_RUN" ]]; then
764
+ RECOVERY_ARGS+=(
765
+ --evidence-path "$SELECTED_PROJECT_RECAP_RUN/manifest.json"
766
+ --evidence-path "$SELECTED_PROJECT_RECAP_RUN/build-record.json"
767
+ )
768
+ fi
769
+
770
+ parse_completion_receipts() {
771
+ node -e '
772
+ const value = JSON.parse(process.argv[1]);
773
+ const sha = /^[0-9a-f]{40}$/;
774
+ if (value.status !== "recovered" || !sha.test(value.projectLinksPinCommit) || !sha.test(value.projectRefCommit)) process.exit(1);
775
+ if (value.evidenceCommit !== null && !sha.test(value.evidenceCommit)) process.exit(1);
776
+ if (typeof value.evidencePushRequired !== "boolean") process.exit(1);
777
+ process.stdout.write([
778
+ value.projectLinksPinCommit,
779
+ value.projectRefCommit,
780
+ value.evidenceCommit ?? "-",
781
+ String(value.evidencePushRequired),
782
+ ].join("\t"));
783
+ ' "$1"
784
+ }
785
+
786
+ ```
787
+
788
+ Use this block only after the Step 3.65 router restored a candidate. Require
789
+ `EVIDENCE_PUSH_REQUIRED` to be `true` or `false`; no other output is accepted.
790
+
791
+ When the recovery result reports `evidencePushRequired: true`, publish the
792
+ existing checkout HEAD with `oat project push "$PROJECT_PATH" --json` before
793
+ continuing. Require `status: "pushed"` or `status: "up-to-date"`, the same
794
+ retained ref, and a receipt SHA exactly equal to `EVIDENCE_COMMIT`. This push
795
+ must not create a commit, rerender the PR artifact, or rewrite any receipt.
796
+ Re-run the recovery surface after the push and require the exact same pin,
797
+ final-artifact, and evidence SHAs with `evidencePushRequired: false`.
798
+
799
+ ```bash
800
+ parse_synced_push_receipt() {
801
+ node -e '
802
+ const value = JSON.parse(process.argv[1]);
803
+ if (!["pushed", "up-to-date"].includes(value.status)) process.exit(1);
804
+ if (!/^[0-9a-f]{40}$/.test(value.sha) || typeof value.ref !== "string") process.exit(1);
805
+ process.stdout.write(`${value.ref}\t${value.sha}`);
806
+ ' "$1"
807
+ }
808
+
809
+ if [[ "$EVIDENCE_PUSH_REQUIRED" == "true" ]]; then
810
+ RECOVERED_EVIDENCE_PUSH_OUTPUT=$(oat project push \
811
+ "$PROJECT_PATH" --json) || exit 1
812
+ RECOVERED_EVIDENCE_PUSH_FIELDS=$( \
813
+ parse_synced_push_receipt "$RECOVERED_EVIDENCE_PUSH_OUTPUT"
814
+ ) || exit 1
815
+ IFS=$'\t' read -r RECOVERED_PUSH_REF RECOVERED_PUSH_SHA \
816
+ <<< "$RECOVERED_EVIDENCE_PUSH_FIELDS"
817
+ test "$RECOVERED_PUSH_REF" = "$PROJECT_RETAINED_REF" || exit 1
818
+ test "$RECOVERED_PUSH_SHA" = "$EVIDENCE_COMMIT" || exit 1
819
+ PUBLISHED_RECOVERY_JSON=$(node "$COMPLETION_RECEIPT_SCRIPT" \
820
+ "${RECOVERY_ARGS[@]}") || exit 1
821
+ PUBLISHED_RECOVERY_FIELDS=$( \
822
+ parse_completion_receipts "$PUBLISHED_RECOVERY_JSON"
823
+ ) || exit 1
824
+ IFS=$'\t' read -r PUBLISHED_PIN_COMMIT PUBLISHED_REF_COMMIT \
825
+ PUBLISHED_EVIDENCE_COMMIT PUBLISHED_EVIDENCE_PUSH_REQUIRED \
826
+ <<< "$PUBLISHED_RECOVERY_FIELDS"
827
+ test "$PUBLISHED_PIN_COMMIT" = "$PROJECT_LINKS_PIN_COMMIT" || exit 1
828
+ test "$PUBLISHED_REF_COMMIT" = "$PROJECT_REF_COMMIT" || exit 1
829
+ test "$PUBLISHED_EVIDENCE_COMMIT" = "$EVIDENCE_COMMIT" || exit 1
830
+ test "$PUBLISHED_EVIDENCE_PUSH_REQUIRED" = "false" || exit 1
831
+ fi
832
+ ```
833
+
834
+ When retry recognition did not set `PROJECT_REF_COMMIT`, publish the pin
835
+ source:
836
+
837
+ ```bash
838
+ if [[ "$PROJECT_SCOPE" == "synced" && -z "$PROJECT_REF_COMMIT" ]]; then
839
+ PROJECT_PUSH_OUTPUT=$(oat project push "$PROJECT_PATH" \
840
+ --message "chore(oat): finalize project lifecycle" --json) || exit 1
841
+ PROJECT_PUSH_FIELDS=$( \
842
+ parse_synced_push_receipt "$PROJECT_PUSH_OUTPUT"
843
+ ) || exit 1
844
+ IFS=$'\t' read -r PROJECT_RETAINED_REF PROJECT_LINKS_PIN_COMMIT \
845
+ <<< "$PROJECT_PUSH_FIELDS"
846
+ printf '%s\n' "$PROJECT_PUSH_OUTPUT"
847
+ fi
848
+ ```
849
+
850
+ Require the structured push result to report `status: "pushed"` or
851
+ `status: "up-to-date"`, the retained project ref, and a full `sha`. Capture the
852
+ exact structured receipt SHA as `PROJECT_LINKS_PIN_COMMIT`. Never infer this
853
+ receipt from the parent branch, a stale local ref, or an earlier push. Verify
854
+ the receipt commit contains the newly created PR-description artifact when
855
+ Step 7 started without one, as well as every other pre-archive project artifact
856
+ write.
857
+
858
+ Both configured and interactive archive-decline paths continue from this exact
859
+ pin-source receipt into Step 8.6. The later final artifact receipt and exact
860
+ parent-branch record commit finish the non-archive transaction when no recap is
861
+ selected. With a selected recap, the evidence commit in Step 10.6 must remain
862
+ the immediate child of the final artifact receipt, never the preliminary pin
863
+ source.
864
+
512
865
  ### Step 8: Archive Project (Conditional)
513
866
 
514
- **Skip if `SHOULD_ARCHIVE` is false or `IS_SHARED_PROJECT` is false.**
867
+ **Skip if `SHOULD_ARCHIVE` is false or `IS_DURABLE_PROJECT` is false.**
515
868
 
516
869
  This conditional skips archive movement only; it does not skip the Step 3.7
517
870
  seal append for an existing project log.
@@ -520,6 +873,9 @@ Archive happens after PR description generation (so artifacts are readable at tr
520
873
 
521
874
  The archive-side effects in this step are CLI-owned. Do not reimplement local archive movement, summary export, S3 sync, AWS credential handling, or worktree durability checks in the skill.
522
875
 
876
+ Archive refuses a dirty or unpushed synced checkout. The correction is
877
+ `oat project push "$PROJECT_PATH"`; never discard or bypass pending artifacts.
878
+
523
879
  ```bash
524
880
  ARCHIVE_OUTPUT=""
525
881
  ARCHIVE_ARGS=("$PROJECT_PATH")
@@ -538,8 +894,14 @@ printf '%s\n' "$ARCHIVE_OUTPUT"
538
894
 
539
895
  Parse `ARCHIVE_OUTPUT` as the `oat project archive --json` report. Require
540
896
  `status: "ok"`, `mode: "apply"`, and a non-empty `archivePath`; use its
541
- `s3Path`, `summaryExportFile`, and `warnings` fields for later reporting. Set
542
- `ARCHIVE_PATH` from `archivePath`, then set `PROJECT_PATH="$ARCHIVE_PATH"`.
897
+ `s3Path`, `summaryExportFile`, `lifecycleCommit`, and `warnings` fields for later reporting. Set
898
+ `ARCHIVE_PATH` from `archivePath`, set `SUMMARY_EXPORT_FILE` from
899
+ `summaryExportFile` (empty when null), then set `PROJECT_PATH="$ARCHIVE_PATH"`.
900
+
901
+ For a synced project, require a full `lifecycleCommit` SHA in the archive
902
+ report and set `LIFECYCLE_COMMIT` to it. This is the parent-branch record and
903
+ summary-export commit owned by archive; do not replace it with
904
+ `git rev-parse HEAD` and do not create another lifecycle commit.
543
905
 
544
906
  When `SELECTED_PROJECT_RECAP_RUN` is non-empty, also require the report's
545
907
  `projectRecapExport.sourceRunRoot`, `projectRecapExport.exportRoot`, and
@@ -557,7 +919,7 @@ authoritative. A missing, malformed, mismatched, outside-root, or gitignored
557
919
  export report is an archive failure; stop before lifecycle bookkeeping.
558
920
  Never use the gitignored archive as evidence or a link target.
559
921
 
560
- SELECTED_PROJECT_RECAP_RUN must be project-relative. Never add `--project-recap-run` when `SELECTED_PROJECT_RECAP_RUN` is empty. The empty case remains the existing archive behavior. Because this step runs only for shared projects, local-scope projects never pass a recap archive argument.
922
+ SELECTED_PROJECT_RECAP_RUN must be project-relative. Never add `--project-recap-run` when `SELECTED_PROJECT_RECAP_RUN` is empty. The empty case remains the existing archive behavior. Because this step runs only for durable projects, local-scope projects never pass a recap archive argument.
561
923
 
562
924
  The no-recap invocation remains `oat project archive "$PROJECT_PATH"` with
563
925
  `--json` added only to select the machine-readable report.
@@ -577,6 +939,113 @@ Omit either link when its containing artifact does not exist.
577
939
 
578
940
  Use the current head branch for the blob URL while the PR is open. Never link to `.oat/projects/archived/`; it is gitignored and will return 404 remotely.
579
941
 
942
+ The final synced links block is rendered independently in Step 8.6. Do not
943
+ make pinned project links conditional on a recap export.
944
+
945
+ #### Step 8.6: Render Final Synced Project Links
946
+
947
+ Run this for every synced completion after the final project ref or archive SHA
948
+ is known. It is required even when no project recap was selected and
949
+ `summaryExportFile` is null.
950
+
951
+ Locate the PR-description artifact under the current `PROJECT_PATH` (which is
952
+ the archive path after Step 8). Render the canonical pinned block:
953
+
954
+ ```bash
955
+ FINAL_LINK_ARGS=("$PROJECT_NAME" --format markdown)
956
+ if [[ -n "${SUMMARY_EXPORT_FILE:-}" ]]; then
957
+ test -f "$SUMMARY_EXPORT_FILE" || exit 1
958
+ git ls-files --error-unmatch -- "$SUMMARY_EXPORT_FILE" >/dev/null || exit 1
959
+ git check-ignore --quiet -- "$SUMMARY_EXPORT_FILE" && exit 1
960
+ SUMMARY_EXPORT_RELATIVE=$(git ls-files --full-name -- "$SUMMARY_EXPORT_FILE") || exit 1
961
+ [[ -n "$SUMMARY_EXPORT_RELATIVE" ]] || exit 1
962
+ FINAL_LINK_ARGS+=(--durable-summary "$SUMMARY_EXPORT_RELATIVE")
963
+ fi
964
+ FINAL_PROJECT_LINKS=$(oat project links "${FINAL_LINK_ARGS[@]}") || exit 1
965
+ ```
966
+
967
+ Insert `FINAL_PROJECT_LINKS` when the body has no
968
+ `<!-- oat:project-links:start -->` block, or replace exactly the existing
969
+ delimited block through `<!-- oat:project-links:end -->`. Do not duplicate the
970
+ markers. The base invocation remains
971
+ `oat project links "$PROJECT_NAME" --format markdown`; add
972
+ `--durable-summary` only for the verified tracked export above.
973
+
974
+ For `PROJECT_SCOPE="synced"` with `SHOULD_ARCHIVE="false"`, render this block
975
+ in the active PR-description artifact before the push whose receipt becomes
976
+ `PROJECT_REF_COMMIT`. Skip the rewrite only when Step 7.5 recognized and
977
+ validated an already-finalized retry receipt. Otherwise publish the rendered
978
+ artifact with a distinct final push:
979
+
980
+ ```bash
981
+ if [[ "$PROJECT_SCOPE" == "synced" && "$SHOULD_ARCHIVE" == "false" && \
982
+ -z "$PROJECT_REF_COMMIT" ]]; then
983
+ FINAL_PROJECT_PUSH_ARGS=("$PROJECT_PATH" \
984
+ --message "chore(oat): publish final project links" --json)
985
+ FINAL_PROJECT_PUSH_OUTPUT=$(oat project push \
986
+ "${FINAL_PROJECT_PUSH_ARGS[@]}") || exit 1
987
+ FINAL_PROJECT_PUSH_FIELDS=$( \
988
+ parse_synced_push_receipt "$FINAL_PROJECT_PUSH_OUTPUT"
989
+ ) || exit 1
990
+ IFS=$'\t' read -r FINAL_PROJECT_PUSH_REF PROJECT_REF_COMMIT \
991
+ <<< "$FINAL_PROJECT_PUSH_FIELDS"
992
+ test "$FINAL_PROJECT_PUSH_REF" = "$PROJECT_RETAINED_REF" || exit 1
993
+ printf '%s\n' "$FINAL_PROJECT_PUSH_OUTPUT"
994
+ fi
995
+ ```
996
+
997
+ Require the structured result to report `status: "pushed"` or
998
+ `status: "up-to-date"`, the same retained project ref, and a full `sha`. Capture
999
+ that exact SHA as `PROJECT_REF_COMMIT`. Verify the checkout is clean, the
1000
+ retained remote ref equals the receipt, and the receipt contains the final
1001
+ PR-description artifact with exactly one links block pinned to
1002
+ `PROJECT_LINKS_PIN_COMMIT`. When the final render produced a commit, require
1003
+ its immediate parent to equal `PROJECT_LINKS_PIN_COMMIT` and require the commit
1004
+ to contain exactly the PR-description artifact. Never substitute the
1005
+ preliminary receipt for `PROJECT_REF_COMMIT` after the artifact changed.
1006
+
1007
+ Both PR paths consume this final body: when
1008
+ `WAS_PR_OPEN_AT_START="false"`, Step 11 creates the new PR with it; when
1009
+ `WAS_PR_OPEN_AT_START="true"`, Step 11.5 updates the already-open PR with it.
1010
+ Neither path depends on `projectRecapExport` or a configured
1011
+ `archive.summaryExportPath`.
1012
+
1013
+ #### Step 8.7: Non-Archive Synced Completion Transaction
1014
+
1015
+ Run this only when `PROJECT_SCOPE="synced"` and `SHOULD_ARCHIVE="false"`.
1016
+ The configured and interactive decline paths converge here after the finalized
1017
+ project tree has been pushed and `PROJECT_REF_COMMIT` has been captured.
1018
+
1019
+ Set `SYNCED_RECORD_PATH="${ACTIVE_PROJECT_PATH}.json"`; require it to be the
1020
+ canonical direct-child discovery record under the configured synced root and
1021
+ require its parsed `slug` and `ref` to match `PROJECT_NAME` and the project
1022
+ target. Mark the discovery record `complete` with `completedAt` using a
1023
+ structured JSON write that preserves the schema's other fields and the
1024
+ formatter-stable trailing newline. Refuse symlinks, malformed records, and any
1025
+ path outside that exact boundary before writing.
1026
+
1027
+ Snapshot unrelated staged state before this write. Step 10 must commit only
1028
+ `SYNCED_RECORD_PATH` on the parent branch with
1029
+ `chore(oat): complete synced project ${PROJECT_NAME}`, then verify the commit
1030
+ contains exactly that path and verify the unrelated staged snapshot is
1031
+ byte-for-byte unchanged. The retained project ref remains the artifact
1032
+ authority after non-archive completion. Do not remove the checkout, delete the
1033
+ ref, create archive exports, or set `PROJECT_PATH` to an archive location.
1034
+
1035
+ When `SELECTED_PROJECT_RECAP_RUN is empty`, the final artifact push plus exact
1036
+ record commit completes this transaction. When
1037
+ `SELECTED_PROJECT_RECAP_RUN is non-empty`, Steps 10.5 and 10.6 additionally
1038
+ attest the active recap within the project-ref history.
1039
+
1040
+ On retry, accept an already-complete record only after its exact-path commit is
1041
+ verified and the final artifact push receipt still names the retained ref SHA.
1042
+ If the final artifact push succeeded but the record commit did not, reuse the
1043
+ validated `PROJECT_LINKS_PIN_COMMIT` and `PROJECT_REF_COMMIT` receipts and retry
1044
+ only the record write/commit. If the record commit succeeded but recap evidence
1045
+ did not, reuse both receipts and retry only recap finalization. Never create a
1046
+ second lifecycle record commit, rerender the links against the final receipt,
1047
+ or rewrite either history.
1048
+
580
1049
  ### Step 9: Regenerate Dashboard
581
1050
 
582
1051
  Regenerate the repo state dashboard so the completion status is reflected before committing.
@@ -588,7 +1057,7 @@ oat state refresh
588
1057
  ### Step 10: Commit + Push Bookkeeping (Required)
589
1058
 
590
1059
  Completion is not done until lifecycle changes are committed. This commit also
591
- anchors commit durability for a selected shared-project recap. Do not push yet
1060
+ anchors commit durability for a selected durable-project recap. Do not push yet
592
1061
  when recap attestation is pending.
593
1062
 
594
1063
  Expected changes may include:
@@ -599,17 +1068,36 @@ Expected changes may include:
599
1068
  - `{PROJECT_PATH}/pr/project-pr-*.md` (PR description artifact)
600
1069
  - `.oat/state.md` is regenerated locally in Step 9 but should not be staged; it is generated dashboard state and normally gitignored.
601
1070
  - `.oat/config.local.json` (if `activeProject` cleared)
602
- - Shared-project deletions under `{PROJECTS_ROOT}/{PROJECT_NAME}` (if archived)
1071
+ - Shared-project deletions or synced-project record updates (if archived)
603
1072
  - The complete tracked recap export and tracked summary export reported by
604
1073
  archive (if present)
605
1074
 
606
1075
  Run:
607
1076
 
608
1077
  ```bash
609
- git status --short
610
- git add -- <exact completion and lifecycle paths>
611
- git commit -m "chore(oat): complete project lifecycle for ${PROJECT_NAME}"
612
- LIFECYCLE_COMMIT=$(git rev-parse HEAD)
1078
+ if [[ "$PROJECT_SCOPE" == "synced" ]]; then
1079
+ if [[ "$SHOULD_ARCHIVE" == "true" ]]; then
1080
+ test -n "$LIFECYCLE_COMMIT"
1081
+ else
1082
+ git add -- "$SYNCED_RECORD_PATH"
1083
+ if git diff --cached --quiet -- "$SYNCED_RECORD_PATH"; then
1084
+ LIFECYCLE_COMMIT=$(git log -1 --format=%H -- "$SYNCED_RECORD_PATH")
1085
+ node "$NONARCHIVE_LIFECYCLE_RECEIPT_SCRIPT" \
1086
+ "$SYNCED_RECORD_PATH" "$LIFECYCLE_COMMIT" "$PROJECT_NAME" || exit 1
1087
+ else
1088
+ git commit --only "$SYNCED_RECORD_PATH" \
1089
+ -m "chore(oat): complete synced project ${PROJECT_NAME}" &&
1090
+ LIFECYCLE_COMMIT=$(git rev-parse HEAD) &&
1091
+ node "$NONARCHIVE_LIFECYCLE_RECEIPT_SCRIPT" \
1092
+ "$SYNCED_RECORD_PATH" "$LIFECYCLE_COMMIT" "$PROJECT_NAME" || exit 1
1093
+ fi
1094
+ fi
1095
+ else
1096
+ git status --short
1097
+ git add -- <exact completion and lifecycle paths>
1098
+ git commit -m "chore(oat): complete project lifecycle for ${PROJECT_NAME}"
1099
+ LIFECYCLE_COMMIT=$(git rev-parse HEAD)
1100
+ fi
613
1101
  ```
614
1102
 
615
1103
  Rules:
@@ -618,6 +1106,13 @@ Rules:
618
1106
  completion/bookkeeping files. Never use a repository-wide `git add -A` when
619
1107
  unrelated changes exist.
620
1108
  - If there is nothing to commit, state that explicitly and verify whether the completion bookkeeping was already committed in a prior commit.
1109
+ - A non-archive synced lifecycle receipt is valid only when it is an ancestor
1110
+ of current parent-branch `HEAD`, changes exactly `SYNCED_RECORD_PATH`, and
1111
+ contains the current byte-identical complete record for `PROJECT_NAME` with
1112
+ the canonical synced ref. Missing records, unrelated paths, stale content,
1113
+ cross-project receipts, and non-ancestor commits fail closed. Validate both
1114
+ recovered and freshly created lifecycle commit SHAs before continuing; a
1115
+ failed commit or hook must not reuse the prior `HEAD` as a receipt.
621
1116
  - The lifecycle bookkeeping commit is the artifact commit for final recap
622
1117
  durability. It must contain the final run's immutable paths.
623
1118
  - Snapshot unrelated working-tree changes before finalization so the shared
@@ -625,9 +1120,11 @@ Rules:
625
1120
 
626
1121
  ### Step 10.5: Re-attest Final Project Recap
627
1122
 
628
- Skip when no final recap was selected, for local-scope projects, or when the
1123
+ Skip when no final recap was selected, for local-scope projects, when the
629
1124
  selected recap is already durable solely through independently verified publish
630
- evidence.
1125
+ evidence, or when Step 7.5 restored an exact `EVIDENCE_COMMIT`. In the recovered
1126
+ evidence case, verify the selected run's manifest and build record are the two
1127
+ exact paths in that commit and continue without re-attesting or rewriting them.
631
1128
 
632
1129
  For an archived recap, consume the exact `projectRecapExport` values recorded
633
1130
  in Step 8. Plan finalization through
@@ -641,8 +1138,14 @@ with:
641
1138
  - relocatedFrom: `sourceRunRoot`; and
642
1139
  - context `artifactCommit`: the full `LIFECYCLE_COMMIT` SHA.
643
1140
 
644
- For a shared project that was not archived, use the selected active run and
645
- omit `relocatedFrom`, but keep the same `completion-bookkeeping` mode.
1141
+ For a durable project that was not archived, use the selected active run and
1142
+ omit `relocatedFrom`, but keep the same `completion-bookkeeping` mode. Resolve
1143
+ the finalizer repository root as `ACTIVE_PROJECT_PATH`, not the parent
1144
+ checkout. Use `PROJECT_REF_COMMIT`, not the parent-branch `LIFECYCLE_COMMIT`,
1145
+ as the active recap artifact commit. Its immutable paths must be present in
1146
+ that exact project-ref commit. The parent record commit is separate durability
1147
+ evidence for discovery and must never be substituted into the project-ref
1148
+ history.
646
1149
 
647
1150
  When the finalization plan is `complete` with `built-needs-review` or `failed`,
648
1151
  preserve that exact outcome and skip both attestation and the evidence commit.
@@ -650,7 +1153,9 @@ These evidence-only plans are already complete for lifecycle retention and
650
1153
  remain unpublishable. Call `verifyTrackedRunFinalization(...)` on the complete
651
1154
  plan; it must not promote either outcome to `built-durable`.
652
1155
 
653
- The lifecycle bookkeeping commit is the artifact commit. Call the compatible
1156
+ For archive completion, the lifecycle bookkeeping commit is the artifact
1157
+ commit. For non-archive synced completion, `PROJECT_REF_COMMIT` is the artifact
1158
+ commit. Call the compatible
654
1159
  core's `recordDurability(...)` with the finalizer's planned request. Submit only immutable paths under `projectRecapExport.exportRoot` as commit evidence for an archived recap; `manifest.json` and `build-record.json` are mutable records and
655
1160
  must not appear in that evidence path list. The successful exported-path
656
1161
  attestation supersedes the prior active-path evidence. Verify the resulting
@@ -666,7 +1171,7 @@ details, and continue to the evidence commit.
666
1171
 
667
1172
  ### Step 10.6: Commit Evidence + Push
668
1173
 
669
- When Step 10.5 ran, create the evidence update. Commit only the exported `manifest.json` and `build-record.json` as the evidence update, including warning-bearing records from a failed attestation. On failure, commit the warning-bearing `manifest.json` and `build-record.json`. Run
1174
+ When Step 10.5 ran and Step 7.5 did not restore `EVIDENCE_COMMIT`, create the evidence update. Commit only the exported `manifest.json` and `build-record.json` as the evidence update, including warning-bearing records from a failed attestation. On failure, commit the warning-bearing `manifest.json` and `build-record.json`. Run
670
1175
  `verifyTrackedRunFinalization(...)` with the artifact commit, immediate evidence
671
1176
  commit parent/order, exact evidence paths, attestation outcome, and unchanged
672
1177
  unrelated-change snapshots.
@@ -681,6 +1186,60 @@ ran, push the lifecycle bookkeeping commit once. If verification detects
681
1186
  contamination or wrong commit order, do not push. If push fails, report the
682
1187
  failure and do not claim completion is fully recorded.
683
1188
 
1189
+ The evidence update remains a direct, exact-path Git commit. Stage only the
1190
+ reported exported `manifest.json` and `build-record.json` under tracked
1191
+ `.oat/repo/reference/project-recaps/`:
1192
+
1193
+ ```bash
1194
+ UNRELATED_STAGED_PATCH_BEFORE=$(git diff --cached --binary)
1195
+ git add -- "$EXPORTED_MANIFEST_PATH" "$EXPORTED_BUILD_RECORD_PATH"
1196
+ git commit --only -m "chore(oat): attest final project recap" -- \
1197
+ "$EXPORTED_MANIFEST_PATH" "$EXPORTED_BUILD_RECORD_PATH"
1198
+ EVIDENCE_COMMIT=$(git rev-parse HEAD)
1199
+ test "$(git rev-parse "$EVIDENCE_COMMIT^")" = "$LIFECYCLE_COMMIT"
1200
+ test "$(git diff --cached --binary)" = "$UNRELATED_STAGED_PATCH_BEFORE"
1201
+ ```
1202
+
1203
+ Verify the evidence commit contains exactly those two paths, the lifecycle
1204
+ commit is its immediate parent, unrelated-change snapshots remain unchanged,
1205
+ and then push once. The archive command does not own this evidence transition;
1206
+ use the direct exact-path commit above.
1207
+
1208
+ For a non-archive synced recap, stage and commit only the active run's reported
1209
+ `manifest.json` and `build-record.json` from inside `ACTIVE_PROJECT_PATH`. The
1210
+ non-archive recap evidence commit must be the immediate child of
1211
+ `PROJECT_REF_COMMIT` in the project checkout. Verify exact commit containment
1212
+ and the unchanged unrelated-state snapshot, then publish the evidence commit
1213
+ with `oat project push`, retaining the custom ref and checkout. Require the
1214
+ push receipt SHA to equal the evidence commit. Recovery reuses the existing
1215
+ project-ref artifact commit and parent-branch record commit; it never moves
1216
+ active recap files into archive-export paths.
1217
+
1218
+ Use the same path-confined commit inside the synced checkout:
1219
+
1220
+ ```bash
1221
+ ACTIVE_MANIFEST_RELATIVE_PATH="$SELECTED_PROJECT_RECAP_RUN/manifest.json"
1222
+ ACTIVE_BUILD_RECORD_RELATIVE_PATH="$SELECTED_PROJECT_RECAP_RUN/build-record.json"
1223
+ UNRELATED_PROJECT_STAGE_BEFORE=$(git -C "$ACTIVE_PROJECT_PATH" diff --cached --binary)
1224
+ git -C "$ACTIVE_PROJECT_PATH" add -- \
1225
+ "$ACTIVE_MANIFEST_RELATIVE_PATH" "$ACTIVE_BUILD_RECORD_RELATIVE_PATH"
1226
+ git -C "$ACTIVE_PROJECT_PATH" commit --only \
1227
+ -m "chore(oat): attest final project recap" -- \
1228
+ "$ACTIVE_MANIFEST_RELATIVE_PATH" "$ACTIVE_BUILD_RECORD_RELATIVE_PATH"
1229
+ EVIDENCE_COMMIT=$(git -C "$ACTIVE_PROJECT_PATH" rev-parse HEAD)
1230
+ test "$(git -C "$ACTIVE_PROJECT_PATH" rev-parse "$EVIDENCE_COMMIT^")" = \
1231
+ "$PROJECT_REF_COMMIT"
1232
+ test "$(git -C "$ACTIVE_PROJECT_PATH" diff --cached --binary)" = \
1233
+ "$UNRELATED_PROJECT_STAGE_BEFORE"
1234
+ ```
1235
+
1236
+ When Step 7.5 restored `EVIDENCE_COMMIT`, do not stage or commit recap records
1237
+ again. Require the retained remote ref to equal that exact evidence SHA, keep
1238
+ its immediate parent equal to `PROJECT_REF_COMMIT`, and preserve the recovered
1239
+ `PROJECT_LINKS_PIN_COMMIT`. The parent discovery-record commit remains confined
1240
+ to its one canonical path, and all unrelated staged-state snapshots must remain
1241
+ byte-for-byte unchanged.
1242
+
684
1243
  ### Step 11: Open PR in GitHub (Conditional)
685
1244
 
686
1245
  **Skip if `SHOULD_OPEN_PR` is false.**
@@ -705,19 +1264,32 @@ Do not assume `gh` is installed; if missing, instruct manual PR creation using t
705
1264
 
706
1265
  ### Step 11.5: Sync Open-PR Description on GitHub (Conditional)
707
1266
 
708
- **Run only when `WAS_PR_OPEN_AT_START="true"` AND `SHOULD_ARCHIVE="true"`.**
1267
+ **Run only when `WAS_PR_OPEN_AT_START="true"` and either
1268
+ `SHOULD_ARCHIVE="true"` or `PROJECT_SCOPE="synced"`.**
709
1269
 
710
- When the PR was already open at the start of this skill (typically because `oat-project-pr-final` ran earlier in the lifecycle) AND we just archived, the GitHub PR description authored by `oat-project-pr-final` still points to the active artifact paths. Step 8 moved those artifacts to a gitignored archive location and Step 10.6 pushed the move, so any blob link in the open PR body now 404s. Push the regenerated archive-aware body to the existing PR.
1270
+ When the PR was already open at the start, push the final validated completion
1271
+ body to the existing PR. Archive completion uses the regenerated archive-aware
1272
+ body. Non-archive synced completion uses the exact PR artifact from
1273
+ `PROJECT_REF_COMMIT`, whose canonical links block is owned by
1274
+ `PROJECT_LINKS_PIN_COMMIT`; do not substitute the parent discovery-record
1275
+ commit or the optional evidence child.
711
1276
 
712
1277
  Skip this step when:
713
1278
 
714
1279
  - The PR was not yet open at the start (`WAS_PR_OPEN_AT_START="false"`) — Step 11 already created the PR with the archive-aware body.
715
- - No archive happened (`SHOULD_ARCHIVE="false"`) — the original blob links still resolve.
716
- - `IS_SHARED_PROJECT="false"` — non-shared projects are not archived in this skill, so no link breakage.
1280
+ - No archive happened and the project is not synced — no final retained-ref
1281
+ body was published.
1282
+ - `IS_DURABLE_PROJECT="false"` — local projects are not archived in this skill, so no link breakage.
717
1283
 
718
1284
  Steps:
719
1285
 
720
- 1. Locate the PR description artifact at `{PROJECT_PATH}/pr/project-pr-*.md`. After Step 8, `PROJECT_PATH` points at the archived location, so the artifact lives at `{ARCHIVE_PATH}/pr/project-pr-*.md`.
1286
+ 1. Locate the exact PR description artifact. After archive, use
1287
+ `{ARCHIVE_PATH}/pr/project-pr-*.md`. For non-archive synced completion, use
1288
+ `PR_DESCRIPTION_PATH`, require the retained remote ref to contain
1289
+ `PROJECT_REF_COMMIT`, and require
1290
+ `git show "$PROJECT_REF_COMMIT:$PR_DESCRIPTION_RELATIVE_PATH"` to equal the
1291
+ body on disk with exactly one links block pinned to
1292
+ `PROJECT_LINKS_PIN_COMMIT`.
721
1293
  2. Strip YAML frontmatter (everything from the opening `---` through and including the closing `---`) and write the result to a temporary file. Verify the temp file does not start with YAML frontmatter keys.
722
1294
  3. Resolve the open PR. Prefer the tracked URL captured in Step 2:
723
1295