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.
- package/README.md +15 -4
- package/bundle/CHANGELOG.md +4 -4
- package/bundle/references/adapters/package-publication.json +4 -4
- package/bundle/references/adapters/package-registry.yaml +2 -1
- package/bundle/references/artifacts/state-storage-authority.yaml +51 -11
- package/bundle/skills/agentera/SKILL.md +11 -2
- package/bundle/skills/agentera/schemas/artifacts/plan.yaml +38 -4
- package/dist/capabilities/orchestrate/instructions.js +2 -0
- package/dist/capabilities/plan/instructions.js +1 -0
- package/dist/capabilities/status/instructions.js +5 -1
- package/dist/cli/commands/compact.js +125 -3
- package/dist/cli/commands/doctor.js +8 -1
- package/dist/cli/commands/prime/collectEntityOrientation.js +22 -7
- package/dist/cli/commands/prime/collectOrientationState.js +3 -3
- package/dist/cli/commands/prime/orientationOutput.js +2 -0
- package/dist/cli/commands/state/write.js +1 -5
- package/dist/cli/commands/validate.js +1 -1
- package/dist/cli/help.js +4 -0
- package/dist/cli/orientation/attention.js +4 -2
- package/dist/core/developmentInvocation.js +1 -1
- package/dist/registries/activationTuples.js +3 -2
- package/dist/registries/packagePublication.js +2 -2
- package/dist/state/entityStorage.js +9 -6
- package/dist/state/planEntities.js +485 -33
- package/dist/state/planLineageValidation.js +52 -0
- package/dist/state/planReplacementTransaction.js +475 -0
- package/dist/state/todoActivationSafety.js +58 -0
- package/dist/state/todoDocsEntities.js +22 -17
- package/dist/state/todoReconciliationActivation.js +12 -4
- package/dist/state/todoReconciliationInspection.js +8 -12
- package/dist/state/write/explain.js +32 -1
- package/dist/state/write/grammar.js +5 -0
- package/dist/state/write/operations.js +1 -0
- package/dist/state/write/runtimeOperations.js +6 -4
- 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
|
-
|
|
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 =
|
|
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
|
|
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 <
|
|
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:
|
|
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:
|
|
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
|
|
108
|
-
|
|
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("
|
|
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 [
|
|
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("
|
|
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
|
|
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;
|
|
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.
|
|
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": "
|
|
80
|
+
"gitRef": "a70ed56a72d633f3f11e94cede11fe4aadba2d2e"
|
|
81
81
|
}
|
|
82
82
|
}
|