agentera 3.0.0-dev.47 → 3.0.0-dev.51

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 (35) hide show
  1. package/README.md +15 -4
  2. package/bundle/CHANGELOG.md +4 -4
  3. package/bundle/references/adapters/package-publication.json +4 -4
  4. package/bundle/references/adapters/package-registry.yaml +2 -1
  5. package/bundle/references/artifacts/state-storage-authority.yaml +51 -11
  6. package/bundle/skills/agentera/SKILL.md +11 -2
  7. package/bundle/skills/agentera/schemas/artifacts/plan.yaml +38 -4
  8. package/dist/capabilities/orchestrate/instructions.js +2 -0
  9. package/dist/capabilities/plan/instructions.js +1 -0
  10. package/dist/capabilities/status/instructions.js +5 -1
  11. package/dist/cli/commands/compact.js +125 -3
  12. package/dist/cli/commands/doctor.js +8 -1
  13. package/dist/cli/commands/prime/collectEntityOrientation.js +22 -7
  14. package/dist/cli/commands/prime/collectOrientationState.js +3 -3
  15. package/dist/cli/commands/prime/orientationOutput.js +2 -0
  16. package/dist/cli/commands/state/write.js +1 -5
  17. package/dist/cli/commands/validate.js +1 -1
  18. package/dist/cli/help.js +4 -0
  19. package/dist/cli/orientation/attention.js +4 -2
  20. package/dist/core/developmentInvocation.js +1 -1
  21. package/dist/registries/activationTuples.js +3 -2
  22. package/dist/registries/packagePublication.js +2 -2
  23. package/dist/state/entityStorage.js +9 -6
  24. package/dist/state/planEntities.js +485 -33
  25. package/dist/state/planLineageValidation.js +52 -0
  26. package/dist/state/planReplacementTransaction.js +475 -0
  27. package/dist/state/todoActivationSafety.js +58 -0
  28. package/dist/state/todoDocsEntities.js +22 -17
  29. package/dist/state/todoReconciliationActivation.js +12 -4
  30. package/dist/state/todoReconciliationInspection.js +8 -12
  31. package/dist/state/write/explain.js +32 -1
  32. package/dist/state/write/grammar.js +5 -0
  33. package/dist/state/write/operations.js +1 -0
  34. package/dist/state/write/runtimeOperations.js +6 -4
  35. package/package.json +2 -2
package/README.md CHANGED
@@ -38,18 +38,29 @@ bytes). Status startup returns
38
38
  bytes). Omitted detail names its authoritative recovery command. `doctor`
39
39
  returns detailed read-only evidence and exact user actions. Entity-mode projects
40
40
  with an absent or unsafe TODO reconciliation marker return `action_required`
41
- instead of `ok` or `up_to_date`; use the reported preview command, then the
42
- preview's exact effect-bound apply command:
41
+ instead of `ok` or `up_to_date`. A safe inactive project reports an activation
42
+ preview and its exact effect-bound apply command:
43
43
 
44
44
  ```bash
45
45
  npx -y agentera@next state todo activate --dry-run --format json
46
46
  npx -y agentera@next state todo activate --effect-sha256 EFFECT_SHA256 --yes --format json
47
+ ```
48
+
49
+ An unsafe active project with an existing marker reports the separate repair
50
+ preview and effect-bound apply path:
51
+
52
+ ```bash
47
53
  npx -y agentera@next state todo repair --dry-run --format json
48
54
  npx -y agentera@next state todo repair --effect-sha256 EFFECT_SHA256 --yes --format json
49
55
  ```
50
56
 
51
- `check validate state` reports the same bounded, read-only diagnosis. Healthy
52
- active TODO projections keep the existing output.
57
+ Unsafe inactive evidence (unmatched projections, duplicate public work, stale
58
+ entity status, or prospective resurrection) reports a bounded, content-private
59
+ diagnosis but no effect digest or apply command. It has no repair path before
60
+ activation. Correct the Markdown-owned public rows and Agentera-owned entity
61
+ state, then replan any required mutation. `check validate state` reports the
62
+ same read-only diagnosis. Healthy active TODO projections keep the existing
63
+ output.
53
64
 
54
65
  ## Shared-skill integration
55
66
 
@@ -146,9 +146,9 @@
146
146
  outside the contract. One bounded activation transaction rejects duplicate
147
147
  ID-less rows before requiring canonical IDs for all newly managed rows.
148
148
  - Fixed TODO cutover and activation to require a complete one-to-one inventory,
149
- preserve Markdown-owned status and order, and block implicit mutation. The
150
- effect-complete `agentera state todo activate --dry-run` preview now binds
151
- its confirmed apply to the exact reported targets and effect digest.
149
+ preserve Markdown-owned status and order, and block implicit mutation. Unsafe inactive
150
+ `agentera state todo activate --dry-run` and confirmed activation reject
151
+ resurrection or duplicate effects before an effect digest, apply command, or project mutation.
152
152
  - Fixed unsafe TODO reconciliation recovery and reporting. `agentera state todo
153
153
  repair` collapses only proven duplicate or stale projections without reopening
