@open-agent-toolkit/cli 0.2.65 → 0.2.67

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 (238) hide show
  1. package/assets/bundle-metadata.json +1 -1
  2. package/assets/docs/cli-utilities/configuration.md +33 -2
  3. package/assets/docs/cli-utilities/index.md +3 -0
  4. package/assets/docs/cli-utilities/project-log.md +63 -4
  5. package/assets/docs/cli-utilities/remote-project-management.md +191 -0
  6. package/assets/docs/contributing/code.md +8 -4
  7. package/assets/docs/contributing/skills.md +29 -17
  8. package/assets/docs/docs-tooling/commands.md +3 -1
  9. package/assets/docs/provider-sync/manifest-and-drift.md +2 -18
  10. package/assets/docs/reference/cli-reference.md +12 -1
  11. package/assets/docs/reference/file-locations.md +22 -0
  12. package/assets/docs/reference/oat-directory-structure.md +30 -17
  13. package/assets/docs/reference/troubleshooting.md +44 -0
  14. package/assets/docs/workflows/projects/lifecycle.md +17 -0
  15. package/assets/docs/workflows/projects/picking-up-projects.md +18 -0
  16. package/assets/docs/workflows/skills/index.md +33 -0
  17. package/assets/docs/workflows/skills/repo-improve.md +7 -3
  18. package/assets/public-package-versions.json +4 -4
  19. package/assets/skills/create-agnostic-skill/SKILL.md +19 -37
  20. package/assets/skills/create-agnostic-skill/references/docs/skills-guide.md +58 -15
  21. package/assets/skills/create-agnostic-skill/references/skill-template.md +17 -15
  22. package/assets/skills/oat-agent-instructions-apply/SKILL.md +1 -1
  23. package/assets/skills/oat-agent-instructions-apply/references/instruction-file-templates/glob-scoped-rule.md +1 -4
  24. package/assets/skills/oat-doctor/SKILL.md +3 -6
  25. package/assets/skills/oat-pjm-remote/SKILL.md +101 -0
  26. package/assets/skills/oat-pjm-remote/references/external-action-protocol.md +43 -0
  27. package/assets/skills/oat-project-autonomous/references/gate-inventory.md +1 -1
  28. package/assets/skills/oat-project-complete/SKILL.md +158 -30
  29. package/assets/skills/oat-project-complete/scripts/finalize-synced-archive.mjs +26 -7
  30. package/assets/skills/oat-project-complete/scripts/validate-durable-archive-receipt.mjs +183 -0
  31. package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +1 -1
  32. package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +1 -1
  33. package/assets/skills/oat-project-lite/references/docs/autonomy-contract.md +1 -1
  34. package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +1 -1
  35. package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +1 -1
  36. package/assets/skills/oat-project-retro/SKILL.md +1 -1
  37. package/assets/skills/oat-project-retro/references/apply-procedure.md +11 -0
  38. package/assets/skills/oat-project-review-provide/SKILL.md +4 -4
  39. package/assets/skills/oat-project-summary/SKILL.md +24 -2
  40. package/assets/skills/oat-repo-improve/SKILL.md +9 -2
  41. package/assets/skills/oat-repo-improve/references/plan-template.md +4 -6
  42. package/assets/skills/oat-repo-knowledge-index/SKILL.md +11 -9
  43. package/assets/skills/oat-wave-execute/SKILL.md +20 -6
  44. package/assets/skills/oat-wave-program/SKILL.md +9 -4
  45. package/dist/commands/backlog/new.d.ts +1 -0
  46. package/dist/commands/backlog/new.d.ts.map +1 -1
  47. package/dist/commands/backlog/new.js +3 -1
  48. package/dist/commands/config/index.d.ts +3 -1
  49. package/dist/commands/config/index.d.ts.map +1 -1
  50. package/dist/commands/config/index.js +362 -36
  51. package/dist/commands/docs/index-generate/index.d.ts.map +1 -1
  52. package/dist/commands/docs/index-generate/index.js +68 -12
  53. package/dist/commands/docs/init/index.d.ts +11 -0
  54. package/dist/commands/docs/init/index.d.ts.map +1 -1
  55. package/dist/commands/docs/init/index.js +19 -1
  56. package/dist/commands/gate/index.d.ts.map +1 -1
  57. package/dist/commands/gate/index.js +10 -2
  58. package/dist/commands/instructions/instructions.utils.d.ts.map +1 -1
  59. package/dist/commands/instructions/instructions.utils.js +42 -9
  60. package/dist/commands/internal/validate-skill-version-bumps.d.ts.map +1 -1
  61. package/dist/commands/internal/validate-skill-version-bumps.js +5 -5
  62. package/dist/commands/pjm/doctor.d.ts +2 -0
  63. package/dist/commands/pjm/doctor.d.ts.map +1 -1
  64. package/dist/commands/pjm/doctor.js +71 -1
  65. package/dist/commands/pjm/index.d.ts +2 -0
  66. package/dist/commands/pjm/index.d.ts.map +1 -1
  67. package/dist/commands/pjm/index.js +3 -0
  68. package/dist/commands/pjm/remote/__integration__/lifecycle-harness.d.ts +60 -0
  69. package/dist/commands/pjm/remote/__integration__/lifecycle-harness.d.ts.map +1 -0
  70. package/dist/commands/pjm/remote/__integration__/lifecycle-harness.js +151 -0
  71. package/dist/commands/pjm/remote/association.d.ts +27 -0
  72. package/dist/commands/pjm/remote/association.d.ts.map +1 -0
  73. package/dist/commands/pjm/remote/association.js +68 -0
  74. package/dist/commands/pjm/remote/authority.d.ts +210 -0
  75. package/dist/commands/pjm/remote/authority.d.ts.map +1 -0
  76. package/dist/commands/pjm/remote/authority.js +341 -0
  77. package/dist/commands/pjm/remote/batch.d.ts +15 -0
  78. package/dist/commands/pjm/remote/batch.d.ts.map +1 -0
  79. package/dist/commands/pjm/remote/batch.js +126 -0
  80. package/dist/commands/pjm/remote/closeout.d.ts +69 -0
  81. package/dist/commands/pjm/remote/closeout.d.ts.map +1 -0
  82. package/dist/commands/pjm/remote/closeout.js +208 -0
  83. package/dist/commands/pjm/remote/create-binding.d.ts +62 -0
  84. package/dist/commands/pjm/remote/create-binding.d.ts.map +1 -0
  85. package/dist/commands/pjm/remote/create-binding.js +126 -0
  86. package/dist/commands/pjm/remote/credential-safety.d.ts +7 -0
  87. package/dist/commands/pjm/remote/credential-safety.d.ts.map +1 -0
  88. package/dist/commands/pjm/remote/credential-safety.js +21 -0
  89. package/dist/commands/pjm/remote/discussion.d.ts +49 -0
  90. package/dist/commands/pjm/remote/discussion.d.ts.map +1 -0
  91. package/dist/commands/pjm/remote/discussion.js +124 -0
  92. package/dist/commands/pjm/remote/doctor.d.ts +22 -0
  93. package/dist/commands/pjm/remote/doctor.d.ts.map +1 -0
  94. package/dist/commands/pjm/remote/doctor.js +380 -0
  95. package/dist/commands/pjm/remote/external-action.d.ts +208 -0
  96. package/dist/commands/pjm/remote/external-action.d.ts.map +1 -0
  97. package/dist/commands/pjm/remote/external-action.js +367 -0
  98. package/dist/commands/pjm/remote/host-execution.d.ts +28 -0
  99. package/dist/commands/pjm/remote/host-execution.d.ts.map +1 -0
  100. package/dist/commands/pjm/remote/host-execution.js +63 -0
  101. package/dist/commands/pjm/remote/index.d.ts +41 -0
  102. package/dist/commands/pjm/remote/index.d.ts.map +1 -0
  103. package/dist/commands/pjm/remote/index.js +280 -0
  104. package/dist/commands/pjm/remote/jira-adf.d.ts +35 -0
  105. package/dist/commands/pjm/remote/jira-adf.d.ts.map +1 -0
  106. package/dist/commands/pjm/remote/jira-adf.js +169 -0
  107. package/dist/commands/pjm/remote/lifecycle.d.ts +163 -0
  108. package/dist/commands/pjm/remote/lifecycle.d.ts.map +1 -0
  109. package/dist/commands/pjm/remote/lifecycle.js +224 -0
  110. package/dist/commands/pjm/remote/local-projection.d.ts +30 -0
  111. package/dist/commands/pjm/remote/local-projection.d.ts.map +1 -0
  112. package/dist/commands/pjm/remote/local-projection.js +95 -0
  113. package/dist/commands/pjm/remote/managed-markdown.d.ts +30 -0
  114. package/dist/commands/pjm/remote/managed-markdown.d.ts.map +1 -0
  115. package/dist/commands/pjm/remote/managed-markdown.js +128 -0
  116. package/dist/commands/pjm/remote/migrate.d.ts +14 -0
  117. package/dist/commands/pjm/remote/migrate.d.ts.map +1 -0
  118. package/dist/commands/pjm/remote/migrate.js +32 -0
  119. package/dist/commands/pjm/remote/operation-state.d.ts +15 -0
  120. package/dist/commands/pjm/remote/operation-state.d.ts.map +1 -0
  121. package/dist/commands/pjm/remote/operation-state.js +143 -0
  122. package/dist/commands/pjm/remote/outbound-projection-safety.d.ts +25 -0
  123. package/dist/commands/pjm/remote/outbound-projection-safety.d.ts.map +1 -0
  124. package/dist/commands/pjm/remote/outbound-projection-safety.js +99 -0
  125. package/dist/commands/pjm/remote/output.d.ts +60 -0
  126. package/dist/commands/pjm/remote/output.d.ts.map +1 -0
  127. package/dist/commands/pjm/remote/output.js +48 -0
  128. package/dist/commands/pjm/remote/preview.d.ts +88 -0
  129. package/dist/commands/pjm/remote/preview.d.ts.map +1 -0
  130. package/dist/commands/pjm/remote/preview.js +154 -0
  131. package/dist/commands/pjm/remote/provider-conformance.d.ts +13 -0
  132. package/dist/commands/pjm/remote/provider-conformance.d.ts.map +1 -0
  133. package/dist/commands/pjm/remote/provider-conformance.js +37 -0
  134. package/dist/commands/pjm/remote/provider.d.ts +57 -0
  135. package/dist/commands/pjm/remote/provider.d.ts.map +1 -0
  136. package/dist/commands/pjm/remote/provider.js +28 -0
  137. package/dist/commands/pjm/remote/providers/github-publication-safety.d.ts +46 -0
  138. package/dist/commands/pjm/remote/providers/github-publication-safety.d.ts.map +1 -0
  139. package/dist/commands/pjm/remote/providers/github-publication-safety.js +126 -0
  140. package/dist/commands/pjm/remote/providers/github.d.ts +207 -0
  141. package/dist/commands/pjm/remote/providers/github.d.ts.map +1 -0
  142. package/dist/commands/pjm/remote/providers/github.js +1456 -0
  143. package/dist/commands/pjm/remote/providers/jira.d.ts +245 -0
  144. package/dist/commands/pjm/remote/providers/jira.d.ts.map +1 -0
  145. package/dist/commands/pjm/remote/providers/jira.js +1650 -0
  146. package/dist/commands/pjm/remote/providers/linear.d.ts +182 -0
  147. package/dist/commands/pjm/remote/providers/linear.d.ts.map +1 -0
  148. package/dist/commands/pjm/remote/providers/linear.js +1289 -0
  149. package/dist/commands/pjm/remote/purpose-policy.d.ts +29 -0
  150. package/dist/commands/pjm/remote/purpose-policy.d.ts.map +1 -0
  151. package/dist/commands/pjm/remote/purpose-policy.js +118 -0
  152. package/dist/commands/pjm/remote/reconcile.d.ts +42 -0
  153. package/dist/commands/pjm/remote/reconcile.d.ts.map +1 -0
  154. package/dist/commands/pjm/remote/reconcile.js +101 -0
  155. package/dist/commands/pjm/remote/resolution.d.ts +122 -0
  156. package/dist/commands/pjm/remote/resolution.d.ts.map +1 -0
  157. package/dist/commands/pjm/remote/resolution.js +243 -0
  158. package/dist/commands/pjm/remote/schema.d.ts +9309 -0
  159. package/dist/commands/pjm/remote/schema.d.ts.map +1 -0
  160. package/dist/commands/pjm/remote/schema.js +1503 -0
  161. package/dist/commands/pjm/remote/service.d.ts +20 -0
  162. package/dist/commands/pjm/remote/service.d.ts.map +1 -0
  163. package/dist/commands/pjm/remote/service.js +5121 -0
  164. package/dist/commands/pjm/remote/shared-storage.d.ts +36 -0
  165. package/dist/commands/pjm/remote/shared-storage.d.ts.map +1 -0
  166. package/dist/commands/pjm/remote/shared-storage.js +110 -0
  167. package/dist/commands/pjm/remote/snapshot.d.ts +32 -0
  168. package/dist/commands/pjm/remote/snapshot.d.ts.map +1 -0
  169. package/dist/commands/pjm/remote/snapshot.js +140 -0
  170. package/dist/commands/pjm/remote/storage-locator.d.ts +29 -0
  171. package/dist/commands/pjm/remote/storage-locator.d.ts.map +1 -0
  172. package/dist/commands/pjm/remote/storage-locator.js +62 -0
  173. package/dist/commands/pjm/remote/store.d.ts +85 -0
  174. package/dist/commands/pjm/remote/store.d.ts.map +1 -0
  175. package/dist/commands/pjm/remote/store.js +553 -0
  176. package/dist/commands/pjm/remote/verification.d.ts +40 -0
  177. package/dist/commands/pjm/remote/verification.d.ts.map +1 -0
  178. package/dist/commands/pjm/remote/verification.js +100 -0
  179. package/dist/commands/project/dispatch-ceiling/index.d.ts.map +1 -1
  180. package/dist/commands/project/dispatch-ceiling/index.js +32 -18
  181. package/dist/commands/project/log/append.d.ts +48 -0
  182. package/dist/commands/project/log/append.d.ts.map +1 -1
  183. package/dist/commands/project/log/append.js +207 -30
  184. package/dist/commands/project/log/check.d.ts +87 -1
  185. package/dist/commands/project/log/check.d.ts.map +1 -1
  186. package/dist/commands/project/log/check.js +139 -5
  187. package/dist/commands/project/log/grammar.d.ts +165 -0
  188. package/dist/commands/project/log/grammar.d.ts.map +1 -1
  189. package/dist/commands/project/log/grammar.js +219 -1
  190. package/dist/commands/project/log/rollup.d.ts +16 -1
  191. package/dist/commands/project/log/rollup.d.ts.map +1 -1
  192. package/dist/commands/project/log/rollup.js +22 -2
  193. package/dist/commands/project/log/synthesize.d.ts +16 -0
  194. package/dist/commands/project/log/synthesize.d.ts.map +1 -1
  195. package/dist/commands/project/log/synthesize.js +85 -9
  196. package/dist/commands/status/index.d.ts.map +1 -1
  197. package/dist/commands/status/index.js +24 -2
  198. package/dist/commands/sync/apply.d.ts.map +1 -1
  199. package/dist/commands/sync/apply.js +6 -4
  200. package/dist/commands/tools/shared/pack-manifest.d.ts.map +1 -1
  201. package/dist/commands/tools/shared/pack-manifest.js +1 -0
  202. package/dist/config/dispatch-matrix.d.ts.map +1 -1
  203. package/dist/config/dispatch-matrix.js +13 -5
  204. package/dist/config/oat-config.d.ts +41 -0
  205. package/dist/config/oat-config.d.ts.map +1 -1
  206. package/dist/config/oat-config.js +247 -9
  207. package/dist/config/own-keys.d.ts +22 -0
  208. package/dist/config/own-keys.d.ts.map +1 -0
  209. package/dist/config/own-keys.js +32 -0
  210. package/dist/config/resolve.d.ts +11 -0
  211. package/dist/config/resolve.d.ts.map +1 -1
  212. package/dist/config/resolve.js +35 -11
  213. package/dist/config/sync-config.d.ts.map +1 -1
  214. package/dist/config/sync-config.js +16 -0
  215. package/dist/drift/detector.d.ts.map +1 -1
  216. package/dist/drift/detector.js +52 -1
  217. package/dist/drift/skill-view-diagnostic.d.ts.map +1 -1
  218. package/dist/drift/skill-view-diagnostic.js +2 -23
  219. package/dist/engine/compute-plan.d.ts.map +1 -1
  220. package/dist/engine/compute-plan.js +22 -72
  221. package/dist/engine/managed-copy-hash.d.ts +69 -0
  222. package/dist/engine/managed-copy-hash.d.ts.map +1 -0
  223. package/dist/engine/managed-copy-hash.js +175 -0
  224. package/dist/fs/assets.d.ts +11 -0
  225. package/dist/fs/assets.d.ts.map +1 -1
  226. package/dist/fs/assets.js +1 -1
  227. package/dist/manifest/hash.d.ts +110 -0
  228. package/dist/manifest/hash.d.ts.map +1 -1
  229. package/dist/manifest/hash.js +111 -13
  230. package/dist/providers/ceiling/registry.d.ts.map +1 -1
  231. package/dist/providers/ceiling/registry.js +6 -1
  232. package/dist/providers/identity/dispatch-report.d.ts.map +1 -1
  233. package/dist/providers/identity/dispatch-report.js +4 -1
  234. package/dist/release/public-package-contract.d.ts.map +1 -1
  235. package/dist/release/public-package-contract.js +21 -0
  236. package/dist/validation/skills.d.ts.map +1 -1
  237. package/dist/validation/skills.js +305 -40
  238. package/package.json +2 -2
