agentera 3.0.0-dev.76 → 3.0.0-dev.78

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 (57) hide show
  1. package/README.md +23 -0
  2. package/bundle/.agentera-build-source.json +5 -5
  3. package/bundle/CHANGELOG.md +10 -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/personalGlossaryCandidateReads.js +12 -1
  30. package/dist/cli/commands/prime/briefOrientation.js +1 -1
  31. package/dist/cli/commands/prime/collectOrientationState.js +0 -7
  32. package/dist/cli/commands/prime/orientationOutput.js +3 -9
  33. package/dist/cli/commands/report.js +98 -8
  34. package/dist/cli/commands/upgrade.js +30 -0
  35. package/dist/cli/dispatch/index.js +7 -0
  36. package/dist/cli/dispatch/lifecycle.js +20 -0
  37. package/dist/cli/help.js +10 -3
  38. package/dist/cli/orientation/attention.js +1 -4
  39. package/dist/cli/productV1Eol.js +55 -0
  40. package/dist/cli/startupCompletenessContract.js +0 -1
  41. package/dist/cli/stateQuery.js +1 -1
  42. package/dist/registries/activationTuples.js +11 -2
  43. package/dist/registries/glossaryCandidateProjectionAuthority.js +154 -6
  44. package/dist/registries/glossaryCandidateProjectionContract.js +4 -0
  45. package/dist/registries/glossaryEntryContract.js +3 -3
  46. package/dist/registries/packagePublication.js +2 -2
  47. package/dist/runtime/nativeResourceCleanup.js +1 -0
  48. package/dist/upgrade/legacyAgentCleanup.js +1 -1
  49. package/dist/upgrade/migrateArtifactsV2ToV3.js +1 -27
  50. package/dist/upgrade/nextMajorDoctor.js +0 -21
  51. package/dist/upgrade/productV1Reset.js +515 -0
  52. package/dist/upgrade/productV1ResetAuthority.js +139 -0
  53. package/dist/upgrade/projectIntegration.js +4 -6
  54. package/dist/upgrade/upgradeOrchestrator.js +4 -12
  55. package/dist/validate/{v1LegacyCruft.js → glossaryVariantGuard.js} +3 -38
  56. package/package.json +2 -2
  57. package/dist/cli/commands/prime/v1Migration.js +0 -38
@@ -7,40 +7,7 @@
7
7
  # The active authority uses one plan or plan_task entity per writer-owned file.
8
8
  # The aggregate model below is the complete atomic plan-create input. Its task
9
9
  # numbers and numeric/string dependency references are create-local symbols,
10
- # not persisted identities. Fields explicitly labeled migration-only remain
11
- # compatibility input and are not ordinary post-cutover entity contracts.
12
- #
13
- # ── Legacy migration-input mapping: v1 PLAN.md → v2 plan.yaml ──
14
- #
15
- # The ordinal task fields in this mapping are create-local symbolic references.
16
- # They are valid only inside one atomic `state plan create --input` document and
17
- # are removed when the writer publishes plan and plan_task entities.
18
- #
19
- # v1 Field v2 Field v2 ID
20
- # ─────────────────────────────────────────────────────────────────────
21
- # <!-- Level: light/full --> HEADER.level PL1
22
- # <!-- Created: date --> HEADER.created PL2
23
- # <!-- Status: active/completed --> HEADER.status PL3 (normalized)
24
- # <!-- Reviewed: date --> HEADER.reviewed PL4
25
- # <!-- Critic issues: N found... --> HEADER.critic_issues PL5
26
- # <!-- Revised: description --> HEADER.revised PL6
27
- # # Plan: <title> HEADER.title PL7
28
- # ## What PLAN.what PL8
29
- # ## Why PLAN.why PL9
30
- # ## Constraints PLAN.constraints PL10
31
- # ## Scope / **In**: <list> SCOPE.included PL11
32
- # ## Scope / **Out**: <list> SCOPE.excluded PL12
33
- # ## Scope / **Deferred**: <list> SCOPE.deferred PL13
34
- # ## Design PLAN.design PL14
35
- # ### Task N: <name> TASK.number PL15 (create-local only)
36
- # (task name from heading) TASK.name PL16
37
- # **Depends on**: <text> TASK.depends_on PL17
38
- # **Status**: <status> TASK.status PL18
39
- # **Acceptance**: <criteria list> TASK.acceptance PL19
40
- # ## Overall Acceptance PLAN.overall_acceptance PL20
41
- # ## Surprises PLAN.surprises PL21
42
- # ## Unknowns UNKNOWN.entry PU1
43
- # ## Rejected Review Findings REJECTED.entry PR1
10
+ # not persisted identities.
44
11
  #