154
154
  completed work, while `agentera prime`, `agentera doctor`, and `agentera check
@@ -213,7 +213,7 @@
213
213
  - Added persisted plan identity across active-to-archive transitions, deterministic read-only identity for legacy plans, explicit lifecycle provenance, mirrored-path reporting, and structured ambiguity failures for identity collisions.
214
214
  - Added `agentera state plan tasks list` and `agentera state plan tasks get --task N` for bounded active-plan task retrieval with declared task-number order, opaque snapshot cursors, structured not-found and compatibility errors, and exact omission recovery in text and JSON plan output.
215
215
  - Added `agentera state decisions amend --id ID --base-sha256 HASH [--question ... --context ... --alternative-chosen ... --alternative-rejected ... --choice ... --reasoning ... --confidence firm|provisional|exploratory --feeds-into ...] [--dry-run] --format json`: validates and publishes one immutable decision revision entity. Effective reads compose the base, ordered revisions, and current satisfaction entity; stale-base and same-base conflicts fail before publication, and identical retries converge.
216
-
216
+ - Added `agentera state plan replace --predecessor PREDECESSOR_ID --successor SUCCESSOR_ID --format json` for targeted recovery of competing open plans; it atomically selects the named successor, rejects invalid requests without effects, and recovers interruptions without partial state.
217
217
  - Added an executable bounded-retrieval authority and reserved CLI grammar for plan tasks, plans, and objective-scoped experiments, including stable and legacy identity rules, experiment `0`, opaque cursors, explicit omissions, and structured compatibility errors; retrieval execution remains staged for subsequent tasks.
218
218
  - Added one shared skill at `~/.agents/skills/agentera` plus the CLI and entity-backed project state as the active integration contract; host-native plugin, hook, agent, and package surfaces are not shipped.
219
219
  - Defined native-resource cleanup evidence for supported hosts and explicit `agentera upgrade --legacy-cleanup RESOURCE_ID --dry-run|--yes` selection. Cleanup retains only ledger-proven whole resources; Codex descriptor reports preserve shared configuration keys as report-only action-required items without durable key-level ownership.
@@ -284,8 +284,8 @@
284
284
  "sha256": "2b48c82bb7ee10388d9ebb4fa7ea3743ea82a7e9aaedfcd5a858eeedb9d891ae"
285
285
  },
286
286
  "state": {
287
- "count": 36,
288
- "sha256": "a9f371e2f6d49e7cb890767d7dfe26edee11d4a7d28ee35ab6c491c0abafbbf0"
287
+ "count": 37,
288
+ "sha256": "ca87acdb0fda04ba46a52163fc30ec6578c077e61fcb64597f861a36fdce1dc4"
289
289
  },
290
290
  "package": {
291
291
  "count": 66,
@@ -297,8 +297,8 @@
297
297
  }
298
298
  },
299
299
  "total": {
300
- "count": 278,
301
- "sha256": "5767645b4d78d8716e7ea1a283125bcfd43e13634311b33b62bc0a307d1abdb3"
300
+ "count": 279,
301
+ "sha256": "b92cd469f858e9584682d05b4d9bb03c2fc4b905f3485475723786c0a978c138"
302
302
  }
303
303
  }
304
304
  },
@@ -130,6 +130,7 @@ records:
130
130
  - {"path":"CHANGELOG.md","region":"line:212","category":"argument_bearing","classification":"exact_exemption","normalized_sha256":"35f2268ae3281a70b7c584c23e4e57a07fc9ee6625eed48ae252d167e8ba83ea","reason":"Changelog line:212 records exact release history for that scalar; it does not direct current execution."}
131
131
  - {"path":"CHANGELOG.md","region":"line:214","category":"argument_bearing","classification":"exact_exemption","normalized_sha256":"67d177100a3c9892643202a255523b9a84f57aa4c28b8c5dd21973c13cc06c7d","reason":"Changelog line:214 records exact release history for that scalar; it does not direct current execution."}
132
132
  - {"path":"CHANGELOG.md","region":"line:215","category":"argument_bearing","classification":"exact_exemption","normalized_sha256":"39f085892a7b3c6e14905d90ab0c3f4cc58c66e0b5ffc2ab6ce0f88e8182cf98","reason":"Changelog line:215 records exact release history for that scalar; it does not direct current execution."}
133
+ - {"path":"CHANGELOG.md","region":"line:216","category":"argument_bearing","classification":"exact_exemption","normalized_sha256":"a2f0254ae0226ce04de0917832fd1911cf27b1b69a832abcfbfe34c01c8d0d41","reason":"Changelog line:216 records targeted plan-replacement behavior in release history; it does not direct current execution."}
133
134
  - {"path":"CHANGELOG.md","region":"line:219","category":"argument_bearing","classification":"exact_exemption","normalized_sha256":"6093e60d6c57f5bee694523c7fe380cd6a2ebaaff36d13e7ef153f1439789e99","reason":"Changelog line:219 records exact release history for that scalar; it does not direct current execution."}
134
135
  - {"path":"CHANGELOG.md","region":"line:220","category":"argument_bearing","classification":"exact_exemption","normalized_sha256":"3dc02d2ca69c6d89705932cf557ffaa2bc089a294b518c0eef1a449dfb3d5173","reason":"Changelog line:220 records exact release history for that scalar; it does not direct current execution."}
135
136
  - {"path":"CHANGELOG.md","region":"line:224","category":"argument_bearing","classification":"exact_exemption","normalized_sha256":"2aac4a56f628aaca71f8003fd0678fed24771a7b10141759f7192808af70215c","reason":"Changelog line:224 records exact release history for that scalar; it does not direct current execution."}
@@ -247,7 +248,7 @@ records:
247
248
  - {"path":"CHANGELOG.md","region":"line:1058","category":"argument_bearing","classification":"exact_exemption","normalized_sha256":"79e767fd2e4baa00cdfc763c6814870dbefe20b6745d181f82bdba5139c0272f","reason":"Changelog line:1058 records exact release history for that scalar; it does not direct current execution."}
248
249
  - {"path":"CHANGELOG.md","region":"line:1065","category":"argument_bearing","classification":"exact_exemption","normalized_sha256":"d57016b29585bb34604fc36cfce740c4e6b83a2c0fa753467730d6840ecebd34","reason":"Changelog line:1065 records exact release history for that scalar; it does not direct current execution."}
249
250
  - {"path":"CHANGELOG.md","region":"line:1084","category":"argument_bearing","classification":"exact_exemption","normalized_sha256":"42cbb848be0de98624a2d008843e13c40efc7c77a02d7c4f489348d0e73b5b60","reason":"Changelog line:1084 records exact release history for that scalar; it does not direct current execution."}
250
- - {"path":"CHANGELOG.md","region":"line:150","category":"argument_bearing","classification":"exact_exemption","normalized_sha256":"8b012fbbc2511af33d73365a893896571ef9f22fcfc47eb981549ce2a34635aa","reason":"Changelog line:150 records exact release history for that scalar; it does not direct current execution."}
251
+ - {"path":"CHANGELOG.md","region":"line:150","category":"argument_bearing","classification":"exact_exemption","normalized_sha256":"273d7dae87662352c49e5723aaa528efa89c9a7dbc7d43d3cbc54c1a1b9771da","reason":"Changelog line:150 records exact release history for that scalar; it does not direct current execution."}
251
252
  - {"path":"CHANGELOG.md","region":"line:152","category":"argument_bearing","classification":"exact_exemption","normalized_sha256":"6d02d187f8e271cc6acc4b6966344a841615326891969dbec67815e40a51fe81","reason":"Changelog line:152 records exact release history for that scalar; it does not direct current execution."}
252
253
  - {"path":"CHANGELOG.md","region":"line:154","category":"argument_bearing","classification":"exact_exemption","normalized_sha256":"a5dd4c79ebfefc03bd1c6e3d159f362fd2c5f584d6ff8de7928edf88414133a9","reason":"Changelog line:154 records exact release history for that scalar; it does not direct current execution."}
253
254
  - {"path":"DESIGN.md","region":"line:41","category":"argument_bearing","classification":"exact_exemption","normalized_sha256":"650f6966f3048b7a15748d4ff652fff31c696e5c2b29e2268a0d60889251ebf1","reason":"Suite-design authority table names the exact prime context label as a source role; it is vocabulary, not an execution step."}
@@ -620,13 +620,18 @@ entity_target:
620
620
  - boundary: plan
621
621
  artifact: plan
622
622
  independently_mutable: true
623
- relationships: []
623
+ relationships: [previous_plan_archived]
624
624
  implementation: implemented
625
- publication: replace_owned_entity
625
+ publication: replace_owned_entity_with_targeted_replacement_journal
626
+ targeted_replacement_journal:
627
+ path: .agentera/.entity-recovery/plan-replacement/<content-digest>.json
628
+ identity: canonical_operation_identity_and_complete_target_before_after_bytes
629
+ visibility: pending_journal_blocks_plan_reads_until_exact_retry_recovers_or_finishes
630
+ replay: exact_existing_roles_or_create_input_sha256_only
626
631
  record:
627
632
  required_fields: [header, what, why, scope]
628
633
  required_paths: [header.title, header.created, header.status]
629
- forbidden_fields: [id, artifact, stable_id, plan_id, tasks, previous_plan_archived]
634
+ forbidden_fields: [id, artifact, stable_id, plan_id, tasks]
630
635
  identity: >-
631
636
  The canonical plan identity is the bare ten-letter entity envelope id.
632
637
  A composite header.id belongs only to legacy migration source and is
@@ -775,6 +780,16 @@ entity_target:
775
780
  canonical ten-letter ID; each retained legacy exemption admits at
776
781
  most one exact row occurrence per reconciliation and cannot
777
782
  authorize a changed or duplicate row.
783
+ unsafe_inactive: >-
784
+ One read-only pre-publication decision gates both activation preview
785
+ and confirmed apply. Every canonical entity must have one exact
786
+ public projection. Unmatched projections, duplicate public work,
787
+ stale entity public fields, or a prospective resurrection reject
788
+ before effects with bounded, content-private risk. Unsafe inactive
789
+ state emits neither an effect digest nor an apply command. Repair
790
+ requires an activation marker and is not pre-activation recovery;
791
+ owners must correct their source data, and any required mutation
792
+ must be replanned.
778
793
  managed_row_identity: every_activated_managed_checkbox_row_has_one_ten_letter_id
779
794
  unmanaged_regions: checkbox_rows_outside_managed_headings_remain_ordinary_markdown
780
795
  public_path_binding: exact_docs_mapped_relative_path_plus_docs_yaml_snapshot_sha256
@@ -877,6 +892,7 @@ entity_target:
877
892
  relationships:
878
893
  representation: bare target entity ID in the named record field
879
894
  integrity: every non-null target must resolve to exactly one entity with the declared target boundary
895
+ plan_lineage: An optional plan.previous_plan_archived points to one distinct archived predecessor, and that predecessor has at most one successor; reverse lineage is derived from the canonical source record rather than stored in a second mutable field.
880
896
  unresolved_behavior: whole-state validation and migration cutover fail without creating a target
881
897
  declarations:
882
898
  - source: decision_satisfaction
@@ -891,6 +907,10 @@ entity_target:
891
907
  field: plan
892
908
  target: plan
893
909
  cardinality: exactly_one
910
+ - source: plan
911
+ field: previous_plan_archived
912
+ target: plan
913
+ cardinality: zero_or_one
894
914
  - source: plan_task
895
915
  field: depends_on
896
916
  target: plan_task
@@ -2713,11 +2733,11 @@ mutation_grammar:
2713
2733
  verb: archive
2714
2734
  class: simple_transition
2715
2735
  selectors: [--plan]
2716
- preconditions: [entity_mode_marker_is_valid, selected_plan_is_not_open]
2736
+ preconditions: [entity_mode_marker_is_valid, lifecycle_effects_are_derived_from_locked_canonical_entities, selected_plan_is_complete_or_archived_or_force_archivable]
2717
2737
  owned_fields: [id, artifact, plan, header.status]
2718
2738
  input: { mode: none, sources: [], cli_owned_fields: [] }
2719
- recovery: Complete or preserve the selected plan, then retry archive with no content flags; no state was changed.
2720
- examples: ['npx -y agentera@next state plan archive --dry-run --format json']
2739
+ recovery: Archive a complete plan normally. With --force, archive the selected open plan unchanged only after the locked canonical snapshot identifies it; multiple implicit open-plan candidates are rejected without effects.
2740
+ examples: ['npx -y agentera@next state plan archive --dry-run --format json', 'npx -y agentera@next state plan archive --force --dry-run --format json']
2721
2741
  bounds: { max_input_utf8_bytes: 0, max_collection_items: 100 }
2722
2742
  allow_force: true
2723
2743
  fields: [{ flag: --plan, field: plan, kind: string }]
@@ -2726,19 +2746,39 @@ mutation_grammar:
2726
2746
  verb: create
2727
2747
  class: batch_transaction
2728
2748
  selectors: []
2729
- preconditions: [entity_mode_marker_is_valid, input_is_one_complete_plan_document, create_local_task_ordinals_are_sequential, dependency_graph_is_valid]
2749
+ preconditions: [entity_mode_marker_is_valid, lifecycle_effects_are_derived_from_locked_canonical_entities, input_is_one_complete_plan_document, create_local_task_ordinals_are_sequential, dependency_graph_is_valid]
2730
2750
  owned_fields: [id, artifact, header.id, previous_plan_archived, task_ids]
2731
2751
  input:
2732
2752
  mode: structured
2733
2753
  root: complete plan document
2734
2754
  sources: [file, stdin]
2735
2755
  cli_owned_fields: [id, artifact, header.id, previous_plan_archived, task_ids]
2736
- recovery: Run npx -y agentera@next state plan explain --verb create --format json, keep task ordinals and dependencies local to this atomic input, remove CLI-owned fields, and correct the first schema or dependency violation.
2737
- examples: ['npx -y agentera@next state plan create --input plan.yaml --format json']
2756
+ recovery: Run npx -y agentera@next state plan explain --verb create --format json, keep task ordinals and dependencies local to this atomic input, remove CLI-owned fields, and use --force only when the locked canonical snapshot has exactly one open predecessor to archive unchanged.
2757
+ examples: ['npx -y agentera@next state plan create --input plan.yaml --format json', 'npx -y agentera@next state plan create --force --input plan.yaml --format json']
2738
2758
  bounds: { max_input_utf8_bytes: 32768, max_collection_items: 100 }
2739
2759
  allow_force: true
2740
2760
  fields: []
2741
2761
 
2762
+ - artifact: plan
2763
+ verb: replace
2764
+ class: batch_transaction
2765
+ selectors: [--predecessor, --successor]
2766
+ preconditions: [entity_mode_marker_is_valid, lifecycle_effects_are_derived_from_locked_canonical_entities, predecessor_and_existing_successor_are_explicit_or_one_complete_successor_input_is_supplied, named_roles_are_distinct_bare_plan_ids, replacement_leaves_no_unnamed_open_plan, exact_lineage_or_logical_create_input_replay_matches]
2767
+ owned_fields: [id, artifact, header.status, header.id, previous_plan_archived, replacement_input_sha256, task_ids]
2768
+ input:
2769
+ mode: structured
2770
+ root: complete plan document when creating a successor
2771
+ optional: true
2772
+ sources: [file, stdin]
2773
+ structured_sources: [file, stdin]
2774
+ cli_owned_fields: [id, artifact, header.id, previous_plan_archived, replacement_input_sha256, task_ids]
2775
+ recovery: Name one bare predecessor and either one existing bare successor or one complete successor plan input. The operation archives only the named predecessor, derives reverse lineage from the successor, and rejects divergent retries before effects. Competing-open diagnostics retain bounded bare IDs without assigning roles and recover through npx -y agentera@next state plan replace --predecessor PREDECESSOR_ID --successor SUCCESSOR_ID --format json.
2776
+ examples: ['npx -y agentera@next state plan replace --predecessor abcdefghij --successor klmnopqrst --format json', 'npx -y agentera@next state plan replace --predecessor abcdefghij --input plan.yaml --format json']
2777
+ bounds: { max_input_utf8_bytes: 32768, max_collection_items: 100 }
2778
+ fields:
2779
+ - { flag: --predecessor, field: predecessor, kind: string, required: true, description: Bare plan ID to archive as the explicit predecessor. }
2780
+ - { flag: --successor, field: successor, kind: string, description: Existing bare open plan ID to retain as the explicit successor. }
2781
+
2742
2782
  - artifact: health
2743
2783
  verb: append
2744
2784
  class: record_payload
@@ -2811,10 +2851,10 @@ mutation_grammar:
2811
2851
  verb: activate
2812
2852
  class: batch_transaction
2813
2853
  selectors: []
2814
- preconditions: [entity_mode_marker_is_valid, activation_is_absent_or_replay, todo_reconciliation_is_valid, confirmed_effect_sha256_is_authorized_for_preview_or_replay]
2854
+ preconditions: [entity_mode_marker_is_valid, activation_is_absent_or_replay, todo_reconciliation_is_valid, inactive_projection_is_complete_one_to_one_and_safe, confirmed_effect_sha256_is_authorized_for_preview_or_replay]
2815
2855
  owned_fields: [reconciliation, public_document, activation]
2816
2856
  input: { mode: none, sources: [], cli_owned_fields: [] }
2817
- recovery: Preview and review every reported activation effect before explicit confirmed apply; use the returned examples without modification.
2857
+ recovery: Preview and review every reported safe activation effect before explicit confirmed apply; unsafe inactive evidence requires non-mutating owner correction and replanning.
2818
2858
  examples: ['npx -y agentera@next state todo activate --dry-run --format json', 'npx -y agentera@next state todo activate --effect-sha256 EFFECT_SHA256 --yes --format json']
2819
2859
  bounds: { max_input_utf8_bytes: 0, max_collection_items: 256 }
2820
2860
  compacts: true
@@ -229,12 +229,13 @@ Common mutations:
229
229
  - `npx -y agentera@next state decisions append --input <path|-> --format json`
230
230
  - `npx -y agentera@next state decisions amend --id ID --base-sha256 HASH --input <path|-> --format json`
231
231
  - `npx -y agentera@next state decisions update --id ID ... --format json`
232
- - `npx -y agentera@next state plan create --input plan.yaml --format json`
232
+ - `npx -y agentera@next state plan create [--force] --input plan.yaml --format json`
233
233
  - `npx -y agentera@next state plan append [--plan PLAN_ID] --input task.yaml --format json`
234
234
  - `npx -y agentera@next state plan update --id TASK_ID [--plan PLAN_ID] --input task-patch.yaml --format json`
235
235
  - `npx -y agentera@next state plan set-status --id TASK_ID --status STATUS --format json`
236
236
  - `npx -y agentera@next state plan set-plan-status [--plan PLAN_ID] --status complete --format json`
237
- - `npx -y agentera@next state plan archive --format json`
237
+ - `npx -y agentera@next state plan archive [--plan ID] [--force] --format json`
238
+ - `npx -y agentera@next state plan replace --predecessor PREDECESSOR_ID --successor SUCCESSOR_ID --format json`
238
239
  - `npx -y agentera@next state health append --input audit.yaml --format json`
239
240
  - `npx -y agentera@next state todo create --input todo.yaml --format json`
240
241
  - `npx -y agentera@next state todo update --id ID --input todo-patch.yaml --format json`
@@ -255,6 +256,14 @@ ordinals inside one atomic input document; the writer removes them before
255
256
  publishing bare ten-letter plan and task envelope IDs. A legacy composite
256
257
  `header.id` is migration-only and is never a public selector. Post-publication
257
258
  task append/update payloads use only mutable task content and bare task IDs.
259
+ Without `--force`, an open plan blocks creation. With exactly one canonical open
260
+ predecessor, forced create archives that plan without changing task, evaluation,
261
+ or completion history, then records its bare ID in the successor's
262
+ writer-owned `previous_plan_archived` field. Forced archive preserves the same
263
+ history. Multiple implicit open candidates reject before effects. When they
264
+ block a reader or implicit writer, use the targeted replacement command only
265
+ after canonical evidence names both roles; list order never assigns predecessor
266
+ or successor roles.
258
267
 
259
268
  Add `--dry-run` to preview any mutation without publishing it. Artifacts not
260
269
  listed above are outside the typed writer contract and remain governed by their
@@ -260,11 +260,22 @@ PLAN:
260
260
  7:
261
261
  id: PL22
262
262
  field: previous_plan_archived
263
- type: string
263
+ type: bare_ten_letter_id
264
+ required: false
265
+ description: >-
266
+ Writer-owned bare ID of the predecessor archived when this successor was
267
+ created. The predecessor's archived lifecycle and this successor reference
268
+ provide bidirectional lineage without changing task, evaluation, or
269
+ completion history.
270
+ 8:
271
+ id: PL23
272
+ field: replacement_input_sha256
273
+ type: lowercase_sha256_hex
264
274
  required: false
265
275
  description: >-
266
- Path to the previous active plan archived when this plan was created.
267
- Preserves plan lineage without changing lifecycle status.
276
+ Writer-owned immutable logical identity for a targeted replacement
277
+ successor. It excludes mutable task lifecycle and evaluation fields, so an
278
+ exact replacement input can replay after task execution changes.
268
279
 
269
280
  UNKNOWN:
270
281
  1:
@@ -545,9 +556,31 @@ LIFECYCLE_CONTRACT:
545
556
  execution:
546
557
  eligible: Only a plan at the current-plan path can supply executable work.
547
558
  archived: An archived plan is historical and non-executable regardless of status.
559
+ lineage:
560
+ field: previous_plan_archived
561
+ representation: bare predecessor plan entity ID
562
+ publication: The writer assigns the field only while atomically archiving a predecessor and publishing its successor.
563
+ reverse: A predecessor's successor is derived from the unique canonical plan record that names its ID in previous_plan_archived; no second mutable lineage field exists.
564
+ replacement_input_identity:
565
+ field: replacement_input_sha256
566
+ representation: lowercase SHA-256 of the normalized logical replacement input
567
+ ownership: Writer-owned at targeted replacement publication and immutable afterward.
568
+ exclusions: [task.status, task.evaluation, task.superseded_by, task.superseded_reason]
548
569
  forced_archive:
549
570
  unfinished_status: archived
550
- effect: Move the plan to immutable history without claiming completion or changing tasks.
571
+ effect: Change the plan lifecycle to archived without claiming completion or changing tasks, evaluations, or completion history.
572
+ forced_create:
573
+ no_open_predecessor: Publish the successor normally.
574
+ one_open_predecessor: --force archives the predecessor unchanged and publishes the successor with previous_plan_archived set to the predecessor bare ID.
575
+ multiple_open_predecessors: Reject before effects because implicit creation cannot choose a predecessor.
576
+ targeted_replacement:
577
+ command: state plan replace --predecessor PREDECESSOR (--successor SUCCESSOR | --input PLAN.yaml)
578
+ existing_successor: Names both bare plan IDs, archives only the named predecessor, and assigns previous_plan_archived on the named open successor.
579
+ create_successor: Names the bare predecessor and supplies one complete plan document when no successor exists.
580
+ selection: Reject before effects if an unnamed open plan would remain, roles are invalid, or the requested successor conflicts with canonical lineage.
581
+ diagnostics: Competing-open readers and implicit writers expose bounded bare candidate IDs and the state plan replace --predecessor PREDECESSOR_ID --successor SUCCESSOR_ID recovery grammar. Canonical state never infers predecessor or successor roles from list order.
582
+ replay: The archived predecessor and unique derived successor identify an existing-successor replay; create retries compare the exact input identity retained on the successor rather than mutable task state.
583
+ atomicity: The complete predecessor, successor, and created-task target set is durable under the private entity recovery authority before any target is visible. Pending journals block plan reads until the exact retry finishes or restores the operation.
551
584
  replacement_evidence_transition:
552
585
  predicate: A replacement is complete and its latest persisted evaluation.last_verdict is pass.
553
586
  supersession: Every superseded_by replacement must satisfy the predicate before a new supersession is published.
@@ -720,6 +753,7 @@ LIFECYCLE_CONTRACT:
720
753
  documented_external_commitments:
721
754
  - packages/cli/src/capabilities/*/instructions.ts
722
755
  - packages/cli/README.md
756
+ - .opencode/skills/*/SKILL.md
723
757
  - skills/agentera/SKILL.md
724
758
  - skills/agentera/references/*
725
759
  - references/**/*.md
@@ -8,6 +8,8 @@ Plans, tasks, progress cycles, health audits, and decisions use entity authority
8
8
 
9
9
  Advance tasks with \`agentera state plan set-status --id ID\` and persist evaluator outcomes with \`agentera state plan record-evaluation --id ID\`. A superseded task is terminal, satisfies dependencies, and stays out of ready and blocked queues; record it only through \`agentera state plan supersede --id ID --by COMPLETED_ID --reason "..." --format json\`. Every replacement must be complete with latest persisted PASS before supersession.
10
10
 
11
+ When competing open plans block startup, do not infer predecessor or successor roles from list order. Use \`agentera state plan replace --predecessor PREDECESSOR_ID --successor SUCCESSOR_ID --format json\` only after canonical evidence establishes the complete recovery pair.
12
+
11
13
  ## The orchestration loop`)
