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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +5 -5
  2. package/bundle/CHANGELOG.md +21 -11
  3. package/bundle/UPGRADE.md +23 -7
  4. package/bundle/references/adapters/package-publication.json +68 -17
  5. package/bundle/references/adapters/package-registry.yaml +145 -145
  6. package/bundle/references/analysis/verification-policy.yaml +93 -18
  7. package/bundle/references/artifacts/state-storage-authority.yaml +50 -11
  8. package/bundle/skills/agentera/SKILL.md +7 -5
  9. package/bundle/skills/agentera/capabilities/build/schemas/artifacts.yaml +7 -6
  10. package/bundle/skills/agentera/capabilities/build/schemas/exit.yaml +6 -5
  11. package/bundle/skills/agentera/capabilities/build/schemas/validation.yaml +18 -10
  12. package/bundle/skills/agentera/schemas/artifacts/plan.yaml +3 -0
  13. package/bundle/skills/agentera/schemas/artifacts/progress.yaml +8 -6
  14. package/dist/capabilities/build/instructions.js +8 -5
  15. package/dist/cli/capabilityContext/build.js +12 -3
  16. package/dist/cli/capabilityContext/planState.js +6 -2
  17. package/dist/cli/capabilityContext/startupAggregation.js +1 -1
  18. package/dist/cli/commands/doctor.js +1 -1
  19. package/dist/cli/commands/prime/briefOrientation.js +0 -1
  20. package/dist/cli/commands/prime/collectOrientationState.js +13 -6
  21. package/dist/cli/commands/prime/orientationOutput.js +27 -5
  22. package/dist/cli/commands/state/write.js +4 -1
  23. package/dist/cli/dispatch/index.js +1 -1
  24. package/dist/cli/help.js +1 -0
  25. package/dist/cli/migrationRequired.js +40 -5
  26. package/dist/registries/activationTuples.js +3 -2
  27. package/dist/registries/packagePublication.js +51 -7
  28. package/dist/state/entityMigrationPreview.js +3 -9
  29. package/dist/state/entityPublicationContext.js +56 -3
  30. package/dist/state/planEntities.js +49 -4
  31. package/dist/state/progressWritePolicy.js +33 -0
  32. package/dist/state/stateMode.js +138 -1
  33. package/dist/state/todoDocsEntities.js +62 -7
  34. package/dist/state/todoReconciliationActivation.js +46 -4
  35. package/dist/state/todoReconciliationInspection.js +5 -3
  36. package/dist/state/todoReconciliationRepair.js +149 -1
  37. package/dist/state/todoReconciliationTransaction.js +20 -7
  38. package/dist/state/write/explain.js +17 -3
  39. package/dist/state/write/input.js +25 -1
  40. package/dist/state/write/runtimeOperations.js +4 -2
  41. package/dist/state/write/transaction.js +6 -1
  42. package/dist/validate/activationArtifactEvidence.js +49 -41
  43. package/dist/validate/activationArtifactEvidenceTypes.js +12 -0
  44. package/dist/validate/activationEvidenceManifest.js +26 -10
  45. package/package.json +11 -6
@@ -12,7 +12,7 @@ import { v1MigrationSummary } from "./v1Migration.js";
12
12
  import { diagnoseCanonicalSkill } from "../../../setup/sharedSkill.js";
13
13
  import { collectEntityOrientation } from "./collectEntityOrientation.js";
14
14
  import { acquireProfile } from "../../profileAcquisition.js";
15
- import { fullEntityUpgradeCommand } from "../../../upgrade/upgradeCommands.js";
15
+ import { fullEntityUpgradeCommand, fullEntityUpgradePreviewCommand } from "../../../upgrade/upgradeCommands.js";
16
16
  import { classifyEntityCutoverProject } from "../../../state/entityMigrationPreview.js";
17
17
  import { preCutoverCommand } from "../../preCutoverCommand.js";
18
18
  const EMPTY_SCHEMAS = Object.freeze({});
@@ -22,7 +22,14 @@ function stateCutover(project, sourceRoot) {
22
22
  if (projectState === "v3") {
23
23
  return { status: "complete", project_state: "v3", recovery_command: null };
24
24
  }
25
- return { status: "required", project_state: projectState, recovery_command: fullEntityUpgradeCommand(project) };
25
+ if (projectState === "fresh_uninitialized") {
26
+ return { status: "fresh_uninitialized", project_state: projectState, recovery_command: null };
27
+ }
28
+ return {
29
+ status: "required",
30
+ project_state: projectState,
31
+ recovery_command: projectState === "legacy" ? fullEntityUpgradeCommand(project) : fullEntityUpgradePreviewCommand(project),
32
+ };
26
33
  }