@@ -0,0 +1,100 @@
1
+ import { createHash } from 'node:crypto';
2
+ const FIELD_ORDER = [
3
+ 'title',
4
+ 'description',
5
+ 'priority',
6
+ ];
7
+ export function buildVerificationRequestDigest(input) {
8
+ const fieldMask = normalizeFieldMask(input.requested.fieldMask);
9
+ return hashCanonical({
10
+ operationId: input.operationId,
11
+ fieldMask,
12
+ expected: Object.fromEntries(fieldMask.map((field) => [field, input.requested.expected[field]])),
13
+ attemptedRevisionDigest: input.revisionEvidence.attemptedRevisionDigest,
14
+ });
15
+ }
16
+ export function verifyRemotePostconditions(input) {
17
+ const fieldMask = normalizeFieldMask(input.requested.fieldMask);
18
+ for (const field of fieldMask) {
19
+ if (!Object.hasOwn(input.requested.expected, field)) {
20
+ throw new Error(`Requested field '${field}' has no expected value.`);
21
+ }
22
+ }
23
+ const requestDigest = buildVerificationRequestDigest(input);
24
+ if (input.priorVerification?.classification === 'verified' &&
25
+ input.priorVerification.requestDigest === requestDigest) {
26
+ return result('already-verified', 'durable-verification-match', requestDigest, [], false);
27
+ }
28
+ if (input.providerOutcome === 'rejected') {
29
+ return result('rejected', 'provider-rejected', requestDigest, unavailableFields(input, fieldMask), false);
30
+ }
31
+ if (input.providerOutcome === 'ambiguous') {
32
+ return result('uncertain', 'ambiguous-provider-outcome', requestDigest, unavailableFields(input, fieldMask), true);
33
+ }
34
+ if (input.revisionEvidence.readbackBaseRevisionDigest !==
35
+ input.revisionEvidence.attemptedRevisionDigest) {
36
+ return result('uncertain', 'revision-drift', requestDigest, unavailableFields(input, fieldMask), true);
37
+ }
38
+ if (!input.readback) {
39
+ return result('uncertain', 'missing-readback', requestDigest, unavailableFields(input, fieldMask), true);
40
+ }
41
+ const fields = fieldMask.map((field) => verifyField(field, input.requested.expected[field], input.readback));
42
+ const verifiedCount = fields.filter((field) => field.status === 'verified').length;
43
+ const unavailableCount = fields.filter((field) => field.status === 'unavailable').length;
44
+ if (verifiedCount === fields.length) {
45
+ return result('verified', 'postconditions-verified', requestDigest, fields, false);
46
+ }
47
+ if (verifiedCount > 0) {
48
+ return result('partial', 'partial-postconditions', requestDigest, fields, true);
49
+ }
50
+ if (unavailableCount > 0) {
51
+ return result('uncertain', 'incomplete-readback', requestDigest, fields, true);
52
+ }
53
+ return result('uncertain', 'postconditions-mismatched', requestDigest, fields, true);
54
+ }
55
+ function verifyField(field, expected, readback) {
56
+ const expectedHash = hashCanonical(expected);
57
+ if (readback.unavailableFields.includes(field) ||
58
+ !Object.hasOwn(readback.values, field)) {
59
+ return { field, expectedHash, observedHash: null, status: 'unavailable' };
60
+ }
61
+ const observed = readback.values[field];
62
+ return {
63
+ field,
64
+ expectedHash,
65
+ observedHash: hashCanonical(observed),
66
+ status: observed === expected ? 'verified' : 'mismatch',
67
+ };
68
+ }
69
+ function unavailableFields(input, fieldMask) {
70
+ return fieldMask.map((field) => ({
71
+ field,
72
+ expectedHash: hashCanonical(input.requested.expected[field]),
73
+ observedHash: null,
74
+ status: 'unavailable',
75
+ }));
76
+ }
77
+ function result(classification, reason, requestDigest, fields, requiresReconciliation) {
78
+ return {
79
+ classification,
80
+ reason,
81
+ requestDigest,
82
+ fields,
83
+ retryDisposition: requiresReconciliation
84
+ ? 'reconcile-required'
85
+ : 'not-applicable',
86
+ requiresReconciliation,
87
+ };
88
+ }
89
+ function normalizeFieldMask(fieldMask) {
90
+ const unique = new Set(fieldMask);
91
+ if (fieldMask.length === 0 || unique.size !== fieldMask.length) {
92
+ throw new Error('Verification field mask must be non-empty and unique.');
93
+ }
94
+ return FIELD_ORDER.filter((field) => unique.has(field));
95
+ }
96
+ function hashCanonical(value) {
97
+ return `sha256:${createHash('sha256')
98
+ .update(JSON.stringify(value))
99
+ .digest('hex')}`;
100
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/dispatch-ceiling/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,sBAAsB,CAAC;AAkB9B,OAAO,EAUL,KAAK,uBAAuB,EAY7B,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAEL,KAAK,cAAc,EAEpB,MAAM,iBAAiB,CAAC;AA6BzB,OAAO,EAAE,OAAO,EAAU,MAAM,WAAW,CAAC;AAiC5C,UAAU,2BAA2B;IACnC,mBAAmB,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,cAAc,CAAC;IAChE,kBAAkB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACrD,sBAAsB,EAAE,CACtB,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,MAAM,EACrB,GAAG,EAAE,MAAM,CAAC,UAAU,KACnB,OAAO,CAAC,cAAc,CAAC,CAAC;IAC7B,oBAAoB,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,uBAAuB,CAAC,CAAC;IAC7E,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAC5C,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC/C,UAAU,EAAE,MAAM,CAAC,UAAU,CAAC;CAC/B;AA0wFD,wBAAgB,mCAAmC,CACjD,SAAS,GAAE,OAAO,CAAC,2BAA2B,CAAM,GACnD,OAAO,CA0GT"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/dispatch-ceiling/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,sBAAsB,CAAC;AAkB9B,OAAO,EAUL,KAAK,uBAAuB,EAY7B,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EAEL,KAAK,cAAc,EAEpB,MAAM,iBAAiB,CAAC;AA6BzB,OAAO,EAAE,OAAO,EAAU,MAAM,WAAW,CAAC;AAiC5C,UAAU,2BAA2B;IACnC,mBAAmB,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,cAAc,CAAC;IAChE,kBAAkB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACrD,sBAAsB,EAAE,CACtB,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,MAAM,EACrB,GAAG,EAAE,MAAM,CAAC,UAAU,KACnB,OAAO,CAAC,cAAc,CAAC,CAAC;IAC7B,oBAAoB,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,uBAAuB,CAAC,CAAC;IAC7E,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAC5C,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC/C,UAAU,EAAE,MAAM,CAAC,UAAU,CAAC;CAC/B;AAkyFD,wBAAgB,mCAAmC,CACjD,SAAS,GAAE,OAAO,CAAC,2BAA2B,CAAM,GACnD,OAAO,CA0GT"}
@@ -8,6 +8,7 @@ import { normalizeDispatchMatrix } from '../../../config/dispatch-matrix.js';
8
8
  import { effectiveTerminalReviewerTarget, formatDispatchNotices, terminalReviewerNoticeForTarget, } from '../../../config/dispatch-notices.js';