12
14
  .replaceAll("--number N", "--id ID")
13
15
  .replaceAll("--task N", "--id ID")
@@ -28,5 +28,6 @@ Advice, clarification, tension, and handoff intent are transient and never write
28
28
  ## Workflow phases`)
29
29
  .replaceAll("--number N", "--id ID")
30
30
  .replaceAll("--task N", "--id ID")
31
+ .replace("Archive completed plan entities with `agentera state plan archive --format json`.", "Archive completed plan entities with `agentera state plan archive --format json`. With exactly one unfinished canonical predecessor, `agentera state plan create --force --input PATH --format json` archives it unchanged, publishes the successor, and records its bare ID in the writer-owned `previous_plan_archived` field. `agentera state plan archive --force` likewise preserves unfinished task, evaluation, and completion history; multiple implicit open candidates reject before effects. When competing open plans block selection, do not infer roles from list order: use `agentera state plan replace --predecessor PREDECESSOR_ID --successor SUCCESSOR_ID --format json` only after canonical evidence establishes the complete recovery pair.")
31
32
  .replace("served via `planning_context.profile.path` — read directly when `status: loaded`; if missing or stale, proceed without persona grounding", "served through `agentera report profile-grounding --format json` — use only returned `content` when shared validity is `valid`; if absent or repair_needed, proceed without persona grounding and preserve exact recovery")
