agentera 3.0.0-dev.75 → 3.0.0-dev.77

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 (64) hide show
  1. package/README.md +23 -0
  2. package/bundle/.agentera-build-source.json +5 -5
  3. package/bundle/CHANGELOG.md +15 -1
  4. package/bundle/UPGRADE.md +24 -5
  5. package/bundle/references/adapters/package-publication.json +4 -4
  6. package/bundle/references/adapters/package-registry.yaml +153 -151
  7. package/bundle/references/adapters/product-v1-reset.yaml +101 -0
  8. package/bundle/references/artifacts/glossary-entry-contract.yaml +163 -9
  9. package/bundle/references/artifacts/state-storage-authority.yaml +0 -5
  10. package/bundle/references/cli/update-channels.yaml +1 -1
  11. package/bundle/references/cli/vocabulary-index.yaml +2 -2
  12. package/bundle/references/cli/vocabulary.md +2 -3
  13. package/bundle/references/meta/retained-reference-authority.yaml +5 -0
  14. package/bundle/skills/agentera/schemas/artifacts/decisions.yaml +1 -20
  15. package/bundle/skills/agentera/schemas/artifacts/docs.yaml +0 -31
  16. package/bundle/skills/agentera/schemas/artifacts/experiments.yaml +0 -25
  17. package/bundle/skills/agentera/schemas/artifacts/glossary.yaml +1 -1
  18. package/bundle/skills/agentera/schemas/artifacts/health.yaml +1 -36
  19. package/bundle/skills/agentera/schemas/artifacts/objective.yaml +0 -31
  20. package/bundle/skills/agentera/schemas/artifacts/plan.yaml +1 -35
  21. package/bundle/skills/agentera/schemas/artifacts/progress.yaml +1 -25
  22. package/bundle/skills/agentera/schemas/artifacts/vision.yaml +0 -19
  23. package/dist/.agentera-build-source.json +5 -5
  24. package/dist/analytics/personalGlossaryAdmission.js +1 -1
  25. package/dist/analytics/personalGlossaryCandidateProjection.js +96 -0
  26. package/dist/analytics/personalGlossaryCandidateProjectionModel.js +1 -0
  27. package/dist/analytics/personalGlossaryRefreshProjection.js +351 -0
  28. package/dist/capabilities/status/instructions.js +1 -1
  29. package/dist/cli/commands/doctor.js +1 -0
  30. package/dist/cli/commands/personalGlossaryCandidateReads.js +12 -1
  31. package/dist/cli/commands/prime/briefOrientation.js +1 -1
  32. package/dist/cli/commands/prime/collectOrientationState.js +0 -7
  33. package/dist/cli/commands/prime/orientationOutput.js +3 -9
  34. package/dist/cli/commands/report.js +98 -8
  35. package/dist/cli/commands/upgrade.js +30 -0
  36. package/dist/cli/dispatch/index.js +7 -0
  37. package/dist/cli/dispatch/lifecycle.js +20 -0
  38. package/dist/cli/help.js +10 -3
  39. package/dist/cli/orientation/attention.js +1 -4
  40. package/dist/cli/productV1Eol.js +55 -0
  41. package/dist/cli/startupCompletenessContract.js +0 -1
  42. package/dist/cli/stateQuery.js +1 -1
  43. package/dist/registries/activationTuples.js +11 -2
  44. package/dist/registries/glossaryCandidateProjectionAuthority.js +154 -6
  45. package/dist/registries/glossaryCandidateProjectionContract.js +4 -0
  46. package/dist/registries/glossaryEntryContract.js +3 -3
  47. package/dist/registries/packagePublication.js +2 -2
  48. package/dist/runtime/nativeResourceCleanup.js +1 -0
  49. package/dist/state/entityMigrationTodo.js +1 -1
  50. package/dist/state/todoDocsEntities.js +8 -3
  51. package/dist/state/todoMarkdownProjection.js +18 -6
  52. package/dist/state/todoReconciliationInspection.js +15 -2
  53. package/dist/state/todoReconciliationRepair.js +3 -1
  54. package/dist/state/todoSeverityHeadings.js +71 -0
  55. package/dist/upgrade/legacyAgentCleanup.js +1 -1
  56. package/dist/upgrade/migrateArtifactsV2ToV3.js +1 -27
  57. package/dist/upgrade/nextMajorDoctor.js +0 -21
  58. package/dist/upgrade/productV1Reset.js +515 -0
  59. package/dist/upgrade/productV1ResetAuthority.js +139 -0
  60. package/dist/upgrade/projectIntegration.js +4 -6
  61. package/dist/upgrade/upgradeOrchestrator.js +4 -12
  62. package/dist/validate/{v1LegacyCruft.js → glossaryVariantGuard.js} +3 -38
  63. package/package.json +2 -2
  64. package/dist/cli/commands/prime/v1Migration.js +0 -38