9
9
  import { getDispatchPolicyChoices, managedDispatchPolicyValueList, renderDispatchPolicyChoicesMarkdown, } from '../../../config/dispatch-policy-options.js';
10
10
  import { resolveActiveProject, VALID_CLAUDE_DISPATCH_CEILINGS, VALID_CODEX_DISPATCH_CEILINGS, VALID_DISPATCH_MATRIX_TIERS, VALID_MANAGED_DISPATCH_POLICIES, isWorkflowDispatchCandidateLadder, isWorkflowDispatchFallbackRoute, toWorkflowDispatchCandidateLadder, validateDispatchRouteTarget, } from '../../../config/oat-config.js';
11
+ import { getOwnKey, setOwnKey } from '../../../config/own-keys.js';
11
12
  import { resolveEffectiveConfig, } from '../../../config/resolve.js';
12
13
  import { resolveProjectRoot } from '../../../fs/paths.js';
13
14
  import TOML from '@iarna/toml';
@@ -141,13 +142,17 @@ function isProjectMatrixRecord(value) {
141
142
  }
142
143
  function toProjectMatrixCompatibility(rawMatrix, matrix) {
143
144
  const compatibility = {};
145
+ // The normalized matrix now keeps a provider named `__proto__` as an own
146
+ // key, so `Object.entries` yields it here: assignment would drop a scalar
147
+ // entry and install a tier map as this output's prototype.
144
148
  for (const [provider, providerValue] of Object.entries(matrix)) {
145
149
  if (typeof providerValue === 'string') {
146
- compatibility[provider] = providerValue;
150
+ setOwnKey(compatibility, provider, providerValue);
147
151
  continue;
148
152
  }
149
- const rawProvider = isProjectMatrixRecord(rawMatrix[provider])
150
- ? rawMatrix[provider]
153
+ const rawProviderValue = getOwnKey(rawMatrix, provider);
154
+ const rawProvider = isProjectMatrixRecord(rawProviderValue)
155
+ ? rawProviderValue
151
156
  : {};
152
157
  const tiers = {};
153
158
  for (const [tier, cell] of Object.entries(providerValue)) {
@@ -167,7 +172,7 @@ function toProjectMatrixCompatibility(rawMatrix, matrix) {
167
172
  }
168
173
  tiers[matrixTier] = cell;
169
174
  }
170
- compatibility[provider] = tiers;
175
+ setOwnKey(compatibility, provider, tiers);
171
176
  }
172
177
  return compatibility;
173
178
  }