32
33
  .replaceAll("planning_context.profile.path", "the shared profile validity summary");
@@ -8,4 +8,8 @@ export default `${instructions
8
8
 
9
9
  ### Profile validity
10
10
 
11
- Render the shared profile validity exactly as \`valid\`, \`absent\`, or \`repair_needed\`. Freshness is separate and may be \`current\`, \`stale\`, or \`unknown\`; only valid stale input may carry stale guidance. Repair-needed input preserves its repair recovery and MUST NOT receive refresh advice. Never render profile path or content.`;
11
+ Render the shared profile validity exactly as \`valid\`, \`absent\`, or \`repair_needed\`. Freshness is separate and may be \`current\`, \`stale\`, or \`unknown\`; only valid stale input may carry stale guidance. Repair-needed input preserves its repair recovery and MUST NOT receive refresh advice. Never render profile path or content.
12
+
13
+ ### Plan selection
14
+
15
+ Render \`status_context.plan.id\` as the canonical selected plan identity. If competing open plans block startup, preserve the failure and its recovery verbatim. Do not infer predecessor or successor roles from a title or list order; only run \`agentera state plan replace --predecessor PREDECESSOR_ID --successor SUCCESSOR_ID --format json\` after canonical evidence establishes both roles.`;
@@ -1,7 +1,108 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import YAML, { isMap, isScalar, isSeq } from "yaml";
1
4
  import { resolvePath } from "../../core/paths.js";
