agentera 3.0.0-dev.20 → 3.0.0-dev.21

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 (228) hide show
  1. package/README.md +19 -0
  2. package/bundle/CHANGELOG.md +63 -70
  3. package/bundle/README.md +17 -0
  4. package/bundle/UPGRADE.md +38 -0
  5. package/bundle/references/adapters/package-surface-characterization.md +7 -0
  6. package/bundle/references/artifacts/artifact-registry-interface-model.yaml +3 -3
  7. package/bundle/references/artifacts/state-storage-authority.yaml +1446 -0
  8. package/bundle/references/cli/agent-ready-state-contract.yaml +11 -39
  9. package/bundle/references/cli/audience-namespace-cli-migration.yaml +30 -15
  10. package/bundle/references/meta/documentation-inventory.md +1 -0
  11. package/bundle/skills/agentera/capabilities/audit/schemas/artifacts.yaml +18 -10
  12. package/bundle/skills/agentera/capabilities/audit/schemas/validation.yaml +10 -10
  13. package/bundle/skills/agentera/capabilities/build/schemas/artifacts.yaml +11 -6
  14. package/bundle/skills/agentera/capabilities/discuss/schemas/artifacts.yaml +3 -2
  15. package/bundle/skills/agentera/capabilities/document/schemas/artifacts.yaml +15 -8
  16. package/bundle/skills/agentera/capabilities/document/schemas/validation.yaml +13 -13
  17. package/bundle/skills/agentera/capabilities/orchestrate/schemas/artifacts.yaml +15 -9
  18. package/bundle/skills/agentera/references/contract.md +65 -23
  19. package/bundle/skills/agentera/schemas/artifacts/decisions.yaml +15 -10
  20. package/bundle/skills/agentera/schemas/artifacts/experiments.yaml +134 -3
  21. package/bundle/skills/agentera/schemas/artifacts/health.yaml +16 -10
  22. package/bundle/skills/agentera/schemas/artifacts/objective.yaml +18 -4
  23. package/bundle/skills/agentera/schemas/artifacts/plan.yaml +23 -1
  24. package/bundle/skills/agentera/schemas/artifacts/progress.yaml +16 -10
  25. package/bundle/skills/agentera/schemas/artifacts/todo.yaml +34 -0
  26. package/dist/capabilities/audit/instructions.js +1 -1
  27. package/dist/capabilities/audit/instructions.js.map +1 -1
  28. package/dist/capabilities/build/instructions.js +1 -1
  29. package/dist/capabilities/build/instructions.js.map +1 -1
  30. package/dist/capabilities/discuss/instructions.js +1 -1
  31. package/dist/capabilities/discuss/instructions.js.map +1 -1
  32. package/dist/capabilities/document/instructions.js +1 -1
  33. package/dist/capabilities/document/instructions.js.map +1 -1
  34. package/dist/capabilities/optimize/instructions.js +1 -1
  35. package/dist/capabilities/optimize/instructions.js.map +1 -1
  36. package/dist/capabilities/orchestrate/instructions.js +1 -1
  37. package/dist/capabilities/orchestrate/instructions.js.map +1 -1
  38. package/dist/capabilities/plan/instructions.js +1 -1
  39. package/dist/capabilities/plan/instructions.js.map +1 -1
  40. package/dist/capabilities/profile/instructions.js +1 -1
  41. package/dist/capabilities/profile/instructions.js.map +1 -1
  42. package/dist/capabilities/vision/instructions.js +1 -1
  43. package/dist/capabilities/vision/instructions.js.map +1 -1
  44. package/dist/cli/appContext.js +14 -6
  45. package/dist/cli/appContext.js.map +1 -1
  46. package/dist/cli/capabilityContext/bespoke.js +3 -1
  47. package/dist/cli/capabilityContext/bespoke.js.map +1 -1
  48. package/dist/cli/capabilityContext/build.js +5 -4
  49. package/dist/cli/capabilityContext/build.js.map +1 -1
  50. package/dist/cli/capabilityContext/closeout.js +2 -1
  51. package/dist/cli/capabilityContext/closeout.js.map +1 -1
  52. package/dist/cli/capabilityContext/contract.js +29 -0
  53. package/dist/cli/capabilityContext/contract.js.map +1 -1
  54. package/dist/cli/capabilityContext/evidence.js +21 -20
  55. package/dist/cli/capabilityContext/evidence.js.map +1 -1
  56. package/dist/cli/capabilityContext/orchestration.js +3 -2
  57. package/dist/cli/capabilityContext/orchestration.js.map +1 -1
  58. package/dist/cli/capabilityContext/progress.js +9 -4
  59. package/dist/cli/capabilityContext/progress.js.map +1 -1
  60. package/dist/cli/capabilityContext/slim.js +37 -6
  61. package/dist/cli/capabilityContext/slim.js.map +1 -1
  62. package/dist/cli/capabilityContext/startup.js +30 -6
  63. package/dist/cli/capabilityContext/startup.js.map +1 -1
  64. package/dist/cli/capabilityContext/types.js +14 -4
  65. package/dist/cli/capabilityContext/types.js.map +1 -1
  66. package/dist/cli/commands/backfill.js +194 -0
  67. package/dist/cli/commands/backfill.js.map +1 -0
  68. package/dist/cli/commands/compact.js +40 -11
  69. package/dist/cli/commands/compact.js.map +1 -1
  70. package/dist/cli/commands/durability.js +85 -0
  71. package/dist/cli/commands/durability.js.map +1 -0
  72. package/dist/cli/commands/migrate.js +677 -0
  73. package/dist/cli/commands/migrate.js.map +1 -0
  74. package/dist/cli/commands/prime/collectOrientationState.js +23 -0
  75. package/dist/cli/commands/prime/collectOrientationState.js.map +1 -1
  76. package/dist/cli/commands/prime/orientationOutput.js +4 -2
  77. package/dist/cli/commands/prime/orientationOutput.js.map +1 -1
  78. package/dist/cli/commands/query.js +20 -2
  79. package/dist/cli/commands/query.js.map +1 -1
  80. package/dist/cli/commands/schema.js +247 -69
  81. package/dist/cli/commands/schema.js.map +1 -1
  82. package/dist/cli/commands/state/decisions.js +28 -10
  83. package/dist/cli/commands/state/decisions.js.map +1 -1
  84. package/dist/cli/commands/state/experimentRecords.js +156 -0
  85. package/dist/cli/commands/state/experimentRecords.js.map +1 -0
  86. package/dist/cli/commands/state/get.js +141 -0
  87. package/dist/cli/commands/state/get.js.map +1 -0
  88. package/dist/cli/commands/state/index.js +2 -2
  89. package/dist/cli/commands/state/index.js.map +1 -1
  90. package/dist/cli/commands/state/list.js +159 -0
  91. package/dist/cli/commands/state/list.js.map +1 -0
  92. package/dist/cli/commands/state/plan.js +146 -41
  93. package/dist/cli/commands/state/plan.js.map +1 -1
  94. package/dist/cli/commands/state/planTasks.js +164 -0
  95. package/dist/cli/commands/state/planTasks.js.map +1 -0
  96. package/dist/cli/commands/state/plans.js +154 -0
  97. package/dist/cli/commands/state/plans.js.map +1 -0
  98. package/dist/cli/commands/state/write.js +8 -3
  99. package/dist/cli/commands/state/write.js.map +1 -1
  100. package/dist/cli/dispatch/check.js +71 -4
  101. package/dist/cli/dispatch/check.js.map +1 -1
  102. package/dist/cli/dispatch/index.js +48 -30
  103. package/dist/cli/dispatch/index.js.map +1 -1
  104. package/dist/cli/dispatch/lifecycle.js +4 -4
  105. package/dist/cli/dispatch/lifecycle.js.map +1 -1
  106. package/dist/cli/dispatch/state.js +23 -3
  107. package/dist/cli/dispatch/state.js.map +1 -1
  108. package/dist/cli/errors.js +13 -14
  109. package/dist/cli/errors.js.map +1 -1
  110. package/dist/cli/help.js +162 -3
  111. package/dist/cli/help.js.map +1 -1
  112. package/dist/cli/orientation.js +146 -34
  113. package/dist/cli/orientation.js.map +1 -1
  114. package/dist/cli/planArtifacts.js +31 -2
  115. package/dist/cli/planArtifacts.js.map +1 -1
  116. package/dist/cli/prime-blob.js +1 -0
  117. package/dist/cli/prime-blob.js.map +1 -1
  118. package/dist/cli/startupCompletenessContract.js +3 -2
  119. package/dist/cli/startupCompletenessContract.js.map +1 -1
  120. package/dist/cli/stateQuery.js +5 -3
  121. package/dist/cli/stateQuery.js.map +1 -1
  122. package/dist/core/text.js +11 -0
  123. package/dist/core/text.js.map +1 -0
  124. package/dist/hooks/compaction/apply.js +224 -46
  125. package/dist/hooks/compaction/apply.js.map +1 -1
  126. package/dist/hooks/compaction/dryRun.js +2 -0
  127. package/dist/hooks/compaction/dryRun.js.map +1 -1
  128. package/dist/hooks/compaction/index.js +4 -4
  129. package/dist/hooks/compaction/index.js.map +1 -1
  130. package/dist/hooks/compaction/parse.js +73 -5
  131. package/dist/hooks/compaction/parse.js.map +1 -1
  132. package/dist/hooks/compaction/retention.js +22 -21
  133. package/dist/hooks/compaction/retention.js.map +1 -1
  134. package/dist/hooks/compaction/status.js +116 -23
  135. package/dist/hooks/compaction/status.js.map +1 -1
  136. package/dist/hooks/sessionStart.js +39 -7
  137. package/dist/hooks/sessionStart.js.map +1 -1
  138. package/dist/hooks/validateArtifact/index.js +2 -2
  139. package/dist/hooks/validateArtifact/index.js.map +1 -1
  140. package/dist/hooks/validateArtifact/markdown.js +9 -9
  141. package/dist/hooks/validateArtifact/markdown.js.map +1 -1
  142. package/dist/hooks/validateArtifact/traversal.js +11 -9
  143. package/dist/hooks/validateArtifact/traversal.js.map +1 -1
  144. package/dist/registries/artifactRegistry.js +147 -44
  145. package/dist/registries/artifactRegistry.js.map +1 -1
  146. package/dist/release/releaseMetadata.js +95 -1
  147. package/dist/release/releaseMetadata.js.map +1 -1
  148. package/dist/state/archiveDiscovery.js +589 -0
  149. package/dist/state/archiveDiscovery.js.map +1 -0
  150. package/dist/state/archivePublication.js +225 -0
  151. package/dist/state/archivePublication.js.map +1 -0
  152. package/dist/state/archiveRecovery.js +238 -0
  153. package/dist/state/archiveRecovery.js.map +1 -0
  154. package/dist/state/archiveReplay.js +54 -0
  155. package/dist/state/archiveReplay.js.map +1 -0
  156. package/dist/state/decisionOverlay.js +255 -0
  157. package/dist/state/decisionOverlay.js.map +1 -0
  158. package/dist/state/directRetrieval.js +322 -0
  159. package/dist/state/directRetrieval.js.map +1 -0
  160. package/dist/state/durability.js +451 -0
  161. package/dist/state/durability.js.map +1 -0
  162. package/dist/state/experimentArchive.js +64 -0
  163. package/dist/state/experimentArchive.js.map +1 -0
  164. package/dist/state/experimentIdentity.js +191 -0
  165. package/dist/state/experimentIdentity.js.map +1 -0
  166. package/dist/state/experimentRetrieval.js +447 -0
  167. package/dist/state/experimentRetrieval.js.map +1 -0
  168. package/dist/state/gitBackfill.js +561 -0
  169. package/dist/state/gitBackfill.js.map +1 -0
  170. package/dist/state/gitBackfillAuthority.js +141 -0
  171. package/dist/state/gitBackfillAuthority.js.map +1 -0
  172. package/dist/state/gitBackfillHistory.js +278 -0
  173. package/dist/state/gitBackfillHistory.js.map +1 -0
  174. package/dist/state/gitBackfillOutput.js +209 -0
  175. package/dist/state/gitBackfillOutput.js.map +1 -0
  176. package/dist/state/healthRepair.js +70 -0
  177. package/dist/state/healthRepair.js.map +1 -0
  178. package/dist/state/legacyIdentity.js +48 -0
  179. package/dist/state/legacyIdentity.js.map +1 -0
  180. package/dist/state/legacyMigration.js +516 -0
  181. package/dist/state/legacyMigration.js.map +1 -0
  182. package/dist/state/legacyMigrationParser.js +489 -0
  183. package/dist/state/legacyMigrationParser.js.map +1 -0
  184. package/dist/state/listAccounting.js +55 -0
  185. package/dist/state/listAccounting.js.map +1 -0
  186. package/dist/state/listClassification.js +18 -0
  187. package/dist/state/listClassification.js.map +1 -0
  188. package/dist/state/listRetrieval.js +782 -0
  189. package/dist/state/listRetrieval.js.map +1 -0
  190. package/dist/state/migrationAuthority.js +413 -0
  191. package/dist/state/migrationAuthority.js.map +1 -0
  192. package/dist/state/migrationEnrichment.js +35 -0
  193. package/dist/state/migrationEnrichment.js.map +1 -0
  194. package/dist/state/planIdentity.js +45 -0
  195. package/dist/state/planIdentity.js.map +1 -0
  196. package/dist/state/planRetrieval.js +314 -0
  197. package/dist/state/planRetrieval.js.map +1 -0
  198. package/dist/state/planTaskRetrieval.js +285 -0
  199. package/dist/state/planTaskRetrieval.js.map +1 -0
  200. package/dist/state/projectionPolicy.js +184 -0
  201. package/dist/state/projectionPolicy.js.map +1 -0
  202. package/dist/state/retrievalAuthority.js +326 -0
  203. package/dist/state/retrievalAuthority.js.map +1 -0
  204. package/dist/state/startupProjection.js +498 -0
  205. package/dist/state/startupProjection.js.map +1 -0
  206. package/dist/state/write/experimentPublication.js +193 -0
  207. package/dist/state/write/experimentPublication.js.map +1 -0
  208. package/dist/state/write/explain.js +28 -14
  209. package/dist/state/write/explain.js.map +1 -1
  210. package/dist/state/write/helpers.js +8 -0
  211. package/dist/state/write/helpers.js.map +1 -0
  212. package/dist/state/write/index.js +1 -0
  213. package/dist/state/write/index.js.map +1 -1
  214. package/dist/state/write/lock.js +24 -1
  215. package/dist/state/write/lock.js.map +1 -1
  216. package/dist/state/write/mutation.js +190 -0
  217. package/dist/state/write/mutation.js.map +1 -0
  218. package/dist/state/write/operations.js +36 -2
  219. package/dist/state/write/operations.js.map +1 -1
  220. package/dist/state/write/planPublication.js +172 -0
  221. package/dist/state/write/planPublication.js.map +1 -0
  222. package/dist/state/write/transaction.js +238 -225
  223. package/dist/state/write/transaction.js.map +1 -1
  224. package/dist/upgrade/migrateArtifactsV2ToV3.js +2 -2
  225. package/dist/upgrade/migrateArtifactsV2ToV3.js.map +1 -1
  226. package/dist/validate/crossCapability.js +9 -6
  227. package/dist/validate/crossCapability.js.map +1 -1
  228. package/package.json +2 -2