45
12
  # Protocol references:
46
13
  # status visual tokens: VT1-VT4 (protocol.yaml VISUAL_TOKENS)
@@ -726,7 +693,6 @@ LIFECYCLE_CONTRACT:
726
693
  - packages/cli/src/upgrade/migrateArtifacts*.ts
727
694
  - packages/cli/src/upgrade/upgradeOrchestrator.ts
728
695
  - packages/cli/src/upgrade/doctor.ts
729
- - packages/cli/src/cli/commands/prime/v1Migration.ts
730
696
  - packages/cli/src/state/entityMigration*.ts
731
697
  - packages/cli/src/state/canonicalMigrationRecord.ts
732
698
  schemas:
@@ -10,31 +10,7 @@
10
10
  # bounded current writer intent for Build and never delivers or persists it in
11
11
  # plan state. Prime projects generic review attention without lifecycle details.
12
12
  #
13
- # The active authority is one progress_cycle entity per writer-owned file. The
14
- # numbered projection model below is retained only as an exact legacy migration
15
- # input schema; it is not an ordinary read or write contract after cutover.
16
- #
17
- # ── Field-by-field mapping: v1 PROGRESS.md → v2 progress.yaml ──
18
- #
19
- # v1 Field v2 Field v2 ID
20
- # ─────────────────────────────────────────────────────────────────────
21
- # ## Cycle N · date · type-prefix CYCLE.number PR1
22
- # (date from heading) CYCLE.timestamp PR2
23
- # (type-prefix from heading) CYCLE.type PR3
24
- # **Phase**: <phase> CYCLE.phase PR4
25
- # **What**: <text> CYCLE.what PR5
26
- # **Inspiration**: <text> CYCLE.inspiration PR6
27
- # **Discovered**: <text> CYCLE.discovered PR7
28
- # **Verified**: <text> CYCLE.verified PR8
29
- # **Next**: <text> CYCLE.next PR9
30
- # **Context**: <text> CYCLE.context PR10
31
- # (intent from Context) CONTEXT.intent PT1
32
- # (constraints from Context) CONTEXT.constraints PT2
33
- # (unknowns from Context) CONTEXT.unknowns PT3
34
- # (scope from Context) CONTEXT.scope PT4
35
- # ## Archived Cycles (implicit section) —
36
- # - Cycle N (date): summary ARCHIVE.summary PA1
37
- #
13
+ # The active authority is one progress_cycle entity per writer-owned file.
38
14
  # Protocol references:
39
15
  # phase: PH1-PH5 (protocol.yaml PHASES)
40
16
  # type: conventional commit prefix (feat/fix/docs/refactor/chore/test)
@@ -7,25 +7,6 @@
7
7
  # Structural pattern: UPPER_CASE groups with numbered entries and stable IDs,
8
8
  # following capability_schema_contract.yaml and protocol.yaml conventions.
9
9
  #
10
- # ── Field-by-field mapping: v1 VISION.md → v2 vision.yaml ──
11
- #
12
- # v1 Field v2 Field v2 ID
13
- # ─────────────────────────────────────────────────────────────────────
14
- # # <project name> HEADER.project_name VN1
15
- # ## North Star NORTH_STAR.content VN2
16
- # ## Who It's For / ### persona name PERSONA.entry VN3
17
- # (persona heading) PERSONA.name VN4
18
- # (persona prose) PERSONA.description VN5
19
- # ## Principles / - principle PRINCIPLE.entry VN6
20
- # (bold name + description) PRINCIPLE.name VN7
21
- # (principle prose) PRINCIPLE.description VN8
22
- # ## Direction DIRECTION.content VN9
23
- # ## Identity / ### Personality IDENTITY.personality VN10
24
- # ## Identity / ### Voice IDENTITY.voice VN11
25
- # ## Identity / ### Emotional register IDENTITY.emotional_register VN12
26
- # ## Identity / ### Naming IDENTITY.naming VN13
27
- # ## The Tension TENSION.content VN14
28
- #
29
10
  # Protocol references:
30
11
  # No protocol primitives directly. Vision defines project-level identity
31
12
  # that capabilities reference for voice and personality consistency.
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": "agentera.generatedBuildSource.v1",
3
- "commit": "70aed21badb985087fd79371fabb61b1ec31ddde",
4
- "tree": "3baa4c8145944638cad38ae98346f98518d01884",
5
- "files": 6750,
6
- "workingTreeSha256": "57aa3b7d804724356e7a3bf9adf7ea9a54e29312cd19b0abcd23f4f9699dfe54",
7
- "identitySha256": "f4acdd4621fd8110b1277f88be7f093929a887c6b4db6c3269e3a1af98a63b36"
3
+ "commit": "eb1d15be07e52525a9b2bd30590e996bb42dc463",
4
+ "tree": "4d8d697897d1186193da51ea43e66f9fa6adebf1",
5
+ "files": 6835,
6
+ "workingTreeSha256": "b114aced044d84f4f9657fe213d1e7db99e564e7d205e3453c09499f4ddb3893",
7
+ "identitySha256": "0692b08db944d0d52a32b08bc71713338482717eee5bf79eb64215a96e1ca9d0"
8
8
  }
