agentera 3.0.0-dev.51 → 3.0.0-dev.69

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 (45) hide show
  1. package/README.md +5 -5
  2. package/bundle/CHANGELOG.md +21 -11
  3. package/bundle/UPGRADE.md +23 -7
  4. package/bundle/references/adapters/package-publication.json +68 -17
  5. package/bundle/references/adapters/package-registry.yaml +145 -145
  6. package/bundle/references/analysis/verification-policy.yaml +93 -18
  7. package/bundle/references/artifacts/state-storage-authority.yaml +50 -11
  8. package/bundle/skills/agentera/SKILL.md +7 -5
  9. package/bundle/skills/agentera/capabilities/build/schemas/artifacts.yaml +7 -6
  10. package/bundle/skills/agentera/capabilities/build/schemas/exit.yaml +6 -5
  11. package/bundle/skills/agentera/capabilities/build/schemas/validation.yaml +18 -10
  12. package/bundle/skills/agentera/schemas/artifacts/plan.yaml +3 -0
  13. package/bundle/skills/agentera/schemas/artifacts/progress.yaml +8 -6
  14. package/dist/capabilities/build/instructions.js +8 -5
  15. package/dist/cli/capabilityContext/build.js +12 -3
  16. package/dist/cli/capabilityContext/planState.js +6 -2
  17. package/dist/cli/capabilityContext/startupAggregation.js +1 -1
  18. package/dist/cli/commands/doctor.js +1 -1
  19. package/dist/cli/commands/prime/briefOrientation.js +0 -1
  20. package/dist/cli/commands/prime/collectOrientationState.js +13 -6
  21. package/dist/cli/commands/prime/orientationOutput.js +27 -5
  22. package/dist/cli/commands/state/write.js +4 -1
  23. package/dist/cli/dispatch/index.js +1 -1
  24. package/dist/cli/help.js +1 -0
  25. package/dist/cli/migrationRequired.js +40 -5
  26. package/dist/registries/activationTuples.js +3 -2
  27. package/dist/registries/packagePublication.js +51 -7
  28. package/dist/state/entityMigrationPreview.js +3 -9
  29. package/dist/state/entityPublicationContext.js +56 -3
  30. package/dist/state/planEntities.js +49 -4
  31. package/dist/state/progressWritePolicy.js +33 -0
  32. package/dist/state/stateMode.js +138 -1
  33. package/dist/state/todoDocsEntities.js +62 -7
  34. package/dist/state/todoReconciliationActivation.js +46 -4
  35. package/dist/state/todoReconciliationInspection.js +5 -3
  36. package/dist/state/todoReconciliationRepair.js +149 -1
  37. package/dist/state/todoReconciliationTransaction.js +20 -7
  38. package/dist/state/write/explain.js +17 -3
  39. package/dist/state/write/input.js +25 -1
  40. package/dist/state/write/runtimeOperations.js +4 -2
  41. package/dist/state/write/transaction.js +6 -1
  42. package/dist/validate/activationArtifactEvidence.js +49 -41
  43. package/dist/validate/activationArtifactEvidenceTypes.js +12 -0
  44. package/dist/validate/activationEvidenceManifest.js +26 -10
  45. package/package.json +11 -6
@@ -1,7 +1,8 @@
1
+ import { spawnSync } from "node:child_process";
1
2
  import fs from "node:fs";
2
3
  import path from "node:path";
3
4
  import { resolveSourceRoot } from "../core/sourceRoot.js";
4
- import { loadYamlMapping } from "../core/yaml.js";
5
+ import { dumpYamlMapping, loadYamlMapping } from "../core/yaml.js";
5
6
  import { EntityPublicationContext } from "./entityPublicationContext.js";
6
7
  import { validateRealProjectRoot } from "./projectRoot.js";
7
8
  import { readProjectFileSnapshot } from "./safeProjectFile.js";
@@ -44,6 +45,142 @@ function readStableMarker(root, markerPath) {
44
45
  }
45
46
  return snapshot.bytes;
46
47
  }