2
5
  import { runCompaction } from "../../hooks/compaction/index.js";
3
6
  import { emitStructured } from "../structured.js";
4
7
  import { boundStructuredProjection } from "../../state/projectionPolicy.js";
8
+ const TODO_REFERENCE_DIAGNOSTIC_LIMIT = 20;
9
+ function compareText(left, right) {
10
+ return left < right ? -1 : left > right ? 1 : 0;
11
+ }
12
+ function isDirectory(directory) {
13
+ try {
14
+ const stat = fs.lstatSync(directory);
15
+ return stat.isDirectory() && !stat.isSymbolicLink();
16
+ }
17
+ catch {
18
+ return false;
19
+ }
20
+ }
21
+ function yamlFilesBelow(directory) {
22
+ if (!isDirectory(directory))
23
+ return [];
24
+ return fs.readdirSync(directory, { withFileTypes: true })
25
+ .sort((left, right) => compareText(left.name, right.name))
26
+ .flatMap((entry) => {
27
+ const candidate = path.join(directory, entry.name);
28
+ if (entry.isDirectory() && !entry.isSymbolicLink())
29
+ return yamlFilesBelow(candidate);
30
+ return entry.isFile() && path.extname(entry.name) === ".yaml" ? [candidate] : [];
31
+ });
32
+ }
33
+ function activeStateYamlFiles(project) {
34
+ const stateRoot = path.join(project, ".agentera");
35
+ if (!isDirectory(stateRoot))
36
+ return [];
37
+ const topLevel = fs.readdirSync(stateRoot, { withFileTypes: true })
38
+ .filter((entry) => entry.isFile() && path.extname(entry.name) === ".yaml")
39
+ .map((entry) => path.join(stateRoot, entry.name));
40
+ return [...topLevel, ...yamlFilesBelow(path.join(stateRoot, "entities"))]
41
+ .sort(compareText);
42
+ }
43
+ function stringScalarValues(node) {
44
+ if (isScalar(node))
45
+ return typeof node.value === "string" ? [node.value] : [];
46
+ if (isMap(node))
47
+ return node.items.flatMap((pair) => stringScalarValues(pair.value));
48
+ if (isSeq(node))
49
+ return node.items.flatMap(stringScalarValues);
50
+ return [];
51
+ }
52
+ function volatileTodoReferenceDiagnostics(project) {
53
+ const matches = [];
54
+ for (const file of activeStateYamlFiles(project)) {
55
+ const relativePath = path.relative(project, file).split(path.sep).join("/");
56
+ const contents = fs.readFileSync(file, "utf8");
57
+ let order = 0;
58
+ for (const document of YAML.parseAllDocuments(contents)) {
59
+ for (const value of stringScalarValues(document.contents)) {
60
+ for (const match of value.matchAll(/TODO line \d+|TODO\.md:\d+/g)) {
61
+ matches.push({ path: relativePath, reference: match[0], order: order++ });
62
+ }
63
+ }
64
+ }
65
+ }
66
+ matches.sort((left, right) => compareText(left.path, right.path) || left.order - right.order);
67
+ return {
68
+ diagnostics: matches.slice(0, TODO_REFERENCE_DIAGNOSTIC_LIMIT).map(({ path, reference }) => ({ path, reference })),
69
+ omitted_count: Math.max(0, matches.length - TODO_REFERENCE_DIAGNOSTIC_LIMIT),
70
+ };
71
+ }
72
+ function volatileTodoReferenceGate(project, mode) {
73
+ const { diagnostics, omitted_count } = volatileTodoReferenceDiagnostics(project);
74
+ if (!diagnostics.length)
75
+ return null;
76
+ const count = diagnostics.length + omitted_count;
77
+ return {
78
+ status: {
79
+ artifact: "state_todo_references",
80
+ path: path.join(project, ".agentera"),
81
+ classification: "hygiene",
82
+ active_count: count,
83
+ archive_count: 0,
84
+ total_count: count,
85
+ over_limit_count: 0,
86
+ protected_overflow_count: 0,
87
+ exists: true,
88
+ reason: `${count} active state TODO reference${count === 1 ? "" : "s"} use volatile line syntax`,
89
+ },
90
+ mode,
91
+ action: "volatile_todo_reference",
92
+ changed: false,
93
+ result: null,
94
+ message: "replace TODO line references with stable TODO anchors",
95
+ diagnostics,
96
+ omitted_count,
97
+ };
98
+ }
99
+ function gatedCompactionOperations(project, mode) {
100
+ const hygiene = volatileTodoReferenceGate(project, mode);
101
+ if (mode === "fix" && hygiene)
102
+ return [hygiene];
103
+ const operations = runCompaction(project, mode);
104
+ return hygiene ? [...operations, hygiene] : operations;
105
+ }
5
106
  function pyStr(value) {
6
107
  if (value === null || value === undefined)
7
108
  return "None";
@@ -33,6 +134,12 @@ function compactionOperationPayload(op) {
33
134
  message: op.message,
34
135
  reason: status.reason,
35
136
  ...(status.projection_recovery ? { recovery: status.projection_recovery } : {}),
137
+ ...(op.diagnostics
138
+ ? {
139
+ diagnostics: op.diagnostics.map(({ path, reference }) => ({ path, reference })),
140
+ omitted_count: op.omitted_count ?? 0,
141
+ }
142
+ : {}),
36
143
  };
37
144
  if (op.result !== null) {
38
145
  const recovery = op.result.recovery;
@@ -57,11 +164,15 @@ function compactionGuidance(mode, operations) {
57
164
  const protectedOps = operations.filter((op) => op.action === "protected_overflow");
58
165
  const errors = operations.filter((op) => op.action === "error");
59
166
  const refused = operations.filter((op) => op.action === "refused");
167
+ const volatileTodoReferences = operations.filter((op) => op.action === "volatile_todo_reference");
60
168
  const checkCommand = "npx -y agentera check compact --mode check --format json";
61
169
  const fixCommand = "npx -y agentera check compact --mode fix --format json";
62
170
  if (errors.length > 0) {
63
171
  return `Inspect the reported errors, repair invalid artifacts, then rerun \`${checkCommand}\`.`;
64
172
  }
173
+ if (volatileTodoReferences.length > 0) {
174
+ return `Replace volatile TODO line references in active state with stable anchors, then rerun \`${checkCommand}\`.`;
175
+ }
65
176
  if (refused.length > 0) {
66
177
  return "Projection changes were safely refused until each full entry has a verified numbered archive; inspect recovery metadata and retry after recovery.";
67
178
  }
@@ -97,8 +208,9 @@ function compactionSummary(mode, operations) {
97
208
  const formatting = operations.filter((op) => op.action === "formatting" || op.action === "pending_formatting").length;
98
209
  const protectedOverflow = operations.filter((op) => op.action === "protected_overflow").length;
99
210
  const errors = operations.filter((op) => op.action === "error").length;
211
+ const volatileTodoReferences = operations.filter((op) => op.action === "volatile_todo_reference").length;
100
212
  const changed = operations.filter((op) => op.changed).length;
101
- const status = errors || (mode === "check" && (overLimit || formatting)) ? "fail" : "pass";
213
+ const status = errors || volatileTodoReferences || (mode === "check" && (overLimit || formatting)) ? "fail" : "pass";
102
214
  return {
103
215
  status,
104
216
  mode,
@@ -116,6 +228,8 @@ function compactionSummary(mode, operations) {
116
228
  function compactionExitCode(mode, operations) {
117
229
  if (operations.some((op) => op.action === "error"))
118
230
  return 2;
231
+ if (operations.some((op) => op.action === "volatile_todo_reference"))
232
+ return 1;
119
233
  if (mode === "check" &&
120
234
  operations.some((op) => op.action === "over_limit" || op.action === "formatting"))
121
235
  return 1;
@@ -154,6 +268,14 @@ function emitCompactionPayload(payload, mode, format, out) {
154
268
  `pending_summarization=${pyStr(item.pending_summarization_count)} | ` +
155
269
  `protected_overflow=${item.protected_overflow_count} | ` +
156
270
  `message=${item.message}\n`);
271
+ if (Array.isArray(item.diagnostics)) {
272
+ for (const diagnostic of item.diagnostics) {
273
+ if (!diagnostic || typeof diagnostic !== "object" || Array.isArray(diagnostic))
274
+ continue;
275
+ out(` diagnostic=${String(diagnostic.path)}: ${String(diagnostic.reference)}\n`);
276
+ }
277
+ out(` omitted_count=${pyStr(item.omitted_count)}\n`);
278
+ }
157
279
  }
158
280
  out(`guidance=${summary.guidance}\n`);
159
281
  }
@@ -161,7 +283,7 @@ export function cmdCompact(args, io = {}) {
161
283
  const out = io.out ?? ((t) => process.stdout.write(t));
162
284
  const mode = args.mode ?? "check";
163
285
  const project = resolvePath(args.project ?? process.cwd());
164
- const operations = runCompaction(project, mode);
286
+ const operations = gatedCompactionOperations(project, mode);
165
287
  const payload = compactionPayload("check compact", project, mode, operations);
166
288
  emitCompactionPayload(payload, mode, args.format ?? "text", out);
167
289
  return compactionExitCode(mode, operations);
@@ -169,7 +291,7 @@ export function cmdCompact(args, io = {}) {
169
291
  export function cmdGate(args, io = {}) {
170
292
  const out = io.out ?? ((t) => process.stdout.write(t));
171
293
  const project = resolvePath(args.project ?? process.cwd());
172
- const operations = runCompaction(project, "check");
294
+ const operations = gatedCompactionOperations(project, "check");
173
295
  const payload = compactionPayload("check compact", project, "check", operations);
174
296
  emitCompactionPayload(payload, "check", args.format ?? "text", out);
175
297
  return compactionExitCode("check", operations);
@@ -87,11 +87,14 @@ export function renderDoctorStatus(status, retiredResources) {
87
87
  lines.push(` Preview: ${signal.previewCommand}`);
88
88
  if (signal.applyCommand)
89
89
  lines.push(` Apply: ${signal.applyCommand}`);
90
+ if (signal.reconciliationState === "unsafe_inactive" && signal.recoveryCommand)
91
+ lines.push(` Recovery: ${signal.recoveryCommand}`);
90
92
  }
91
93
  }
92
94
  }
93
95
  const resources = Array.isArray(retiredResources?.resources) ? retiredResources.resources : [];
94
96
  const todoReconciliation = status.signals?.some((signal) => signal.kind === "todo_reconciliation") ?? false;
97
+ const unsafeInactiveTodo = status.signals?.some((signal) => signal.kind === "todo_reconciliation" && signal.reconciliationState === "unsafe_inactive") ?? false;
95
98
  if (resources.length > 0) {
96
99
  lines.push("");
97
100
  lines.push("Retired native resources:");
@@ -116,6 +119,10 @@ export function renderDoctorStatus(status, retiredResources) {
116
119
  lines.push(" 3. Then retry Agentera once a retry command is available.");
117
120
  }
118
121
  }
122
+ else if (unsafeInactiveTodo) {
123
+ lines.push("");
124
+ lines.push("Next: complete the owner correction and replan before any TODO mutation.");
125
+ }
119
126
  else if (todoReconciliation) {
120
127
  lines.push("");
121
128
  lines.push("Next: run the reported TODO reconciliation preview, review its bounded effect, then use its exact apply_command.");
@@ -170,7 +177,7 @@ export function cmdDoctor(args, io = {}) {
170
177
  status.signals.push({
171
178
  status: APP_MANUAL_REVIEW_NEEDED,
172
179
  kind: "todo_reconciliation",
173
- message: `action-required: TODO reconciliation is ${todoReconciliation.state === "inactive" ? "inactive" : todoReconciliation.state === "unsafe_active" ? "unsafe active" : "in an invalid lifecycle state"}`,
180
+ message: `action-required: TODO reconciliation is ${todoReconciliation.state === "inactive" ? "inactive" : todoReconciliation.state === "unsafe_inactive" ? "unsafe inactive" : todoReconciliation.state === "unsafe_active" ? "unsafe active" : "in an invalid lifecycle state"}`,
174
181
  reconciliationState: todoReconciliation.state,
175
182
  reconciliationCounts: todoReconciliation.counts,
176
183
  reconciliationOmittedCount: todoReconciliation.omitted_count,