@@ -1,17 +1,24 @@
1
- import { createHash } from "node:crypto";
1
+ import { createHash, randomUUID } from "node:crypto";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { isDeepStrictEqual } from "node:util";
5
5
  import { splitLinesKeepEnds, unifiedDiff } from "../../core/difflib.js";
6
6
  import { dumpYamlMapping, loadYamlMapping } from "../../core/yaml.js";
7
- import { lintFullArtifactPayload } from "../../cli/commands/lint.js";
8
7
  import { compactYamlFile, decisionProtectedOverflowCount, } from "../../hooks/compaction/index.js";
9
8
  import { assertRealpathBoundary, loadArtifactRegistry, resolveArtifactPath, } from "../../registries/artifactRegistry.js";
10
- import { acquireWriterLock } from "./lock.js";
9
+ import { withStateMutation, } from "./mutation.js";
11
10
  import { localDate, localTimestamp, nextEntryNumber, nextTaskNumber } from "./assign.js";
12
11
  import { reject } from "./errors.js";
12
+ import { findByNumber, schemaViolation } from "./helpers.js";
13
13
  import { validateArtifactBytes } from "./validate.js";
14
+ import { planEvaluationViolations, validatePlanCreateInput, validatePlanPublicationCandidate, } from "./planPublication.js";
14
15
  import { dependencyReadyTasks } from "../../cli/capabilityContext/planState.js";