48
+ function pathKind(root, relativePath) {
49
+ const absolute = path.join(root.path, ...relativePath.split("/"));
50
+ try {
51
+ const stat = fs.lstatSync(absolute);
52
+ if (stat.isSymbolicLink())
53
+ return "unsafe";
54
+ if (stat.isFile())
55
+ return "file";
56
+ if (stat.isDirectory())
57
+ return "directory";
58
+ return "unsafe";
59
+ }
60
+ catch (error) {
61
+ if (error.code === "ENOENT")
62
+ return "missing";
63
+ return "unsafe";
64
+ }
65
+ }
66
+ function isGitWorktreeRoot(root) {
67
+ const env = { ...process.env };
68
+ for (const name of ["GIT_INDEX_FILE", "GIT_DIR", "GIT_WORK_TREE", "GIT_COMMON_DIR"])
69
+ delete env[name];
70
+ const result = spawnSync("git", ["rev-parse", "--show-toplevel"], {
71
+ cwd: root.path,
72
+ env,
73
+ encoding: "utf8",
74
+ });
75
+ if (result.error || result.status !== 0)
76
+ return false;
77
+ try {
78
+ return fs.realpathSync(String(result.stdout).trim()) === root.path;
79
+ }
80
+ catch {
81
+ return false;
82
+ }
83
+ }
84
+ function classifyMarkerAbsentProject(root, markerPath) {
85
+ const stateRoot = pathKind(root, ".agentera");
86
+ if (stateRoot === "missing") {
87
+ return {
88
+ state: isGitWorktreeRoot(root) ? "fresh_uninitialized" : "unknown",
89
+ root,
90
+ markerPath,
91
+ };
92
+ }
93
+ if (stateRoot !== "directory") {
94
+ return {
95
+ state: "corrupt",
96
+ root,
97
+ markerPath,
98
+ reason: "Agentera state root '.agentera' is not a safe directory",
99
+ };
100
+ }
101
+ const entities = pathKind(root, ".agentera/entities");
102
+ if (entities !== "missing" && entities !== "directory") {
103
+ return {
104
+ state: "corrupt",
105
+ root,
106
+ markerPath,
107
+ reason: "Agentera entity root '.agentera/entities' is not a safe directory",
108
+ };
109
+ }
110
+ if (entities === "directory")
111
+ return { state: "partial", root, markerPath };
112
+ const aggregates = ["plan.yaml", "progress.yaml", "decisions.yaml", "health.yaml"]
113
+ .map((name) => pathKind(root, `.agentera/${name}`));
114
+ if (aggregates.includes("unsafe")) {
115
+ return {
116
+ state: "corrupt",
117
+ root,
118
+ markerPath,
119
+ reason: "a recognized Agentera aggregate is not a safe regular file",
120
+ };
121
+ }
122
+ const aggregateCount = aggregates.filter((kind) => kind === "file").length;
123
+ if (aggregateCount >= 2)
124
+ return { state: "legacy", root, markerPath };
125
+ if (aggregateCount === 1)
126
+ return { state: "partial", root, markerPath };
127
+ return { state: "unknown", root, markerPath };
128
+ }
129
+ /** Classify local state without creating, repairing, or adopting project files. */
130
+ export function classifyProjectState(projectRoot, sourceRoot = resolveSourceRoot()) {
131
+ const root = validateRealProjectRoot(projectRoot);
132
+ const declared = contract(sourceRoot);
133
+ let bytes;
134
+ try {
135
+ bytes = readStableMarker(root, declared.markerPath);
136
+ }
137
+ catch (error) {
138
+ return {
139
+ state: "corrupt",
140
+ root,
141
+ markerPath: declared.markerPath,
142
+ reason: error.message,
143
+ };
144
+ }
145
+ if (bytes === null)
146
+ return classifyMarkerAbsentProject(root, declared.markerPath);
147
+ try {
148
+ const document = loadYamlMapping(bytes.toString("utf8"));
149
+ if (document.schemaVersion !== declared.schemaVersion || document.mode !== declared.mode) {
150
+ return {
151
+ state: "corrupt",
152
+ root,
153
+ markerPath: declared.markerPath,
154
+ reason: `state mode marker '${declared.markerPath}' must declare schemaVersion '${declared.schemaVersion}' and mode '${declared.mode}'; restore the durable migration marker before retrying`,
155
+ };
156
+ }
157
+ }
158
+ catch (error) {
159
+ return {
160
+ state: "corrupt",
161
+ root,
162
+ markerPath: declared.markerPath,
163
+ reason: `state mode marker '${declared.markerPath}' is corrupt: ${error.message}; restore the durable migration marker before retrying`,
164
+ };
165
+ }
166
+ return { state: "entities", root, markerPath: declared.markerPath, markerBytes: bytes };
167
+ }
168
+ export function freshEntityStateMarker(sourceRoot = resolveSourceRoot()) {
169
+ const declared = contract(sourceRoot);
170
+ return { schemaVersion: declared.schemaVersion, mode: declared.mode };
171
+ }
172
+ export function freshPlanInitialization(projectRoot, sourceRoot = resolveSourceRoot()) {
173
+ const classified = classifyProjectState(projectRoot, sourceRoot);
174
+ if (classified.state !== "fresh_uninitialized")
175
+ return null;
176
+ const marker = freshEntityStateMarker(sourceRoot);
177
+ return {
178
+ root: classified.root,
179
+ markerPath: classified.markerPath,
180
+ marker,
181
+ markerBytes: dumpYamlMapping(marker),
182
+ };
183
+ }
47
184
  function detectValidatedStateMode(projectRoot, sourceRoot) {
48
185
  const root = validateRealProjectRoot(projectRoot);
49
186
  const declared = contract(sourceRoot);
@@ -4,7 +4,7 @@ import path from "node:path";
4
4
  import { resolveSourceRoot } from "../core/sourceRoot.js";
5
5
  import { canonicalRecordJson } from "./archiveDiscovery.js";
6
6
  import { StateRetrievalFailure } from "./directRetrieval.js";
7
- import { allocateEntityId, assertEntityDiscoveryOrigin, canonicalEntityEnvelopeBytes, canonicalEntityRecordViolations, discoverEntities, entityExactGetMaxBytes, exactDiscoveredEntityBytes, publishEntityUnderLock, replaceEntityUnderLock, validateEntityState, withEntityWriterLock, } from "./entityStorage.js";
7
+ import { allocateEntityId, assertEntityDiscoveryOrigin, canonicalEntityEnvelopeBytes, canonicalEntityRecordViolations, discoverEntities, entityExactGetMaxBytes, exactDiscoveredEntityBytes, publishEntityUnderLock, replaceEntityUnderLock, validateEntityState, withEntityWriterLock } from "./entityStorage.js";
8
8
  import { detectStateModeBinding } from "./stateMode.js";
9
9
  import { reject, StateWriteInputError } from "./write/errors.js";
10
10
  import { TODO_SEVERITIES, TODO_STATUSES, todoDocsRecordViolations, todoInputViolations } from "./todoDocsEntityValidation.js";
@@ -18,8 +18,8 @@ import { parseTodoMarkdownListItem, renderTodoPublicRecord } from "../cli/todoMa
18
18
  import { evaluateTodoReadinessQueue } from "../cli/todoReadinessSelection.js";
19
19
  import { artifactSchemasDir, loadArtifactRecord, registryModelPath, resolveArtifactPath } from "../registries/artifactRegistry.js";
20
20
  import { inspectTodoReconciliation, publishTodoReconciliation, recoverTodoReconciliation, todoCreateRequestSha256 } from "./todoReconciliationTransaction.js";
21
- import { loadTodoReconciliationActivation, todoLegacyRowFingerprint, todoReconciliationActivationBytes, TODO_RECONCILIATION_ACTIVATION_PATH, TODO_RECONCILIATION_ITEM_LIMIT, TODO_ACTIVATION_APPLY_COMMAND, TODO_ACTIVATION_PREVIEW_COMMAND, TODO_ACTIVATION_RISK_LIMIT, TODO_REPAIR_APPLY_COMMAND, TODO_REPAIR_PREVIEW_COMMAND, todoActivationEffect, todoRepairEffect, unchangedTodoActivationEffect, } from "./todoReconciliationActivation.js";
22
- import { planTodoRepair } from "./todoReconciliationRepair.js";
21
+ import { loadTodoReconciliationActivation, todoLegacyRowFingerprint, todoReconciliationActivationBytes, TODO_RECONCILIATION_ACTIVATION_PATH, TODO_RECONCILIATION_ITEM_LIMIT, TODO_ACTIVATION_APPLY_COMMAND, TODO_ACTIVATION_PREVIEW_COMMAND, TODO_ACTIVATION_RISK_LIMIT, TODO_OWNER_CORRECTION_APPLY_COMMAND, TODO_OWNER_CORRECTION_PREVIEW_COMMAND, TODO_REPAIR_APPLY_COMMAND, TODO_REPAIR_PREVIEW_COMMAND, todoActivationEffect, todoOwnerCorrectionEffect, todoRepairEffect, unchangedTodoActivationEffect } from "./todoReconciliationActivation.js";
22
+ import { normalizeTodoOwnerCorrectionEvidence, planTodoOwnerCorrection, planTodoRepair } from "./todoReconciliationRepair.js";
23
23
  import { readTodoMarkdown, renderManagedMarkdown } from "./todoMarkdownProjection.js";
24
24
  import { inactiveTodoActivationSafety, rejectUnsafeInactiveTodoActivation, unsafeInactiveDuplicateDiagnosis } from "./todoActivationSafety.js";
25
25
  const ID = /^[a-z]{10}$/;
@@ -412,6 +412,7 @@ function withBaseline(record, value) { const publicValue = Object.fromEntries(Ob
412
412
  function inactiveTodoMutation() { reject({ class: "conflict", message: "TODO reconciliation is inactive; ordinary TODO mutations cannot activate it implicitly", syntax: TODO_ACTIVATION_PREVIEW_COMMAND, example: TODO_ACTIVATION_APPLY_COMMAND, recovery: `Run exactly '${TODO_ACTIVATION_PREVIEW_COMMAND}', review every reported effect, then run exactly '${TODO_ACTIVATION_APPLY_COMMAND}'; no state was changed.` }); }
413
413
  function activationEnvelope(effect, dryRun, replay, transactionId, targets, recovered) { return { schemaVersion: "agentera.stateWrite.v1", command: "state todo activate", status: "pass", path: TODO_RECONCILIATION_ACTIVATION_PATH, artifact: "todo", operation: { verb: "activate", dry_run: dryRun, idempotent_replay: replay }, validation: { status: "pass", violations: [] }, activation: effect, apply_command: TODO_ACTIVATION_APPLY_COMMAND.replace("EFFECT_SHA256", String(effect.effect_sha256)), reconciliation: { transaction_id: transactionId, targets, recovered } }; }
414
414
  function repairEnvelope(effect, dryRun, replay, transactionId, targets, recovered) { return { schemaVersion: "agentera.stateWrite.v1", command: "state todo repair", status: "pass", path: TODO_RECONCILIATION_ACTIVATION_PATH, artifact: "todo", operation: { verb: "repair", dry_run: dryRun, idempotent_replay: replay }, validation: { status: "pass", violations: [] }, repair: effect, apply_command: TODO_REPAIR_APPLY_COMMAND.replace("EFFECT_SHA256", String(effect.effect_sha256)), reconciliation: { transaction_id: transactionId, targets, recovered } }; }
415
+ function ownerCorrectionEnvelope(effect, dryRun, replay, transactionId, targets, recovered) { return { schemaVersion: "agentera.stateWrite.v1", command: "state todo correct-owners", status: "pass", path: TODO_RECONCILIATION_ACTIVATION_PATH, artifact: "todo", operation: { verb: "correct-owners", dry_run: dryRun, idempotent_replay: replay }, validation: { status: "pass", violations: [] }, correction: effect, apply_command: TODO_OWNER_CORRECTION_APPLY_COMMAND.replace("EFFECT_SHA256", String(effect.effect_sha256)), reconciliation: { transaction_id: transactionId, targets, recovered } }; }
415
416
  function envelope(command, entity, artifact, record, dryRun) { return { schemaVersion: "agentera.stateWrite.v1", command, status: "pass", path: entity.path, id: entity.id, artifact, record, operation: { verb: command.split(" ").at(-1), dry_run: dryRun, idempotent_replay: entity.replay }, validation: { status: "pass", violations: [] } }; }
416
417
  function targetPath(root, sourceRoot, artifact, id) {
417
418
  const model = definition(artifact);
@@ -665,12 +666,14 @@ export function mutateTodoDocsEntity(req, options = {}) {
665
666
  const sourceBinding = { kind: "project", projectRoot: context.validatedRoot };
666
667
  context.assertValid();
667
668
  const todoBinding = artifact === "todo" ? todoReconciliationBinding(req.projectRoot, sourceRoot) : null;
669
+ const correctingOwners = artifact === "todo" && req.spec.verb === "correct-owners";
670
+ const ownerEvidence = correctingOwners ? normalizeTodoOwnerCorrectionEvidence(req.input ?? {}) : null;
668
671
  const pending = todoBinding ? inspectTodoReconciliation(pinnedRoot, todoBinding) : [];
669
672
  const initialActivation = artifact === "todo" ? loadTodoReconciliationActivation(pinnedRoot) : null;
670
- if (artifact === "todo" && ["activate", "repair"].includes(req.spec.verb)) {
673
+ if (artifact === "todo" && ["activate", "repair", "correct-owners"].includes(req.spec.verb)) {
671
674
  const repairing = req.spec.verb === "repair";
672
- const previewCommand = repairing ? TODO_REPAIR_PREVIEW_COMMAND : TODO_ACTIVATION_PREVIEW_COMMAND;
673
- const applyCommand = repairing ? TODO_REPAIR_APPLY_COMMAND : TODO_ACTIVATION_APPLY_COMMAND;
675
+ const previewCommand = correctingOwners ? TODO_OWNER_CORRECTION_PREVIEW_COMMAND : repairing ? TODO_REPAIR_PREVIEW_COMMAND : TODO_ACTIVATION_PREVIEW_COMMAND;
676
+ const applyCommand = correctingOwners ? TODO_OWNER_CORRECTION_APPLY_COMMAND : repairing ? TODO_REPAIR_APPLY_COMMAND : TODO_ACTIVATION_APPLY_COMMAND;
674
677
  if (repairing && !initialActivation)
675
678
  reject({ class: "conflict", message: "TODO repair requires an existing reconciliation activation marker", recovery: `Use '${TODO_ACTIVATION_PREVIEW_COMMAND}' for an inactive project; no state was changed.` });
676
679
  if (req.dryRun && (req.values.confirmed === true || req.values.effect_sha256 !== undefined))
@@ -687,7 +690,8 @@ export function mutateTodoDocsEntity(req, options = {}) {
687
690
  const recoveryReceipts = todoBinding && !req.dryRun
688
691
  ? recoverTodoReconciliation(context, sourceRoot, todoBinding, {
689
692
  createRequestSha256,
690
- ...(["activate", "repair"].includes(req.spec.verb) ? { activationEffectSha256: String(req.values.effect_sha256) } : {}),
693
+ ...(["activate", "repair", "correct-owners"].includes(req.spec.verb) ? { activationEffectSha256: String(req.values.effect_sha256) } : {}),
694
+ ...(correctingOwners ? { ownerMappingSha256: ownerEvidence.sha256 } : {}),
691
695
  beforeCommit: () => assertState(pinnedRoot, sourceRoot, sourceBinding),
692
696
  })
693
697
  : [];
@@ -767,6 +771,57 @@ export function mutateTodoDocsEntity(req, options = {}) {
767
771
  context.assertValid();
768
772
  return activationEnvelope(effect, false, false, transaction.id, transaction.targetCount, recovered);
769
773
  }
774
+ if (req.spec.verb === "correct-owners") {
775
+ if (activation) {
776
+ if (!req.dryRun && activation.effect_operation === "correct-owners" && activation.effect_sha256 === req.values.effect_sha256 && activation.owner_mapping_sha256 === ownerEvidence.sha256) {
777
+ const replayEffect = todoOwnerCorrectionEffect({ counts: { matched: 0, converted: 0, retained: 0, duplicate: 0, stale: 0, conflicting: 0 }, items: [], omitted_count: 0 }, ownerEvidence.sha256, [], publicRelative, markdownBefore, markdown);
778
+ replayEffect.effect_sha256 = activation.effect_sha256;
779
+ return ownerCorrectionEnvelope(replayEffect, false, true, null, 0, recovered);
780
+ }
781
+ reject({ class: "conflict", message: "TODO owner correction requires marker-absent unsafe-inactive state", recovery: "Use state todo repair only for an unsafe active reconciliation, or retry the exact owner-correction apply command when its stored effect and owner mapping match; no state was changed." });
782
+ }
783
+ let unsafe = true;
784
+ try {
785
+ unsafe = !inactiveTodoActivationSafety(managedRows(markdown, null, todoEntities), todoEntities).safe;
786
+ }
787
+ catch {
788
+ unsafe = true;
789
+ }
790
+ if (!unsafe)
791
+ reject({ class: "conflict", message: "TODO owner correction requires marker-absent unsafe-inactive state", recovery: `Use '${TODO_ACTIVATION_PREVIEW_COMMAND}' for a safe inactive project; no state was changed.` });
792
+ const plan = planTodoOwnerCorrection(markdown, todoEntities, ownerEvidence);
793
+ for (const [todoId, record] of plan.records) {
794
+ const violations = recordViolations("todo", record, sourceRoot);
795
+ if (violations.length)
796
+ reject({ class: "schema_violation", message: "todo entity input is invalid", violations });
797
+ if (record.readiness !== undefined)
798
+ assertTodoReferences(todoId, record, [...plan.records].map(([entityId, entityRecord]) => ({ boundary: TODO.boundary, id: entityId, record: entityRecord })));
799
+ }
800
+ let activationBytesAfter = todoReconciliationActivationBytes([], "0".repeat(64), "correct-owners", ownerEvidence.sha256);
801
+ const activationAfter = JSON.parse(activationBytesAfter);
802
+ const finalRows = managedRows(plan.rendered, activationAfter, todoEntities).rows;
803
+ for (const [todoId, record] of plan.records) {
804
+ const row = finalRows.get(todoId);
805
+ plan.records.set(todoId, withBaseline(record, row ? rowSnapshot(row, record) : { present: false }));
806
+ }
807
+ const targets = todoEntities.map((entity) => ({ path: entity.relativePath, before: exactDiscoveredEntityBytes(entity), after: canonicalEntityEnvelopeBytes({ id: entity.id, artifact: "todo", record: plan.records.get(entity.id), migrationProvenance: entity.migrationProvenance ?? undefined }) }));
808
+ targets.push({ path: TODO_RECONCILIATION_ACTIVATION_PATH, before: null, after: activationBytesAfter });
809
+ targets.push({ path: publicRelative, before: publicExists ? markdownBefore : null, after: plan.rendered });
810
+ const preliminaryEffect = todoOwnerCorrectionEffect(plan.diagnosis, ownerEvidence.sha256, targets, publicRelative, markdownBefore, plan.rendered);
811
+ activationBytesAfter = todoReconciliationActivationBytes([], String(preliminaryEffect.effect_sha256), "correct-owners", ownerEvidence.sha256);
812
+ targets.find((target) => target.path === TODO_RECONCILIATION_ACTIVATION_PATH).after = activationBytesAfter;
813
+ const effect = todoOwnerCorrectionEffect(plan.diagnosis, ownerEvidence.sha256, targets, publicRelative, markdownBefore, plan.rendered);
814
+ if (effect.effect_sha256 !== preliminaryEffect.effect_sha256)
815
+ throw new Error("TODO owner correction effect authorization is not self-consistent");
816
+ if (req.dryRun)
817
+ return ownerCorrectionEnvelope(effect, true, false, null, effect.targets.length, recovered);
818
+ if (req.values.effect_sha256 !== effect.effect_sha256)
819
+ reject({ class: "conflict", message: "TODO owner correction effects changed after preview", syntax: TODO_OWNER_CORRECTION_PREVIEW_COMMAND, example: TODO_OWNER_CORRECTION_APPLY_COMMAND, recovery: `Rerun exactly '${TODO_OWNER_CORRECTION_PREVIEW_COMMAND}', review the changed bounded effects, then run its new exact apply_command; no state was changed.` });
820
+ const transaction = publishTodoReconciliation(context, sourceRoot, todoBinding, targets, { activationEffectSha256: String(effect.effect_sha256), ownerMappingSha256: ownerEvidence.sha256, interruptAfterTarget: options.interruptAfterTarget, beforeCommit: () => { assertState(pinnedRoot, sourceRoot, sourceBinding); const currentBinding = todoReconciliationBinding(req.projectRoot, sourceRoot); if (currentBinding.publicPath !== todoBinding.publicPath || currentBinding.mappingSha256 !== todoBinding.mappingSha256)
821
+ reject({ class: "conflict", message: "TODO reconciliation mapping changed during owner correction publication", recovery: "Preserve the changed docs mapping and retry owner correction after every transaction target is restored; no mapping bytes were overwritten." }); } });
822
+ context.assertValid();
823
+ return ownerCorrectionEnvelope(effect, false, false, transaction.id, transaction.targetCount, recovered);
824
+ }
770
825
  if (req.spec.verb === "repair") {
771
826
  if (!activation || !loadedActivation)
772
827
  throw new Error("TODO repair lost its required activation marker");
@@ -13,7 +13,10 @@ export const TODO_ACTIVATION_PREVIEW_COMMAND = `${CANONICAL_DEVELOPMENT_CLI} sta
13
13
  export const TODO_ACTIVATION_APPLY_COMMAND = `${CANONICAL_DEVELOPMENT_CLI} state todo activate --effect-sha256 EFFECT_SHA256 --yes --format json`;
14
14
  export const TODO_REPAIR_PREVIEW_COMMAND = `${CANONICAL_DEVELOPMENT_CLI} state todo repair --dry-run --format json`;
15
15
  export const TODO_REPAIR_APPLY_COMMAND = `${CANONICAL_DEVELOPMENT_CLI} state todo repair --effect-sha256 EFFECT_SHA256 --yes --format json`;
16
- export const TODO_UNSAFE_INACTIVE_RECOVERY = "Owner correction required: restore Markdown-owned public rows and Agentera-owned entity status to exact one-to-one pre-activation evidence. Do not activate or repair. If a correction needs a mutation, stop and request replanning; no state was changed.";
16
+ export const TODO_OWNER_CORRECTION_INPUT_VERSION = "agentera.todoOwnerCorrection.v1";
17
+ export const TODO_OWNER_CORRECTION_PREVIEW_COMMAND = `${CANONICAL_DEVELOPMENT_CLI} state todo correct-owners --input OWNER_MAPPING.yaml --dry-run --format json`;
18
+ export const TODO_OWNER_CORRECTION_APPLY_COMMAND = `${CANONICAL_DEVELOPMENT_CLI} state todo correct-owners --input OWNER_MAPPING.yaml --effect-sha256 EFFECT_SHA256 --yes --format json`;
19
+ export const TODO_UNSAFE_INACTIVE_RECOVERY = `Owner correction required: run exactly '${TODO_OWNER_CORRECTION_PREVIEW_COMMAND}', then use its exact apply_command; no state was changed.`;
17
20
  export const TODO_ACTIVATION_RISK_LIMIT = 20;
18
21
  function activationFailure(message) {
19
22
  reject({
@@ -25,7 +28,7 @@ function activationFailure(message) {
25
28
  export function todoLegacyRowFingerprint(section, line) {
26
29
  return createHash("sha256").update(section).update("\0").update(line).digest("hex");
27
30
  }
28
- export function todoReconciliationActivationBytes(retainedLegacyRows, effectSha256, effectOperation = "activate") {
31
+ export function todoReconciliationActivationBytes(retainedLegacyRows, effectSha256, effectOperation = "activate", ownerMappingSha256) {
29
32
  const retained = [...retainedLegacyRows].sort();
30
33
  if (retained.length > TODO_RECONCILIATION_ITEM_LIMIT
31
34
  || new Set(retained).size !== retained.length
@@ -33,11 +36,18 @@ export function todoReconciliationActivationBytes(retainedLegacyRows, effectSha2
33
36
  activationFailure("TODO reconciliation activation has invalid retained legacy-row identities");
34
37
  if (effectSha256 !== undefined && !SHA256.test(effectSha256))
35
38
  activationFailure("TODO reconciliation activation has an invalid effect authorization");
39
+ if (ownerMappingSha256 !== undefined && !SHA256.test(ownerMappingSha256))
40
+ activationFailure("TODO reconciliation activation has an invalid owner-mapping authorization");
41
+ if (effectOperation === "correct-owners" && (effectSha256 === undefined || ownerMappingSha256 === undefined))
42
+ activationFailure("TODO owner correction activation requires effect and owner-mapping authorization");
43
+ if (effectOperation !== "correct-owners" && ownerMappingSha256 !== undefined)
44
+ activationFailure("TODO reconciliation activation owner-mapping authorization requires owner correction");
36
45
  return `${JSON.stringify({
37
46
  schema_version: TODO_RECONCILIATION_ACTIVATION_VERSION,
38
47
  retained_legacy_rows: retained,
39
48
  ...(effectSha256 ? { effect_sha256: effectSha256 } : {}),
40
49
  ...(effectSha256 ? { effect_operation: effectOperation } : {}),
50
+ ...(ownerMappingSha256 ? { owner_mapping_sha256: ownerMappingSha256 } : {}),
41
51
  })}\n`;
42
52
  }
43
53
  export function loadTodoReconciliationActivation(root) {
@@ -63,14 +73,17 @@ export function loadTodoReconciliationActivation(root) {
63
73
  activationFailure("TODO reconciliation activation is not a mapping");
64
74
  }
65
75
  const record = value;
66
- if (!["retained_legacy_rows,schema_version", "effect_operation,effect_sha256,retained_legacy_rows,schema_version", "effect_sha256,retained_legacy_rows,schema_version"].includes(Object.keys(record).sort().join(","))
76
+ if (!["retained_legacy_rows,schema_version", "effect_operation,effect_sha256,retained_legacy_rows,schema_version", "effect_operation,effect_sha256,owner_mapping_sha256,retained_legacy_rows,schema_version", "effect_sha256,retained_legacy_rows,schema_version"].includes(Object.keys(record).sort().join(","))
67
77
  || record.schema_version !== TODO_RECONCILIATION_ACTIVATION_VERSION
68
78
  || !Array.isArray(record.retained_legacy_rows)
69
79
  || record.retained_legacy_rows.length > TODO_RECONCILIATION_ITEM_LIMIT
70
80
  || record.retained_legacy_rows.some((digest) => typeof digest !== "string" || !SHA256.test(digest))
71
81
  || new Set(record.retained_legacy_rows).size !== record.retained_legacy_rows.length
72
82
  || (record.effect_sha256 !== undefined && (typeof record.effect_sha256 !== "string" || !SHA256.test(record.effect_sha256)))
73
- || (record.effect_operation !== undefined && !["activate", "repair"].includes(record.effect_operation)))
83
+ || (record.effect_operation !== undefined && !["activate", "repair", "correct-owners"].includes(record.effect_operation))
84
+ || (record.owner_mapping_sha256 !== undefined && (typeof record.owner_mapping_sha256 !== "string" || !SHA256.test(record.owner_mapping_sha256)))
85
+ || (record.effect_operation === "correct-owners" && (record.effect_sha256 === undefined || record.owner_mapping_sha256 === undefined))
86
+ || (record.effect_operation !== "correct-owners" && record.owner_mapping_sha256 !== undefined))
74
87
  activationFailure("TODO reconciliation activation has an invalid canonical record");
75
88
  return { record: record, bytes };
76
89
  }
@@ -138,3 +151,32 @@ export function todoRepairEffect(diagnosis, targets, publicPath, markdownBefore,
138
151
  const authorizedTargets = changedTargets.map((target) => target.path === TODO_RECONCILIATION_ACTIVATION_PATH ? { ...target, after_sha256: normalizedActivationSha256 } : target);
139
152
  return { ...evidence, effect_sha256: sha256(canonicalRecordJson({ ...evidence, targets: authorizedTargets })) };
140
153
  }
154
+ export function todoOwnerCorrectionEffect(diagnosis, ownerMappingSha256, targets, publicPath, markdownBefore, rendered) {
155
+ const changedTargets = targets
156
+ .filter((target) => target.before === null || !target.before.equals(Buffer.from(target.after)))
157
+ .map((target) => ({ path: target.path, before_sha256: target.before === null ? null : sha256(target.before), after_sha256: sha256(target.after) }));
158
+ const evidence = {
159
+ diagnosis,
160
+ owner_mapping_sha256: ownerMappingSha256,
161
+ targets: changedTargets,
162
+ public_document: {
163
+ path: publicPath,
164
+ changed: !markdownBefore.equals(Buffer.from(rendered)),
165
+ before_bytes: markdownBefore.length,
166
+ after_bytes: Buffer.byteLength(rendered),
167
+ before_sha256: sha256(markdownBefore),
168
+ after_sha256: sha256(rendered),
169
+ changed_lines: boundedPublicChanges(markdownBefore.toString("utf8"), rendered),
170
+ },
171
+ };
172
+ const activationTarget = targets.find((target) => target.path === TODO_RECONCILIATION_ACTIVATION_PATH);
173
+ let normalizedActivationSha256 = null;
174
+ if (activationTarget) {
175
+ const parsed = JSON.parse(activationTarget.after);
176
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed) || typeof parsed.effect_sha256 !== "string")
177
+ throw new Error("TODO owner correction activation target cannot be normalized for effect authorization");
178
+ normalizedActivationSha256 = sha256(canonicalRecordJson({ ...parsed, effect_sha256: "activation_effect_authorization" }));
179
+ }
180
+ const authorizedTargets = changedTargets.map((target) => target.path === TODO_RECONCILIATION_ACTIVATION_PATH ? { ...target, after_sha256: normalizedActivationSha256 } : target);
181
+ return { ...evidence, effect_sha256: sha256(canonicalRecordJson({ ...evidence, targets: authorizedTargets })) };
182
+ }
@@ -5,7 +5,7 @@ import { renderTodoPublicRecord } from "../cli/todoMarkdown.js";
5
5
  import { canonicalRecordJson } from "./archiveDiscovery.js";
6
6
  import { detectStateMode } from "./stateMode.js";
7
7
  import { StateWriteInputError } from "./write/errors.js";
8
- import { TODO_ACTIVATION_APPLY_COMMAND, TODO_ACTIVATION_PREVIEW_COMMAND, TODO_RECONCILIATION_ACTIVATION_PATH, TODO_REPAIR_APPLY_COMMAND, TODO_REPAIR_PREVIEW_COMMAND, TODO_UNSAFE_INACTIVE_RECOVERY, loadTodoReconciliationActivation, todoReconciliationActivationBytes, } from "./todoReconciliationActivation.js";
8
+ import { TODO_ACTIVATION_APPLY_COMMAND, TODO_ACTIVATION_PREVIEW_COMMAND, TODO_OWNER_CORRECTION_APPLY_COMMAND, TODO_OWNER_CORRECTION_PREVIEW_COMMAND, TODO_RECONCILIATION_ACTIVATION_PATH, TODO_REPAIR_APPLY_COMMAND, TODO_REPAIR_PREVIEW_COMMAND, TODO_UNSAFE_INACTIVE_RECOVERY, loadTodoReconciliationActivation, todoReconciliationActivationBytes, } from "./todoReconciliationActivation.js";
9
9
  import { planTodoRepair } from "./todoReconciliationRepair.js";
10
10
  import { readTodoMarkdown } from "./todoMarkdownProjection.js";
11
11
  import { inactiveTodoActivationSafety } from "./todoActivationSafety.js";
@@ -47,8 +47,8 @@ function invalidLifecycle() {
47
47
  function inspection(state, rawCounts, risks) {
48
48
  const bounded = boundedCounts(rawCounts);
49
49
  const active = state === "healthy_active" || state === "unsafe_active";
50
- const preview = state === "inactive" ? TODO_ACTIVATION_PREVIEW_COMMAND : active ? TODO_REPAIR_PREVIEW_COMMAND : null;
51
- const apply = state === "inactive" ? TODO_ACTIVATION_APPLY_COMMAND : active ? TODO_REPAIR_APPLY_COMMAND : null;
50
+ const preview = state === "unsafe_inactive" ? TODO_OWNER_CORRECTION_PREVIEW_COMMAND : state === "inactive" ? TODO_ACTIVATION_PREVIEW_COMMAND : active ? TODO_REPAIR_PREVIEW_COMMAND : null;
51
+ const apply = state === "unsafe_inactive" ? TODO_OWNER_CORRECTION_APPLY_COMMAND : state === "inactive" ? TODO_ACTIVATION_APPLY_COMMAND : active ? TODO_REPAIR_APPLY_COMMAND : null;
52
52
  return {
53
53
  state,
54
54
  status: state === "healthy_active" ? "operable" : "action_required",
@@ -106,6 +106,8 @@ export function inspectTodoReconciliationState(root, sourceRoot = resolveSourceR
106
106
  if (!activation) {
107
107
  try {
108
108
  const scan = managedRows(readTodoMarkdown(todoPublicPath(root, sourceRoot)).text, null, entities);
109
+ if (entities.length === 0 && scan.matchedRows === 0 && scan.retainedLegacyRows.length === 0)
110
+ return null;
109
111
  const safety = inactiveTodoActivationSafety(scan, entities);
110
112
  return inspection(safety.safe ? "inactive" : "unsafe_inactive", safety.counts, safety.safe ? undefined : safety.risks);
111
113
  }
@@ -1,6 +1,8 @@
1
+ import { createHash } from "node:crypto";
1
2
  import { parseTodoMarkdownListItem, renderTodoPublicRecord } from "../cli/todoMarkdown.js";
3
+ import { canonicalRecordJson } from "./archiveDiscovery.js";
2
4
  import { reject } from "./write/errors.js";
3
- import { todoLegacyRowFingerprint, TODO_REPAIR_PREVIEW_COMMAND } from "./todoReconciliationActivation.js";
5
+ import { todoLegacyRowFingerprint, TODO_OWNER_CORRECTION_INPUT_VERSION, TODO_OWNER_CORRECTION_PREVIEW_COMMAND, TODO_REPAIR_PREVIEW_COMMAND } from "./todoReconciliationActivation.js";
4
6
  const RECONCILIATION_VERSION = "agentera.todoReconciliation.v1";
5
7
  const DIAGNOSTIC_LIMIT = 20;
6
8
  function mapping(value) { return value !== null && typeof value === "object" && !Array.isArray(value); }
@@ -30,6 +32,64 @@ function importMarkdown(record, row) {
30
32
  return result;
31
33
  }
32
34
  function rowFor(id, record) { return `- [${record.status === "resolved" ? "x" : " "}] [id:${id}] ${renderTodoPublicRecord(record)}`; }
35
+ export function todoOwnerCorrectionInputViolations(input) {
36
+ const violations = [];
37
+ const allowed = new Set(["schema_version", "owners"]);
38
+ for (const key of Object.keys(input))
39
+ if (!allowed.has(key))
40
+ violations.push(`input field '${key}' is not accepted for owner correction`);
41
+ if (input.schema_version !== TODO_OWNER_CORRECTION_INPUT_VERSION)
42
+ violations.push(`schema_version must be '${TODO_OWNER_CORRECTION_INPUT_VERSION}'`);
43
+ if (!Array.isArray(input.owners)) {
44
+ violations.push("owners must be a list of owner mappings");
45
+ return violations;
46
+ }
47
+ if (input.owners.length === 0 || input.owners.length > 256)
48
+ violations.push("owners must contain 1..256 mappings");
49
+ const ids = new Set();
50
+ const lines = new Set();
51
+ input.owners.forEach((owner, index) => {
52
+ if (!mapping(owner)) {
53
+ violations.push(`owners[${index}] must be a mapping`);
54
+ return;
55
+ }
56
+ const keys = Object.keys(owner).sort().join(",");
57
+ if (keys !== "id,source_line")
58
+ violations.push(`owners[${index}] must contain only id and source_line`);
59
+ if (typeof owner.id !== "string" || !/^[a-z]{10}$/.test(owner.id))
60
+ violations.push(`owners[${index}].id must be a bare ten-letter TODO ID`);
61
+ else if (ids.has(owner.id))
62
+ violations.push(`owners[${index}].id duplicates another owner claim`);
63
+ else
64
+ ids.add(owner.id);
65
+ if (!Number.isSafeInteger(owner.source_line) || Number(owner.source_line) < 1)
66
+ violations.push(`owners[${index}].source_line must be a positive integer`);
67
+ else if (lines.has(Number(owner.source_line)))
68
+ violations.push(`owners[${index}].source_line duplicates another owner claim`);
69
+ else
70
+ lines.add(Number(owner.source_line));
71
+ });
72
+ return violations.length <= DIAGNOSTIC_LIMIT
73
+ ? violations
74
+ : [...violations.slice(0, DIAGNOSTIC_LIMIT), `${violations.length - DIAGNOSTIC_LIMIT} additional input violations omitted`];
75
+ }
76
+ export function normalizeTodoOwnerCorrectionEvidence(input) {
77
+ const violations = todoOwnerCorrectionInputViolations(input);
78
+ if (violations.length)
79
+ reject({
80
+ class: "schema_violation",
81
+ message: "TODO owner correction input is invalid",
82
+ violations,
83
+ recovery: `Run exactly '${TODO_OWNER_CORRECTION_PREVIEW_COMMAND}' with one complete schema-valid owner mapping; no state was changed.`,
84
+ });
85
+ const owners = input.owners
86
+ .map((owner) => ({ id: String(owner.id), source_line: Number(owner.source_line) }))
87
+ .sort((left, right) => left.id.localeCompare(right.id));
88
+ return {
89
+ owners,
90
+ sha256: createHash("sha256").update(canonicalRecordJson({ schema_version: TODO_OWNER_CORRECTION_INPUT_VERSION, owners })).digest("hex"),
91
+ };
92
+ }
33
93
  function scanRows(markdown) {
34
94
  const managed = new Map();
35
95
  const legacy = [];
@@ -168,3 +228,91 @@ export function planTodoRepair(markdown, activation, entities) {
168
228
  const retainedLegacyRows = retainedRows.filter((row) => !claimedLegacy.has(row.line)).map((row) => todoLegacyRowFingerprint(row.section, row.sourceLine)).sort();
169
229
  return { records, retainedLegacyRows, rendered, diagnosis: { counts: { duplicate, stale, matched, retained: retainedLegacyRows.length, conflicting: 0 }, items, omitted_count: Math.max(0, duplicate + matched + retainedLegacyRows.length - items.length) } };
170
230
  }
231
+ function correctedRow(row, id, record) {
232
+ const indent = row.sourceLine.match(/^\s*/)?.[0] ?? "";
233
+ return `${indent}${rowFor(id, record)}`;
234
+ }
235
+ export function planTodoOwnerCorrection(markdown, entities, evidence) {
236
+ const { managed, legacy } = scanRows(markdown);
237
+ const rows = new Map();
238
+ for (const row of legacy)
239
+ rows.set(row.line, row);
240
+ for (const entries of managed.values())
241
+ for (const row of entries)
242
+ rows.set(row.line, row);
243
+ const byId = new Map(entities.filter((entity) => Boolean(entity.id && entity.record)).map((entity) => [entity.id, entity]));
244
+ const conflicts = [];
245
+ const claimedIds = new Set();
246
+ const claimedLines = new Set();
247
+ const records = new Map();
248
+ const replacements = new Map();
249
+ const items = [];
250
+ let stale = 0;
251
+ for (const owner of evidence.owners) {
252
+ const entity = byId.get(owner.id);
253
+ const row = rows.get(owner.source_line - 1);
254
+ if (!entity) {
255
+ conflicts.push(`owner ID '${owner.id}' has no canonical entity`);
256
+ continue;
257
+ }
258
+ if (!row) {
259
+ conflicts.push(`owner source line ${owner.source_line} has no managed TODO row`);
260
+ continue;
261
+ }
262
+ if (row.item.id && row.item.id !== owner.id) {
263
+ conflicts.push(`owner source line ${owner.source_line} already names '${row.item.id}', not '${owner.id}'`);
264
+ continue;
265
+ }
266
+ if (!claimedIds.add(owner.id) || !claimedLines.add(row.line)) {
267
+ conflicts.push(`owner mapping duplicates ID '${owner.id}' or source line ${owner.source_line}`);
268
+ continue;
269
+ }
270
+ if (entity.record.status === "resolved" && row.item.status === "open") {
271
+ conflicts.push(`owner source line ${owner.source_line} would reopen completed TODO '${owner.id}'`);
272
+ continue;
273
+ }
274
+ const record = importMarkdown(entity.record, row);
275
+ const stalePublic = !samePublic(publicSnapshot(entity.record), rowSnapshot(row, entity.record));
276
+ if (stalePublic)
277
+ stale += 1;
278
+ records.set(owner.id, record);
279
+ replacements.set(row.line, correctedRow(row, owner.id, record));
280
+ if (items.length < DIAGNOSTIC_LIMIT)
281
+ items.push({ id: owner.id, source_line: owner.source_line, stale_public: stalePublic });
282
+ }
283
+ for (const id of byId.keys())
284
+ if (!claimedIds.has(id))
285
+ conflicts.push(`canonical TODO entity '${id}' has no owner mapping`);
286
+ for (const [line, row] of rows)
287
+ if (!claimedLines.has(line))
288
+ conflicts.push(`managed TODO row at line ${row.line + 1} has no owner mapping`);
289
+ for (const [id, entries] of managed) {
290
+ if (entries.length > 1)
291
+ conflicts.push(`managed ID '${id}' occurs at lines ${entries.map((row) => row.line + 1).join(", ")}`);
292
+ if (!byId.has(id))
293
+ conflicts.push(`managed ID '${id}' has no canonical entity`);
294
+ }
295
+ if (conflicts.length)
296
+ reject({
297
+ class: "conflict",
298
+ message: `TODO owner correction requires complete one-to-one evidence; found ${conflicts.length} conflicting claim${conflicts.length === 1 ? "" : "s"}`,
299
+ violations: [...conflicts.slice(0, DIAGNOSTIC_LIMIT), ...(conflicts.length > DIAGNOSTIC_LIMIT ? [`${conflicts.length - DIAGNOSTIC_LIMIT} additional conflicts omitted`] : [])],
300
+ diagnosis: {
301
+ counts: { matched: records.size, converted: legacy.length, retained: 0, duplicate: 0, stale, conflicting: conflicts.length },
302
+ items,
303
+ omitted_count: Math.max(0, evidence.owners.length - items.length),
304
+ conflicts_omitted_count: Math.max(0, conflicts.length - DIAGNOSTIC_LIMIT),
305
+ },
306
+ recovery: `Correct every id/source_line claim and rerun exactly '${TODO_OWNER_CORRECTION_PREVIEW_COMMAND}'; no state was changed.`,
307
+ });
308
+ const rendered = `${markdown.split(/\r?\n/).map((line, index) => replacements.get(index) ?? line).join("\n").replace(/\n*$/, "")}\n`;
309
+ return {
310
+ records,
311
+ rendered,
312
+ diagnosis: {
313
+ counts: { matched: records.size, converted: legacy.length, retained: 0, duplicate: 0, stale, conflicting: 0 },
314
+ items,
315
+ omitted_count: Math.max(0, evidence.owners.length - items.length),
316
+ },
317
+ };
318
+ }
@@ -99,7 +99,7 @@ function parseJournal(bytes, fileName) {
99
99
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
100
100
  invalidJournal("TODO reconciliation journal is not a mapping");
101
101
  const value = parsed;
102
- const expectedKeys = ["schema_version", "id", "public_path", "mapping_sha256", "targets", ...(value.create === undefined ? [] : ["create"]), ...(value.activation_effect_sha256 === undefined ? [] : ["activation_effect_sha256"])].sort().join(",");
102
+ const expectedKeys = ["schema_version", "id", "public_path", "mapping_sha256", "targets", ...(value.create === undefined ? [] : ["create"]), ...(value.activation_effect_sha256 === undefined ? [] : ["activation_effect_sha256"]), ...(value.owner_mapping_sha256 === undefined ? [] : ["owner_mapping_sha256"])].sort().join(",");
103
103
  if (Object.keys(value).sort().join(",") !== expectedKeys
104
104
  || value.schema_version !== VERSION
105
105
  || typeof value.id !== "string"
@@ -113,6 +113,8 @@ function parseJournal(bytes, fileName) {
113
113
  invalidJournal("TODO reconciliation journal is malformed");
114
114
  if (value.activation_effect_sha256 !== undefined && !/^[a-f0-9]{64}$/.test(value.activation_effect_sha256))
115
115
  invalidJournal("TODO reconciliation journal has an invalid activation effect authorization");
116
+ if (value.owner_mapping_sha256 !== undefined && !/^[a-f0-9]{64}$/.test(value.owner_mapping_sha256))
117
+ invalidJournal("TODO reconciliation journal has an invalid owner-mapping authorization");
116
118
  if (value.create !== undefined && (!value.create
117
119
  || typeof value.create !== "object"
118
120
  || Array.isArray(value.create)
@@ -140,15 +142,17 @@ function parseJournal(bytes, fileName) {
140
142
  }
141
143
  const body = value.targets;
142
144
  const activates = body.some((target) => target.path === TODO_RECONCILIATION_ACTIVATION_PATH);
143
- if (!activates && value.activation_effect_sha256 !== undefined)
145
+ if (!activates && (value.activation_effect_sha256 !== undefined || value.owner_mapping_sha256 !== undefined))
144
146
  invalidJournal("TODO reconciliation journal effect authorization has no activation target");
147
+ if (value.owner_mapping_sha256 !== undefined && value.activation_effect_sha256 === undefined)
148
+ invalidJournal("TODO reconciliation journal owner-mapping authorization has no effect authorization");
145
149
  const inferredCreate = createReceiptFromTargets(body);
146
150
  if (value.create && (!inferredCreate
147
151
  || value.create.created_id !== inferredCreate.created_id
148
152
  || value.create.request_sha256 !== inferredCreate.request_sha256))
149
153
  invalidJournal("TODO reconciliation create receipt does not match its canonical entity target");
150
- const identity = value.create || value.activation_effect_sha256
151
- ? { ...(value.create ? { create: value.create } : {}), ...(value.activation_effect_sha256 ? { activation_effect_sha256: value.activation_effect_sha256 } : {}), targets: body }
154
+ const identity = value.create || value.activation_effect_sha256 || value.owner_mapping_sha256
155
+ ? { ...(value.create ? { create: value.create } : {}), ...(value.activation_effect_sha256 ? { activation_effect_sha256: value.activation_effect_sha256 } : {}), ...(value.owner_mapping_sha256 ? { owner_mapping_sha256: value.owner_mapping_sha256 } : {}), targets: body }
152
156
  : body;
153
157
  const expectedId = createHash("sha256").update(canonicalRecordJson(identity)).digest("hex").slice(0, 24);
154
158
  if (value.id !== expectedId || (fileName !== undefined && fileName !== `${value.id}.json`)) {
@@ -454,6 +458,12 @@ export function recoverTodoReconciliation(context, sourceRoot, binding, options
454
458
  message: "pending TODO activation does not match the authorized preview effect",
455
459
  recovery: "Retry the exact activation apply command returned by the original preview; no transaction target bytes were changed.",
456
460
  });
461
+ if (journal.owner_mapping_sha256 && options.ownerMappingSha256 !== journal.owner_mapping_sha256)
462
+ reject({
463
+ class: "conflict",
464
+ message: "pending TODO owner correction does not match the supplied owner mapping",
465
+ recovery: "Retry the exact correction input and apply command returned by the original preview; no transaction target bytes were changed.",
466
+ });
457
467
  options.beforeRecovery?.(journal.targets.map((target) => ({
458
468
  path: target.path,
459
469
  before: target.before === null ? null : decode(target.before),
@@ -498,10 +508,12 @@ export function publishTodoReconciliation(context, sourceRoot, binding, targets,
498
508
  || Number(left.path === binding.publicPath) - Number(right.path === binding.publicPath)
499
509
  || left.path.localeCompare(right.path));
500
510
  const body = normalized.map((target) => ({ path: target.path, before: target.before === null ? null : encode(target.before), after: encode(target.after) }));
501
- if (options.activationEffectSha256 && !normalized.some((target) => target.path === TODO_RECONCILIATION_ACTIVATION_PATH))
511
+ if ((options.activationEffectSha256 || options.ownerMappingSha256) && !normalized.some((target) => target.path === TODO_RECONCILIATION_ACTIVATION_PATH))
502
512
  reject({ class: "schema_violation", message: "TODO activation authorization requires its activation target", recovery: "Recompute the complete activation target set from a fresh dry-run; no state was changed." });
503
- const identity = options.create || options.activationEffectSha256
504
- ? { ...(options.create ? { create: options.create } : {}), ...(options.activationEffectSha256 ? { activation_effect_sha256: options.activationEffectSha256 } : {}), targets: body }
513
+ if (options.ownerMappingSha256 && !options.activationEffectSha256)
514
+ reject({ class: "schema_violation", message: "TODO owner-mapping authorization requires its effect authorization", recovery: "Recompute the complete owner correction target set from a fresh dry-run; no state was changed." });
515
+ const identity = options.create || options.activationEffectSha256 || options.ownerMappingSha256
516
+ ? { ...(options.create ? { create: options.create } : {}), ...(options.activationEffectSha256 ? { activation_effect_sha256: options.activationEffectSha256 } : {}), ...(options.ownerMappingSha256 ? { owner_mapping_sha256: options.ownerMappingSha256 } : {}), targets: body }
505
517
  : body;
506
518
  const id = createHash("sha256").update(canonicalRecordJson(identity)).digest("hex").slice(0, 24);
507
519
  if (!normalized.length)
@@ -533,6 +545,7 @@ export function publishTodoReconciliation(context, sourceRoot, binding, targets,
533
545
  mapping_sha256: binding.mappingSha256,
534
546
  ...(options.create ? { create: options.create } : {}),
535
547
  ...(options.activationEffectSha256 ? { activation_effect_sha256: options.activationEffectSha256 } : {}),
548
+ ...(options.ownerMappingSha256 ? { owner_mapping_sha256: options.ownerMappingSha256 } : {}),
536
549
  targets: body,
537
550
  };
538
551
  const bytes = `${JSON.stringify(journal)}\n`;