@@ -108,6 +108,6 @@ export function admitPersonalGlossaryEvidence(input, resolveAnchor = resolveEvid
108
108
  }
109
109
  export { classifyExplicitGlossaryLanguage, discoverExplicitGlossaryCues, mineExplicitGlossaryCandidates, mineExplicitGlossaryEvidence, minePersonalExplicitGlossaryCandidates, EXPLICIT_GLOSSARY_REASONS, } from "./personalGlossaryExplicit.js";
110
110
  export { mineRecurringGlossaryCandidates, mineRecurringGlossaryEvidence, PERSONAL_GLOSSARY_MINING_POLICY_VERSION, RECURRING_MEANING_PENDING_REVIEW, RECURRING_REASONS, } from "./personalGlossaryRecurrence.js";
111
- export { maintainPersonalGlossaryCandidateProjection, personalGlossaryCandidateProjectionPath, personalGlossaryProjectionProjectIdentity, persistPersonalGlossaryCandidateProjection, projectPersonalGlossaryCandidates, readPersonalGlossaryCandidateProjection, } from "./personalGlossaryCandidateProjection.js";
111
+ export { maintainPersonalGlossaryCandidateProjection, personalGlossaryCandidateProjectionPath, personalGlossaryProjectionProjectIdentity, persistPersonalGlossaryCandidateProjection, persistPersonalGlossaryCandidateProjectionAfterRefresh, projectPersonalGlossaryCandidates, readPersonalGlossaryCandidateProjection, } from "./personalGlossaryCandidateProjection.js";
112
112
  export { decidePersonalGlossaryCandidate, } from "./personalGlossaryDecision.js";
113
113
  export { currentPersonalGlossaryReviewRecords, maintainPersonalGlossaryReviewRecords, personalGlossaryReviewRecordsPath, queuePersonalGlossaryReviewRecord, readPersonalGlossaryReviewRecords, } from "./personalGlossaryReviewRecords.js";
@@ -7,6 +7,7 @@ import { defaultProfileDir } from "./extractCorpus/core.js";
7
7
  import { EXCERPT_OMISSION_REASONS, containsPersonalGlossarySensitiveContent, personalGlossaryCandidateProjectionExcerptExpiry, selectPersonalGlossarySafeExcerpt, validPersonalGlossarySafeExcerpt, } from "./personalGlossaryCandidateProjectionExcerpts.js";
8
8
  const PROJECTION_SCHEMA_VERSION = "agentera.personalGlossaryCandidateProjection.v1";
9
9
  const PROJECTION_REPORT_SCHEMA_VERSION = "agentera.personalGlossaryCandidateProjectionReport.v1";
10
+ const MINING_SUMMARY_SCHEMA_VERSION = "agentera.personalGlossaryMiningSummary.v1";
10
11
  const PROJECT_IDENTITY_SCHEMA_VERSION = "agentera.personalGlossaryProjectionProjectIdentity.v1";
11
12
  const PROJECTION_OWNER = "deterministic_discovery_projection";
12
13
  const SHA256_RE = /^[a-f0-9]{64}$/u;
@@ -50,6 +51,9 @@ function projectionContract() {
50
51
  contract.storageFile !== "candidate-projection.json" ||
51
52
  contract.candidateSecretReason !== "secret_content" ||
52
53
  contract.excerptSensitiveContentAction !== "omit_complete_excerpt_before_projection" ||
54
+ contract.miningSummarySchemaVersion !== MINING_SUMMARY_SCHEMA_VERSION ||
55
+ contract.explicitAbstentionKeys.length !== 24 ||
56
+ contract.recurringAbstentionKeys.length !== 12 ||
53
57
  JSON.stringify(Object.keys(families)) !== JSON.stringify(["explicit", "recurring"]) ||
54
58
  JSON.stringify(families.explicit) !== JSON.stringify(["personal_explicit_definition"]) ||
55
59
  JSON.stringify(families.recurring) !==
@@ -58,6 +62,61 @@ function projectionContract() {
58
62
  }
59
63
  return contract;
60
64
  }