27
34
  catch {
28
35
  return { status: "invalid_lifecycle", project_state: "invalid_lifecycle", recovery_command: preCutoverCommand("check validate state --format json") };
@@ -99,13 +106,13 @@ export function collectOrientationState(opts) {
99
106
  const readiness = selectStatusReadiness(plan, health, objective, todoItems, decision, savedContext, entity.todoReadiness);
100
107
  const reconciliationReadiness = entity.todoReconciliation?.status === "action_required"
101
108
  ? withRecommended(readiness, {
102
- object: entity.todoReconciliation.state === "unsafe_inactive" ? "Replan unsafe TODO activation correction" : entity.todoReconciliation.state === "inactive" ? "Activate TODO reconciliation" : "Repair TODO reconciliation",
103
- capability: entity.todoReconciliation.state === "unsafe_inactive" ? "plan" : "build",
109
+ object: entity.todoReconciliation.state === "unsafe_inactive" ? "Correct unsafe TODO ownership" : entity.todoReconciliation.state === "inactive" ? "Activate TODO reconciliation" : "Repair TODO reconciliation",
110
+ capability: "build",
104
111
  reason: entity.todoReconciliation.recovery_command,
105
- phase: entity.todoReconciliation.state === "unsafe_inactive" ? "plan" : entity.todoReconciliation.state === "invalid_lifecycle" ? "audit" : "build",
112
+ phase: entity.todoReconciliation.state === "unsafe_inactive" ? "build" : entity.todoReconciliation.state === "invalid_lifecycle" ? "audit" : "build",
106
113
  })
107
114
  : readiness;
108
- const nextAction = cutover.status !== "complete"
115
+ const nextAction = cutover.status === "required" || cutover.status === "invalid_lifecycle"
109
116
  ? withRecommended(readiness, {
110
117
  object: "Complete Agentera entity-state cutover",
111
118
  capability: "status",
@@ -32,8 +32,7 @@ function nextActionEntry(action) {
32
32
  * and `alternatives` carries the cascade branches the early-return model
33
33
  * would have skipped, each with the same `{object, capability, reason, phase}`
34
34
  * shape. */
35
- function nextActionPayload(state) {
36
- const { recommended, alternatives } = state.next_action;
35
+ function nextActionPayload({ recommended, alternatives }) {
37
36
  return {
38
37
  ...nextActionEntry(recommended),
39
38
  alternatives: alternatives.map(nextActionEntry),
@@ -163,7 +162,7 @@ export function buildOrientationJsonPayload(state, command, options = {}) {
163
162
  attention: projectPublicOrientationAttention(state).map((item) => truncateCodePoints(item, 200, "…")),
164
163
  decision_attention: state.decision_attention,
165
164
  history: state.history,
166
- next_action: nextActionPayload(state),
165
+ next_action: nextActionPayload(state.next_action),
167
166
  startup,
168
167
  orchestration_context: bespoke.orchestration_context,
169
168
  closeout_context: bespoke.closeout_context,
@@ -192,6 +191,29 @@ export function buildStatusContextState(state, _command = "prime", _options = {}
192
191
  const firstPending = plan.first_pending && typeof plan.first_pending === "object" && !Array.isArray(plan.first_pending)
193
192
  ? plan.first_pending
194
193
  : null;
194
+ const risks = todoReconciliation?.risks;
195
+ const unsafeDetailOmitted = todoReconciliation?.state === "unsafe_inactive"
196
+ && Boolean(risks && typeof risks === "object" && !Array.isArray(risks) && Number(risks.omitted_count) > 0);
197
+ const recovery = unsafeDetailOmitted && typeof todoReconciliation.recovery_command === "string"
198
+ ? todoReconciliation.recovery_command
199
+ : null;
200
+ const todoAttention = unsafeDetailOmitted
201
+ ? new Set(state.todo_items.map((item) => `${String(item.severity)}: TODO: ${String(item.text)}`))
202
+ : new Set();
203
+ const attention = projectPublicOrientationAttention(state)
204
+ .flatMap((item) => item === `action-required: TODO reconciliation is unsafe inactive; ${recovery}`
205
+ ? ["action-required: TODO reconciliation is unsafe inactive; see capability_context.startup.todo_reconciliation"]
206
+ : todoAttention.has(item) ? [] : [item])
207
+ .map((item) => truncateCodePoints(item, 200, "…"));
208
+ const redactRecovery = (action) => recovery !== null && action.reason === recovery
209
+ ? { ...action, reason: "See capability_context.startup.todo_reconciliation for recovery." }
210
+ : action;
211
+ const nextAction = {
212
+ recommended: redactRecovery(state.next_action.recommended),
213
+ alternatives: state.next_action.alternatives
214
+ .filter((action) => !unsafeDetailOmitted || action.artifact !== "todo")
215
+ .map(redactRecovery),
216
+ };
195
217
  return {
196
218
  outcome: startup.outcome,
197
219
  mode: state.mode,
@@ -231,8 +253,8 @@ export function buildStatusContextState(state, _command = "prime", _options = {}
231
253
  progress: { exists: state.progress.exists, status: state.progress.status ?? null, latest: state.progress.latest ?? null },
232
254
  objective: { exists: state.objective.exists, active: state.objective.active ?? false, title: state.objective.title ?? null },
233
255
  state_presence: state.state_presence,
234
- attention: projectPublicOrientationAttention(state).map((item) => truncateCodePoints(item, 200, "…")),
235
- next_action: nextActionPayload(state),
256
+ attention,
257
+ next_action: nextActionPayload(nextAction),
236
258
  };
237
259
  }
238
260
  export function emitPrime(command, payload, format, fieldsArg, out, err, options = {}) {
@@ -280,7 +280,10 @@ function parseWrite(artifactRaw, argv) {
280
280
  example: exampleFor(artifact, verb),
281
281
  });
282
282
  const selectorFields = new Set(spec.fields.filter((field) => spec.selectors.includes(field.flag)).map((field) => field.field));
283
- if (spec.inputRoot && Object.keys(values).some((field) => !selectorFields.has(field)))
283
+ const correctionApplyFields = artifact === "todo" && verb === "correct-owners"
284
+ ? new Set(["effect_sha256", "confirmed"])
285
+ : new Set();
286
+ if (spec.inputRoot && Object.keys(values).some((field) => !selectorFields.has(field) && !correctionApplyFields.has(field)))
284
287
  invalid({
285
288
  class: "mutually_exclusive",
286
289
  message: `--input cannot be combined with field flags for ${artifact} ${verb}`,
@@ -63,7 +63,7 @@ export function main(argv, io = {}) {
63
63
  // must report its source-checkout boundary before project migration checks.
64
64
  const sourceOnlyReferenceValidation = args[0] === "check" && args[1] === "validate" && ["retained-references", "activation-conjunction"].includes(args[2] ?? "");
65
65
  if (!sourceOnlyReferenceValidation && requiresCompletedEntityCutover(args)) {
66
- const failure = enforceCompletedEntityCutover(migrationProject(args), requestedMigrationFailureFormat(args), io);
66
+ const failure = enforceCompletedEntityCutover(migrationProject(args), requestedMigrationFailureFormat(args), io, args);
67
67
  if (failure !== null)
68
68
  return failure;
69
69
  }
package/dist/cli/help.js CHANGED
@@ -204,6 +204,7 @@ export function printStateHelp(sub) {
204
204
  return [
205
205
  ...recordFamilyReadSection("todo"),
206
206
  " agentera state todo activate|repair --dry-run|--effect-sha256 SHA256 --yes --format json",
207
+ " agentera state todo correct-owners --input OWNER_MAPPING.yaml --dry-run|--effect-sha256 SHA256 --yes --format json",
207
208
  " agentera state todo create --input TODO.yaml --format json",
208
209
  " agentera state todo update --id ID --input TODO-PATCH.yaml --format json",
209
210
  " agentera state todo set-severity --id ID --severity LEVEL --reason TEXT --date YYYY-MM-DD --format json",
@@ -1,8 +1,8 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { emitStructured } from "./structured.js";
4
- import { detectStateMode } from "../state/stateMode.js";
5
- import { fullEntityUpgradeCommand } from "../upgrade/upgradeCommands.js";
4
+ import { classifyProjectState, detectStateMode } from "../state/stateMode.js";
5
+ import { commandText, fullEntityUpgradeCommand, fullEntityUpgradePreviewCommand } from "../upgrade/upgradeCommands.js";
6
6
  import { preCutoverCommand } from "./preCutoverCommand.js";
7
7
  function value(argv, flag) {
8
8
  const equals = argv.find((arg) => typeof arg === "string" && arg.startsWith(`${flag}=`));
@@ -46,8 +46,19 @@ export function migrationProject(argv, fallback = process.cwd()) {
46
46
  candidate = parent;
47
47
  }
48
48
  }
49
+ function permitsFreshPlanInitialization(argv) {
50
+ const [command, subcommand, verb] = argv;
51
+ return command === "state" && subcommand === "plan" && verb === "create"
52
+ || command === "prime" && value(argv, "--context") === "plan";
53
+ }
54
+ function freshPlanCreateCommand(project) {
55
+ return commandText([
56
+ "npx", "-y", "agentera@next", "state", "plan", "create", "--project", project,
57
+ "--input", "PLAN.yaml", "--format", "json",
58
+ ]);
59
+ }
49
60
  /** Read-only cutover gate. It only inspects the durable marker and emits a failure. */
50
- export function enforceCompletedEntityCutover(projectRoot, format, io = {}) {
61
+ export function enforceCompletedEntityCutover(projectRoot, format, io = {}, argv = []) {
51
62
  const project = path.resolve(projectRoot);
52
63
  let mode;
53
64
  try {
@@ -68,13 +79,37 @@ export function enforceCompletedEntityCutover(projectRoot, format, io = {}) {
68
79
  }
69
80
  if (mode === "entities")
70
81
  return null;
71
- const recovery = fullEntityUpgradeCommand(project);
82
+ const classification = classifyProjectState(project);
83
+ if (classification.state === "fresh_uninitialized") {
84
+ if (permitsFreshPlanInitialization(argv))
85
+ return null;
86
+ const recovery = freshPlanCreateCommand(project);
87
+ const envelope = {
88
+ schemaVersion: "agentera.stateFailure.v1",
89
+ status: "fail",
90
+ error: {
91
+ class: "fresh_initialization_required",
92
+ message: "Fresh project state is initialized only by state plan create; no other state writer can create entity authority.",
93
+ project,
94
+ recovery,
95
+ },
96
+ };
97
+ if (format === "json" || format === "yaml")
98
+ emitStructured(envelope, format, io.out ?? ((text) => process.stdout.write(text)));
99
+ else
100
+ (io.err ?? ((text) => process.stderr.write(text)))(`Error: ${envelope.error.message}\nRecovery: ${recovery}\n`);
101
+ return 1;
102
+ }
103
+ const legacy = classification.state === "legacy";
104
+ const recovery = legacy ? fullEntityUpgradeCommand(project) : fullEntityUpgradePreviewCommand(project);
72
105
  const envelope = {
73
106
  schemaVersion: "agentera.stateFailure.v1",
74
107
  status: "fail",
75
108
  error: {
76
109
  class: "migration_required",
77
- message: "This command requires the completed entity-state cutover; legacy aggregates remain migration input only.",
110
+ message: legacy
111
+ ? "This command requires the completed entity-state cutover; legacy aggregates remain migration input only."
112
+ : `This command cannot adopt marker-absent ${classification.state} Agentera state; inspect it with the read-only recovery command before any mutation.`,
78
113
  project,
79
114
  recovery,
80
115
  },
@@ -76,6 +76,7 @@ const rawTuples = JSON.parse(decodedCatalog.slice(decodedCatalog.indexOf("[", 2)
76
76
  const addedTuples = [
77
77
  { class: "state", surface_id: "write:plan.replace", owner_path: "packages/cli/src/state/write/runtimeOperations.ts", owner_symbol_or_selector: "runtimeOperationSpecs", owner_selector: "plan.replace", semantic_selector_if_any: null, canonical_correction: "node packages/cli/dist/bin/agentera.js check validate state --format json" },
78
78
  { class: "state", surface_id: "write:todo.activate", owner_path: "packages/cli/src/state/write/runtimeOperations.ts", owner_symbol_or_selector: "runtimeOperationSpecs", owner_selector: "todo.activate", semantic_selector_if_any: null, canonical_correction: "node packages/cli/dist/bin/agentera.js check validate state --format json" },
79
+ { class: "state", surface_id: "write:todo.correct-owners", owner_path: "packages/cli/src/state/write/runtimeOperations.ts", owner_symbol_or_selector: "runtimeOperationSpecs", owner_selector: "todo.correct-owners", semantic_selector_if_any: null, canonical_correction: "node packages/cli/dist/bin/agentera.js check validate state --format json" },
79
80
  { class: "state", surface_id: "write:todo.repair", owner_path: "packages/cli/src/state/write/runtimeOperations.ts", owner_symbol_or_selector: "runtimeOperationSpecs", owner_selector: "todo.repair", semantic_selector_if_any: null, canonical_correction: "node packages/cli/dist/bin/agentera.js check validate state --format json" },
80
81
  { class: "package", surface_id: "emitted:packages/cli/src/cli/commands/doctor.ts", owner_path: "packages/cli/src/registries/packageRegistry.ts", owner_symbol_or_selector: "loadRegistry", owner_selector: "packages/cli/src/cli/commands/doctor.ts", semantic_selector_if_any: JSON.stringify({ path: "packages/cli/src/cli/commands/doctor.ts", selector: null, format: null, classification: null, reason: "Doctor project-state signals publish bounded reconciliation preview and apply guidance." }), canonical_correction: "pnpm -C packages/cli run verify:package" },
81
82
  { class: "package", surface_id: "emitted:packages/cli/src/state/todoReconciliationInspection.ts", owner_path: "packages/cli/src/registries/packageRegistry.ts", owner_symbol_or_selector: "loadRegistry", owner_selector: "packages/cli/src/state/todoReconciliationInspection.ts", semantic_selector_if_any: JSON.stringify({ path: "packages/cli/src/state/todoReconciliationInspection.ts", selector: null, format: null, classification: null, reason: "TODO reconciliation inspection publishes bounded preview and effect-bound apply guidance." }), canonical_correction: "pnpm -C packages/cli run verify:package" },
@@ -95,11 +96,11 @@ export const ACTIVATION_TUPLE_AUTHORITY = Object.freeze({
95
96
  capability: { count: 12, sha256: "892e6e5e2a57b41064bc44fa2946453225f1b1195aff77aad05365fd0a1071c2" },
96
97
  runtime: { count: 81, sha256: "99b2abff3ebff889b54b1781c563ab4b32609a479c4e90d6aad854f48fba7edc" },
97
98
  reference: { count: 22, sha256: "243fd3c317553f8924c56a52c61af90ccf2793b7b0018af1373b73c69b6c3afb" },
98
- state: { count: 37, sha256: "210bd831c5d62beac149efd888ee4a99af49a9469cf373031f8db9893904d0c1" },
99
+ state: { count: 38, sha256: "697de8dcd13ac521124c35058ba222aa5cb6cae546f00bb54652ceaca4b662aa" },
99
100
  package: { count: 66, sha256: "83e6971af7f7564e42369aaccc445b32a0793bfffade843d149b6abd4fd3dbbc" },
100
101
  bootstrap: { count: 34, sha256: "9a7dd7e27110d85cf5c08835fdd8f08119e75579858e63bc6d396c733961d0bc" },
101
102
  },
102
- total: { count: 279, sha256: "c2d7a66bc9af04a5511348825995317a5968357c6cacbfd87eb2cc4fbe77156c" },
103
+ total: { count: 280, sha256: "cd9396b45a82383e2f58da851f00061fa4c23c504ba9f41cf9fd7c7c39b0b092" },
103
104
  });
104
105
  export function canonicalTupleJson(value) { return JSON.stringify(value); }
105
106
  export function digestCanonicalTuples(values) {
@@ -53,11 +53,11 @@ export const ACTIVATION_CENSUS_AUTHORITY = deepFreeze({
53
53
  capability: { count: 12, sha256: "007e1157a892fec54182b1803aeaa442cf8e2e332e6a055c3bd020e2b0731867" },
54
54
  runtime: { count: 81, sha256: "03fe600a3a27f05daf0d1b59c16b00bedc354a13c70ea60be0b457175fdf743c" },
55
55
  reference: { count: 22, sha256: "2b48c82bb7ee10388d9ebb4fa7ea3743ea82a7e9aaedfcd5a858eeedb9d891ae" },
56
- state: { count: 37, sha256: "ca87acdb0fda04ba46a52163fc30ec6578c077e61fcb64597f861a36fdce1dc4" },
56
+ state: { count: 38, sha256: "7ab0dd6ef1b1b1ce66bd9ea94d1de3d542528233e928a44102e289bf12416681" },
57
57
  package: { count: 66, sha256: "3548af7e84151c90690a6eb3d1cb6c7847f39b290161ecab599d8cb1bf0d2cb0" },
58
58
  bootstrap: { count: 34, sha256: "71c2038744e2518a6adb722acbb5f9352bddfb5d1c92eeb0e347297ef2ca2f1e" },
59
59
  },
60
- total: { count: 279, sha256: "b92cd469f858e9584682d05b4d9bb03c2fc4b905f3485475723786c0a978c138" },
60
+ total: { count: 280, sha256: "051ed9a57857a39e11ed8fb583bff9d2a67a306c5ab897a0ee829212468bc23a" },
61
61
  });
62
62
  /** Each dimension names the production contract it observes independently. */
63
63
  export const ACTIVATION_EVIDENCE_SOURCES = deepFreeze({
@@ -71,12 +71,13 @@ export const ACTIVATION_EVIDENCE_SOURCES = deepFreeze({
71
71
  });
72
72
  export const ACTIVATION_CHECK_IDS = Object.freeze(ACTIVATION_CLASSES.flatMap((classId) => ACTIVATION_DIMENSIONS.map((dimension) => `${classId}.${dimension}`)));
73
73
  export const SOURCE_GATE_IDS = [
74
- "source", "stress", "performance", "package", "generated-overlap", "typecheck", "build",
74
+ "source", "stress", "performance", "capacity", "package", "generated-overlap", "typecheck", "build",
75
75
  "compact", "capability-contract", "activation-conjunction",
76
76
  ];
77
77
  export const SOURCE_DAG_PHASES = {
78
78
  batchA: ["generated-overlap", "stress", "typecheck"],
79
79
  performanceBarrier: ["performance"],
80
+ capacityBarrier: ["capacity"],
80
81
  barrierB: ["compact", "capability-contract", "activation-conjunction"],
81
82
  generatedOverlapOrigins: ["source", "package", "build", "generated-overlap"],
82
83
  };
@@ -84,6 +85,7 @@ const EXACT_COMMANDS = {
84
85
  source: ["pnpm", "-C", "packages/cli", "run", "test:source"],
85
86
  stress: ["pnpm", "-C", "packages/cli", "run", "test:stress"],
86
87
  performance: ["pnpm", "-C", "packages/cli", "run", "test:performance"],
88
+ capacity: ["pnpm", "-C", "packages/cli", "run", "test:capacity"],
87
89
  package: ["pnpm", "-C", "packages/cli", "run", "verify:package"],
88
90
  "generated-overlap": ["pnpm", "-C", "packages/cli", "run", "verify:generated-overlap"],
89
91
  typecheck: ["pnpm", "-C", "packages/cli", "run", "typecheck"],
@@ -133,11 +135,33 @@ export function validatePackagePublicationDocument(raw) {
133
135
  if (raw?.schemaVersion !== "agentera.packagePublication.v2")
134
136
  fail("schemaVersion is invalid");
135
137
  const source = raw?.qualification?.source;
138
+ const readiness = raw?.qualification?.readiness;
136
139
  const activation = source?.activationConjunction;
137
140
  if (!source || !activation || activation.gateIdentity !== "agentera.activationConjunction.v1")
138
141
  fail("source activation conjunction is missing or invalid");
142
+ if (!readiness
143
+ || readiness.schemaVersion !== "agentera.releaseReadiness.v1"
144
+ || readiness.component !== "release-readiness"
145
+ || readiness.adapter !== "development")
146
+ fail("release readiness authority is missing or invalid");
147
+ const readinessCommand = boundedText(readiness.command, "release readiness command", 320, /^node packages\/cli\/scripts\/release-readiness\.mjs development --candidate-dir DIR --target-version VERSION --source-commit COMMIT \[--metadata-commit COMMIT\] \[--json\]$/);
148
+ const readinessPhases = exactList(readiness.phases, ["source-readiness", "metadata-review", "candidate-readiness"], "release readiness phases");
149
+ if (!readiness.receipts
150
+ || Object.keys(readiness.receipts).sort().join("\0") !== "candidate\0source"
151
+ || readiness.receipts.source !== "source-receipt.json"
152
+ || readiness.receipts.candidate !== "candidate-receipt.json")
153
+ fail("release readiness receipts must name the governed source and candidate receipts");
154
+ const readinessReuse = boundedText(readiness.reuse, "release readiness reuse rule", 1200, /^[^\0\r\n]+$/);
155
+ const readinessMetadataReview = boundedText(readiness.metadataReview, "release readiness metadata review rule", 1200, /^[^\0\r\n]+$/);
156
+ const readinessOutcomes = exactList(readiness.outcomes, ["paused", "ready", "rejected"], "release readiness outcomes");
157
+ if (!readiness.exitCodes
158
+ || Object.keys(readiness.exitCodes).sort().join("\0") !== "paused\0ready\0rejected"
159
+ || readiness.exitCodes.paused !== 0
160
+ || readiness.exitCodes.ready !== 0
161
+ || readiness.exitCodes.rejected !== 1)
162
+ fail("release readiness exit codes must map paused and ready to 0 and rejected to 1");
139
163
  if (!Array.isArray(source.gates) || source.gates.length !== SOURCE_GATE_IDS.length)
140
- fail("source gates must contain exactly ten entries");
164
+ fail(`source gates must contain exactly ${SOURCE_GATE_IDS.length} entries`);
141
165
  const sourceGates = source.gates.map((gate, index) => {
142
166
  const name = SOURCE_GATE_IDS[index];
143
167
  if (!gate || gate.name !== name)
@@ -152,9 +176,10 @@ export function validatePackagePublicationDocument(raw) {
152
176
  const dag = source.dag;
153
177
  const batchA = exactList(dag?.batchA, SOURCE_DAG_PHASES.batchA, "source batch A");
154
178
  const performanceBarrier = exactList(dag?.performanceBarrier, SOURCE_DAG_PHASES.performanceBarrier, "source performance barrier");
179
+ const capacityBarrier = exactList(dag?.capacityBarrier, SOURCE_DAG_PHASES.capacityBarrier, "source capacity barrier");
155
180
  const barrierB = exactList(dag?.barrierB, SOURCE_DAG_PHASES.barrierB, "source barrier B");
156
181
  const generatedOverlapOrigins = exactList(dag?.generatedOverlapOrigins, SOURCE_DAG_PHASES.generatedOverlapOrigins, "generated overlap origins");
157
- const phases = [...batchA, ...performanceBarrier, ...barrierB];
182
+ const phases = [...batchA, ...performanceBarrier, ...capacityBarrier, ...barrierB];
158
183
  if (phases.length !== new Set(phases).size)
159
184
  fail("source execution phases must be disjoint");
160
185
  const scheduled = new Set([...phases, ...generatedOverlapOrigins]);
@@ -211,12 +236,31 @@ export function validatePackagePublicationDocument(raw) {
211
236
  return {
212
237
  sourceGates,
213
238
  sourceDag: {
214
- batchA, performanceBarrier, barrierB, generatedOverlapOrigins,
239
+ batchA, performanceBarrier, capacityBarrier, barrierB, generatedOverlapOrigins,
215
240
  overlapCleanupMarginMs: positiveInteger(dag.overlapCleanupMarginMs, "overlap cleanup margin", 60_000),
216
241
  overlapParentReconciliationMarginMs: positiveInteger(dag.overlapParentReconciliationMarginMs, "parent reconciliation margin", 60_000),
217
242
  minimumExecutionWindowMs,
218
243
  },
219
- sourceQualificationMs: positiveInteger(raw?.benchmark?.timeouts?.sourceQualificationMs, "source qualification timeout", 900_000),
244
+ sourceQualificationMs: positiveInteger(raw?.benchmark?.timeouts?.sourceQualificationMs, "source qualification timeout", 2_400_000),
245
+ readiness: {
246
+ schemaVersion: readiness.schemaVersion,
247
+ component: readiness.component,
248
+ adapter: readiness.adapter,
249
+ command: readinessCommand,
250
+ phases: readinessPhases,
251
+ receipts: {
252
+ source: readiness.receipts.source,
253
+ candidate: readiness.receipts.candidate,
254
+ },
255
+ reuse: readinessReuse,
256
+ metadataReview: readinessMetadataReview,
257
+ outcomes: readinessOutcomes,
258
+ exitCodes: {
259
+ paused: readiness.exitCodes.paused,
260
+ ready: readiness.exitCodes.ready,
261
+ rejected: readiness.exitCodes.rejected,
262
+ },
263
+ },
220
264
  activationConjunction: {
221
265
  gateIdentity: activation.gateIdentity, classes, dimensions, checkIds, bounds, owners,
222
266
  census: { algorithm: ACTIVATION_CENSUS_AUTHORITY.algorithm, classes: censusClasses, total: censusTotal },
@@ -22,18 +22,12 @@ import { canonicalMigrationRecord } from "./canonicalMigrationRecord.js";
22
22
  import { legacySummaryRecord } from "./legacySummaryRecord.js";
23
23
  import { applyCausalBlockers } from "./entityMigrationCausality.js";
24
24
  import { todoMigrationObservations, todoReconciliationMigrationPlan } from "./entityMigrationTodo.js";
25
- import { detectStateMode } from "./stateMode.js";
25
+ import { classifyProjectState } from "./stateMode.js";
26
26
  import { TODO_RECONCILIATION_ACTIVATION_PATH } from "./todoReconciliationActivation.js";
27
27
  import { projectPathIsStable as migrationPathIsStable, readProjectFileSnapshot, resolveProjectDescriptorPath, snapshotProjectPath as snapshotMigrationPath, } from "./safeProjectFile.js";
28
28
  export function classifyEntityCutoverProject(project, sourceRoot) {
29
- if (detectStateMode(project, sourceRoot) === "entities")
30
- return "v3";
31
- const stateRoot = path.join(project, ".agentera");
32
- const aggregateNames = ["plan.yaml", "progress.yaml", "decisions.yaml", "health.yaml"];
33
- const aggregateCount = aggregateNames.filter((name) => fs.existsSync(path.join(stateRoot, name))).length;
34
- if (!fs.existsSync(stateRoot) || aggregateCount === 0 && !fs.existsSync(path.join(stateRoot, "entities")))
35
- return "clean";
36
- return fs.existsSync(path.join(stateRoot, "entities")) || aggregateCount === 1 ? "partial" : "v2";
29
+ const state = classifyProjectState(project, sourceRoot).state;
30
+ return state === "entities" ? "v3" : state;
37
31
  }
38
32
  export const ENTITY_MIGRATION_PREVIEW_MAX_OUTPUT_BYTES = 32_768;
39
33
  const DEFAULT_LIMIT = 100;
@@ -245,6 +245,14 @@ function publicationConflict(markerPath) {
245
245
  example: `restore the exact '${markerPath}' marker selected at command start and retry`,
246
246
  });
247
247
  }
248
+ function initializationConflict(markerPath) {
249
+ reject({
250
+ class: "conflict",
251
+ message: `state mode marker '${markerPath}' appeared during fresh plan initialization`,
252
+ syntax: "agentera state plan create --input PLAN.yaml --format json",
253
+ example: "remove no files; inspect the competing state publication and retry",
254
+ });
255
+ }
248
256
  /**
249
257
  * Binds mutations to a validated project and exact entity-mode marker. Every
250
258
  * pathname and byte identity is checked at declared boundaries. Publication
@@ -264,14 +272,24 @@ export class EntityPublicationContext {
264
272
  this.validatedRoot = root;
265
273
  this.markerPath = markerPath;
266
274
  const markerAbsolute = path.join(root.path, ...safeSegments(markerPath, "state mode marker"));
267
- this.marker = readStableFile(markerAbsolute, `state mode marker '${markerPath}'`);
268
- if (!this.marker.bytes.equals(expectedBytes))
269
- publicationConflict(markerPath);
275
+ if (expectedBytes === null) {
276
+ if (readFileIfPresent(markerAbsolute, `state mode marker '${markerPath}'`))
277
+ initializationConflict(markerPath);
278
+ this.marker = null;
279
+ }
280
+ else {
281
+ this.marker = readStableFile(markerAbsolute, `state mode marker '${markerPath}'`);
282
+ if (!this.marker.bytes.equals(expectedBytes))
283
+ publicationConflict(markerPath);
284
+ }
270
285
  this.assertValid();
271
286
  }
272
287
  static open(root, markerPath, expectedBytes) {
273
288
  return new EntityPublicationContext(root, markerPath, expectedBytes);
274
289
  }
290
+ static beginInitialization(root, markerPath) {
291
+ return new EntityPublicationContext(root, markerPath, null);
292
+ }
275
293
  pinnedPath(relativePath = "") {
276
294
  if (this.closed)
277
295
  throw new Error("entity publication context is closed");
@@ -283,6 +301,17 @@ export class EntityPublicationContext {
283
301
  if (this.closed)
284
302
  throw new Error("entity publication context is closed");
285
303
  assertValidatedProjectRoot(this.validatedRoot);
304
+ if (this.marker === null) {
305
+ try {
306
+ if (readFileIfPresent(path.join(this.projectRoot, ...safeSegments(this.markerPath, "state mode marker")), `state mode marker '${this.markerPath}'`)) {
307
+ initializationConflict(this.markerPath);
308
+ }
309
+ }
310
+ catch {
311
+ initializationConflict(this.markerPath);
312
+ }
313
+ return;
314
+ }
286
315
  let current;
287
316
  try {
288
317
  current = readStableFile(this.marker.absolute, `state mode marker '${this.markerPath}'`);
@@ -294,9 +323,25 @@ export class EntityPublicationContext {
294
323
  publicationConflict(this.markerPath);
295
324
  }
296
325
  publishImmutable(relativeTarget, bytes) {
326
+ return this.publishImmutableInternal(relativeTarget, bytes, false);
327
+ }
328
+ isInitializing() {
329
+ return this.marker === null;
330
+ }
331
+ publishStateMarker(bytes) {
332
+ if (this.marker !== null)
333
+ throw new Error("entity state marker is already active");
334
+ const published = this.publishImmutableInternal(this.markerPath, bytes, true);
335
+ if (!published)
336
+ initializationConflict(this.markerPath);
337
+ return published;
338
+ }
339
+ publishImmutableInternal(relativeTarget, bytes, activateMarker) {
297
340
  const segments = safeSegments(relativeTarget, "immutable entity target");
298
341
  if (segments.length < 2)
299
342
  throw new Error(`unsafe immutable entity target '${relativeTarget}'`);
343
+ if (activateMarker && relativeTarget !== this.markerPath)
344
+ throw new Error("only the state mode marker can activate fresh entity state");
300
345
  const directories = [];
301
346
  const target = path.join(this.projectRoot, ...segments);
302
347
  let stage;
@@ -338,11 +383,15 @@ export class EntityPublicationContext {
338
383
  throw new ExactReplacementConflictError(`immutable entity target '${relativeTarget}' changed at its publication boundary`);
339
384
  }
340
385
  publicationOwned = true;
386
+ if (activateMarker)
387
+ this.marker = published;
341
388
  this.assertBoundary(directories, published, [stage]);
342
389
  removeExactFile(stage);
343
390
  stage = undefined;
344
391
  syncDirectory(directory);
345
392
  published = readStableFile(target, `published entity '${relativeTarget}'`);
393
+ if (activateMarker)
394
+ this.marker = published;
346
395
  this.assertBoundary(directories, published);
347
396
  for (const entry of directories.filter(({ created }) => created)) {
348
397
  this.createdDirectories.set(relativeDisplay(this.projectRoot, entry.absolute), entry);
@@ -350,6 +399,8 @@ export class EntityPublicationContext {
350
399
  return publishedIdentity(published);
351
400
  }
352
401
  catch (error) {
402
+ if (activateMarker)
403
+ this.marker = null;
353
404
  if (publicationOwned && published) {
354
405
  if (stage) {
355
406
  try {
@@ -434,6 +485,8 @@ export class EntityPublicationContext {
434
485
  return "identity_mismatch";
435
486
  fs.unlinkSync(target);
436
487
  syncDirectory(directory);
488
+ if (relativeTarget === this.markerPath)
489
+ this.marker = null;
437
490
  this.removeAttemptDirectories();
438
491
  return "removed";
439
492
  }
@@ -6,8 +6,9 @@ import { dumpYamlMapping } from "../core/yaml.js";
6
6
  import { canonicalRecordJson } from "./archiveDiscovery.js";
7
7
  import { StateRetrievalFailure } from "./directRetrieval.js";
8
8
  import { allocateEntityId, canonicalEntityEnvelopeBytes, entityExactGetMaxBytes, exactDiscoveredEntityBytes, publishEntity, replaceEntity, replaceEntityUnderLock, validateEntityDiscovery, validateEntityState, withEntityWriterLock } from "./entityStorage.js";
9
+ import { EntityPublicationContext } from "./entityPublicationContext.js";
9
10
  import { inspectPendingPlanReplacement, publishPlanReplacement, recoverPendingPlanReplacement } from "./planReplacementTransaction.js";
10
- import { detectStateModeBinding } from "./stateMode.js";
11
+ import { detectStateModeBinding, freshEntityStateMarker, freshPlanInitialization } from "./stateMode.js";
11
12
  import { normalizeAndValidatePlanCreateInput, validatePlanPublicationCandidate } from "./write/planPublication.js";
12
13
  import { reject } from "./write/errors.js";
13
14
  import { mutatePlanTaskEvaluation, planTaskRecordViolations } from "./write/planEvaluation.js";
@@ -249,8 +250,24 @@ export function createPlanEntities(req, options = {}) {
249
250
  }
250
251
  return withEntityWriterLock(options.publicationContext, () => createPlanEntitiesUnderLock(req, options));
251
252
  }
253
+ /** The first Plan is the sole marker-absent entity initializer. */
254
+ export function createFreshPlanEntities(req, options = {}) {
255
+ const sourceRoot = options.sourceRoot ?? resolveSourceRoot();
256
+ const initialization = freshPlanInitialization(req.projectRoot, sourceRoot);
257
+ if (!initialization)
258
+ return null;
259
+ const publicationContext = EntityPublicationContext.beginInitialization(initialization.root, initialization.markerPath);
260
+ try {
261
+ return createPlanEntities(req, { ...options, sourceRoot, publicationContext });
262
+ }
263
+ finally {
264
+ publicationContext.close();
265
+ }
266
+ }
252
267
  function createPlanEntitiesUnderLock(req, options) {
253
268
  const sourceRoot = options.sourceRoot ?? resolveSourceRoot();
269
+ const initializing = options.publicationContext.isInitializing();
270
+ const marker = initializing ? freshEntityStateMarker(sourceRoot) : null;
254
271
  const input = preparedPlanCreateInput(req);
255
272
  const discovery = validateEntityState(options.publicationContext.pinnedPath(), sourceRoot, { kind: "project", projectRoot: options.publicationContext.validatedRoot });
256
273
  const entities = all(options.publicationContext.pinnedPath(), sourceRoot, discovery);
@@ -292,8 +309,21 @@ function createPlanEntitiesUnderLock(req, options) {
292
309
  return record;
293
310
  });
294
311
  const publications = [{ boundary: PLAN, id: planId, record: planRecord }, ...taskRecords.map((record, index) => ({ boundary: TASK, id: taskIds[index], record }))];
312
+ const resultExtra = {
313
+ tasks: taskRecords.map((record, index) => ({ id: taskIds[index], artifact: ARTIFACT, record })),
314
+ effects: lifecycle.effects,
315
+ ...(marker ? {
316
+ initialization: {
317
+ atomic: true,
318
+ marker: {
319
+ path: path.join(req.projectRoot, options.publicationContext.markerPath),
320
+ record: marker,
321
+ },
322
+ },
323
+ } : {}),
324
+ };
295
325
  if (req.dryRun)
296
- return envelope(command, { id: planId, path: entityPath(req.projectRoot, sourceRoot, PLAN, planId), replay: false }, planRecord, true, { tasks: taskRecords.map((record, index) => ({ id: taskIds[index], artifact: ARTIFACT, record })), effects: lifecycle.effects });
326
+ return envelope(command, { id: planId, path: entityPath(req.projectRoot, sourceRoot, PLAN, planId), replay: false }, planRecord, true, resultExtra);
297
327
  if (command === "state plan replace" && lifecycle.predecessor && lifecycle.archivedRecord && lifecycle.replacementInputSha256) {
298
328
  const targets = [
299
329
  {
@@ -314,10 +344,11 @@ function createPlanEntitiesUnderLock(req, options) {
314
344
  throw new Error(`targeted plan replacement graph failed state validation: ${validation.issues.map(({ message }) => message).join("; ")}`);
315
345
  },
316
346
  });
317
- return envelope(command, { id: planId, path: entityPath(req.projectRoot, sourceRoot, PLAN, planId), replay: false }, planRecord, false, { tasks: taskRecords.map((record, index) => ({ id: taskIds[index], artifact: ARTIFACT, record })), effects: lifecycle.effects });
347
+ return envelope(command, { id: planId, path: entityPath(req.projectRoot, sourceRoot, PLAN, planId), replay: false }, planRecord, false, resultExtra);
318
348
  }
319
349
  const published = [];
320
350
  let predecessor;
351
+ let markerIdentity;
321
352
  try {
322
353
  if (lifecycle.predecessor && lifecycle.archivedRecord) {
323
354
  const archived = replaceEntityUnderLock({ projectRoot: req.projectRoot, sourceRoot, publicationContext: options.publicationContext, artifact: ARTIFACT, boundary: PLAN, id: lifecycle.predecessor.id, expectedRecord: lifecycle.predecessor.record, expectedBytes: exactDiscoveredEntityBytes(lifecycle.predecessor), migrationProvenance: lifecycle.predecessor.migrationProvenance, record: lifecycle.archivedRecord });
@@ -338,6 +369,10 @@ function createPlanEntitiesUnderLock(req, options) {
338
369
  if (!validation.valid)
339
370
  throw new Error(`created plan graph failed state validation: ${validation.issues.map(({ message }) => message).join("; ")}`);
340
371
  options.publicationContext.assertValid();
372
+ if (marker) {
373
+ markerIdentity = options.publicationContext.publishStateMarker(dumpYamlMapping(marker));
374
+ options.publicationContext.assertValid();
375
+ }
341
376
  }
342
377
  catch (error) {
343
378
  const recoveryFailures = [];
@@ -359,11 +394,21 @@ function createPlanEntitiesUnderLock(req, options) {
359
394
  recoveryFailures.push(`predecessor restoration failed because ownership changed or publication was unsafe: ${restoreError.message}`);
360
395
  }
361
396
  }
397
+ if (markerIdentity) {
398
+ try {
399
+ const removed = options.publicationContext.removeExact(options.publicationContext.markerPath, markerIdentity, false);
400
+ if (removed === "identity_mismatch")
401
+ recoveryFailures.push("cleanup ownership changed for fresh state marker");
402
+ }
403
+ catch (cleanupError) {
404
+ recoveryFailures.push(`cleanup failed for fresh state marker: ${cleanupError.message}`);
405
+ }
406
+ }
362
407
  if (recoveryFailures.length)
363
408
  throw new Error(`plan replacement failed: ${error.message}; recovery failed: ${recoveryFailures.join("; ")}`, { cause: error });
364
409
  throw error;
365
410
  }
366
- return envelope(command, { id: planId, path: entityPath(req.projectRoot, sourceRoot, PLAN, planId), replay: false }, planRecord, false, { tasks: taskRecords.map((record, index) => ({ id: taskIds[index], artifact: ARTIFACT, record })), effects: lifecycle.effects });
411
+ return envelope(command, { id: planId, path: entityPath(req.projectRoot, sourceRoot, PLAN, planId), replay: false }, planRecord, false, resultExtra);
367
412
  }
368
413
  function replacementEffects(predecessor, successor) {
369
414
  return {
@@ -0,0 +1,33 @@
1
+ const APPEND_GUIDANCE = "Append progress only for durable project truth that future work needs, a required glossary_caveat lifecycle event, or a plan-completion sweep. Glossary caveats and plan-completion sweeps require a record; an ordinary build or release attempt with no durable project truth change, and an outcome future work does not need as a milestone, require no progress append.";
2
+ const RECEIPT_GUIDANCE = "Qualification and publication receipts own timings, integrity, digests, retries, and replay; do not duplicate receipt detail in progress.";
3
+ const PROGRESS_WRITE_POLICY = {
4
+ schemaVersion: "agentera.progressWritePolicy.v1",
5
+ append: {
6
+ mode: "conditional",
7
+ allowed_when: [
8
+ "durable_project_truth_needed_by_future_work",
9
+ "required_glossary_caveat",
10
+ "plan_completion_sweep",
11
+ ],
12
+ required_when: [
13
+ "required_glossary_caveat",
14
+ "plan_completion_sweep",
15
+ ],
16
+ no_append_required_when: [
17
+ "ordinary_build_or_release_attempt_without_durable_project_truth_change",
18
+ "durable_outcome_not_needed_by_future_work",
19
+ ],
20
+ guidance: APPEND_GUIDANCE,
21
+ },
22
+ receipt_detail: {
23
+ owners: ["qualification_receipt", "publication_receipt"],
24
+ fields: ["timings", "integrity", "digests", "retries", "replay"],
25
+ guidance: RECEIPT_GUIDANCE,
26
+ },
27
+ };
28
+ export function progressWritePolicy() {
29
+ return structuredClone(PROGRESS_WRITE_POLICY);
30
+ }
31
+ export function progressWriteGuidance() {
32
+ return [APPEND_GUIDANCE, RECEIPT_GUIDANCE];
33
+ }