16
+ import { publishImmutableFile, } from "../archivePublication.js";
17
+ import { findArchivedReplay, isExactArchiveReplay, recoverArchivedEntry, } from "../archiveReplay.js";
18
+ import { discoverNumberedArchives } from "../archiveDiscovery.js";
19
+ import { hydrateDecisionRecords, updateDecisionOverlay, } from "../decisionOverlay.js";
20
+ import { repairHealthDuplicates, repairHealthProjectionBytes } from "../healthRepair.js";
21
+ import { executeExperimentPublication } from "./experimentPublication.js";
15
22
  function mapping(value) {
16
23
  return value && typeof value === "object" && !Array.isArray(value)
17
24
  ? value
@@ -22,25 +29,6 @@ function array(doc, key) {
22
29
  ? doc[key].filter((v) => Boolean(v && typeof v === "object" && !Array.isArray(v)))
23
30
  : [];
24
31
  }
25
- function setNested(target, field, value) {
26
- const parts = field.split(".");
27
- let cursor = target;
28
- for (const part of parts.slice(0, -1)) {
29
- if (!cursor[part] || typeof cursor[part] !== "object" || Array.isArray(cursor[part]))
30
- cursor[part] = {};
31
- cursor = cursor[part];
32
- }
33
- cursor[parts.at(-1)] = value;
34
- }
35
- function project(entry, payload) {
36
- const result = {};
37
- for (const [key, expected] of Object.entries(payload)) {
38
- const actual = mappingPath(entry, key);
39
- if (expected !== undefined)
40
- setNested(result, key, actual);
41
- }
42
- return result;
43
- }
44
32
  function mappingPath(entry, field) {
45
33
  let value = entry;
46
34
  for (const part of field.split(".")) {
@@ -50,9 +38,6 @@ function mappingPath(entry, field) {
50
38
  }
51
39
  return value;
52
40
  }
53
- function exactReplay(entry, payload) {
54
- return Boolean(entry && isDeepStrictEqual(project(entry, payload), payload));
55
- }
56
41
  function readExisting(target) {
57
42
  if (!fs.existsSync(target))
58
43
  return { doc: {}, bytes: "" };
@@ -116,41 +101,28 @@ function normalizedDecisionPayload(values) {
116
101
  ];
117
102
  return payload;
118
103
  }
119
- function schemaViolation(violations) {
120
- reject({ class: "schema_violation", message: violations[0], violations });
121
- }
122
- function validatePlanPublicationCandidate(bytes) {
123
- const lint = lintFullArtifactPayload("plan", bytes);
124
- const lintViolations = lint.checks
125
- .filter((check) => check.status === "fail")
126
- .map((check) => `strict prose lint ${check.name}: ${check.detail}; action: ${check.action}`);
127
- const schemaViolations = validateArtifactBytes("plan", bytes).map((violation) => `schema validation: ${violation}`);
128
- const violations = [...lintViolations, ...schemaViolations];
129
- if (violations.length === 0)
130
- return;
131
- reject({
132
- class: "schema_violation",
133
- message: "plan publication candidate failed strict prose lint or schema validation; correct the reported violations and retry",
134
- violations,
135
- syntax: "agentera state plan create --input plan.yaml --format json",
136
- example: "agentera state plan create --input plan.yaml --format json",
137
- });
138
- }
139
- function findByNumber(entries, number) {
140
- return entries.find((entry) => Number(entry.number) === number);
141
- }
142
- function mutateCandidate(req, doc) {
104
+ function mutateCandidate(req, doc, recovered) {
143
105
  const candidate = structuredClone(doc);
144
106
  if (req.artifact === "progress") {
145
107
  const payload = req.callerPayload;
146
108
  const entries = array(candidate, "cycles");
147
- if (exactReplay(entries[0], payload))
109
+ const replay = entries.find((entry) => isExactArchiveReplay(entry, payload));
110
+ if (replay)
111
+ return {
112
+ candidate,
113
+ written: replay,
114
+ assigned: { number: replay.number, timestamp: replay.timestamp },
115
+ replay: true,
116
+ };
117
+ if (recovered) {
118
+ recoverArchivedEntry(candidate, "cycles", entries, recovered, true);
148
119
  return {
149
120
  candidate,
150
- written: entries[0],
151
- assigned: { number: entries[0].number, timestamp: entries[0].timestamp },
121
+ written: recovered,
122
+ assigned: { number: recovered.number, timestamp: recovered.timestamp },
152
123
  replay: true,
153
124
  };
125
+ }
154
126
  const entry = buildProgress(candidate, req.values);
155
127
  candidate.cycles = [entry, ...entries];
156
128
  return {
@@ -163,8 +135,9 @@ function mutateCandidate(req, doc) {
163
135
  if (req.artifact === "decisions" && req.spec.verb === "append") {
164
136
  const entries = array(candidate, "decisions");
165
137
  const payload = normalizedDecisionPayload(req.callerPayload);
166
- if (exactReplay(entries.at(-1), payload)) {
167
- const entry = entries.at(-1);
138
+ const replay = entries.find((entry) => isExactArchiveReplay(entry, payload));
139
+ if (replay) {
140
+ const entry = replay;
168
141
  return {
169
142
  candidate,
170
143
  written: entry,
@@ -172,14 +145,17 @@ function mutateCandidate(req, doc) {
172
145
  replay: true,
173
146
  };
174
147
  }
148
+ if (recovered) {
149
+ recoverArchivedEntry(candidate, "decisions", entries, recovered, false);
150
+ return {
151
+ candidate,
152
+ written: recovered,
153
+ assigned: { number: recovered.number, date: recovered.date },
154
+ replay: true,
155
+ };
156
+ }
175
157
  const entry = buildDecision(candidate, req.values);
176
158
  candidate.decisions = [...entries, entry];
177
- if (decisionProtectedOverflowCount(candidate.decisions, Array.isArray(candidate.archive) ? candidate.archive : []) > 0) {
178
- reject({
179
- class: "conflict",
180
- message: "decision append would exceed protected review capacity; review decision satisfaction first",
181
- });
182
- }
183
159
  return {
184
160
  candidate,
185
161
  written: entry,
@@ -187,40 +163,17 @@ function mutateCandidate(req, doc) {
187
163
  replay: false,
188
164
  };
189
165
  }
190
- if (req.artifact === "decisions") {
191
- const entries = array(candidate, "decisions");
192
- const archived = array(candidate, "archive");
193
- const number = Number(req.values.number);
194
- const entry = findByNumber(entries, number) ?? findByNumber(archived, number);
195
- if (!entry)
196
- reject({
197
- class: "unsupported_target",
198
- message: `no decision with number ${number}; highest is ${Math.max(0, ...entries.map((e) => Number(e.number) || 0), ...archived.map((e) => Number(e.number) || 0))}`,
199
- });
200
- const satisfaction = mapping(req.values.satisfaction);
201
- if (satisfaction.state === "provisionally_satisfied" &&
202
- !String(satisfaction.evidence ?? "").trim()) {
203
- reject({
204
- class: "schema_violation",
205
- message: "provisionally_satisfied requires non-empty --satisfaction-evidence",
206
- });
207
- }
208
- if (satisfaction.state === "user_confirmed_satisfied") {
209
- const confirmation = mapping(satisfaction.user_confirmation);
210
- if (!String(confirmation.confirmed_by ?? "").trim() ||
211
- !String(confirmation.confirmed_at ?? "").trim()) {
212
- reject({
213
- class: "schema_violation",
214
- message: "user_confirmed_satisfied requires --confirmed-by and --confirmed-at",
215
- });
216
- }
217
- }
218
- if (isDeepStrictEqual(entry.satisfaction, satisfaction))
219
- return { candidate, written: entry, assigned: { number }, replay: true };
220
- entry.satisfaction = satisfaction;
221
- return { candidate, written: entry, assigned: { number }, replay: false };
222
- }
223
166
  if (req.artifact === "health") {
167
+ if (req.spec.verb === "repair") {
168
+ if (!req.force)
169
+ reject({ class: "conflict", message: "health repair requires --force; use --dry-run to preview the edit" });
170
+ const number = Number(req.values.number);
171
+ const keep = req.values.keep === "last" ? "last" : "first";
172
+ const repaired = repairHealthDuplicates(candidate, number, keep);
173
+ if (repaired.removed === 0)
174
+ reject({ class: "unsupported_target", message: `health audit ${number} has no duplicate current-history rows to repair` });
175
+ return { candidate: repaired.candidate, written: { number, removed_rows: repaired.removed, retained: keep }, assigned: { number, removed_rows: repaired.removed }, replay: false };
176
+ }
224
177
  if (req.input && ("audits" in req.input || "archive" in req.input)) {
225
178
  reject({
226
179
  class: "schema_violation",
@@ -230,8 +183,9 @@ function mutateCandidate(req, doc) {
230
183
  }
231
184
  const entries = array(candidate, "audits");
232
185
  const payload = req.callerPayload;
233
- if (exactReplay(entries.at(-1), payload)) {
234
- const entry = entries.at(-1);
186
+ const replay = entries.find((entry) => isExactArchiveReplay(entry, payload));
187
+ if (replay) {
188
+ const entry = replay;
235
189
  return {
236
190
  candidate,
237
191
  written: entry,
@@ -239,6 +193,15 @@ function mutateCandidate(req, doc) {
239
193
  replay: true,
240
194
  };
241
195
  }
196
+ if (recovered) {
197
+ recoverArchivedEntry(candidate, "audits", entries, recovered, false);
198
+ return {
199
+ candidate,
200
+ written: recovered,
201
+ assigned: { number: recovered.number, date: recovered.date },
202
+ replay: true,
203
+ };
204
+ }
242
205
  const entry = {
243
206
  number: nextEntryNumber(candidate, "audits"),
244
207
  date: req.input?.date ?? localDate(),
@@ -257,7 +220,7 @@ function mutateCandidate(req, doc) {
257
220
  const sameName = tasks.find((task) => task.name === req.values.name);
258
221
  const payload = req.callerPayload;
259
222
  if (sameName) {
260
- if (exactReplay(sameName, payload))
223
+ if (isExactArchiveReplay(sameName, payload))
261
224
  return {
262
225
  candidate,
263
226
  written: sameName,
@@ -416,7 +379,9 @@ function activeKey(artifact) {
416
379
  ? "decisions"
417
380
  : artifact === "health"
418
381
  ? "audits"
419
- : "tasks";
382
+ : artifact === "experiments"
383
+ ? "experiments"
384
+ : "tasks";
420
385
  }
421
386
  function stateSlice(artifact, doc, archivePath) {
422
387
  if (artifact === "plan") {
@@ -435,6 +400,7 @@ function stateSlice(artifact, doc, archivePath) {
435
400
  active_count: active.length,
436
401
  archive_count: archive.length,
437
402
  next_number: nextEntryNumber(doc, activeKey(artifact)),
403
+ ...(archivePath ? { archive_path: archivePath } : {}),
438
404
  };
439
405
  }
440
406
  function diffText(before, after, target) {
@@ -468,6 +434,13 @@ function archivePathFor(projectRoot, target, doc, bytes) {
468
434
  assertWriterBoundary(projectRoot, archivePath, "plan archive");
469
435
  return archivePath;
470
436
  }
437
+ function decisionRecordForUpdate(projectRoot, doc, number) {
438
+ const current = findByNumber(array(doc, "decisions"), number) ?? findByNumber(array(doc, "archive"), number);
439
+ if (current)
440
+ return current;
441
+ const archived = discoverNumberedArchives(projectRoot).entries.find((entry) => entry.artifactId === "decisions" && entry.entryNumber === number);
442
+ return archived?.record ?? null;
443
+ }
471
444
  function latestArchivePath(archiveDirectory) {
472
445
  if (!fs.existsSync(archiveDirectory))
473
446
  return null;
@@ -478,26 +451,6 @@ function latestArchivePath(archiveDirectory) {
478
451
  .at(-1);
479
452
  return latest ? path.join(archiveDirectory, latest) : null;
480
453
  }
481
- function validatePlanCreateInput(input) {
482
- const tasks = array(input, "tasks");
483
- const numbers = tasks.map((task) => Number(task.number));
484
- const expected = tasks.map((_, index) => index + 1);
485
- if (!isDeepStrictEqual(numbers, expected)) {
486
- reject({
487
- class: "schema_violation",
488
- message: "plan create task numbers must be unique and sequential starting from 1",
489
- violations: ["PV1: task numbers must equal 1..N"],
490
- });
491
- }
492
- if (mapping(input.header).status === "complete" &&
493
- tasks.some((task) => task.status !== "complete")) {
494
- reject({
495
- class: "schema_violation",
496
- message: "a complete plan cannot contain incomplete tasks",
497
- violations: ["header.status complete requires every task complete"],
498
- });
499
- }
500
- }
501
454
  function canonicalPlanDocumentForWrite(doc) {
502
455
  const header = mapping(doc.header);
503
456
  const status = String(header.status ?? "");
@@ -525,48 +478,15 @@ function fsyncDirectory(directory) {
525
478
  }
526
479
  }
527
480
  function ensureArchive(archivePath, bytes) {
528
- const directory = path.dirname(archivePath);
529
- fs.mkdirSync(directory, { recursive: true });
530
- const stage = path.join(directory, `.${path.basename(archivePath)}.writer.${process.pid}.${Date.now()}.tmp`);
531
- let stageCreated = false;
532
- let fd;
533
- try {
534
- fd = fs.openSync(stage, "wx");
535
- stageCreated = true;
536
- fs.writeFileSync(fd, bytes, "utf8");
537
- fs.fsyncSync(fd);
538
- fs.closeSync(fd);
539
- fd = undefined;
540
- try {
541
- fs.linkSync(stage, archivePath);
542
- fsyncDirectory(directory);
543
- return true;
544
- }
545
- catch (error) {
546
- if (error.code !== "EEXIST")
547
- throw error;
548
- if (fs.readFileSync(archivePath, "utf8") !== bytes) {
481
+ return publishImmutableFile(archivePath, bytes, {
482
+ onExisting: () => {
483
+ if (fs.readFileSync(archivePath, "utf8") !== bytes)
549
484
  reject({
550
485
  class: "conflict",
551
486
  message: `archive path '${archivePath}' already exists with different content; historical archives are immutable`,
552
487
  });
553
- }
554
- return false;
555
- }
556
- }
557
- finally {
558
- if (fd !== undefined)
559
- fs.closeSync(fd);
560
- if (stageCreated) {
561
- try {
562
- fs.unlinkSync(stage);
563
- fsyncDirectory(directory);
564
- }
565
- catch {
566
- // A published archive is authoritative; a leftover private stage is ignored on retry.
567
- }
568
- }
569
- }
488
+ },
489
+ });
570
490
  }
571
491
  function publishStagedPlan(stage, target) {
572
492
  fs.renameSync(stage, target);
@@ -605,10 +525,13 @@ function planLifecycle(req, target, existing, legacyLifecycle = false) {
605
525
  message: `active plan "${mapping(existing.doc.header).title ?? existing.doc.title ?? "untitled"}" has status ${status || "unknown"} or incomplete tasks; archiving would discard incomplete work`,
606
526
  syntax: "agentera state plan archive --force",
607
527
  });
608
- const archiveDoc = canonicalPlanDocumentForArchive(existing.doc);
609
- const archiveBytes = dumpYamlMapping(archiveDoc);
528
+ const preserveExactBytes = req.force && !legacyLifecycle;
529
+ const archiveDoc = preserveExactBytes ? existing.doc : canonicalPlanDocumentForArchive(existing.doc);
530
+ const archiveBytes = preserveExactBytes ? existing.bytes : dumpYamlMapping(archiveDoc);
610
531
  const archivePath = archivePathFor(req.projectRoot, target, archiveDoc, archiveBytes);
611
- validatePlanPublicationCandidate(archiveBytes);
532
+ const predecessorValidation = validatePlanPublicationCandidate(archiveBytes, {
533
+ allowHistoricalBudgetOverflow: req.force,
534
+ });
612
535
  if (!req.dryRun) {
613
536
  // Archive creation and current-plan removal cannot be one filesystem transaction.
614
537
  // The durable archive is published first; retries remove the current plan only after
@@ -621,6 +544,7 @@ function planLifecycle(req, target, existing, legacyLifecycle = false) {
621
544
  diff: diffText(existing.bytes, "", target),
622
545
  before: existing.doc,
623
546
  after: {},
547
+ validationDiagnostics: predecessorValidation.diagnostics,
624
548
  });
625
549
  }
626
550
  const input = structuredClone(req.input ?? {});
@@ -634,10 +558,23 @@ function planLifecycle(req, target, existing, legacyLifecycle = false) {
634
558
  const oldStatus = String(mapping(existing.doc.header).status ?? existing.doc.status ?? "");
635
559
  const existingWithoutLineage = structuredClone(existing.doc);
636
560
  delete existingWithoutLineage.previous_plan_archived;
561
+ const existingHeader = mapping(existingWithoutLineage.header);
562
+ delete existingHeader.id;
563
+ existingWithoutLineage.header = existingHeader;
637
564
  const sameWithoutLineage = isDeepStrictEqual(existingWithoutLineage, input);
638
565
  if (sameWithoutLineage && !legacyLifecycle) {
639
- validatePlanPublicationCandidate(dumpYamlMapping(existing.doc));
640
- return envelope(req, target, existing.doc, mapping(existing.doc.header), {}, true, null, req.dryRun ? { diff: "", before: existing.doc, after: existing.doc } : {});
566
+ const predecessorBytes = req.force ? existing.bytes : dumpYamlMapping(existing.doc);
567
+ const predecessorValidation = validatePlanPublicationCandidate(predecessorBytes, {
568
+ allowHistoricalBudgetOverflow: req.force,
569
+ });
570
+ return envelope(req, target, existing.doc, mapping(existing.doc.header), {}, true, null, req.dryRun
571
+ ? {
572
+ diff: "",
573
+ before: existing.doc,
574
+ after: existing.doc,
575
+ validationDiagnostics: predecessorValidation.diagnostics,
576
+ }
577
+ : { validationDiagnostics: predecessorValidation.diagnostics });
641
578
  }
642
579
  if (!sameWithoutLineage) {
643
580
  if (oldStatus !== "complete" && !req.force)
@@ -645,14 +582,16 @@ function planLifecycle(req, target, existing, legacyLifecycle = false) {
645
582
  class: "conflict",
646
583
  message: `active plan "${mapping(existing.doc.header).title ?? existing.doc.title ?? "untitled"}" has status ${oldStatus || "unknown"}; replacing it would discard incomplete work`,
647
584
  });
648
- predecessorBytes = dumpYamlMapping(existing.doc);
585
+ predecessorBytes = req.force && !legacyLifecycle ? existing.bytes : dumpYamlMapping(existing.doc);
649
586
  archivePath = archivePathFor(req.projectRoot, target, existing.doc, predecessorBytes);
650
587
  input.previous_plan_archived = path.relative(req.projectRoot, archivePath);
651
588
  }
652
589
  }
590
+ input.header = { ...mapping(input.header), id: `plan:${randomUUID()}` };
653
591
  const bytes = dumpYamlMapping(input);
654
- if (predecessorBytes)
655
- validatePlanPublicationCandidate(predecessorBytes);
592
+ const predecessorValidation = predecessorBytes
593
+ ? validatePlanPublicationCandidate(predecessorBytes, { allowHistoricalBudgetOverflow: req.force })
594
+ : { diagnostics: [] };
656
595
  validatePlanPublicationCandidate(bytes);
657
596
  const finalDoc = loadYamlMapping(bytes);
658
597
  if (!req.dryRun) {
@@ -679,6 +618,7 @@ function planLifecycle(req, target, existing, legacyLifecycle = false) {
679
618
  diff: diffText(existing.bytes, bytes, target),
680
619
  before: existing.doc,
681
620
  after: finalDoc,
621
+ validationDiagnostics: predecessorValidation.diagnostics,
682
622
  });
683
623
  }
684
624
  function envelope(req, target, finalDoc, written, assigned, replay, compaction, extra = {}) {
@@ -697,9 +637,15 @@ function envelope(req, target, finalDoc, written, assigned, replay, compaction,
697
637
  assigned,
698
638
  written,
699
639
  state: stateSlice(req.artifact, finalDoc, extra.archivePath),
700
- validation: { status: "pass", violations: [] },
640
+ validation: {
641
+ status: "pass",
642
+ violations: [],
643
+ ...(extra.validationDiagnostics && extra.validationDiagnostics.length > 0
644
+ ? { diagnostics: extra.validationDiagnostics }
645
+ : {}),
646
+ },
701
647
  compaction: compaction && req.artifact === "decisions"
702
- ? { ...compaction, protected_overflow_count: 0 }
648
+ ? { ...compaction, protected_overflow_count: extra.protectedOverflowCount ?? 0 }
703
649
  : compaction,
704
650
  };
705
651
  if (req.dryRun && extra.diff !== undefined) {
@@ -709,79 +655,146 @@ function envelope(req, target, finalDoc, written, assigned, replay, compaction,
709
655
  }
710
656
  return result;
711
657
  }
712
- export function executeStateWrite(req) {
658
+ export function executeStateWrite(req, options = {}) {
713
659
  assertWriterBoundary(req.projectRoot, path.join(req.projectRoot, ".agentera"), "writer lock");
714
660
  const planPublication = req.artifact === "plan" && ["archive", "create"].includes(req.spec.verb);
715
- const lock = req.dryRun && planPublication ? null : acquireWriterLock(req.projectRoot);
661
+ if (req.dryRun && planPublication)
662
+ return executeStateWriteUnlocked(req);
663
+ return withStateMutation(req.projectRoot, (transaction) => executeStateWriteUnlocked(req, transaction), options);
664
+ }
665
+ function executeStateWriteUnlocked(req, transaction) {
666
+ if (req.artifact === "experiments" && req.spec.verb === "publish")
667
+ return executeExperimentPublication(req, transaction);
668
+ const record = loadArtifactRegistry().get(req.artifact);
669
+ if (!record)
670
+ throw new Error(`artifact '${req.artifact}' is not registered`);
671
+ let target;
716
672
  try {
717
- const record = loadArtifactRegistry().get(req.artifact);
718
- if (!record)
719
- throw new Error(`artifact '${req.artifact}' is not registered`);
720
- let target;
721
- try {
722
- target = resolveArtifactPath(record, req.projectRoot, { strictWrite: true });
723
- }
724
- catch (error) {
725
- const message = error.message;
726
- if (message.startsWith("artifact '"))
727
- reject({ class: "unsupported_target", message });
728
- throw error;
673
+ target = resolveArtifactPath(record, req.projectRoot, { strictWrite: true });
674
+ }
675
+ catch (error) {
676
+ const message = error.message;
677
+ if (message.startsWith("artifact '"))
678
+ reject({ class: "unsupported_target", message });
679
+ throw error;
680
+ }
681
+ const existing = readExisting(target);
682
+ const legacyLifecycle = req.artifact === "plan" && ["active", "completed"].includes(String(mapping(existing.doc.header).status ?? ""));
683
+ if (legacyLifecycle)
684
+ existing.doc = canonicalPlanDocumentForWrite(existing.doc);
685
+ if (existing.bytes) {
686
+ const validationBytes = req.artifact === "plan" ? dumpYamlMapping(existing.doc) : existing.bytes;
687
+ const violations = validateArtifactBytes(req.artifact, validationBytes);
688
+ if (violations.length) {
689
+ throw new Error(`existing artifact '${target}' is schema-invalid: ${violations.join("; ")}; repair it before retrying`);
729
690
  }
730
- const existing = readExisting(target);
731
- const legacyLifecycle = req.artifact === "plan" && ["active", "completed"].includes(String(mapping(existing.doc.header).status ?? ""));
732
- if (legacyLifecycle)
733
- existing.doc = canonicalPlanDocumentForWrite(existing.doc);
734
- if (existing.bytes) {
735
- const validationBytes = req.artifact === "plan" ? dumpYamlMapping(existing.doc) : existing.bytes;
736
- const violations = validateArtifactBytes(req.artifact, validationBytes);
737
- if (violations.length) {
738
- throw new Error(`existing artifact '${target}' is schema-invalid: ${violations.join("; ")}; repair it before retrying`);
739
- }
691
+ if (req.artifact === "plan") {
692
+ const evaluationViolations = planEvaluationViolations(existing.doc);
693
+ if (evaluationViolations.length > 0)
694
+ throw new Error(`existing artifact '${target}' is schema-invalid: ${evaluationViolations.join("; ")}; repair it before retrying`);
740
695
  }
741
- if (req.artifact === "plan" && ["archive", "create"].includes(req.spec.verb))
742
- return planLifecycle(req, target, existing, legacyLifecycle);
743
- if (!existing.bytes && req.artifact === "plan")
696
+ }
697
+ if (req.artifact === "decisions" && req.spec.verb === "update") {
698
+ const number = Number(req.values.number);
699
+ if (!Number.isSafeInteger(number) || number < 1)
700
+ reject({
701
+ class: "schema_violation",
702
+ message: "decision update requires a positive decision number",
703
+ syntax: "agentera state decisions update --number N --satisfaction-state STATE --format json",
704
+ example: "agentera state decisions update --number 53 --satisfaction-state open --format json",
705
+ });
706
+ const record = decisionRecordForUpdate(req.projectRoot, existing.doc, number);
707
+ if (!record)
744
708
  reject({
745
709
  class: "unsupported_target",
746
- message: "no active plan exists; create one before appending a task",
747
- example: "agentera state plan create --input plan.yaml",
710
+ message: `no decision with number ${number}; numbered archive or current projection record is required`,
711
+ syntax: "agentera state decisions update --number N --satisfaction-state STATE --format json",
712
+ example: "agentera state decisions update --number 53 --satisfaction-state open --format json",
748
713
  });
749
- const mutated = mutateCandidate(req, existing.doc);
750
- if (mutated.replay)
751
- return envelope(req, target, mutated.candidate, mutated.written, mutated.assigned, true, null, req.dryRun ? { diff: "", before: existing.doc, after: mutated.candidate } : {});
752
- const candidateBytes = dumpYamlMapping(mutated.candidate);
753
- const candidateViolations = validateArtifactBytes(req.artifact, candidateBytes);
754
- if (candidateViolations.length)
755
- schemaViolation(candidateViolations);
756
- const stage = stagingPath(req, target);
757
- let compaction = null;
758
- try {
714
+ if (!transaction)
715
+ throw new Error("state mutation transaction is unavailable");
716
+ const update = updateDecisionOverlay(req.projectRoot, number, req.values.satisfaction, record.satisfaction, transaction, !req.dryRun);
717
+ const before = dumpYamlMapping(update.before);
718
+ const after = dumpYamlMapping(update.after);
719
+ return envelope(req, update.path, existing.doc, { satisfaction: update.satisfaction }, { number }, update.replay, null, { diff: diffText(before, after, update.path), before: update.before, after: update.after });
720
+ }
721
+ if (req.artifact === "plan" && ["archive", "create"].includes(req.spec.verb))
722
+ return planLifecycle(req, target, existing, legacyLifecycle);
723
+ if (!existing.bytes && req.artifact === "plan")
724
+ reject({
725
+ class: "unsupported_target",
726
+ message: "no active plan exists; create one before appending a task",
727
+ example: "agentera state plan create --input plan.yaml",
728
+ });
729
+ const numberedAppend = req.spec.verb === "append" && ["progress", "decisions", "health"].includes(req.artifact);
730
+ const replayPayload = req.artifact === "decisions"
731
+ ? normalizedDecisionPayload(req.callerPayload)
732
+ : req.callerPayload;
733
+ const mutated = mutateCandidate(req, existing.doc, numberedAppend ? findArchivedReplay(req.projectRoot, req.artifact, replayPayload) : undefined);
734
+ if (mutated.replay && isDeepStrictEqual(mutated.candidate, existing.doc)) {
735
+ let archive;
736
+ if (numberedAppend && !req.dryRun) {
737
+ if (!transaction)
738
+ throw new Error("state mutation transaction is unavailable");
739
+ archive = transaction.publishArchive(req.artifact, Number(mutated.written.number), mutated.written);
740
+ }
741
+ return envelope(req, target, mutated.candidate, mutated.written, mutated.assigned, true, null, {
742
+ ...(archive ? { archivePath: archive.path } : {}),
743
+ ...(req.dryRun ? { diff: "", before: existing.doc, after: mutated.candidate } : {}),
744
+ });
745
+ }
746
+ const repairedBytes = req.artifact === "health" && req.spec.verb === "repair"
747
+ ? repairHealthProjectionBytes(existing.bytes, Number(req.values.number), req.values.keep === "last" ? "last" : "first")
748
+ : null;
749
+ if (repairedBytes && repairedBytes.removed === 0)
750
+ reject({ class: "unsupported_target", message: `health audit ${Number(req.values.number)} has no duplicate current-history rows to repair` });
751
+ const candidateBytes = repairedBytes?.bytes ?? dumpYamlMapping(mutated.candidate);
752
+ const candidateViolations = validateArtifactBytes(req.artifact, candidateBytes);
753
+ if (candidateViolations.length)
754
+ schemaViolation(candidateViolations);
755
+ const stage = req.dryRun
756
+ ? stagingPath(req, target)
757
+ : transaction?.stageProjection(target, candidateBytes);
758
+ if (!stage)
759
+ throw new Error("state mutation transaction is unavailable");
760
+ let compaction = null;
761
+ const protectedOverflowCount = req.artifact === "decisions"
762
+ ? decisionProtectedOverflowCount(hydrateDecisionRecords(array(mutated.candidate, "decisions"), req.projectRoot), hydrateDecisionRecords(array(mutated.candidate, "archive"), req.projectRoot))
763
+ : 0;
764
+ try {
765
+ if (req.dryRun)
759
766
  fs.writeFileSync(stage, candidateBytes);
760
- if (req.spec.compacts)
761
- compaction = compactYamlFile(stage, req.artifact);
762
- const finalBytes = fs.readFileSync(stage, "utf8");
763
- const finalViolations = validateArtifactBytes(req.artifact, finalBytes);
764
- if (finalViolations.length)
765
- throw new Error(`writer/compactor invariant failure: ${finalViolations.join("; ")}`);
766
- const finalDoc = loadYamlMapping(finalBytes);
767
- const diff = diffText(existing.bytes, finalBytes, target);
768
- if (req.dryRun) {
769
- return envelope(req, target, finalDoc, mutated.written, mutated.assigned, false, compaction, { diff, before: existing.doc, after: finalDoc });
770
- }
771
- fs.renameSync(stage, target);
772
- return envelope(req, target, finalDoc, mutated.written, mutated.assigned, false, compaction);
767
+ if (req.spec.compacts) {
768
+ compaction = compactYamlFile(stage, req.artifact, req.projectRoot);
773
769
  }
774
- finally {
775
- try {
776
- fs.unlinkSync(stage);
777
- }
778
- catch {
779
- /* stage published or absent */
780
- }
770
+ const finalBytes = fs.readFileSync(stage, "utf8");
771
+ const finalViolations = validateArtifactBytes(req.artifact, finalBytes);
772
+ if (finalViolations.length)
773
+ throw new Error(`writer/compactor invariant failure: ${finalViolations.join("; ")}`);
774
+ if (!req.dryRun)
775
+ transaction?.syncStaged(stage);
776
+ const finalDoc = loadYamlMapping(finalBytes);
777
+ const diff = diffText(existing.bytes, finalBytes, target);
778
+ if (req.dryRun) {
779
+ return envelope(req, target, finalDoc, mutated.written, mutated.assigned, false, compaction, { diff, before: existing.doc, after: finalDoc, protectedOverflowCount });
781
780
  }
781
+ const archive = numberedAppend
782
+ ? transaction?.publishArchive(req.artifact, Number(mutated.written.number), mutated.written)
783
+ : undefined;
784
+ if (!req.dryRun)
785
+ transaction?.publishProjection(stage, target);
786
+ return envelope(req, target, finalDoc, mutated.written, mutated.assigned, mutated.replay, compaction, { ...(archive ? { archivePath: archive.path } : {}), protectedOverflowCount });
782
787
  }
783
788
  finally {
784
- lock?.release();
789
+ try {
790
+ if (req.dryRun)
791
+ fs.unlinkSync(stage);
792
+ else
793
+ transaction?.removeStage(stage);
794
+ }
795
+ catch {
796
+ /* stage published or absent */
797
+ }
785
798
  }
786
799
  }
787
800
  //# sourceMappingURL=transaction.js.map