65
+ function zeroReasons(keys) {
66
+ return Object.fromEntries(keys.map((key) => [key, 0]));
67
+ }
68
+ function projectionMiningSummary(input, contract) {
69
+ if (input.mining_summary)
70
+ return input.mining_summary;
71
+ let explicit = 0;
72
+ let recurring = 0;
73
+ for (const candidate of input.candidates) {
74
+ if (candidate.capsule.provenance_kind === "personal_explicit_definition")
75
+ explicit += 1;
76
+ else
77
+ recurring += 1;
78
+ }
79
+ return {
80
+ schema_version: MINING_SUMMARY_SCHEMA_VERSION,
81
+ explicit: {
82
+ candidate_count: explicit,
83
+ abstention_count: 0,
84
+ abstentions_by_reason: zeroReasons(contract.explicitAbstentionKeys),
85
+ },
86
+ recurring: {
87
+ candidate_count: recurring,
88
+ abstention_count: 0,
89
+ abstentions_by_reason: zeroReasons(contract.recurringAbstentionKeys),
90
+ },
91
+ total_candidate_count: explicit + recurring,
92
+ total_abstention_count: 0,
93
+ };
94
+ }
95
+ function validMiningFamily(value, keys) {
96
+ const family = mapping(value);
97
+ const reasons = mapping(family?.abstentions_by_reason);
98
+ return family !== null && reasons !== null &&
99
+ exactKeys(family, ["candidate_count", "abstention_count", "abstentions_by_reason"]) &&
100
+ nonNegativeInteger(family.candidate_count) && nonNegativeInteger(family.abstention_count) &&
101
+ exactKeys(reasons, keys) && Object.values(reasons).every(nonNegativeInteger) &&
102
+ family.abstention_count === Object.values(reasons).reduce((sum, count) => sum + Number(count), 0);
103
+ }
104
+ function validMiningSummary(value, inputCount, contract) {
105
+ const summary = mapping(value);
106
+ if (summary === null || !exactKeys(summary, [
107
+ "schema_version", "explicit", "recurring", "total_candidate_count", "total_abstention_count",
108
+ ]) || summary.schema_version !== MINING_SUMMARY_SCHEMA_VERSION ||
109
+ !validMiningFamily(summary.explicit, contract.explicitAbstentionKeys) ||
110
+ !validMiningFamily(summary.recurring, contract.recurringAbstentionKeys) ||
111
+ !nonNegativeInteger(summary.total_candidate_count) ||
112
+ !nonNegativeInteger(summary.total_abstention_count))
113
+ return false;
114
+ const explicit = summary.explicit;
115
+ const recurring = summary.recurring;
116
+ return summary.total_candidate_count === explicit.candidate_count + recurring.candidate_count &&
117
+ summary.total_candidate_count === inputCount &&
118
+ summary.total_abstention_count === explicit.abstention_count + recurring.abstention_count;
119
+ }
61
120
  function sourceFamily(capsule, contract) {
62
121
  const matches = Object.entries(contract.sourceFamilies)
63
122
  .filter(([, kinds]) => kinds.includes(capsule.provenance_kind))
@@ -218,6 +277,10 @@ export function projectPersonalGlossaryCandidates(input) {
218
277
  if (!Array.isArray(input.candidates)) {
219
278
  throw new TypeError("projection candidates must be a list");
220
279
  }
280
+ const miningSummary = projectionMiningSummary(input, contract);
281
+ if (!validMiningSummary(miningSummary, input.candidates.length, contract)) {
282
+ throw new TypeError("candidate mining summary is invalid");
283
+ }
221
284
  const merged = mergeCandidates(input, contract);
222
285
  const selected = selectCandidates(merged.candidates, contract);
223
286
  const availableByFamily = countByFamily(merged.candidates);
@@ -295,6 +358,7 @@ export function projectPersonalGlossaryCandidates(input) {
295
358
  expired: 0,
296
359
  omissions,
297
360
  },
361
+ mining_summary: miningSummary,
298
362
  };
299
363
  const projection = {
300
364
  schema_version: PROJECTION_SCHEMA_VERSION,
@@ -378,6 +442,7 @@ function validProjectionReport(value) {
378
442
  "projects",
379
443
  "coverage",
380
444
  "excerpts",
445
+ "mining_summary",
381
446
  ]) ||
382
447
  report.schema_version !== PROJECTION_REPORT_SCHEMA_VERSION ||
383
448
  ![
@@ -415,6 +480,7 @@ function validProjectionReport(value) {
415
480
  !Array.isArray(coverage.uncovered_source_families) ||
416
481
  coverage.uncovered_source_families.some((family) => family !== "explicit" && family !== "recurring") ||
417
482
  !nonNegativeInteger(coverage.uncovered_projects) ||
483
+ !validMiningSummary(report.mining_summary, Number(report.input_count), projectionContract()) ||
418
484
  excerpts === null ||
419
485
  !exactKeys(excerpts, [
420
486
  "provided",
@@ -645,6 +711,36 @@ export function persistPersonalGlossaryCandidateProjection(projection, options =
645
711
  privateWrite(pathname, text);
646
712
  return { status: "changed", path: pathname };
647
713
  }
714
+ /** Persist after explicit refresh consent, replacing only a malformed owned regular file. */
715
+ export function persistPersonalGlossaryCandidateProjectionAfterRefresh(projection, options = {}) {
716
+ if (!validProjection(projection))
717
+ throw new TypeError("candidate projection is invalid");
718
+ const pathname = personalGlossaryCandidateProjectionPath(options);
719
+ const root = path.resolve(defaultProfileDir(options.env ?? process.env, options.platform ?? process.platform), "intermediate", "personal-glossary");
720
+ if (path.resolve(pathname) !== path.join(root, projectionContract().storageFile)) {
721
+ throw new TypeError("candidate projection path escapes its configured storage root");
722
+ }
723
+ try {
724
+ const metadata = fs.lstatSync(pathname);
725
+ if (metadata.isSymbolicLink() || !metadata.isFile()) {
726
+ throw new TypeError("candidate projection replacement requires the exact regular file");
727
+ }
728
+ const text = `${canonicalGlossaryJson(projection)}\n`;
729
+ const current = readPersonalGlossaryCandidateProjection(options);
730
+ if (current.status === "current" && fs.readFileSync(pathname, "utf8") === text) {
731
+ ensurePrivateProjectionMode(pathname);
732
+ return { status: "unchanged_replay", path: pathname };
733
+ }
734
+ privateWrite(pathname, text);
735
+ return { status: "changed", path: pathname };
736
+ }
737
+ catch (error) {
738
+ if (error.code !== "ENOENT")
739
+ throw error;
740
+ }
741
+ privateWrite(pathname, `${canonicalGlossaryJson(projection)}\n`);
742
+ return { status: "changed", path: pathname };
743
+ }
648
744
  /**
649
745
  * Apply only expiry or a local-host-authorized purge to the private projection.
650
746
  * No CLI command exposes this primitive. The later review lifecycle owns the
@@ -0,0 +1,351 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { personalGlossaryCandidateProjectionContract } from "../registries/glossaryCandidateProjectionContract.js";
5
+ import { PERSONAL_GLOSSARY_MINING_POLICY_VERSION } from "../registries/glossaryMiningAuthority.js";
6
+ import { readCurrentGeneration } from "./extractCorpus/evidenceTiers.js";
7
+ import { mineExplicitGlossaryCandidates } from "./personalGlossaryExplicitMining.js";
8
+ import { mineRecurringGlossaryCandidates } from "./personalGlossaryRecurrence.js";
9
+ import { personalGlossaryCandidateProjectionPath, persistPersonalGlossaryCandidateProjectionAfterRefresh, projectPersonalGlossaryCandidates, } from "./personalGlossaryCandidateProjection.js";
10
+ const LOCK_SCHEMA_VERSION = "agentera.personalGlossaryRefreshLock.v1";
11
+ const MAX_LOCK_BYTES = 8 * 1024;
12
+ const FILE_FLAGS = fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0);
13
+ const TOKEN_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u;
14
+ export class PersonalGlossaryRefreshCommitLockError extends Error {
15
+ recovery;
16
+ constructor(message, recovery) {
17
+ super(message);
18
+ this.recovery = recovery;
19
+ this.name = "PersonalGlossaryRefreshCommitLockError";
20
+ }
21
+ }
22
+ export class PersonalGlossaryRefreshCommitBusyError extends PersonalGlossaryRefreshCommitLockError {
23
+ constructor(message = "another consented refresh is still publishing the current candidate projection") {
24
+ super(message, "npx -y agentera@next report refresh --consent local-history");
25
+ this.name = "PersonalGlossaryRefreshCommitBusyError";
26
+ }
27
+ }
28
+ function unsafeLock(lockPath, detail) {
29
+ throw new PersonalGlossaryRefreshCommitLockError(`personal glossary refresh lock is unsafe: ${detail}`, `Inspect ${lockPath}; remove it only after verifying no refresh owns it, then rerun npx -y agentera@next report refresh --consent local-history`);
30
+ }
31
+ function identity(fd) {
32
+ const stat = fs.fstatSync(fd, { bigint: true });
33
+ return { dev: stat.dev, ino: stat.ino };
34
+ }
35
+ function sameIdentity(left, right) {
36
+ return left.dev === right.dev && left.ino === right.ino;
37
+ }
38
+ function sameRecord(left, right) {
39
+ return left.schema_version === right.schema_version
40
+ && left.pid === right.pid
41
+ && left.token === right.token
42
+ && left.created_at === right.created_at;
43
+ }
44
+ function parseRecord(value) {
45
+ if (value === null
46
+ || typeof value !== "object"
47
+ || value.schema_version !== LOCK_SCHEMA_VERSION
48
+ || !Number.isSafeInteger(value.pid)
49
+ || value.pid <= 0
50
+ || typeof value.token !== "string"
51
+ || !TOKEN_PATTERN.test(value.token)
52
+ || typeof value.created_at !== "string"
53
+ || !Number.isFinite(Date.parse(value.created_at)))
54
+ return null;
55
+ return value;
56
+ }
57
+ function readRecord(fd) {
58
+ try {
59
+ const stat = fs.fstatSync(fd, { bigint: true });
60
+ if (!stat.isFile() || stat.size === 0n || stat.size > BigInt(MAX_LOCK_BYTES))
61
+ return null;
62
+ const bytes = Buffer.alloc(Number(stat.size));
63
+ let offset = 0;
64
+ while (offset < bytes.length) {
65
+ const count = fs.readSync(fd, bytes, offset, bytes.length - offset, offset);
66
+ if (count === 0)
67
+ return null;
68
+ offset += count;
69
+ }
70
+ return parseRecord(JSON.parse(bytes.toString("utf8")));
71
+ }
72
+ catch {
73
+ return null;
74
+ }
75
+ }
76
+ function openOwnedRecord(recordPath, label) {
77
+ let stat;
78
+ try {
79
+ stat = fs.lstatSync(recordPath);
80
+ }
81
+ catch (error) {
82
+ if (error.code === "ENOENT")
83
+ return null;
84
+ throw error;
85
+ }
86
+ if (stat.isSymbolicLink())
87
+ unsafeLock(recordPath, `${label} is a symbolic link`);
88
+ if (!stat.isFile())
89
+ unsafeLock(recordPath, `${label} is not a regular file`);
90
+ let descriptor;
91
+ try {
92
+ descriptor = fs.openSync(recordPath, FILE_FLAGS);
93
+ }
94
+ catch (error) {
95
+ if (error.code === "ENOENT")
96
+ unsafeLock(recordPath, `${label} changed during inspection`);
97
+ throw error;
98
+ }
99
+ const opened = fs.fstatSync(descriptor);
100
+ if (!opened.isFile() || opened.dev !== stat.dev || opened.ino !== stat.ino) {
101
+ fs.closeSync(descriptor);
102
+ unsafeLock(recordPath, `${label} changed identity during inspection`);
103
+ }
104
+ const record = readRecord(descriptor);
105
+ if (!record) {
106
+ fs.closeSync(descriptor);
107
+ unsafeLock(recordPath, `${label} has a malformed or foreign ownership record`);
108
+ }
109
+ return { descriptor, record };
110
+ }
111
+ function ownerState(pid) {
112
+ if (pid === process.pid)
113
+ return "live";
114
+ try {
115
+ process.kill(pid, 0);
116
+ return "live";
117
+ }
118
+ catch (error) {
119
+ return error.code === "ESRCH" ? "dead" : "indeterminate";
120
+ }
121
+ }
122
+ function pathMatches(pathname, descriptor) {
123
+ let observed;
124
+ try {
125
+ observed = fs.openSync(pathname, FILE_FLAGS);
126
+ return sameIdentity(identity(observed), identity(descriptor));
127
+ }
128
+ catch {
129
+ return false;
130
+ }
131
+ finally {
132
+ if (observed !== undefined)
133
+ fs.closeSync(observed);
134
+ }
135
+ }
136
+ function unlinkOwned(pathname, descriptor) {
137
+ if (!pathMatches(pathname, descriptor))
138
+ return false;
139
+ try {
140
+ fs.unlinkSync(pathname);
141
+ return true;
142
+ }
143
+ catch (error) {
144
+ if (error.code === "ENOENT")
145
+ return false;
146
+ throw error;
147
+ }
148
+ }
149
+ function prepareRecord(directory, basename, record) {
150
+ const preparedPath = path.join(directory, `.${basename}.${record.token}.tmp`);
151
+ const descriptor = fs.openSync(preparedPath, fs.constants.O_RDWR | fs.constants.O_CREAT | fs.constants.O_EXCL | (fs.constants.O_NOFOLLOW ?? 0), 0o600);
152
+ try {
153
+ fs.writeFileSync(descriptor, `${JSON.stringify(record)}\n`);
154
+ fs.fsyncSync(descriptor);
155
+ return { path: preparedPath, descriptor };
156
+ }
157
+ catch (error) {
158
+ fs.closeSync(descriptor);
159
+ fs.rmSync(preparedPath, { force: true });
160
+ throw error;
161
+ }
162
+ }
163
+ function publishPrepared(prepared, targetPath) {
164
+ try {
165
+ fs.linkSync(prepared.path, targetPath);
166
+ try {
167
+ fs.unlinkSync(prepared.path);
168
+ }
169
+ catch (error) {
170
+ unlinkOwned(targetPath, prepared.descriptor);
171
+ throw error;
172
+ }
173
+ return true;
174
+ }
175
+ catch (error) {
176
+ if (error.code === "EEXIST")
177
+ return false;
178
+ throw error;
179
+ }
180
+ }
181
+ function removeDeadClaim(claimPath) {
182
+ const claim = openOwnedRecord(claimPath, "reclaim claim");
183
+ if (!claim)
184
+ return;
185
+ try {
186
+ const state = ownerState(claim.record.pid);
187
+ if (state === "live")
188
+ throw new PersonalGlossaryRefreshCommitBusyError("another refresh is reclaiming an orphaned personal glossary refresh lock");
189
+ if (state === "indeterminate")
190
+ unsafeLock(claimPath, "reclaim claim owner liveness is indeterminate");
191
+ if (!unlinkOwned(claimPath, claim.descriptor))
192
+ unsafeLock(claimPath, "reclaim claim changed identity before recovery");
193
+ }
194
+ finally {
195
+ fs.closeSync(claim.descriptor);
196
+ }
197
+ }
198
+ function reclaimDeadOwner(lockPath, current, prepared, record) {
199
+ const claimPath = `${lockPath}.reclaim`;
200
+ const claim = prepareRecord(path.dirname(lockPath), path.basename(claimPath), record);
201
+ if (!publishPrepared(claim, claimPath)) {
202
+ fs.rmSync(claim.path, { force: true });
203
+ fs.closeSync(claim.descriptor);
204
+ throw new PersonalGlossaryRefreshCommitBusyError("another refresh is reclaiming an orphaned personal glossary refresh lock");
205
+ }
206
+ let adopted = false;
207
+ try {
208
+ const observed = readRecord(current.descriptor);
209
+ if (!pathMatches(lockPath, current.descriptor) || !observed || !sameRecord(observed, current.record)) {
210
+ unsafeLock(lockPath, "owner changed identity before orphan recovery");
211
+ }
212
+ const state = ownerState(current.record.pid);
213
+ if (state === "live")
214
+ throw new PersonalGlossaryRefreshCommitBusyError();
215
+ if (state === "indeterminate")
216
+ unsafeLock(lockPath, "owner liveness is indeterminate");
217
+ if (!unlinkOwned(lockPath, current.descriptor))
218
+ unsafeLock(lockPath, "owner changed identity during orphan recovery");
219
+ if (!publishPrepared(prepared, lockPath))
220
+ unsafeLock(lockPath, "a successor appeared during orphan recovery");
221
+ if (!unlinkOwned(claimPath, claim.descriptor)) {
222
+ unlinkOwned(lockPath, prepared.descriptor);
223
+ unsafeLock(claimPath, "reclaim claim changed identity before release");
224
+ }
225
+ adopted = true;
226
+ return { descriptor: prepared.descriptor, path: lockPath, record };
227
+ }
228
+ finally {
229
+ if (!adopted)
230
+ unlinkOwned(claimPath, claim.descriptor);
231
+ fs.closeSync(claim.descriptor);
232
+ }
233
+ }
234
+ /** Exclude another consented refresh until evidence and its projection are committed together. */
235
+ export function acquirePersonalGlossaryRefreshCommitLock(options = {}) {
236
+ const directory = path.dirname(personalGlossaryCandidateProjectionPath(options));
237
+ fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
238
+ fs.chmodSync(directory, 0o700);
239
+ const lockPath = path.join(directory, ".refresh.lock");
240
+ removeDeadClaim(`${lockPath}.reclaim`);
241
+ const record = {
242
+ schema_version: LOCK_SCHEMA_VERSION,
243
+ pid: process.pid,
244
+ token: randomUUID(),
245
+ created_at: new Date().toISOString(),
246
+ };
247
+ const prepared = prepareRecord(directory, path.basename(lockPath), record);
248
+ try {
249
+ if (publishPrepared(prepared, lockPath))
250
+ return { descriptor: prepared.descriptor, path: lockPath, record };
251
+ const current = openOwnedRecord(lockPath, "owner");
252
+ if (!current)
253
+ unsafeLock(lockPath, "owner disappeared during inspection");
254
+ try {
255
+ const state = ownerState(current.record.pid);
256
+ if (state === "live")
257
+ throw new PersonalGlossaryRefreshCommitBusyError();
258
+ if (state === "indeterminate")
259
+ unsafeLock(lockPath, "owner liveness is indeterminate");
260
+ return reclaimDeadOwner(lockPath, current, prepared, record);
261
+ }
262
+ finally {
263
+ fs.closeSync(current.descriptor);
264
+ }
265
+ }
266
+ catch (error) {
267
+ if (pathMatches(prepared.path, prepared.descriptor))
268
+ fs.rmSync(prepared.path, { force: true });
269
+ fs.closeSync(prepared.descriptor);
270
+ throw error;
271
+ }
272
+ }
273
+ export function releasePersonalGlossaryRefreshCommitLock(lock) {
274
+ try {
275
+ if (pathMatches(lock.path, lock.descriptor)) {
276
+ const observed = readRecord(lock.descriptor);
277
+ if (observed && sameRecord(observed, lock.record))
278
+ unlinkOwned(lock.path, lock.descriptor);
279
+ }
280
+ }
281
+ finally {
282
+ fs.closeSync(lock.descriptor);
283
+ }
284
+ }
285
+ function familySummary(keys, candidateCount, abstentions) {
286
+ const counts = Object.fromEntries(keys.map((key) => [key, 0]));
287
+ for (const abstention of abstentions) {
288
+ if (!(abstention.reason in counts))
289
+ throw new TypeError("mining produced an unknown abstention reason");
290
+ counts[abstention.reason] += 1;
291
+ }
292
+ return {
293
+ candidate_count: candidateCount,
294
+ abstention_count: abstentions.length,
295
+ abstentions_by_reason: counts,
296
+ };
297
+ }
298
+ /** Mine and publish the projection bound to the current successfully published evidence generation. */
299
+ export function produceCurrentPersonalGlossaryProjection(options) {
300
+ const before = readCurrentGeneration(options.tiersDir);
301
+ if (!before)
302
+ throw new TypeError("current evidence generation is unavailable");
303
+ const explicit = mineExplicitGlossaryCandidates({ tiersDir: options.tiersDir });
304
+ const recurring = mineRecurringGlossaryCandidates({ tiersDir: options.tiersDir });
305
+ const after = readCurrentGeneration(options.tiersDir);
306
+ if (!after || explicit.generation !== before.manifest.generation ||
307
+ recurring.generation !== before.manifest.generation ||
308
+ after.manifest.generation !== before.manifest.generation) {
309
+ throw new TypeError("current evidence generation changed during projection production");
310
+ }
311
+ const contract = personalGlossaryCandidateProjectionContract();
312
+ const explicitSummary = familySummary(contract.explicitAbstentionKeys, explicit.candidates.length, explicit.abstentions);
313
+ const recurringSummary = familySummary(contract.recurringAbstentionKeys, recurring.candidates.length, recurring.abstentions);
314
+ const miningSummary = {
315
+ schema_version: "agentera.personalGlossaryMiningSummary.v1",
316
+ explicit: explicitSummary,
317
+ recurring: recurringSummary,
318
+ total_candidate_count: explicitSummary.candidate_count + recurringSummary.candidate_count,
319
+ total_abstention_count: explicitSummary.abstention_count + recurringSummary.abstention_count,
320
+ };
321
+ const projection = projectPersonalGlossaryCandidates({
322
+ generation: before.manifest.generation,
323
+ policy_version: PERSONAL_GLOSSARY_MINING_POLICY_VERSION,
324
+ retained_at: before.manifest.published_at,
325
+ candidates: [
326
+ ...explicit.candidates.map((candidate) => ({
327
+ capsule: candidate.capsule,
328
+ project_ids: candidate.project_ids,
329
+ })),
330
+ ...recurring.candidates.map((candidate) => ({
331
+ capsule: candidate.capsule,
332
+ project_ids: candidate.project_ids,
333
+ })),
334
+ ],
335
+ mining_summary: miningSummary,
336
+ });
337
+ const persisted = persistPersonalGlossaryCandidateProjectionAfterRefresh(projection, options);
338
+ const committed = readCurrentGeneration(options.tiersDir);
339
+ if (!committed || committed.manifest.generation !== projection.generation) {
340
+ throw new TypeError("current evidence generation changed before projection commit completed");
341
+ }
342
+ return {
343
+ status: persisted.status,
344
+ generation: projection.generation,
345
+ policy_version: projection.policy_version,
346
+ candidate_projection_sha256: projection.projection_sha256,
347
+ candidate_count: projection.report.mining_summary.total_candidate_count,
348
+ abstention_count: projection.report.mining_summary.total_abstention_count,
349
+ path: persisted.path,
350
+ };
351
+ }