@@ -346,7 +351,7 @@ function resolveProviderMatrixCell(provider, tier, resolvedConfig, projectMatrix
346
351
  { source: 'project-state', providers: projectMatrix },
347
352
  ];
348
353
  for (const layer of layers) {
349
- const resolved = resolveProviderCellFromValue(provider, layer.providers?.[provider], tier, layer.source, escalationLevel);
354
+ const resolved = resolveProviderCellFromValue(provider, layer.providers ? getOwnKey(layer.providers, provider) : undefined, tier, layer.source, escalationLevel);
350
355
  if (resolved) {
351
356
  selected = resolved;
352
357
  }
@@ -371,7 +376,9 @@ function resolveProviderMatrixCellDefinition(provider, tier, resolvedConfig, pro
371
376
  { source: 'project-state', providers: projectMatrix },
372
377
  ];
373
378
  for (const layer of layers) {
374
- const providerValue = layer.providers?.[provider];
379
+ const providerValue = layer.providers
380
+ ? getOwnKey(layer.providers, provider)
381
+ : undefined;
375
382
  if (typeof providerValue === 'string') {
376
383
  selected = null;
377
384
  continue;
@@ -1305,17 +1312,20 @@ function mergeEffectiveDispatchMatrix(resolvedConfig) {
1305
1312
  resolvedConfig.local.workflow?.dispatchCeiling?.providers,
1306
1313
  ];
1307
1314
  for (const providers of layers) {
1315
+ // Provider names come from user config, which now preserves a key named
1316
+ // `__proto__` as an own key, so `Object.entries` yields it here. Plain
1317
+ // member access would reinstall it as this map's prototype and lose it.
1308
1318
  for (const [provider, value] of Object.entries(providers ?? {})) {
1309
- const previous = merged[provider];
1310
- merged[provider] =
1311
- typeof value === 'object' &&
1312
- value !== null &&
1313
- !Array.isArray(value) &&
1314
- typeof previous === 'object' &&
1315
- previous !== null &&
1316
- !Array.isArray(previous)
1317
- ? { ...previous, ...value }
1318
- : value;
1319
+ const previous = getOwnKey(merged, provider);
1320
+ const mergedValue = typeof value === 'object' &&
1321
+ value !== null &&
1322
+ !Array.isArray(value) &&
1323
+ typeof previous === 'object' &&
1324
+ previous !== null &&
1325
+ !Array.isArray(previous)
1326
+ ? { ...previous, ...value }
1327
+ : value;
1328
+ setOwnKey(merged, provider, mergedValue);
1319
1329
  }
1320
1330
  }
1321
1331
  return merged;
@@ -1648,7 +1658,9 @@ function writeHumanResolution(context, resolution) {
1648
1658
  context.logger.info(`${label} dispatch policy: ${policyLabel(resolution.policy)}`);
1649
1659
  context.logger.info(`Resolved cap: ${resolution.value ?? 'none'}`);
1650
1660
  context.logger.info(`Source: ${sourceLabel(resolution.source)}`);
1651
- const providerResolution = resolution.providers[resolution.provider];
1661
+ // `resolution.provider` is an open string union carrying a user-supplied
1662
+ // `--provider` value, so an unowned prototype name must read as absent.
1663
+ const providerResolution = getOwnKey(resolution.providers, resolution.provider);
1652
1664
  if (providerResolution) {
1653
1665
  context.logger.info(`Mode: ${providerResolution.mode} (${providerResolution.mechanism})`);
1654
1666
  context.logger.info(`Selection: ${providerResolution.selection.selectionMode}`);
@@ -1760,7 +1772,9 @@ function buildResolutionReport(resolution, options) {
1760
1772
  }
1761
1773
  const action = options.reportAction;
1762
1774
  const role = reportRole(action);
1763
- const providerResolution = resolution.providers[resolution.provider];
1775
+ // `resolution.provider` is an open string union carrying a user-supplied
1776
+ // `--provider` value, so an unowned prototype name must read as absent.
1777
+ const providerResolution = getOwnKey(resolution.providers, resolution.provider);
1764
1778
  if (!providerResolution) {
1765
1779
  throw new Error(`Dispatch report resolution is missing provider data for "${resolution.provider}".`);
1766
1780
  }
@@ -2,6 +2,7 @@ import { type CommandContext, type GlobalOptions } from '../../../app/command-co
2
2
  import { type ActiveProjectResolution } from '../../../config/oat-config.js';
3
3
  import { type ResolvedConfig } from '../../../config/resolve.js';
4
4
  import { Command } from 'commander';
5
+ import { type ProjectLogSeal } from './check.js';
5
6
  export declare const PROJECT_LOG_FILENAME = "project-log.md";
6
7
  /**
7
8
  * Directory, relative to a project, that holds gate partial-finalization
@@ -34,6 +35,25 @@ export interface ProjectLogLockDependencies {
34
35
  now: () => number;
35
36
  }
36
37
  export declare function projectLogLockPath(logPath: string): string;
38
+ /**
39
+ * Runs `mutate` with the log's advisory lock held, releasing it on every path.
40
+ *
41
+ * `required` is for mutations that rewrite the file. Running one of those
42
+ * without the lock is exactly the read/modify/write race this exists to close,
43
+ * so an unavailable lock refuses rather than proceeding: a loud, retryable
44
+ * failure that the caller reports is strictly better than a silent clobber of
45
+ * another writer's entry, and the recovery command replays the append safely.
46
+ *
47
+ * `best-effort` is for work that cannot lose an entry — committing does not
48
+ * rewrite the log — where holding the lock only keeps this process from racing
49
+ * its own sibling into `.git/index.lock`.
50
+ *
51
+ * Exported so `synthesize` enters the same critical section rather than a
52
+ * second one of its own: its whole-file rewrite races an overlapping append
53
+ * exactly as two appends race each other, and two locks would serialize
54
+ * neither.
55
+ */
56
+ export declare function withProjectLogLock<T>(logPath: string, overrides: Partial<ProjectLogLockDependencies>, policy: 'required' | 'best-effort', mutate: () => Promise<T>): Promise<T>;
37
57
  export type GitLockFailureClass = 'transient-index-lock' | 'persistent-index-lock' | 'other';
38
58
  export type ProjectLogCommitOutcome = 'committed' | 'already-committed' | 'nothing-to-commit' | 'not-a-repo' | 'blocked-by-index-lock' | 'entry-missing-after-commit' | 'commit-unverified' | 'failed';
39
59
  export interface ProjectLogCommitResult {
@@ -186,11 +206,39 @@ export interface AppendProjectLogInput {
186
206
  versionNote?: string;
187
207
  idempotencyKey?: string;
188
208
  }
209
+ /**
210
+ * Raised when an append that is not the completion seal targets a sealed log.
211
+ *
212
+ * This is a throw rather than a fourth `ProjectLogAppendResult` variant because
213
+ * every in-tree caller of `appendProjectLog` runs *before* the seal: the gate's
214
+ * partial-finalization receipt narrows the result's `status` into its own
215
+ * `'appended' | 'already-appended'` field, so widening the union would force a
216
+ * change on a caller that can never legitimately reach this state. Throwing
217
+ * refuses that caller too — the command layer maps this to
218
+ * `status: 'sealed'` with a non-zero exit, and any other caller fails loudly
219
+ * instead of silently appending past the seal.
220
+ */
221
+ export declare class ProjectLogSealedError extends Error {
222
+ readonly logPath: string;
223
+ readonly seal: ProjectLogSeal;
224
+ /**
225
+ * `action` names the refused mutation so `synthesize`, which rewrites rather
226
+ * than appends, reports what it actually declined. It defaults to `append`,
227
+ * which is the wording every existing caller and its pinned control expect.
228
+ */
229
+ constructor(logPath: string, seal: ProjectLogSeal, action?: string);
230
+ }
189
231
  export type ProjectLogAppendResult = {
190
232
  status: 'appended';
191
233
  logPath: string;
192
234
  heading: string;
193
235
  created: boolean;
236
+ /**
237
+ * Present and true when the accepted `--body` carried CRLF and was stored
238
+ * as LF. The command reports it rather than normalizing silently, because
239
+ * the bytes on disk then differ from the bytes the caller passed.
240
+ */
241
+ normalizedLineEndings?: true;
194
242
  } | {
195
243
  status: 'already-appended';
196
244
  logPath: string;
@@ -1 +1 @@
1
- {"version":3,"file":"append.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/log/append.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"append.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/log/append.ts"],"names":[],"mappings":"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"}
@@ -12,7 +12,8 @@ import { resolveAssetsRoot } from '../../../fs/assets.js';
12
12
  import { dirExists, fileExists } from '../../../fs/io.js';
13
13
  import { resolveProjectRoot } from '../../../fs/paths.js';
14
14
  import { Command } from 'commander';
15
- import { composeJudgmentHeading, composeStructuralHeading, isProjectLogEntryMarker, isProjectLogSectionMarker, PROJECT_LOG_AREA_MAX_LENGTH, PROJECT_LOG_HEADING_DELIMITER, PROJECT_LOG_SCOPES, PROJECT_LOG_TYPES, } from './grammar.js';
15
+ import { findProjectLogAmbiguity, findProjectLogSeal, unreachableProjectLogSealError, } from './check.js';
16
+ import { composeJudgmentHeading, composeStructuralHeading, containsAmbiguousProjectLogMarker, isProjectLogEntryMarker, findProjectLogSealHeadingLine, isProjectLogSealHeading, isProjectLogSectionMarker, PROJECT_LOG_AREA_MAX_LENGTH, PROJECT_LOG_HEADING_DELIMITER, PROJECT_LOG_LINE_TERMINATOR_RE, PROJECT_LOG_LONE_TERMINATOR_RE, PROJECT_LOG_SCOPES, PROJECT_LOG_TYPES, normalizeProjectLogLineEndings, splitProjectLogLines, } from './grammar.js';
16
17
  export const PROJECT_LOG_FILENAME = 'project-log.md';
17
18
  const SYNTHESIS_HEADING_PREFIX = '\n## End-of-run synthesis';
18
19
  /**
@@ -331,8 +332,13 @@ function releaseProjectLogLock(handle) {
331
332
  * `best-effort` is for work that cannot lose an entry — committing does not
332
333
  * rewrite the log — where holding the lock only keeps this process from racing
333
334
  * its own sibling into `.git/index.lock`.
335
+ *
336
+ * Exported so `synthesize` enters the same critical section rather than a
337
+ * second one of its own: its whole-file rewrite races an overlapping append
338
+ * exactly as two appends race each other, and two locks would serialize
339
+ * neither.
334
340
  */
335
- async function withProjectLogLock(logPath, overrides, policy, mutate) {
341
+ export async function withProjectLogLock(logPath, overrides, policy, mutate) {
336
342
  const handle = await acquireProjectLogLock(logPath, overrides);
337
343
  if (!handle.held && policy === 'required') {
338
344
  throw new Error(`Timed out waiting for the project log lock at ${handle.path}. Another writer is holding it; retry once it finishes.`);
@@ -794,6 +800,33 @@ export function findProjectLogEntryByIdempotencyKey(content, key, body) {
794
800
  }
795
801
  return undefined;
796
802
  }
803
+ /**
804
+ * Raised when an append that is not the completion seal targets a sealed log.
805
+ *
806
+ * This is a throw rather than a fourth `ProjectLogAppendResult` variant because
807
+ * every in-tree caller of `appendProjectLog` runs *before* the seal: the gate's
808
+ * partial-finalization receipt narrows the result's `status` into its own
809
+ * `'appended' | 'already-appended'` field, so widening the union would force a
810
+ * change on a caller that can never legitimately reach this state. Throwing
811
+ * refuses that caller too — the command layer maps this to
812
+ * `status: 'sealed'` with a non-zero exit, and any other caller fails loudly
813
+ * instead of silently appending past the seal.
814
+ */
815
+ export class ProjectLogSealedError extends Error {
816
+ logPath;
817
+ seal;
818
+ /**
819
+ * `action` names the refused mutation so `synthesize`, which rewrites rather
820
+ * than appends, reports what it actually declined. It defaults to `append`,
821
+ * which is the wording every existing caller and its pinned control expect.
822
+ */
823
+ constructor(logPath, seal, action = 'append') {
824
+ super(`No project-log ${action} may follow the completion seal (${seal.heading}) in ${logPath}.`);
825
+ this.name = 'ProjectLogSealedError';
826
+ this.logPath = logPath;
827
+ this.seal = seal;
828
+ }
829
+ }
797
830
  const DEFAULT_APPEND_DEPENDENCIES = {
798
831
  resolveActiveProject,
799
832
  resolveEffectiveConfig,
@@ -825,7 +858,7 @@ function validateSingleLine(value, option, maxLength = PROJECT_LOG_AREA_MAX_LENG
825
858
  if (!normalized) {
826
859
  throw new Error(`${option} is required and must be a non-empty single line.`);
827
860
  }
828
- if (/[\r\n]/.test(value ?? '')) {
861
+ if (PROJECT_LOG_LINE_TERMINATOR_RE.test(value ?? '')) {
829
862
  throw new Error(`${option} must be a single line without newline characters.`);
830
863
  }
831
864
  if (normalized.length > maxLength) {
@@ -846,16 +879,43 @@ function validateVersionNote(value) {
846
879
  }
847
880
  return validateSingleLine(value, '--version-note', Number.MAX_SAFE_INTEGER);
848
881
  }
882
+ /**
883
+ * Whether any line of `body` is a `## ` or `### ` marker the command owns.
884
+ *
885
+ * The split is `splitProjectLogLines` rather than `/\r?\n/` so validation
886
+ * enumerates exactly the LineTerminators a regular expression's multiline `^`
887
+ * anchors after. Splitting on fewer of them was the injection: a body reading
888
+ * `carrier<CR>## Injected` was one unbroken line here and so passed, while the
889
+ * section parser saw a real `## ` boundary and truncated the entries region,
890
+ * voiding an already-written completion seal.
891
+ */
849
892
  function containsCommandOwnedMarker(body) {
850
- return body
851
- .split(/\r?\n/)
852
- .some((line) => isProjectLogSectionMarker(line) || isProjectLogEntryMarker(line));
893
+ return splitProjectLogLines(body).some((line) => isProjectLogSectionMarker(line) || isProjectLogEntryMarker(line));
853
894
  }
854
895
  function validateEntry(input, date) {
855
- const body = input.body?.trim();
856
- if (!body) {
896
+ // The lone-terminator rule is tested against the *raw* body, before trimming.
897
+ // `String.prototype.trim` strips CR, LF, U+2028 and U+2029 as whitespace, so a
898
+ // body edge-terminated by one of them — `'ab\r'`, `'\rabc'` — never reached
899
+ // the test and appended with the terminator silently removed, while the error
900
+ // text said such a body is not accepted. Refusing it narrows what is accepted,
901
+ // which is the safe direction, and makes the message true.
902
+ const raw = input.body ?? '';
903
+ const supplied = raw.trim();
904
+ if (!supplied) {
857
905
  throw new Error('--body is required and must contain non-whitespace text.');
858
906
  }
907
+ // Parse leniently, write strictly: CRLF is accepted from the caller and stored
908
+ // as LF, so no log this command writes ever carries a terminator other than
909
+ // LF.
910
+ //
911
+ // The lone-terminator rule below is deliberately applied to `raw`: before
912
+ // trimming, and before normalization. Normalizing first would launder `\r\r\n` — a lone CR
913
+ // followed by a CRLF — into `\r\n`, which passes the rule and leaves a CR in
914
+ // the stored bytes, because one `replaceAll` pass cannot re-examine the CR it
915
+ // just exposed. Refusing at the door means every CR that reaches
916
+ // normalization is already part of a CRLF, and one pass is then exact.
917
+ const normalized = supplied.includes('\r\n');
918
+ const body = normalizeProjectLogLineEndings(supplied);
859
919
  const versionNote = validateVersionNote(input.versionNote);
860
920
  if (input.structural) {
861
921
  if (input.type !== undefined ||
@@ -863,7 +923,7 @@ function validateEntry(input, date) {
863
923
  input.area !== undefined) {
864
924
  throw new Error('--structural cannot be combined with judgment flags --type, --scope, or --area.');
865
925
  }
866
- if (/[\r\n]/.test(body)) {
926
+ if (PROJECT_LOG_LINE_TERMINATOR_RE.test(body)) {
867
927
  throw new Error('--body for structural entries must be one line without newline characters.');
868
928
  }
869
929
  if (containsCommandOwnedMarker(body)) {
@@ -875,6 +935,7 @@ function validateEntry(input, date) {
875
935
  heading: composeStructuralHeading({ date, producer, ref }),
876
936
  body,
877
937
  versionNote,
938
+ normalized,
878
939
  };
879
940
  }
880
941
  if (input.producer !== undefined || input.ref !== undefined) {
@@ -892,6 +953,12 @@ function validateEntry(input, date) {
892
953
  if (!PROJECT_LOG_SCOPES.includes(input.scope)) {
893
954
  throw new Error(`Invalid --scope '${input.scope}'; accepted values: ${PROJECT_LOG_SCOPES.join(' | ')}.`);
894
955
  }
956
+ // Tested against `raw`: before trimming, and before normalization. Trimming
957
+ // hides an edge terminator; normalizing first launders `\r\r\n` into
958
+ // `\r\n`, which passes this rule and leaves a carriage return in the bytes.
959
+ if (PROJECT_LOG_LONE_TERMINATOR_RE.test(raw)) {
960
+ throw new Error('--body for judgment entries must break lines with line feeds or CRLF; a lone carriage return and the U+2028 and U+2029 separators are not accepted.');
961
+ }
895
962
  if (containsCommandOwnedMarker(body)) {
896
963
  throw new Error('--body for judgment entries must not contain command-owned level-two or level-three Markdown headings.');
897
964
  }
@@ -905,6 +972,7 @@ function validateEntry(input, date) {
905
972
  }),
906
973
  body,
907
974
  versionNote,
975
+ normalized,
908
976
  };
909
977
  }
910
978
  /**
@@ -1000,30 +1068,114 @@ async function appendLockedProjectLog(input, dependencies, logPath) {
1000
1068
  }
1001
1069
  }
1002
1070
  const date = dependencies.now().toISOString().slice(0, 10);
1003
- const { heading, body, versionNote } = validateEntry(input, date);
1071
+ const { heading, body, versionNote, normalized } = validateEntry(input, date);
1004
1072
  const idempotencyKey = validateIdempotencyKey(input.idempotencyKey, body);
1005
1073
  let created = false;
1006
- if (logExists && idempotencyKey !== undefined) {
1007
- const existing = findProjectLogEntryByIdempotencyKey(await dependencies.readLog(logPath), idempotencyKey, body);
1008
- if (existing !== undefined) {
1009
- // Append-only order is preserved (DR-260714): a replay observes the
1010
- // entry it already wrote instead of writing a second one.
1011
- return {
1012
- status: 'already-appended',
1013
- logPath,
1014
- heading: existing,
1015
- created: false,
1016
- };
1074
+ if (logExists) {
1075
+ const content = await dependencies.readLog(logPath);
1076
+ // A log whose markers two readers resolve differently cannot answer
1077
+ // "is this sealed?", so nothing may be written onto it until a human
1078
+ // resolves it. This refusal is what keeps the LF-only section parser from
1079
+ // widening such a log: without it, a pre-existing `preamble<U+2028>##
1080
+ // Entries` stops parsing, reports unsealed, and accepts a second seal.
1081
+ const ambiguity = findProjectLogAmbiguity(content, logPath);
1082
+ if (ambiguity !== undefined) {
1083
+ throw new Error(ambiguity);
1084
+ }
1085
+ const seal = findProjectLogSeal(content);
1086
+ // The parser found no seal, but the raw text may still hold a seal heading
1087
+ // it cannot reach — a log with no `## Entries` section, or a seal below
1088
+ // another `## ` section. Writing onto such a log is the outcome that must
1089
+ // never happen, and reporting it unsealed would contradict what is plainly
1090
+ // in the file, so both branches below fail closed on it.
1091
+ //
1092
+ // Refusing is never weaker than the reading it replaces: the pre-fix code
1093
+ // answered these logs `already-appended` and wrote nothing when a key
1094
+ // happened to match, and appended past the seal when it did not.
1095
+ const unreachableSeal = seal === null ? findProjectLogSealHeadingLine(content) : undefined;
1096
+ // Read the requested identity back out of the composed heading, so the
1097
+ // comparison runs on the same normalized producer/ref the parser reads out
1098
+ // of the log.
1099
+ const requestsSeal = isProjectLogSealHeading(heading);
1100
+ // The seal routes on structure before the generic keyed short-circuit, and
1101
+ // is never suppressed by it. `idempotencyToken` matches any whole word
1102
+ // equal to the key *anywhere* in the log, so with the keyed scan first a
1103
+ // single ordinary entry whose prose happened to mention `oat-seal:<project>`
1104
+ // answered the completion seal with `already-appended` naming that entry —
1105
+ // no seal was written, the log stayed open to every later append, and the
1106
+ // completing agent saw success. Structural recognition cannot collide that
1107
+ // way, and the keyed fallback below is admitted only when it names a seal,
1108
+ // so an `already-appended` seal always reports a seal heading.
1109
+ if (requestsSeal) {
1110
+ if (seal !== null) {
1111
+ // The seal is idempotent by structure, not only by key: a log sealed
1112
+ // before the keyed convention carries no token to match, and a resumed
1113
+ // completion must still observe the existing seal rather than write a
1114
+ // second one.
1115
+ return {
1116
+ status: 'already-appended',
1117
+ logPath,
1118
+ heading: seal.heading,
1119
+ created: false,
1120
+ };
1121
+ }
1122
+ if (unreachableSeal !== undefined) {
1123
+ throw unreachableProjectLogSealError(logPath, content, unreachableSeal);
1124
+ }
1125
+ }
1126
+ else {
1127
+ // Recognizing an entry this key already wrote comes before the sealed
1128
+ // guard, and deliberately so: it appends nothing, and the entry it finds
1129
+ // necessarily predates any later seal. Refusing it would turn the gate's
1130
+ // idempotent recovery replay into a hard failure on every project that
1131
+ // has since been completed.
1132
+ if (idempotencyKey !== undefined) {
1133
+ const existing = findProjectLogEntryByIdempotencyKey(content, idempotencyKey, body);
1134
+ if (existing !== undefined) {
1135
+ // Append-only order is preserved (DR-260714): a replay observes the
1136
+ // entry it already wrote instead of writing a second one.
1137
+ return {
1138
+ status: 'already-appended',
1139
+ logPath,
1140
+ heading: existing,
1141
+ created: false,
1142
+ };
1143
+ }
1144
+ }
1145
+ if (seal !== null) {
1146
+ throw new ProjectLogSealedError(logPath, seal);
1147
+ }
1148
+ // The same refusal the seal path makes, on the same file. Leaving it out
1149
+ // of this branch is what let a log whose seal the parser could not reach
1150
+ // hard-fail the completion seal while still accepting ordinary content
1151
+ // past that seal — the two writers disagreeing about one file.
1152
+ if (unreachableSeal !== undefined) {
1153
+ throw unreachableProjectLogSealError(logPath, content, unreachableSeal);
1154
+ }
1017
1155
  }
1018
1156
  }
1019
1157
  if (!logExists) {
1020
1158
  const assetsRoot = await dependencies.resolveAssetsRoot();
1021
1159
  const template = await readFile(join(assetsRoot, 'templates', PROJECT_LOG_FILENAME), 'utf8');
1022
- await dependencies.writeLog(logPath, instantiateProjectLogTemplate(template, basename(dirname(logPath)), date));
1160
+ const instantiated = instantiateProjectLogTemplate(template, basename(dirname(logPath)), date);
1161
+ // The project directory's basename lands in the template's title line, so a
1162
+ // directory named `demo<U+2028>## Notes` would create a log this command
1163
+ // then refuses on its next append. A mutator must never write content it
1164
+ // would decline to read.
1165
+ if (containsAmbiguousProjectLogMarker(instantiated)) {
1166
+ throw new Error(`Project name ${basename(dirname(logPath))} would put a '## ' heading after a carriage return, U+2028, or U+2029 in the new log's title, leaving a file with two readings. Rename the project directory using line feeds only.`);
1167
+ }
1168
+ await dependencies.writeLog(logPath, instantiated);
1023
1169
  created = true;
1024
1170
  }
1025
1171
  await appendEntry(logPath, heading, body, versionNote, dependencies);
1026
- return { status: 'appended', logPath, heading, created };
1172
+ return {
1173
+ status: 'appended',
1174
+ logPath,
1175
+ heading,
1176
+ created,
1177
+ ...(normalized ? { normalizedLineEndings: true } : {}),
1178
+ };
1027
1179
  }
1028
1180
  async function samePath(left, right) {
1029
1181
  if (resolve(left) === resolve(right)) {
@@ -1169,12 +1321,35 @@ async function runAppendCommand(options, context, dependencies) {
1169
1321
  receiptPath = candidate;
1170
1322
  }
1171
1323
  }
1172
- const result = await appendProjectLog({
1173
- repoRoot,
1174
- home: context.home,
1175
- ...options,
1176
- body,
1177
- }, dependencies);
1324
+ let result;
1325
+ try {
1326
+ result = await appendProjectLog({
1327
+ repoRoot,
1328
+ home: context.home,
1329
+ ...options,
1330
+ body,
1331
+ }, dependencies);
1332
+ }
1333
+ catch (error) {
1334
+ if (!(error instanceof ProjectLogSealedError)) {
1335
+ throw error;
1336
+ }
1337
+ // A terminal refusal, reported in the same shape as the error path below
1338
+ // so a caller reading JSON can tell a sealed log from a malformed append.
1339
+ if (context.json) {
1340
+ context.logger.json({
1341
+ status: 'sealed',
1342
+ logPath: error.logPath,
1343
+ heading: error.seal.heading,
1344
+ message: error.message,
1345
+ });
1346
+ }
1347
+ else {
1348
+ context.logger.error(error.message);
1349
+ }
1350
+ process.exitCode = 1;
1351
+ return;
1352
+ }
1178
1353
  let commit;
1179
1354
  let receiptRemoved = false;
1180
1355
  if (options.commit && result.status !== 'skipped') {
@@ -1249,7 +1424,9 @@ Entry contract:
1249
1424
  High-value judgments may use Observation:, Impact:, and Recommendation: fields.
1250
1425
  Structural bodies are one line and reference artifacts by path instead of inlining them.
1251
1426
  Add --version-note for tool-related observations.
1252
- Pass --idempotency-key with --commit to finalize a gate partial-finalization receipt; a replay reports already-appended instead of duplicating the entry.
1427
+ Pass --idempotency-key so a replay reports already-appended instead of duplicating the entry; the key must appear in --body as its own word. Adding --commit also finalizes a gate partial-finalization receipt.
1428
+ The completion seal (--structural --producer oat-project-complete --ref seal) dedupes on its own, and every append carrying new content onto a sealed log is refused.
1429
+ A replay recognized by its own --idempotency-key still reports already-appended on a sealed log and writes nothing.
1253
1430
  Never record secret values (tokens, keys, signed URLs, or credentials); reference secrets by name or source.
1254
1431
  Prior entries are never edited or struck through. Append a new judgment entry that references and explains a correction.
1255
1432