@@ -12,13 +12,61 @@ function sameStrings(actual, expected) {
12
12
  function nonEmpty(value) {
13
13
  return typeof value === "string" && value.trim().length > 0;
14
14
  }
15
+ const EXPLICIT_ABSTENTION_KEYS = [
16
+ "attributed_quotation",
17
+ "ambiguous_reference",
18
+ "conflicting_meaning",
19
+ "empty_meaning",
20
+ "empty_term",
21
+ "example_context",
22
+ "hypothetical_definition",
23
+ "malformed_span",
24
+ "meaning_bound_exceeded",
25
+ "negated_definition",
26
+ "indirect_question",
27
+ "future_definition",
28
+ "project_only_scope",
29
+ "question_definition",
30
+ "retracted_definition",
31
+ "sarcasm_marker",
32
+ "stale_anchor",
33
+ "structural_fragment",
34
+ "term_bound_exceeded",
35
+ "unsafe_syntax",
36
+ "uncertain_scope",
37
+ "unresolved_anchor",
38
+ "provenance_incomplete",
39
+ "user_authorship_required",
40
+ ];
41
+ const RECURRING_ABSTENTION_KEYS = [
42
+ "agent_only_evidence",
43
+ "copied_content",
44
+ "provenance_missing",
45
+ "insufficient_independent_origins",
46
+ "insufficient_projects",
47
+ "insufficient_sessions",
48
+ "conversation_evidence_incomplete",
49
+ "conversation_evidence_bound_exceeded",
50
+ "project_only_scope",
51
+ "noise_term",
52
+ "too_many_qualifying_origins",
53
+ "user_authorship_required",
54
+ ];
15
55
  /** Validate the authority-owned bounds for the internal candidate projection. */
16
56
  export function validatePersonalCandidateProjectionAuthority(authority) {
17
57
  const mining = mapping(authority.personal_mining_authority);
18
58
  const projection = mapping(mining?.candidate_projection);
59
+ const refreshProduction = mapping(projection?.refresh_production);
60
+ const refreshProjection = mapping(refreshProduction?.projection);
61
+ const refreshConcurrency = mapping(refreshProduction?.concurrency);
62
+ const refreshContender = mapping(refreshConcurrency?.contender);
63
+ const partialFailure = mapping(refreshProduction?.partial_failure);
64
+ const safeReplacement = mapping(refreshProduction?.safe_replacement);
19
65
  const selection = mapping(projection?.selection);
20
66
  const sourceFamilies = mapping(selection?.source_families);
21
67
  const projectIdentity = mapping(selection?.project_identity);
68
+ const miningSummary = mapping(projection?.mining_summary);
69
+ const summaryReconciliation = mapping(miningSummary?.reconciliation);
22
70
  const excerpts = mapping(projection?.excerpts);
23
71
  const excerptCompatibility = mapping(excerpts?.compatibility);
24
72
  const retention = mapping(projection?.retention);
@@ -35,6 +83,109 @@ export function validatePersonalCandidateProjectionAuthority(authority) {
35
83
  const privacy = mapping(mining?.privacy);
36
84
  const contentExclusion = mapping(privacy?.content_exclusion);
37
85
  const storageFilesystem = mapping(mapping(privacy?.storage)?.filesystem);
86
+ const malformedProjectionReplacementException = mapping(storageFilesystem?.malformed_candidate_projection_replacement_exception);
87
+ const errors = [];
88
+ if (refreshProduction?.status !== "active" ||
89
+ refreshProduction?.runtime !==
90
+ "packages/cli/src/analytics/personalGlossaryRefreshProjection.ts#produceCurrentPersonalGlossaryProjection" ||
91
+ refreshProduction?.trigger !== "explicit_consented_evidence_refresh_success" ||
92
+ refreshProjection?.count !== "exactly_one" ||
93
+ refreshProjection?.generation_binding !== "exact_published_evidence_generation" ||
94
+ refreshProjection?.policy_binding !== "exact_personal_mining_policy_version" ||
95
+ !nonEmpty(refreshProduction?.rule)) {
96
+ errors.push("personal_mining_authority refresh production must require one projection bound to the exact evidence generation and mining policy");
97
+ }
98
+ if (refreshConcurrency?.serialization !== "profile_local_exclusive_refresh_commit" ||
99
+ refreshConcurrency?.lock_lifetime !==
100
+ "before_evidence_publication_through_projection_binding_verification" ||
101
+ refreshContender?.refresh_outcome !== "failure" ||
102
+ refreshContender?.process_exit !== "nonzero" ||
103
+ refreshContender?.local_history_read !== false ||
104
+ refreshContender?.tier_write !== false ||
105
+ refreshContender?.projection_status !== "failed" ||
106
+ refreshContender?.projection_write !== false ||
107
+ refreshContender?.evidence_status !== "current_generation_remains_readable" ||
108
+ refreshContender?.recovery !== "npx -y agentera@next report refresh --consent local-history") {
109
+ errors.push("personal_mining_authority refresh production must serialize evidence and projection commit and fail contenders without writes");
110
+ }
111
+ if (partialFailure?.refresh_outcome !== "failure" ||
112
+ partialFailure?.process_exit !== "nonzero" ||
113
+ partialFailure?.evidence_status !== "published" ||
114
+ partialFailure?.projection_status !== "failed" ||
115
+ partialFailure?.tier_write_reporting !== "evidence_published_projection_not_written" ||
116
+ partialFailure?.current_generation !== "published_evidence_remains_current") {
117
+ errors.push("personal_mining_authority refresh production must report evidence and projection outcomes separately and preserve published evidence on projection failure");
118
+ }
119
+ if (safeReplacement?.authorization !== "explicit_consented_refresh_only" ||
120
+ safeReplacement?.target !== "exact_configured_candidate_projection_file" ||
121
+ safeReplacement?.precondition !== "existing_owned_regular_file_is_malformed" ||
122
+ safeReplacement?.operation !== "atomic_regular_file_replacement" ||
123
+ !sameStrings(safeReplacement?.reject, [
124
+ "symlink",
125
+ "non_regular_file",
126
+ "configured_path_mismatch",
127
+ "path_escape",
128
+ ]) ||
129
+ safeReplacement?.filesystem_exception !==
130
+ "personal_mining_authority.privacy.storage.filesystem.malformed_candidate_projection_replacement_exception" ||
131
+ !sameStrings(safeReplacement?.replaceable_state, ["candidate_projection"]) ||
132
+ !sameStrings(safeReplacement?.preserved_state, [
133
+ "current_evidence_generation",
134
+ "review_records",
135
+ "personal_profile",
136
+ "project_state",
137
+ "project_glossary",
138
+ ])) {
139
+ errors.push("personal_mining_authority refresh replacement must target only the exact owned regular projection and preserve evidence, review, profile, and project state");
140
+ }
141
+ if (malformedProjectionReplacementException?.scope !==
142
+ "malformed_candidate_projection_replacement_only" ||
143
+ malformedProjectionReplacementException?.authorization !== "explicit_consented_refresh_only" ||
144
+ malformedProjectionReplacementException?.precondition !==
145
+ "existing_owned_regular_file_is_malformed" ||
146
+ malformedProjectionReplacementException?.target !==
147
+ "exact_configured_candidate_projection_file" ||
148
+ malformedProjectionReplacementException?.inspection !==
149
+ "lstat_exact_target_without_following_symlinks" ||
150
+ malformedProjectionReplacementException?.required_type !== "regular_file" ||
151
+ !sameStrings(malformedProjectionReplacementException?.reject, [
152
+ "symlink",
153
+ "non_regular_file",
154
+ "configured_path_mismatch",
155
+ "path_escape",
156
+ ]) ||
157
+ malformedProjectionReplacementException?.operation !== "atomic_regular_file_replacement" ||
158
+ malformedProjectionReplacementException?.outside_scope !== "configured_path_use_as_provided") {
159
+ errors.push("personal_mining_authority filesystem exception must confine malformed projection replacement to the exact configured regular file with no-follow inspection");
160
+ }
161
+ if (miningSummary?.schema_version !== "agentera.personalGlossaryMiningSummary.v1" ||
162
+ miningSummary?.location !== "report.mining_summary" ||
163
+ !sameStrings(miningSummary?.family_fields, [
164
+ "candidate_count",
165
+ "abstention_count",
166
+ "abstentions_by_reason",
167
+ ]) ||
168
+ !sameStrings(miningSummary?.explicit_abstention_keys, EXPLICIT_ABSTENTION_KEYS) ||
169
+ !sameStrings(miningSummary?.recurring_abstention_keys, RECURRING_ABSTENTION_KEYS) ||
170
+ summaryReconciliation?.fixed_keys !== "all_keys_present_as_nonnegative_integers" ||
171
+ summaryReconciliation?.family_abstentions !== "abstention_count_equals_sum_of_reason_counts" ||
172
+ summaryReconciliation?.total_candidates !==
173
+ "total_equals_explicit_plus_recurring_candidate_counts" ||
174
+ summaryReconciliation?.total_abstentions !==
175
+ "total_equals_explicit_plus_recurring_abstention_counts" ||
176
+ summaryReconciliation?.projection_input !== "report_input_count_equals_total_candidate_count" ||
177
+ !sameStrings(miningSummary?.forbidden_content, [
178
+ "terms",
179
+ "source_ids",
180
+ "evidence_anchors",
181
+ "project_ids",
182
+ "project_keys",
183
+ "paths",
184
+ "excerpts",
185
+ ]) ||
186
+ !nonEmpty(miningSummary?.rule)) {
187
+ errors.push("personal_mining_authority candidate projection must retain reconciled fixed-key aggregate mining counts without sensitive identity or content");
188
+ }
38
189
  if (projection?.status !== "active" ||
39
190
  projection?.runtime !==
40
191
  "packages/cli/src/analytics/personalGlossaryCandidateProjection.ts#projectPersonalGlossaryCandidates" ||
@@ -106,8 +257,7 @@ export function validatePersonalCandidateProjectionAuthority(authority) {
106
257
  currentGeneration?.unavailable_behavior !== "current_generation_unavailable" ||
107
258
  currentGeneration?.stale_projection_behavior !== "projection_stale" ||
108
259
  !nonEmpty(retrieval?.rule) ||
109
- retrievalCommand?.canonical !==
110
- "npx -y agentera@next report personal-glossary-candidates" ||
260
+ retrievalCommand?.canonical !== "npx -y agentera@next report personal-glossary-candidates" ||
111
261
  retrievalCommand?.namespace !== "report" ||
112
262
  retrievalCommand?.format !== "json" ||
113
263
  retrievalCommand?.project_checkout !== "not_required" ||
@@ -189,9 +339,7 @@ export function validatePersonalCandidateProjectionAuthority(authority) {
189
339
  storageFilesystem?.symlink_confinement !== "not_claimed" ||
190
340
  !nonEmpty(storageFilesystem?.rule) ||
191
341
  !nonEmpty(persistence?.rule)) {
192
- return [
193
- "personal_mining_authority candidate projection must bound deterministic allocation, private retrieval, content exclusion, safe excerpts, host-filesystem storage, retention, and user-local replay",
194
- ];
342
+ errors.push("personal_mining_authority candidate projection must bound deterministic allocation, private retrieval, content exclusion, safe excerpts, host-filesystem storage, retention, and user-local replay");
195
343
  }
196
- return [];
344
+ return errors;
197
345
  }
@@ -23,6 +23,7 @@ export function personalGlossaryCandidateProjectionContract(pathname = glossaryE
23
23
  const retention = mapping(privacy?.retention);
24
24
  const projectIdentity = mapping(selection?.project_identity);
25
25
  const persistence = mapping(projection?.persistence);
26
+ const miningSummary = mapping(projection?.mining_summary);
26
27
  const families = mapping(selection?.source_families);
27
28
  const sourceFamilies = Object.fromEntries(Object.entries(families ?? {}).flatMap(([family, values]) => Array.isArray(values) && values.every((value) => typeof value === "string")
28
29
  ? [[family, [...values]]]
@@ -53,6 +54,9 @@ export function personalGlossaryCandidateProjectionContract(pathname = glossaryE
53
54
  ? contentExclusion.candidate_reason
54
55
  : "",
55
56
  excerptSensitiveContentAction: typeof contentExclusion?.excerpt_action === "string" ? contentExclusion.excerpt_action : "",
57
+ miningSummarySchemaVersion: typeof miningSummary?.schema_version === "string" ? miningSummary.schema_version : "",
58
+ explicitAbstentionKeys: strings(miningSummary?.explicit_abstention_keys),
59
+ recurringAbstentionKeys: strings(miningSummary?.recurring_abstention_keys),
56
60
  candidateReadCommand: projectGlossaryDevelopmentValue(retrievalCommand?.canonical, "candidate_retrieval.command"),
57
61
  candidateReadSchemaVersion: typeof list?.schema_version === "string" ? list.schema_version : "",
58
62
  candidateReadDefaultLimit: typeof list?.default_limit === "number" ? list.default_limit : 0,
@@ -388,8 +388,8 @@ export function validateGlossaryEntryContract(pathname = glossaryEntryAuthorityP
388
388
  errors.push("project glossary digest and build publication must remain active under the declared consumer boundary");
389
389
  }
390
390
  if (confirmedVariantGuard?.owner !==
391
- "packages/cli/src/validate/v1LegacyCruft.ts#scanPost30CruftViolations" ||
392
- confirmedVariantGuard?.validation_surface !== "packages/cli/test/cli/v1LegacyCruft.test.ts" ||
391
+ "packages/cli/src/validate/glossaryVariantGuard.ts#scanConfirmedVariantViolations" ||
392
+ confirmedVariantGuard?.validation_surface !== "packages/cli/test/cli/glossaryVariantGuard.test.ts" ||
393
393
  confirmedVariantGuard?.loader !==
394
394
  "packages/cli/src/state/write/glossaryPublication.ts#loadProjectGlossaryDocument" ||
395
395
  confirmedVariantGuard?.matching !== "exact_case_sensitive_boundary_aware_literal" ||
@@ -424,7 +424,7 @@ export function validateGlossaryEntryContract(pathname = glossaryEntryAuthorityP
424
424
  "vendor",
425
425
  ]) ||
426
426
  !nonEmpty(confirmedVariantGuard?.exclusion_rule)) {
427
- errors.push("confirmed variant guard must reuse validated project glossary pairs and bounded legacy-cruft exclusions");
427
+ errors.push("confirmed variant guard must reuse validated project glossary pairs and bounded exclusions");
428
428
  }
429
429
  if (proposalValidation?.owner !== "audit" ||
430
430
  proposalValidation?.shared_runtime !==
@@ -52,12 +52,12 @@ export const ACTIVATION_CENSUS_AUTHORITY = deepFreeze({
52
52
  cli: { count: 27, sha256: "986a637f5c11c57ab7a897a01bd291d0811846e5339c3831c715932cf9659ad4" },
53
53
  capability: { count: 12, sha256: "007e1157a892fec54182b1803aeaa442cf8e2e332e6a055c3bd020e2b0731867" },
54
54
  runtime: { count: 81, sha256: "03fe600a3a27f05daf0d1b59c16b00bedc354a13c70ea60be0b457175fdf743c" },
55
- reference: { count: 25, sha256: "2094946cd6e189c6ab3db16dd0725596cec759abcea88d8335b05215b65ccfc2" },
55
+ reference: { count: 26, sha256: "aa1ad513d89b66c9133a8572cef2dce760e76d6ee39d2ffa2d4e312578dcc847" },
56
56
  state: { count: 38, sha256: "7ab0dd6ef1b1b1ce66bd9ea94d1de3d542528233e928a44102e289bf12416681" },
57
57
  package: { count: 67, sha256: "d73cd86fed39057cf9ae73e462a59e842372392b1e88846f2b220378a2ff75cc" },
58
58
  bootstrap: { count: 34, sha256: "71c2038744e2518a6adb722acbb5f9352bddfb5d1c92eeb0e347297ef2ca2f1e" },
59
59
  },
60
- total: { count: 284, sha256: "3f30b26839aec47639f2197c9efda5caf2a73d5afca9fb38ff9a341e18a72b98" },
60
+ total: { count: 285, sha256: "042700a26b34afba57b60eaf4a4529d9e02ce39d8574e6883dfb892eebc64fd5" },
61
61
  });
62
62
  /** Each dimension names the production contract it observes independently. */
63
63
  export const ACTIVATION_EVIDENCE_SOURCES = deepFreeze({
@@ -313,6 +313,7 @@ export function loadNativeResourceCleanupContract(contractPath = path.join(resol
313
313
  configuration: data.configuration_inventory.map((unit) => ({
314
314
  id: unit.id,
315
315
  host: unit.host,
316
+ destination: unit.destination,
316
317
  key: unit.key,
317
318
  })),
318
319
  };
@@ -110,7 +110,7 @@ export function todoReconciliationMigrationPlan(todoPath, files, entries) {
110
110
  }));
111
111
  if (byLine.size !== rows.length)
112
112
  return undefined;
113
- const lines = todo.bytes.toString("utf8").split(/\r?\n/);
113
+ const lines = todo.bytes.toString("utf8").split(/\r?\n/).map((line) => line === "## Resolved" ? "## ✓ Resolved" : line);
114
114
  for (const row of rows) {
115
115
  const entry = byLine.get(row.index);
116
116
  if (!entry?.proposed_target)
@@ -22,6 +22,7 @@ import { loadTodoReconciliationActivation, todoLegacyRowFingerprint, todoReconci
22
22
  import { normalizeTodoOwnerCorrectionEvidence, planTodoOwnerCorrection, planTodoRepair } from "./todoReconciliationRepair.js";
23
23
  import { readTodoMarkdown, renderManagedMarkdown } from "./todoMarkdownProjection.js";
24
24
  import { inactiveTodoActivationSafety, rejectUnsafeInactiveTodoActivation, unsafeInactiveDuplicateDiagnosis } from "./todoActivationSafety.js";
25
+ import { assertTodoSeverityHeadingStructure, todoSeveritySectionForHeading } from "./todoSeverityHeadings.js";
25
26
  const ID = /^[a-z]{10}$/;
26
27
  const SHA256 = /^[a-f0-9]{64}$/;
27
28
  const TODO = { artifact: "todo", boundary: "todo_item", order: "severity_then_status_then_markdown_order_then_id" };
@@ -139,6 +140,7 @@ function todoAuthority() {
139
140
  };
140
141
  }
141
142
  export function managedRows(markdown, activation, entities) {
143
+ assertTodoSeverityHeadingStructure(markdown);
142
144
  const result = new Map();
143
145
  const order = new Map();
144
146
  const legacy = [];
@@ -146,7 +148,7 @@ export function managedRows(markdown, activation, entities) {
146
148
  markdown.split(/\r?\n/).forEach((line, index) => {
147
149
  const heading = line.trim().match(/^##\s+(.+)$/)?.[1]?.toLowerCase();
148
150
  if (heading)
149
- section = heading.includes("critical") ? "critical" : heading.includes("degraded") ? "degraded" : heading.includes("annoying") ? "annoying" : heading.includes("resolved") ? "resolved" : heading.includes("normal") ? "normal" : heading === "notes" ? null : section;
151
+ section = todoSeveritySectionForHeading(line) ?? (heading === "notes" ? null : section);
150
152
  const item = parseTodoMarkdownListItem(line.trim());
151
153
  if (!item)
152
154
  return;
@@ -291,16 +293,17 @@ function inspectTodoReadView(root, sourceRoot, entities) {
291
293
  catch (error) {
292
294
  if (error instanceof StateWriteInputError && /Markdown/i.test(error.body.message))
293
295
  throw error;
296
+ const diagnosis = error instanceof StateWriteInputError && mapping(error.body.diagnosis) ? error.body.diagnosis : undefined;
294
297
  return {
295
298
  rows: new Map(),
296
299
  drift: {
297
300
  schema_version: RECONCILIATION_VERSION,
298
- status: "unsafe",
301
+ status: "conflict", action_required: true,
299
302
  read_effect: "none",
300
303
  next_write_boundary: "atomic_reconciliation",
301
304
  authority: todoAuthority(),
302
305
  counts: { managed: 0, drifted: 0, conflicts: 1 },
303
- items: [],
306
+ items: [], ...(diagnosis ? { diagnosis } : {}),
304
307
  },
305
308
  };
306
309
  }
@@ -669,6 +672,8 @@ export function mutateTodoDocsEntity(req, options = {}) {
669
672
  const correctingOwners = artifact === "todo" && req.spec.verb === "correct-owners";
670
673
  const ownerEvidence = correctingOwners ? normalizeTodoOwnerCorrectionEvidence(req.input ?? {}) : null;
671
674
  const pending = todoBinding ? inspectTodoReconciliation(pinnedRoot, todoBinding) : [];
675
+ if (artifact === "todo")
676
+ assertTodoSeverityHeadingStructure(readTodoMarkdown(todoPublicPath(pinnedRoot, sourceRoot)).text);
672
677
  const initialActivation = artifact === "todo" ? loadTodoReconciliationActivation(pinnedRoot) : null;
673
678
  if (artifact === "todo" && ["activate", "repair", "correct-owners"].includes(req.spec.verb)) {
674
679
  const repairing = req.spec.verb === "repair";
@@ -1,6 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import { renderTodoPublicRecord } from "../cli/todoMarkdown.js";
3
3
  import { reject } from "./write/errors.js";
4
+ import { TODO_SEVERITY_HEADINGS, todoSeveritySectionForHeading } from "./todoSeverityHeadings.js";
4
5
  const TODO_MARKDOWN_MAX_BYTES = 1024 * 1024;
5
6
  export function readTodoMarkdown(target) {
6
7
  if (!fs.existsSync(target))
@@ -21,7 +22,7 @@ export function readTodoMarkdown(target) {
21
22
  return { bytes, text: text };
22
23
  }
23
24
  function sectionFor(record) { return record.status === "resolved" ? "resolved" : String(record.severity); }
24
- function headingFor(section) { return section === "resolved" ? "## ✓ Resolved" : `## → ${section[0].toUpperCase()}${section.slice(1)}`; }
25
+ function headingFor(section) { return TODO_SEVERITY_HEADINGS.find((entry) => entry.section === section).heading; }
25
26
  function rowFor(id, record) { return `- [${record.status === "resolved" ? "x" : " "}] [id:${id}] ${renderTodoPublicRecord(record)}`; }
26
27
  export function renderManagedMarkdown(markdown, records, existing) {
27
28
  const byLine = new Map([...existing.values()].map((row) => [row.line, row]));
@@ -29,16 +30,27 @@ export function renderManagedMarkdown(markdown, records, existing) {
29
30
  const lines = markdown.split(/\r?\n/).flatMap((line, index) => { const row = byLine.get(index); if (!row)
30
31
  return [line]; const record = records.get(row.id); if (!record || sectionFor(record) !== row.section)
31
32
  return []; retained.add(row.id); return [rowFor(row.id, record)]; });
32
- for (const section of ["critical", "degraded", "normal", "annoying", "resolved"]) {
33
+ for (const { section } of TODO_SEVERITY_HEADINGS) {
33
34
  const ids = [...records.entries()].filter(([id, record]) => !retained.has(id) && sectionFor(record) === section).sort(([left], [right]) => { const a = existing.get(left); const b = existing.get(right); return (a?.section === section ? a.snapshot.order : Number.MAX_SAFE_INTEGER) - (b?.section === section ? b.snapshot.order : Number.MAX_SAFE_INTEGER) || left.localeCompare(right); }).map(([id]) => id);
34
35
  if (!ids.length)
35
36
  continue;
36
37
  let heading = lines.findIndex((line) => line.trim().toLowerCase() === headingFor(section).toLowerCase());
37
38
  if (heading < 0) {
38
- if (lines.at(-1)?.trim())
39
- lines.push("");
40
- lines.push(headingFor(section));
41
- heading = lines.length - 1;
39
+ const order = TODO_SEVERITY_HEADINGS.findIndex((entry) => entry.section === section);
40
+ const later = lines.findIndex((line) => {
41
+ const found = todoSeveritySectionForHeading(line);
42
+ return found !== null && TODO_SEVERITY_HEADINGS.findIndex((entry) => entry.section === found) > order;
43
+ });
44
+ if (later >= 0) {
45
+ lines.splice(later, 0, headingFor(section), "");
46
+ heading = later;
47
+ }
48
+ else {
49
+ if (lines.at(-1)?.trim())
50
+ lines.push("");
51
+ lines.push(headingFor(section));
52
+ heading = lines.length - 1;
53
+ }
42
54
  }
43
55
  let insert = heading + 1;
44
56
  while (insert < lines.length && !/^##\s+/.test(lines[insert].trim()))
@@ -76,6 +76,19 @@ function errorCounts(error) {
76
76
  ...(diagnosis.omitted_count !== undefined ? { omitted_count: diagnosis.omitted_count } : {}),
77
77
  };
78
78
  }
79
+ function errorRisks(error) {
80
+ return error instanceof StateWriteInputError && mapping(error.body.diagnosis) ? error.body.diagnosis : undefined;
81
+ }
82
+ function errorInspection(state, error) {
83
+ const risks = errorRisks(error);
84
+ const result = inspection(state, errorCounts(error), risks);
85
+ if (risks?.classification === "todo_severity_heading_structure" && error instanceof StateWriteInputError) {
86
+ result.preview_command = null;
87
+ result.apply_command = null;
88
+ result.recovery_command = error.body.recovery ?? result.recovery_command;
89
+ }
90
+ return result;
91
+ }
79
92
  /** Read-only classification shared by prime, doctor, and whole-state validation. */
80
93
  export function inspectTodoReconciliationState(root, sourceRoot = resolveSourceRoot(), discovery) {
81
94
  try {
@@ -112,7 +125,7 @@ export function inspectTodoReconciliationState(root, sourceRoot = resolveSourceR
112
125
  return inspection(safety.safe ? "inactive" : "unsafe_inactive", safety.counts, safety.safe ? undefined : safety.risks);
113
126
  }
114
127
  catch (error) {
115
- return inspection("unsafe_inactive", errorCounts(error));
128
+ return errorInspection("unsafe_inactive", error);
116
129
  }
117
130
  }
118
131
  try {
@@ -122,7 +135,7 @@ export function inspectTodoReconciliationState(root, sourceRoot = resolveSourceR
122
135
  return inspection(unsafe ? "unsafe_active" : "healthy_active", counts);
123
136
  }
124
137
  catch (error) {
125
- return inspection("unsafe_active", errorCounts(error));
138
+ return errorInspection("unsafe_active", error);
126
139
  }
127
140
  }
128
141
  function publicSnapshot(record, order) {
@@ -3,6 +3,7 @@ import { parseTodoMarkdownListItem, renderTodoPublicRecord } from "../cli/todoMa
3
3
  import { canonicalRecordJson } from "./archiveDiscovery.js";
4
4
  import { reject } from "./write/errors.js";
5
5
  import { todoLegacyRowFingerprint, TODO_OWNER_CORRECTION_INPUT_VERSION, TODO_OWNER_CORRECTION_PREVIEW_COMMAND, TODO_REPAIR_PREVIEW_COMMAND } from "./todoReconciliationActivation.js";
6
+ import { assertTodoSeverityHeadingStructure, todoSeveritySectionForHeading } from "./todoSeverityHeadings.js";
6
7
  const RECONCILIATION_VERSION = "agentera.todoReconciliation.v1";
7
8
  const DIAGNOSTIC_LIMIT = 20;
8
9
  function mapping(value) { return value !== null && typeof value === "object" && !Array.isArray(value); }
@@ -91,6 +92,7 @@ export function normalizeTodoOwnerCorrectionEvidence(input) {
91
92
  };
92
93
  }
93
94
  function scanRows(markdown) {
95
+ assertTodoSeverityHeadingStructure(markdown);
94
96
  const managed = new Map();
95
97
  const legacy = [];
96
98
  const order = new Map();
@@ -98,7 +100,7 @@ function scanRows(markdown) {
98
100
  markdown.split(/\r?\n/).forEach((line, index) => {
99
101
  const heading = line.trim().match(/^##\s+(.+)$/)?.[1]?.toLowerCase();
100
102
  if (heading)
101
- section = heading.includes("critical") ? "critical" : heading.includes("degraded") ? "degraded" : heading.includes("annoying") ? "annoying" : heading.includes("resolved") ? "resolved" : heading.includes("normal") ? "normal" : heading === "notes" ? null : section;
103
+ section = todoSeveritySectionForHeading(line) ?? (heading === "notes" ? null : section);
102
104
  const item = parseTodoMarkdownListItem(line.trim());
103
105
  if (!item)
104
106
  return;
@@ -0,0 +1,71 @@
1
+ import { reject } from "./write/errors.js";
2
+ export const TODO_SEVERITY_HEADINGS = [
3
+ { section: "critical", name: "Critical", heading: "## ⇶ Critical" },
4
+ { section: "degraded", name: "Degraded", heading: "## ⇉ Degraded" },
5
+ { section: "normal", name: "Normal", heading: "## → Normal" },
6
+ { section: "annoying", name: "Annoying", heading: "## ⇢ Annoying" },
7
+ { section: "resolved", name: "Resolved", heading: "## ✓ Resolved" },
8
+ ];
9
+ const DIAGNOSTIC_LIMIT = 20;
10
+ const byName = new Map(TODO_SEVERITY_HEADINGS.map((entry) => [entry.name.toLowerCase(), entry]));
11
+ function candidate(line) {
12
+ const content = line.trim().match(/^##\s+(.+?)\s*$/)?.[1];
13
+ if (!content)
14
+ return null;
15
+ const name = content.match(/^(?:[^\p{L}\p{N}\s]+\s+)?(Critical|Degraded|Normal|Annoying|Resolved)$/iu)?.[1];
16
+ return name ? byName.get(name.toLowerCase()) ?? null : null;
17
+ }
18
+ export function todoSeveritySectionForHeading(line) {
19
+ const entry = candidate(line);
20
+ return entry && line.trim() === entry.heading ? entry.section : null;
21
+ }
22
+ export function inspectTodoSeverityHeadings(markdown) {
23
+ const diagnostics = [];
24
+ const seen = new Set();
25
+ let greatestOrder = -1;
26
+ for (const [index, line] of markdown.split(/\r?\n/).entries()) {
27
+ const entry = candidate(line);
28
+ if (!entry)
29
+ continue;
30
+ const order = TODO_SEVERITY_HEADINGS.indexOf(entry);
31
+ const add = (diagnostic) => { diagnostics.push(diagnostic); };
32
+ if (line.trim() !== entry.heading)
33
+ add({
34
+ code: "todo_severity_heading_mismatch",
35
+ classification: "glyph_name_mismatch",
36
+ line: index + 1,
37
+ expected_heading: entry.heading,
38
+ recovery: `Replace TODO.md line ${index + 1} with '${entry.heading}'.`,
39
+ });
40
+ if (seen.has(entry.section))
41
+ add({
42
+ code: "todo_severity_heading_duplicate",
43
+ classification: "duplicate",
44
+ line: index + 1,
45
+ expected_heading: entry.heading,
46
+ recovery: `Keep exactly one '${entry.heading}' section and move its rows there.`,
47
+ });
48
+ if (order < greatestOrder)
49
+ add({
50
+ code: "todo_severity_heading_out_of_order",
51
+ classification: "out_of_order",
52
+ line: index + 1,
53
+ expected_heading: entry.heading,
54
+ recovery: "Order managed TODO sections as Critical, Degraded, Normal, Annoying, then Resolved; missing sections are allowed.",
55
+ });
56
+ seen.add(entry.section);
57
+ greatestOrder = Math.max(greatestOrder, order);
58
+ }
59
+ return { diagnostics: diagnostics.slice(0, DIAGNOSTIC_LIMIT), omitted_count: Math.max(0, diagnostics.length - DIAGNOSTIC_LIMIT) };
60
+ }
61
+ export function assertTodoSeverityHeadingStructure(markdown) {
62
+ const diagnosis = inspectTodoSeverityHeadings(markdown);
63
+ if (!diagnosis.diagnostics.length)
64
+ return;
65
+ reject({
66
+ class: "conflict",
67
+ message: "TODO.md managed severity section structure is malformed",
68
+ diagnosis: { classification: "todo_severity_heading_structure", ...diagnosis },
69
+ recovery: "Apply every bounded TODO.md heading correction exactly as reported, then retry; no state was changed.",
70
+ });
71
+ }
@@ -204,7 +204,7 @@ export function scanLegacyCapabilityAgentPaths(agentsDir) {
204
204
  }
205
205
  return hits.sort();
206
206
  }
207
- /** Scan a tree for reintroduced Swedish-verb agents (v1LegacyCruft-style guard). */
207
+ /** Scan a tree for reintroduced Swedish-verb agents. */
208
208
  export function scanLegacySwedishVerbAgentViolations(root) {
209
209
  const resolved = resolvePath(root);
210
210
  const violations = [];
@@ -19,14 +19,6 @@ import { applyInstalledHooksRetirementItems, planInstalledHooksRetirementItems,
19
19
  * v2→v3 migration phases: artifacts (noop for YAML), runtime rewire, cleanup.
20
20
  */
21
21
  export const MIGRATION_STATUSES = ["pending", "applied", "noop", "blocked", "failed"];
22
- export const V1_ARTIFACT_PAIRS = [
23
- [".agentera/PROGRESS.md", ".agentera/progress.yaml"],
24
- [".agentera/PLAN.md", ".agentera/plan.yaml"],
25
- [".agentera/DECISIONS.md", ".agentera/decisions.yaml"],
26
- [".agentera/HEALTH.md", ".agentera/health.yaml"],
27
- [".agentera/DOCS.md", ".agentera/docs.yaml"],
28
- ["VISION.md", ".agentera/vision.yaml"],
29
- ];
30
22
  const PLAN_ARCHIVE_FILE = /^PLAN-.+\.ya?ml$/i;
31
23
  const PLAN_LIFECYCLE_ACTION = "normalize-plan-lifecycle";
32
24
  function isMapping(value) {
@@ -178,16 +170,6 @@ export function summarizePhase(name, items, message = "") {
178
170
  }
179
171
  return { name, status, summary, items, message };
180
172
  }
181
- export function detectV1ArtifactPairs(project) {
182
- const root = resolvePath(project);
183
- const found = [];
184
- for (const [md, yaml] of V1_ARTIFACT_PAIRS) {
185
- if (isFile(path.join(root, md)) && !isFile(path.join(root, yaml))) {
186
- found.push(md);
187
- }
188
- }
189
- return found;
190
- }
191
173
  function listV2YamlArtifacts(project) {
192
174
  const root = resolvePath(project);
193
175
  const agenteraDir = path.join(root, ".agentera");
@@ -218,21 +200,13 @@ export function planArtifactsPhase(project) {
218
200
  },
219
201
  ]);
220
202
  }
221
- const v1Pairs = detectV1ArtifactPairs(root);
222
- const v1Items = v1Pairs.map((source) => ({
223
- status: "blocked",
224
- action: "manual-v1-handoff",
225
- source,
226
- message: "v1 Markdown state is unsupported; convert it with a v2 CLI before running the v2-to-v3 upgrade",
227
- }));
228
203
  const yamlArtifacts = listV2YamlArtifacts(root);
229
204
  const lifecycleItems = lifecyclePlanArtifacts(root);
230
- if (yamlArtifacts.length === 0 && v1Items.length === 0 && lifecycleItems.length === 0) {
205
+ if (yamlArtifacts.length === 0 && lifecycleItems.length === 0) {
231
206
  return summarizePhase("artifacts", [], "no project artifacts found");
232
207
  }
233
208
  const lifecycleSources = new Set(lifecycleItems.flatMap((item) => item.source ? [item.source] : []));
234
209
  const items = [
235
- ...v1Items,
236
210
  ...yamlArtifacts.filter((source) => !lifecycleSources.has(source)).map((source) => ({
237
211
  status: "noop",
238
212
  action: "preserve",
@@ -7,19 +7,6 @@ import { resolveRunningVersion } from "./versionResolution.js";
7
7
  */
8
8
  export const NEXT_MAJOR_SECTION_HEADER = "Next major";
9
9
  export const NEXT_MAJOR_LINE_CAP = 6;
10
- /** v1 installs predate the channel authority; hardcoded stable-line successor mapping. */
11
- export const V1_NEXT_MAJOR_FALLBACK = {
12
- concept: "forward_successor_line",
13
- currentVersion: "1.x",
14
- currentChannel: "stable",
15
- channel: "stable",
16
- version: "2.x",
17
- announced: true,
18
- npmOnlyAdvisory: "",
19
- guideUrl: "https://github.com/jgabor/agentera/blob/main/UPGRADE.md#recommended-upgrade-v1--v2-stable-channel",
20
- previewCommand: "npx -y agentera@latest upgrade --dry-run",
21
- irreversibleAdvisory: "Forward migration upgrades artifact format from Markdown to YAML; review the preview before applying.",
22
- };
23
10
  function parseNextMajorBlock(raw) {
24
11
  if (raw === null || raw === undefined) {
25
12
  return null;
@@ -120,14 +107,6 @@ export function resolveNextMajorDoctorLines(ctx) {
120
107
  const displayChannel = install?.kind === "v2_managed_app_home" ? "stable" : currentChannel;
121
108
  const runningDistributionMajor = ctx.runningDistributionMajor ??
122
109
  (install?.kind === "v2_managed_app_home" ? 2 : cliDistributionMajor(sourceRoot));
123
- if (runningDistributionMajor === 1) {
124
- const block = V1_NEXT_MAJOR_FALLBACK;
125
- return formatNextMajorDoctorLines({
126
- currentVersion: block.currentVersion,
127
- currentChannel: block.currentChannel,
128
- block,
129
- });
130
- }
131
110
  const authorityBlock = loadChannelNextMajor(sourceRoot, successorChannel);
132
111
  if (!authorityBlock || !authorityBlock.announced) {
133
112
  return null;