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
@@ -18,9 +18,10 @@ 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_REPAIR_APPLY_COMMAND, TODO_REPAIR_PREVIEW_COMMAND, todoActivationEffect, todoRepairEffect, unchangedTodoActivationEffect, } from "./todoReconciliationActivation.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
22
  import { planTodoRepair } from "./todoReconciliationRepair.js";
23
23
  import { readTodoMarkdown, renderManagedMarkdown } from "./todoMarkdownProjection.js";
24
+ import { inactiveTodoActivationSafety, rejectUnsafeInactiveTodoActivation, unsafeInactiveDuplicateDiagnosis } from "./todoActivationSafety.js";
24
25
  const ID = /^[a-z]{10}$/;
25
26
  const SHA256 = /^[a-f0-9]{64}$/;
26
27
  const TODO = { artifact: "todo", boundary: "todo_item", order: "severity_then_status_then_markdown_order_then_id" };
@@ -194,11 +195,20 @@ export function managedRows(markdown, activation, entities) {
194
195
  if (ambiguous)
195
196
  reject({
196
197
  class: "conflict",
197
- message: `pre-activation TODO contains identical ID-less managed rows at lines ${ambiguous.map((row) => row.line + 1).join(", ")}`,
198
+ message: `pre-activation TODO contains identical ID-less managed rows at lines ${ambiguous.slice(0, TODO_ACTIVATION_RISK_LIMIT).map((row) => row.line + 1).join(", ")}${ambiguous.length > TODO_ACTIVATION_RISK_LIMIT ? `; ${ambiguous.length - TODO_ACTIVATION_RISK_LIMIT} additional lines omitted` : ""}`,
199
+ diagnosis: unsafeInactiveDuplicateDiagnosis(),
198
200
  recovery: "Give each identical row its distinct canonical '[id:abcdefghij]' tag or remove the duplicate, then retry once; no state was changed.",
199
201
  });
200
202
  const claimed = new Set(result.keys());
201
203
  for (const row of legacy) {
204
+ const duplicate = entities.find((entity) => entity.boundary === TODO.boundary && entity.id && entity.record && claimed.has(entity.id) && samePublic(publicSnapshot(entity.record), rowSnapshot({ ...row, id: entity.id }, entity.record), false));
205
+ if (duplicate)
206
+ reject({
207
+ class: "conflict",
208
+ message: `pre-activation TODO row at line ${row.line + 1} duplicates canonical public work`,
209
+ diagnosis: unsafeInactiveDuplicateDiagnosis(),
210
+ recovery: "Restore exactly one public row for each canonical entity before activation, then retry once; no state was changed.",
211
+ });
202
212
  const matches = entities.filter((entity) => {
203
213
  if (entity.boundary !== TODO.boundary || !entity.id || !entity.record || claimed.has(entity.id))
204
214
  return false;
@@ -208,6 +218,7 @@ export function managedRows(markdown, activation, entities) {
208
218
  reject({
209
219
  class: "conflict",
210
220
  message: `pre-activation TODO row at line ${row.line + 1} matches multiple canonical entities`,
221
+ diagnosis: unsafeInactiveDuplicateDiagnosis(matches.length),
211
222
  recovery: `Add one exact '[id:abcdefghij]' tag to TODO.md line ${row.line + 1} and retry once; no state was changed.`,
212
223
  });
213
224
  const matched = matches[0];
@@ -222,6 +233,7 @@ export function managedRows(markdown, activation, entities) {
222
233
  reject({
223
234
  class: "conflict",
224
235
  message: `pre-activation TODO contains duplicate unmatched legacy rows at line ${row.line + 1}`,
236
+ diagnosis: unsafeInactiveDuplicateDiagnosis(),
225
237
  recovery: `Give each duplicate row a distinct canonical '[id:abcdefghij]' tag or move it outside managed sections, then retry once; no state was changed.`,
226
238
  });
227
239
  retainedLegacyRows.push(fingerprint);
@@ -379,13 +391,8 @@ export function projectTodoReadEntities(root, sourceRoot = resolveSourceRoot(),
379
391
  };
380
392
  });
381
393
  }
382
- function publicReadRecord(record, row) {
383
- return row ? importMarkdown(record, row) : record;
384
- }
385
- function publicReadMetadata(record, row) {
386
- const value = row ? rowSnapshot(row, record) : { present: false };
387
- return { ...value, owner: "markdown", source: "TODO.md" };
388
- }
394
+ function publicReadRecord(record, row) { return row ? importMarkdown(record, row) : record; }
395
+ function publicReadMetadata(record, row) { const value = row ? rowSnapshot(row, record) : { present: false }; return { ...value, owner: "markdown", source: "TODO.md" }; }
389
396
  function importMarkdown(record, row) {
390
397
  const result = structuredClone(record);
391
398
  const description = row.item.public_description ?? row.item.description;
@@ -401,16 +408,11 @@ function importMarkdown(record, row) {
401
408
  result.status = row.item.status;
402
409
  return result;
403
410
  }
404
- function withBaseline(record, value) {
405
- const publicValue = Object.fromEntries(Object.entries(value).filter(([, field]) => field !== undefined));
406
- return { ...record, reconciliation: { schema_version: RECONCILIATION_VERSION, public: publicValue } };
407
- }
411
+ function withBaseline(record, value) { const publicValue = Object.fromEntries(Object.entries(value).filter(([, field]) => field !== undefined)); return { ...record, reconciliation: { schema_version: RECONCILIATION_VERSION, public: publicValue } }; }
408
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.` }); }
409
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 } }; }
410
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 } }; }
411
- function envelope(command, entity, artifact, record, dryRun) {
412
- 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: [] } };
413
- }
415
+ 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: [] } }; }
414
416
  function targetPath(root, sourceRoot, artifact, id) {
415
417
  const model = definition(artifact);
416
418
  return path.join(root, contract(model.boundary, sourceRoot).entityRoot, artifact, model.boundary, `${id}.yaml`);
@@ -718,6 +720,9 @@ export function mutateTodoDocsEntity(req, options = {}) {
718
720
  }
719
721
  const scan = managedRows(markdown, null, todoEntities);
720
722
  const rows = scan.rows;
723
+ const safety = inactiveTodoActivationSafety(scan, todoEntities);
724
+ if (!safety.safe)
725
+ rejectUnsafeInactiveTodoActivation(safety);
721
726
  const reconciled = reconcileTodoRecords(todoEntities, rows, true);
722
727
  for (const [todoId, record] of reconciled.records) {
723
728
  const violations = recordViolations("todo", record, sourceRoot);
@@ -738,7 +743,7 @@ export function mutateTodoDocsEntity(req, options = {}) {
738
743
  const targets = todoEntities.map((entity) => ({ path: entity.relativePath, before: exactDiscoveredEntityBytes(entity), after: canonicalEntityEnvelopeBytes({ id: entity.id, artifact: "todo", record: reconciled.records.get(entity.id), migrationProvenance: entity.migrationProvenance ?? undefined }) }));
739
744
  targets.push({ path: TODO_RECONCILIATION_ACTIVATION_PATH, before: null, after: activationBytesAfter });
740
745
  targets.push({ path: publicRelative, before: publicExists ? markdownBefore : null, after: rendered });
741
- const resurrectedIds = [...reconciled.visible].filter((todoId) => { const row = rows.get(todoId); return !row || (reconciled.records.get(todoId)?.status === "resolved" && !row.item.id); }).sort();
746
+ const resurrectedIds = safety.resurrectedIds;
742
747
  const preliminaryEffect = todoActivationEffect(scan, targets, publicRelative, markdownBefore, rendered, resurrectedIds);
743
748
  activationBytesAfter = todoReconciliationActivationBytes(scan.retainedLegacyRows, String(preliminaryEffect.effect_sha256));
744
749
  targets.find((target) => target.path === TODO_RECONCILIATION_ACTIVATION_PATH).after = activationBytesAfter;
@@ -13,7 +13,8 @@ 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
- const CHANGE_LIMIT = 20;
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.";
17
+ export const TODO_ACTIVATION_RISK_LIMIT = 20;
17
18
  function activationFailure(message) {
18
19
  reject({
19
20
  class: "conflict",
@@ -74,6 +75,13 @@ export function loadTodoReconciliationActivation(root) {
74
75
  return { record: record, bytes };
75
76
  }
76
77
  function sha256(bytes) { return createHash("sha256").update(bytes).digest("hex"); }
78
+ export function todoActivationRisks(resurrectedIds) {
79
+ return {
80
+ resurrected_count: resurrectedIds.length,
81
+ resurrected_ids: resurrectedIds.slice(0, TODO_ACTIVATION_RISK_LIMIT),
82
+ omitted_count: Math.max(0, resurrectedIds.length - TODO_ACTIVATION_RISK_LIMIT),
83
+ };
84
+ }
77
85
  function boundedPublicChanges(before, after) {
78
86
  const beforeLines = before.split(/\r?\n/);
79
87
  const afterLines = after.split(/\r?\n/);
@@ -83,7 +91,7 @@ function boundedPublicChanges(before, after) {
83
91
  if (beforeLines[index] === afterLines[index])
84
92
  continue;
85
93
  count += 1;
86
- if (changes.length < CHANGE_LIMIT)
94
+ if (changes.length < TODO_ACTIVATION_RISK_LIMIT)
87
95
  changes.push({ line: index + 1, before: (beforeLines[index] ?? "").slice(0, 240), after: (afterLines[index] ?? "").slice(0, 240) });
88
96
  }
89
97
  return { count, items: changes, omitted_count: count - changes.length };
@@ -93,13 +101,13 @@ export function todoActivationEffect(scan, targets, publicPath, markdownBefore,
93
101
  const evidence = {
94
102
  counts: { matched: scan.matchedRows, converted: scan.convertedRows, retained: scan.retainedLegacyRows.length, conflicting: 0 }, targets: changedTargets,
95
103
  public_document: { path: publicPath, changed: !markdownBefore.equals(Buffer.from(rendered)), before_bytes: markdownBefore.length, after_bytes: Buffer.byteLength(rendered), before_sha256: sha256(markdownBefore), after_sha256: sha256(rendered), changed_lines: boundedPublicChanges(markdownBefore.toString("utf8"), rendered) },
96
- risks: { resurrected_count: resurrectedIds.length, resurrected_ids: resurrectedIds.slice(0, CHANGE_LIMIT), omitted_count: Math.max(0, resurrectedIds.length - CHANGE_LIMIT) },
104
+ risks: todoActivationRisks(resurrectedIds),
97
105
  };
98
106
  const authorizedTargets = changedTargets.map((target) => target.path === TODO_RECONCILIATION_ACTIVATION_PATH ? { ...target, after_sha256: "activation_effect_authorization" } : target);
99
107
  return { ...evidence, effect_sha256: sha256(canonicalRecordJson({ ...evidence, targets: authorizedTargets })) };
100
108
  }
101
109
  export function unchangedTodoActivationEffect(publicPath, markdown, retained, authorizedEffectSha256) {
102
- const evidence = { counts: { matched: 0, converted: 0, retained, conflicting: 0 }, targets: [], public_document: { path: publicPath, changed: false, before_bytes: markdown.length, after_bytes: markdown.length, before_sha256: sha256(markdown), after_sha256: sha256(markdown), changed_lines: { count: 0, items: [], omitted_count: 0 } }, risks: { resurrected_count: 0, resurrected_ids: [], omitted_count: 0 } };
110
+ const evidence = { counts: { matched: 0, converted: 0, retained, conflicting: 0 }, targets: [], public_document: { path: publicPath, changed: false, before_bytes: markdown.length, after_bytes: markdown.length, before_sha256: sha256(markdown), after_sha256: sha256(markdown), changed_lines: { count: 0, items: [], omitted_count: 0 } }, risks: todoActivationRisks([]) };
103
111
  return { ...evidence, effect_sha256: authorizedEffectSha256 ?? sha256(canonicalRecordJson(evidence)) };
104
112
  }
105
113
  export function todoRepairEffect(diagnosis, targets, publicPath, markdownBefore, rendered) {
@@ -5,9 +5,10 @@ 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, loadTodoReconciliationActivation, todoReconciliationActivationBytes, } from "./todoReconciliationActivation.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";
9
9
  import { planTodoRepair } from "./todoReconciliationRepair.js";
10
10
  import { readTodoMarkdown } from "./todoMarkdownProjection.js";
11
+ import { inactiveTodoActivationSafety } from "./todoActivationSafety.js";
11
12
  import { managedRows, relevant, todoPublicPath } from "./todoDocsEntities.js";
12
13
  const DIAGNOSTIC_LIMIT = 20;
13
14
  function mapping(value) {
@@ -43,7 +44,7 @@ function invalidLifecycle() {
43
44
  recovery_command: "Restore the valid committed TODO reconciliation lifecycle metadata, then rerun `npx -y agentera@next check validate state --format json`; no state was changed.",
44
45
  };
45
46
  }
46
- function inspection(state, rawCounts) {
47
+ function inspection(state, rawCounts, risks) {
47
48
  const bounded = boundedCounts(rawCounts);
48
49
  const active = state === "healthy_active" || state === "unsafe_active";
49
50
  const preview = state === "inactive" ? TODO_ACTIVATION_PREVIEW_COMMAND : active ? TODO_REPAIR_PREVIEW_COMMAND : null;
@@ -60,9 +61,10 @@ function inspection(state, rawCounts) {
60
61
  conflicting: bounded.conflicting,
61
62
  },
62
63
  omitted_count: bounded.omitted,
64
+ ...(risks === undefined ? {} : { risks }),
63
65
  preview_command: preview,
64
66
  apply_command: apply,
65
- recovery_command: preview && apply ? recovery(preview, apply) : "",
67
+ recovery_command: state === "unsafe_inactive" ? TODO_UNSAFE_INACTIVE_RECOVERY : preview && apply ? recovery(preview, apply) : "",
66
68
  };
67
69
  }
68
70
  function errorCounts(error) {
@@ -104,17 +106,11 @@ export function inspectTodoReconciliationState(root, sourceRoot = resolveSourceR
104
106
  if (!activation) {
105
107
  try {
106
108
  const scan = managedRows(readTodoMarkdown(todoPublicPath(root, sourceRoot)).text, null, entities);
107
- const entityIds = new Set(entities.map(({ id }) => id));
108
- const orphaned = [...scan.rows.keys()].filter((id) => !entityIds.has(id)).length;
109
- return inspection("inactive", {
110
- matched: scan.matchedRows,
111
- converted: scan.convertedRows,
112
- retained: scan.retainedLegacyRows.length,
113
- conflicting: orphaned,
114
- });
109
+ const safety = inactiveTodoActivationSafety(scan, entities);
110
+ return inspection(safety.safe ? "inactive" : "unsafe_inactive", safety.counts, safety.safe ? undefined : safety.risks);
115
111
  }
116
112
  catch (error) {
117
- return inspection("inactive", errorCounts(error));
113
+ return inspection("unsafe_inactive", errorCounts(error));
118
114
  }
119
115
  }
120
116
  try {
@@ -70,6 +70,7 @@ function inputProjection(spec) {
70
70
  return {
71
71
  mode: spec.inputMode,
72
72
  ...(spec.inputRoot ? { root: spec.inputRoot } : {}),
73
+ ...(spec.inputOptional ? { optional: true } : {}),
73
74
  sources: spec.inputSources,
74
75
  structured_sources: spec.structuredInputSources,
75
76
  cli_owned_fields: spec.cliOwnedFields,
@@ -112,6 +113,14 @@ export function buildExplain(artifact, projectRoot, requestedVerb) {
112
113
  recovery: declaration.recovery,
113
114
  examples: declaration.examples,
114
115
  bounds: declaration.bounds,
116
+ allow_force: spec.allowForce,
117
+ ...(spec.allowForce ? {
118
+ force_semantics: artifact === "plan" && verb === "create"
119
+ ? "With exactly one canonical open predecessor, --force archives it unchanged and publishes a successor whose previous_plan_archived field contains the predecessor's bare ID. Multiple open predecessors are rejected."
120
+ : artifact === "plan" && verb === "archive"
121
+ ? "--force archives an open selected plan without changing task, evaluation, or completion history. An implicit archive rejects multiple open candidates."
122
+ : "--force is accepted only where the operation's locked canonical-state decision permits it.",
123
+ } : {}),
115
124
  next: {},
116
125
  budget: schemaBudget(artifact, validator),
117
126
  fields: exposedFields(artifact, verb, spec, validator),
@@ -227,7 +236,14 @@ function decisionsGuidance(artifact, verb, _entityHealth = false, entityArtifact
227
236
  ...base,
228
237
  ];
229
238
  if (entityArtifact && artifact === "plan" && verb === "create")
230
- return ["the CLI assigns bare ten-letter envelope IDs to the plan and each task and publishes one canonical file per entity", "task numbers and dependency values in this atomic input are create-local symbolic ordinals; the writer removes them and resolves dependencies to bare task IDs", "legacy composite header.id values are migration-only and never public selectors", ...base];
239
+ return [
240
+ "the CLI assigns bare ten-letter envelope IDs to the plan and each task and publishes one canonical file per entity",
241
+ "task numbers and dependency values in this atomic input are create-local symbolic ordinals; the writer removes them and resolves dependencies to bare task IDs",
242
+ "legacy composite header.id values are migration-only and never public selectors",
243
+ "without --force, an open plan blocks creation; with exactly one open predecessor, --force archives it unchanged and writes its bare ID to successor.previous_plan_archived",
244
+ "multiple open predecessors reject before effects; preview and apply derive the same lifecycle decision under the writer lock",
245
+ ...base,
246
+ ];
231
247
  if (entityArtifact && artifact === "plan" && verb === "record-evaluation")
232
248
  return [
233
249
  "select one task entity with its bare ten-letter --id; numeric and composite selectors are unavailable",
@@ -250,6 +266,19 @@ function decisionsGuidance(artifact, verb, _entityHealth = false, entityArtifact
250
266
  return ["select one task entity with its bare ten-letter --id; lifecycle status is a flag-only transition", ...base];
251
267
  if (entityArtifact && artifact === "plan" && verb === "append")
252
268
  return ["the CLI assigns a bare ten-letter ID to the new task entity", "supply one complete YAML/JSON task record through --input; dependencies must be bare ten-letter IDs in the selected plan", ...base];
269
+ if (entityArtifact && artifact === "plan" && verb === "archive")
270
+ return [
271
+ "archive a complete plan normally; --force archives an unfinished selected plan without changing task, evaluation, or completion history",
272
+ "an implicit archive rejects multiple open candidates; select an exact historical plan with its bare --plan ID",
273
+ ...base,
274
+ ];
275
+ if (entityArtifact && artifact === "plan" && verb === "replace")
276
+ return [
277
+ "name the predecessor and successor with bare IDs from canonical evidence; never infer roles from list order",
278
+ "when competing open plans block selection, use npx -y agentera@next state plan replace --predecessor PREDECESSOR_ID --successor SUCCESSOR_ID --format json only after the complete recovery pair is known",
279
+ "pending plan replacement journals block plan reads until the exact retry completes or restores the operation",
280
+ ...base,
281
+ ];
253
282
  if (entityArtifact && artifact === "plan")
254
283
  return [
255
284
  "the active plan entity is selected by lifecycle state",
@@ -327,6 +356,8 @@ export function renderExplainText(explain) {
327
356
  const input = explain.input;
328
357
  if (input?.mode === "structured")
329
358
  lines.push("", "Required:", " --input PATH YAML/JSON document; use - for stdin");
359
+ if (explain.allow_force === true)
360
+ lines.push("", "Optional:", ` --force ${String(explain.force_semantics ?? "Apply the operation's force contract.")}`);
330
361
  lines.push("", "Recovery:", ` ${String(explain.recovery ?? "Correct the input and retry; no state was changed.")}`, "", "Example:", ` ${String(explain.example ?? "")}`);
331
362
  return lines.join("\n") + "\n";
332
363
  }
@@ -99,12 +99,15 @@ function parseOperation(raw, index) {
99
99
  const input = {
100
100
  mode: inputMode,
101
101
  ...(raw.input.root === undefined ? {} : { root: requiredString(raw.input.root, `${p}.input.root`) }),
102
+ ...(raw.input.optional === true ? { optional: true } : {}),
102
103
  sources: strings(raw.input.sources, `${p}.input.sources`),
103
104
  ...(raw.input.structured_sources === undefined
104
105
  ? (inputMode === "structured" ? { structuredSources: strings(raw.input.sources, `${p}.input.sources`) } : {})
105
106
  : { structuredSources: strings(raw.input.structured_sources, `${p}.input.structured_sources`) }),
106
107
  cliOwnedFields: strings(raw.input.cli_owned_fields, `${p}.input.cli_owned_fields`),
107
108
  };
109
+ if (raw.input.optional !== undefined && raw.input.optional !== true)
110
+ throw new Error(`invalid mutation grammar: ${p}.input.optional must be true when present`);
108
111
  if (inputMode === "structured" && (!input.root || input.sources.length === 0))
109
112
  throw new Error(`invalid mutation grammar: ${p} structured input needs a root and source`);
110
113
  if (inputMode === "none" && (input.sources.length > 0 || input.root !== undefined))
@@ -159,6 +162,7 @@ function operationParityProjection(operation) {
159
162
  input: {
160
163
  mode: operation.input.mode,
161
164
  ...(operation.input.root ? { root: operation.input.root } : {}),
165
+ ...(operation.input.optional ? { optional: true } : {}),
162
166
  sources: operation.input.sources,
163
167
  structured_sources: operation.input.structuredSources ?? [],
164
168
  cli_owned_fields: operation.input.cliOwnedFields,
@@ -183,6 +187,7 @@ function runtimeParityProjection(operation) {
183
187
  input: {
184
188
  mode: operation.inputMode,
185
189
  ...(operation.inputRoot ? { root: operation.inputRoot } : {}),
190
+ ...(operation.inputOptional ? { optional: true } : {}),
186
191
  sources: operation.inputSources,
187
192
  structured_sources: operation.structuredInputSources,
188
193
  cli_owned_fields: operation.cliOwnedFields,
@@ -53,6 +53,7 @@ function operationProjection(operation) {
53
53
  input: {
54
54
  mode: operation.input.mode,
55
55
  ...(operation.input.root ? { root: operation.input.root } : {}),
56
+ ...(operation.input.optional ? { optional: true } : {}),
56
57
  sources: operation.input.sources,
57
58
  structured_sources: operation.input.structuredSources ?? [],
58
59
  cli_owned_fields: operation.input.cliOwnedFields,
@@ -5,7 +5,7 @@ export const RUNTIME_WRITABLE_ARTIFACTS = [
5
5
  ];
6
6
  export const RUNTIME_WRITE_VERBS = [
7
7
  "append", "update", "amend", "set-status", "supersede", "set-plan-status",
8
- "record-evaluation", "archive", "create", "publish", "activate", "repair", "set-severity", "resolve", "reopen", "explain",
8
+ "record-evaluation", "archive", "create", "replace", "publish", "activate", "repair", "set-severity", "resolve", "reopen", "explain",
9
9
  ];
10
10
  const readiness = loadTodoReadinessContract();
11
11
  const f = (flag, field, kind, options = {}) => ({ flag, field, kind, required: false, ...options });
@@ -43,6 +43,7 @@ const RUNTIME_OPERATION_CORES = [
43
43
  op("plan", "record-evaluation", planEvaluationFields, { selectors: ["--id", "--plan"], ownedFields: ["id", "artifact", "plan", "evaluation"] }),
44
44
  op("plan", "archive", [f("--plan", "plan", "string")], { selectors: ["--plan"], ownedFields: ["id", "artifact", "plan", "header.status"], allowForce: true }),
45
45
  op("plan", "create", [], { ownedFields: ["id", "artifact", "header.id", "previous_plan_archived", "task_ids"], inputMode: "structured", inputRoot: "complete plan document", inputSources: ["file", "stdin"], structuredInputSources: ["file", "stdin"], cliOwnedFields: ["id", "artifact", "header.id", "previous_plan_archived", "task_ids"], inputMaxBytes: 32768, allowForce: true }),
46
+ op("plan", "replace", [f("--predecessor", "predecessor", "string", { required: true, description: "Bare plan ID to archive as the explicit predecessor." }), f("--successor", "successor", "string", { description: "Existing bare open plan ID to retain as the explicit successor." })], { selectors: ["--predecessor", "--successor"], ownedFields: ["id", "artifact", "header.status", "header.id", "previous_plan_archived", "replacement_input_sha256", "task_ids"], inputMode: "structured", inputRoot: "complete plan document when creating a successor", inputOptional: true, inputSources: ["file", "stdin"], structuredInputSources: ["file", "stdin"], cliOwnedFields: ["id", "artifact", "header.id", "previous_plan_archived", "replacement_input_sha256", "task_ids"], inputMaxBytes: 32768 }),
46
47
  op("health", "append", [], { ownedFields: ["id", "artifact", "appended_at"], inputMode: "structured", inputRoot: "one audit entry", inputSources: ["file", "stdin"], structuredInputSources: ["file", "stdin"], cliOwnedFields: ["id", "artifact", "appended_at"], inputMaxBytes: 32768, compacts: true, recoveryCommand: ["check", "validate", "state", "--format", "json"] }),
47
48
  op("objective", "create", [], { ownedFields: ["id", "artifact", "header.id"], inputMode: "structured", inputRoot: "one objective document", inputSources: ["file", "stdin"], structuredInputSources: ["file", "stdin"], cliOwnedFields: ["id", "artifact", "header.id"], inputMaxBytes: 32768 }),
48
49
  op("objective", "update", [f("--id", "id", "string", { required: true })], { selectors: ["--id"], ownedFields: ["id", "artifact", "header.id"], inputMode: "structured", inputRoot: "one objective document", inputSources: ["file", "stdin"], structuredInputSources: ["file", "stdin"], cliOwnedFields: ["id", "artifact", "header.id"], inputMaxBytes: 32768 }),
@@ -87,13 +88,14 @@ const RUNTIME_OPERATION_PROJECTIONS = {
87
88
  "plan.supersede": projection("Complete and evaluate each replacement task with latest PASS evidence, then retry with the returned bare IDs.", developmentCommand('state plan supersede --id qjtrmnpvka --by zqtrmnpvka --reason "Replacement task" --format json')),
88
89
  "plan.set-plan-status": projection("Keep the plan open or resolve every incomplete task and replacement evaluation before retrying completion.", developmentCommand("state plan set-plan-status --status complete --format json")),
89
90
  "plan.record-evaluation": projection("Use the task's bare ID, a stable attempt ID, and evaluator provenance, then retry without changing published evidence.", developmentCommand('state plan record-evaluation --id qjtrmnpvka --attempt-id audit-1 --verdict pass --provenance "audit report" --format json')),
90
- "plan.archive": projection("Complete or preserve the selected plan, then retry archive with no content flags; no state was changed.", developmentCommand("state plan archive --dry-run --format json")),
91
- "plan.create": projection(`Run ${developmentCommand("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.`, developmentCommand("state plan create --input plan.yaml --format json")),
91
+ "plan.archive": projection("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.", developmentCommand("state plan archive --dry-run --format json"), developmentCommand("state plan archive --force --dry-run --format json")),
92
+ "plan.create": projection(`Run ${developmentCommand("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.`, developmentCommand("state plan create --input plan.yaml --format json"), developmentCommand("state plan create --force --input plan.yaml --format json")),
93
+ "plan.replace": projection("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.", developmentCommand("state plan replace --predecessor abcdefghij --successor klmnopqrst --format json"), developmentCommand("state plan replace --predecessor abcdefghij --input plan.yaml --format json")),
92
94
  "health.append": projection(`Run ${developmentCommand("check validate state --format json")}, preserve audit evidence, and retry with one schema-valid audit entry.`, developmentCommand("state health append --input audit.yaml --format json")),
93
95
  "objective.create": projection("Remove identity fields assigned by the CLI and retry with one schema-valid objective document.", developmentCommand("state objective create --input objective.yaml --format json")),
94
96
  "objective.update": projection("Reread the objective, copy its bare ID to --id, remove CLI-owned fields, and retry.", developmentCommand("state objective update --id qjtrmnpvka --input objective.yaml --format json")),
95
97
  "experiments.publish": projection("Use a bare objective ID, omit numeric legacy selectors, and retry the exact input; divergent immutable identities remain untouched.", developmentCommand("state experiments publish --objective qjtrmnpvka --input experiment.yaml --format json")),
96
- "todo.activate": projection("Preview and review every reported activation effect before explicit confirmed apply; use the returned examples without modification.", developmentCommand("state todo activate --dry-run --format json"), developmentCommand("state todo activate --effect-sha256 EFFECT_SHA256 --yes --format json")),
98
+ "todo.activate": projection("Preview and review every reported safe activation effect before explicit confirmed apply; unsafe inactive evidence requires non-mutating owner correction and replanning.", developmentCommand("state todo activate --dry-run --format json"), developmentCommand("state todo activate --effect-sha256 EFFECT_SHA256 --yes --format json")),
97
99
  "todo.repair": projection("Preview and review every diagnosed repair decision before explicit confirmed apply; ambiguous evidence is rejected without effects.", developmentCommand("state todo repair --dry-run --format json"), developmentCommand("state todo repair --effect-sha256 EFFECT_SHA256 --yes --format json")),
98
100
  "todo.create": projection(`Run ${developmentCommand("state todo explain --verb create --format json")}, remove CLI-owned fields, provide the full typed TODO record, and retry.`, developmentCommand("state todo create --input todo.yaml --format json")),
99
101
  "todo.update": projection("Reread the TODO item, use its bare ID, supply only typed patch fields, and use null or an empty list only for declared clearable fields.", developmentCommand("state todo update --id qjtrmnpvka --input todo-patch.yaml --format json")),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentera",
3
- "version": "3.0.0-dev.47",
3
+ "version": "3.0.0-dev.51",
4
4
  "description": "One CLI, many capabilities: project state, artifact validation, and capability routing for AI coding agents",
5
5
  "keywords": [
6
6
  "agentera",
@@ -77,6 +77,6 @@
77
77
  },
78
78
  "agentera": {
79
79
  "suiteVersion": "3.0.0",
80
- "gitRef": "91b25910acca8676d0442e874ceff713c44fae0c"
80
+ "gitRef": "a70ed56a72d633f3f11e94cede11fe4aadba2d2e"
81
81
  }
82
82
  }