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
@@ -0,0 +1,515 @@
1
+ import { createHash } from "node:crypto";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { resolveProfileDirOverride } from "../core/envPaths.js";
6
+ import { expanduser } from "../core/paths.js";
7
+ import { resolveSourceRoot } from "../core/sourceRoot.js";
8
+ import { loadNativeResourceCleanupContract, } from "../runtime/nativeResourceCleanup.js";
9
+ import { defaultAppHome } from "../state/installRoot.js";
10
+ import { classifyProjectState } from "../state/stateMode.js";
11
+ import { applyAppContentRefresh } from "./appContentRefresh.js";
12
+ import { loadProductV1ResetAuthority } from "./productV1ResetAuthority.js";
13
+ function hash(bytes) {
14
+ return createHash("sha256").update(bytes).digest("hex");
15
+ }
16
+ function lstat(target) {
17
+ try {
18
+ return fs.lstatSync(target);
19
+ }
20
+ catch (error) {
21
+ if (error.code === "ENOENT")
22
+ return null;
23
+ throw error;
24
+ }
25
+ }
26
+ function existingAncestor(target) {
27
+ let candidate = target;
28
+ while (lstat(candidate) === null) {
29
+ const parent = path.dirname(candidate);
30
+ if (parent === candidate)
31
+ break;
32
+ candidate = parent;
33
+ }
34
+ return candidate;
35
+ }
36
+ function safeRoot(name, value) {
37
+ const absolute = path.resolve(expanduser(value));
38
+ if (lstat(absolute)?.isSymbolicLink())
39
+ throw new Error(`${name} must not be a symbolic link: ${absolute}`);
40
+ const ancestor = existingAncestor(absolute);
41
+ const resolved = path.join(fs.realpathSync(ancestor), path.relative(ancestor, absolute));
42
+ if (resolved !== absolute)
43
+ throw new Error(`${name} resolves through an alias outside its declared root: ${absolute}`);
44
+ return absolute;
45
+ }
46
+ function resolvedRoots(options) {
47
+ const env = options.env ?? process.env;
48
+ const home = safeRoot("runtime home", options.home ?? os.homedir());
49
+ const project = safeRoot("project root", options.project ?? process.cwd());
50
+ const installCandidate = options.installRoot ?? env.AGENTERA_HOME ?? env.AGENTERA_DEFAULT_INSTALL_ROOT
51
+ ?? defaultAppHome(env, home);
52
+ const installRoot = safeRoot("install root", installCandidate);
53
+ const profileRoot = safeRoot("profile root", resolveProfileDirOverride(env) ?? installRoot);
54
+ return { project, profile_root: profileRoot, install_root: installRoot, runtime_home: home };
55
+ }
56
+ function assertContained(root, target) {
57
+ const relative = path.relative(root, target);
58
+ if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
59
+ throw new Error(`reset target is outside its declared root: ${target}`);
60
+ }
61
+ let current = root;
62
+ for (const part of relative.split(path.sep).filter(Boolean).slice(0, -1)) {
63
+ current = path.join(current, part);
64
+ if (lstat(current)?.isSymbolicLink()) {
65
+ throw new Error(`reset target resolves through a symbolic link: ${current}`);
66
+ }
67
+ }
68
+ }
69
+ function snapshot(target) {
70
+ if (lstat(target) === null)
71
+ return [{ path: target, type: "absent" }];
72
+ const result = [];
73
+ const visit = (current) => {
74
+ const stat = fs.lstatSync(current);
75
+ if (stat.isSymbolicLink()) {
76
+ result.push({ path: current, type: "symlink", link_target: fs.readlinkSync(current) });
77
+ return;
78
+ }
79
+ if (stat.isDirectory()) {
80
+ result.push({ path: current, type: "directory" });
81
+ for (const name of fs.readdirSync(current).sort())
82
+ visit(path.join(current, name));
83
+ return;
84
+ }
85
+ if (stat.isFile()) {
86
+ result.push({ path: current, type: "file", sha256: hash(fs.readFileSync(current)) });
87
+ return;
88
+ }
89
+ result.push({ path: current, type: "other" });
90
+ };
91
+ visit(target);
92
+ return result;
93
+ }
94
+ function inFileState(target) {
95
+ const stat = lstat(target);
96
+ if (stat === null)
97
+ return { type: "absent" };
98
+ if (!stat.isFile() || stat.isSymbolicLink()) {
99
+ throw new Error(`in-file reset target must be a regular file, not a link or directory: ${target}`);
100
+ }
101
+ return { type: "file", sha256: hash(fs.readFileSync(target)) };
102
+ }
103
+ function expandTemplate(template, roots) {
104
+ return path.resolve(template
105
+ .replaceAll("{project}", roots.project)
106
+ .replaceAll("{home}", roots.runtime_home)
107
+ .replaceAll("{install_root}", roots.install_root));
108
+ }
109
+ function runtimeTargets(roots, contract) {
110
+ const targets = [];
111
+ for (const resource of contract.diagnosticResources) {
112
+ const names = resource.names.length > 0 ? resource.names : [null];
113
+ for (const name of names)
114
+ for (const destination of resource.destinations) {
115
+ const declared = name === null ? destination : destination.replaceAll("{name}", name);
116
+ const target = expandTemplate(declared, roots);
117
+ const root = declared.startsWith("{project}") ? roots.project
118
+ : declared.startsWith("{install_root}") ? roots.install_root : roots.runtime_home;
119
+ assertContained(root, target);
120
+ targets.push(resource.contains === null
121
+ ? { declared, operation: "remove_path", path: target, entries: snapshot(target) }
122
+ : {
123
+ declared,
124
+ operation: "remove_in_file_selector",
125
+ path: target,
126
+ selector: { kind: "contains", value: resource.contains },
127
+ file_state: inFileState(target),
128
+ });
129
+ }
130
+ }
131
+ for (const unit of contract.configuration) {
132
+ const target = expandTemplate(unit.destination, roots);
133
+ assertContained(roots.runtime_home, target);
134
+ targets.push({
135
+ declared: unit.destination,
136
+ operation: "remove_in_file_selector",
137
+ path: target,
138
+ selector: { kind: "key", value: unit.key },
139
+ file_state: inFileState(target),
140
+ });
141
+ }
142
+ return targets;
143
+ }
144
+ function evidence(project, installRoot, manifest) {
145
+ const authority = loadProductV1ResetAuthority();
146
+ const found = authority.projectArtifacts
147
+ .filter((item) => item.triggersReset && fs.existsSync(path.join(project, item.path)))
148
+ .map((item) => path.join(project, item.path));
149
+ const registryPath = path.join(installRoot, manifest);
150
+ if (fs.existsSync(registryPath)) {
151
+ try {
152
+ const value = JSON.parse(fs.readFileSync(registryPath, "utf8"));
153
+ if (typeof value.skills?.[0]?.version === "string" && /^1\./.test(value.skills[0].version))
154
+ found.push(registryPath);
155
+ }
156
+ catch { /* malformed state is not product-generation evidence */ }
157
+ }
158
+ return found.sort();
159
+ }
160
+ export function previewProductV1Reset(options = {}, dependencies = {}) {
161
+ const roots = resolvedRoots(options);
162
+ const project = roots.project;
163
+ const installRoot = roots.install_root;
164
+ const authority = loadProductV1ResetAuthority();
165
+ const foundEvidence = evidence(project, installRoot, authority.installationPackage.manifest);
166
+ if (foundEvidence.length === 0)
167
+ throw new Error("product-v1 reset requires declared product-v1 generation evidence");
168
+ const rootFor = (name) => name === "runtime_declared_roots" ? roots.runtime_home : roots[name];
169
+ const deletions = authority.scope.filter((item) => item.action === "delete").map((item) => ({
170
+ id: item.id,
171
+ owner: item.owner,
172
+ root: rootFor(item.boundedRoot),
173
+ targets: item.boundedRoot === "runtime_declared_roots"
174
+ ? runtimeTargets(roots, dependencies.runtimeContract ?? loadNativeResourceCleanupContract())
175
+ : item.targets.map((declared) => {
176
+ const target = path.resolve(rootFor(item.boundedRoot), declared);
177
+ assertContained(rootFor(item.boundedRoot), target);
178
+ return { declared, operation: "remove_path", path: target, entries: snapshot(target) };
179
+ }),
180
+ }));
181
+ const recreations = authority.scope.filter((item) => item.action === "recreate").map((item) => ({
182
+ id: item.id,
183
+ owner: item.owner,
184
+ root: rootFor(item.boundedRoot),
185
+ targets: item.targets.map((declared) => ({ declared })),
186
+ }));
187
+ const unsigned = {
188
+ schemaVersion: "agentera.productV1ResetPreview.v1",
189
+ mode: "preview",
190
+ status: "review_required",
191
+ evidence: foundEvidence,
192
+ roots,
193
+ deletions,
194
+ recreations,
195
+ irreversible_loss: deletions.map(({ id }) => `${id}: permanently removes only the listed paths and in-file selectors; no backup, rollback, or restore is available.`),
196
+ mutation_performed: false,
197
+ };
198
+ return { ...unsigned, authorization: `sha256:${hash(JSON.stringify(unsigned))}` };
199
+ }
200
+ const RESET_JOURNAL = ".agentera-product-v1-reset.json";
201
+ const RESET_JOURNAL_STAGING = `${RESET_JOURNAL}.staging`;
202
+ const SELECTOR_STAGING_SUFFIX = ".agentera-product-v1-reset.staging";
203
+ function validatedScope(preview) {
204
+ return { roots: preview.roots, deletions: preview.deletions, recreations: preview.recreations };
205
+ }
206
+ function journalPath(project) {
207
+ return path.join(project, RESET_JOURNAL);
208
+ }
209
+ function journalStagingPath(project) {
210
+ return path.join(project, RESET_JOURNAL_STAGING);
211
+ }
212
+ function selectorStagingPath(target) {
213
+ return `${target}${SELECTOR_STAGING_SUFFIX}`;
214
+ }
215
+ function journalDigest(journal) {
216
+ return hash(JSON.stringify(journal));
217
+ }
218
+ function assertRegularJournalFile(target) {
219
+ const stat = lstat(target);
220
+ if (stat === null)
221
+ return;
222
+ if (!stat.isFile() || stat.isSymbolicLink())
223
+ throw new Error(`product-v1 reset journal is not a regular file: ${target}`);
224
+ }
225
+ function parseJournal(project, authorization) {
226
+ const target = journalPath(project);
227
+ if (lstat(target) === null)
228
+ return null;
229
+ assertRegularJournalFile(target);
230
+ const journal = JSON.parse(fs.readFileSync(target, "utf8"));
231
+ if (journal.schemaVersion !== "agentera.productV1ResetJournal.v1" || journal.preview?.authorization !== authorization) {
232
+ throw new Error("product-v1 reset journal does not match the approved operation");
233
+ }
234
+ const { authorization: _stored, ...unsigned } = journal.preview;
235
+ if (`sha256:${hash(JSON.stringify(unsigned))}` !== authorization)
236
+ throw new Error("product-v1 reset journal scope is corrupt");
237
+ if (!["prepared", "deleting", "initializing", "complete"].includes(journal.stage))
238
+ throw new Error("product-v1 reset journal stage is corrupt");
239
+ const digestInput = {
240
+ schemaVersion: journal.schemaVersion,
241
+ preview: journal.preview,
242
+ selector_states: journal.selector_states,
243
+ };
244
+ if (journal.digest !== journalDigest(digestInput))
245
+ throw new Error("product-v1 reset journal is corrupt");
246
+ return journal;
247
+ }
248
+ function writeJournal(project, journal) {
249
+ const staging = journalStagingPath(project);
250
+ assertRegularJournalFile(staging);
251
+ fs.rmSync(staging, { force: true });
252
+ fs.writeFileSync(staging, JSON.stringify(journal) + "\n", { flag: "wx", mode: 0o600 });
253
+ fs.renameSync(staging, journalPath(project));
254
+ }
255
+ function journalFor(preview, stage) {
256
+ const selector_states = selectorStates(validatedScope(preview));
257
+ const base = { schemaVersion: "agentera.productV1ResetJournal.v1", preview, selector_states };
258
+ return { ...base, stage, digest: journalDigest(base) };
259
+ }
260
+ function setJournalStage(project, journal, stage) {
261
+ const updated = { ...journal, stage };
262
+ writeJournal(project, updated);
263
+ return updated;
264
+ }
265
+ function removeTomlSection(lines, header) {
266
+ const escaped = header.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
267
+ const exactHeader = new RegExp(`^\\s*${escaped}\\s*(?:#.*)?$`);
268
+ const start = lines.findIndex((line) => exactHeader.test(line));
269
+ if (start < 0)
270
+ return lines;
271
+ let end = start + 1;
272
+ while (end < lines.length && !/^\s*\[.+\]\s*(?:#.*)?$/.test(lines[end]))
273
+ end += 1;
274
+ return [...lines.slice(0, start), ...lines.slice(end)];
275
+ }
276
+ function removeTomlKey(lines, dottedKey) {
277
+ const parts = dottedKey.split(".");
278
+ const key = parts.pop();
279
+ const section = parts.join(".");
280
+ let current = "";
281
+ return lines.filter((line) => {
282
+ const header = line.match(/^\s*\[([^\]]+)\]\s*(?:#.*)?$/);
283
+ if (header)
284
+ current = header[1].trim();
285
+ return !(current === section && new RegExp(`^\\s*${key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*=`).test(line));
286
+ });
287
+ }
288
+ function transformedSelectors(original, selectors) {
289
+ const trailingNewline = original.endsWith("\n");
290
+ let lines = original.split("\n");
291
+ if (trailingNewline)
292
+ lines.pop();
293
+ for (const selector of selectors) {
294
+ if (selector.kind === "key")
295
+ lines = removeTomlKey(lines, selector.value);
296
+ else if (selector.value.startsWith("[") && selector.value.endsWith("]"))
297
+ lines = removeTomlSection(lines, selector.value);
298
+ else
299
+ lines = removeTomlKey(lines, selector.value);
300
+ }
301
+ return lines.join("\n") + (trailingNewline ? "\n" : "");
302
+ }
303
+ function selectorGroups(scope) {
304
+ const groups = new Map();
305
+ for (const deletion of scope.deletions)
306
+ for (const target of deletion.targets) {
307
+ if (target.operation === "remove_in_file_selector" && target.path && target.selector) {
308
+ groups.set(target.path, [...(groups.get(target.path) ?? []), target.selector]);
309
+ }
310
+ }
311
+ return groups;
312
+ }
313
+ function selectorStates(scope) {
314
+ return [...selectorGroups(scope)].map(([target, selectors]) => {
315
+ const stat = lstat(target);
316
+ if (stat === null)
317
+ return { path: target, before: "absent", after: "absent" };
318
+ if (!stat.isFile() || stat.isSymbolicLink())
319
+ throw new Error(`in-file reset target must be a regular file: ${target}`);
320
+ const original = fs.readFileSync(target, "utf8");
321
+ const approved = scope.deletions.flatMap((deletion) => deletion.targets)
322
+ .find((candidate) => candidate.path === target)?.file_state?.sha256;
323
+ if (approved !== hash(original))
324
+ throw new Error("product-v1 reset scope changed after preview; run a new preview and review its authorization");
325
+ return { path: target, before: hash(original), after: hash(transformedSelectors(original, selectors)) };
326
+ });
327
+ }
328
+ function cleanSelectorStaging(states) {
329
+ for (const state of states)
330
+ fs.rmSync(selectorStagingPath(state.path), { force: true });
331
+ }
332
+ function removeSelectors(target, selectors, afterEffect) {
333
+ const stat = lstat(target);
334
+ if (stat === null)
335
+ return;
336
+ if (!stat.isFile() || stat.isSymbolicLink())
337
+ throw new Error(`in-file reset target must remain a regular file: ${target}`);
338
+ const original = fs.readFileSync(target, "utf8");
339
+ const updated = transformedSelectors(original, selectors);
340
+ if (updated === original)
341
+ return;
342
+ const staging = selectorStagingPath(target);
343
+ const mode = stat.mode & 0o7777;
344
+ fs.writeFileSync(staging, updated, { flag: "wx", mode });
345
+ fs.chmodSync(staging, mode);
346
+ afterEffect?.(`selector-staged:${target}`);
347
+ fs.renameSync(staging, target);
348
+ }
349
+ function sameScopePath(left, right) {
350
+ return left.path === right.path && left.type === right.type && left.sha256 === right.sha256 && left.link_target === right.link_target;
351
+ }
352
+ function validateRemovePath(target, exact) {
353
+ const approved = target.entries ?? [];
354
+ const current = snapshot(target.path);
355
+ if (current.length === 1 && current[0].type === "absent") {
356
+ if (exact && !(approved.length === 1 && approved[0].type === "absent"))
357
+ throw new Error(`reset retry target changed after approval: ${target.path}`);
358
+ return;
359
+ }
360
+ const approvedByPath = new Map(approved.map((entry) => [entry.path, entry]));
361
+ if (current.some((entry) => !approvedByPath.has(entry.path) || !sameScopePath(entry, approvedByPath.get(entry.path)))) {
362
+ throw new Error(`reset retry target has a new or changed entry: ${target.path}`);
363
+ }
364
+ if (exact && (current.length !== approved.length || approved.some((entry) => !current.some((candidate) => sameScopePath(candidate, entry))))) {
365
+ throw new Error(`reset retry target changed after approval: ${target.path}`);
366
+ }
367
+ }
368
+ function validateSelectorState(state, stage) {
369
+ const stat = lstat(state.path);
370
+ if (stat === null) {
371
+ if (stage === "prepared" && state.before !== "absent")
372
+ throw new Error(`reset retry selector target changed after approval: ${state.path}`);
373
+ return;
374
+ }
375
+ if (!stat.isFile() || stat.isSymbolicLink())
376
+ throw new Error(`reset retry selector target changed after approval: ${state.path}`);
377
+ const current = hash(fs.readFileSync(state.path));
378
+ const allowed = stage === "initializing" ? [state.after] : stage === "prepared" ? [state.before] : [state.before, state.after];
379
+ if (!allowed.includes(current))
380
+ throw new Error(`reset retry selector target has changed: ${state.path}`);
381
+ }
382
+ function validateRetryState(journal) {
383
+ const scope = validatedScope(journal.preview);
384
+ const exact = journal.stage === "prepared";
385
+ for (const deletion of scope.deletions)
386
+ for (const target of deletion.targets) {
387
+ if (target.operation === "remove_path" && target.path) {
388
+ if (journal.stage === "initializing") {
389
+ const current = snapshot(target.path);
390
+ if (!(current.length === 1 && current[0].type === "absent")) {
391
+ throw new Error(`reset retry target reappeared after deletion: ${target.path}`);
392
+ }
393
+ }
394
+ else
395
+ validateRemovePath(target, exact);
396
+ }
397
+ }
398
+ for (const state of journal.selector_states)
399
+ validateSelectorState(state, journal.stage);
400
+ }
401
+ function loadResetJournal(options, authorization, dependencies, project) {
402
+ const target = journalPath(project);
403
+ const staging = journalStagingPath(project);
404
+ const hasTarget = lstat(target) !== null;
405
+ const hasStaging = lstat(staging) !== null;
406
+ if (hasStaging)
407
+ assertRegularJournalFile(staging);
408
+ if (hasTarget) {
409
+ try {
410
+ const journal = parseJournal(project, authorization);
411
+ if (hasStaging)
412
+ fs.rmSync(staging, { force: true });
413
+ return { journal, retry: true };
414
+ }
415
+ catch (error) {
416
+ try {
417
+ const preview = approvedProductV1ResetPreview(options, authorization, dependencies);
418
+ const journal = journalFor(preview, "prepared");
419
+ writeJournal(project, journal);
420
+ return { journal, retry: true };
421
+ }
422
+ catch {
423
+ throw new Error(`product-v1 reset journal cannot be recovered after effects may have begun: ${error.message}`);
424
+ }
425
+ }
426
+ }
427
+ const preview = approvedProductV1ResetPreview(options, authorization, dependencies);
428
+ const journal = journalFor(preview, "prepared");
429
+ writeJournal(project, journal);
430
+ return { journal, retry: false };
431
+ }
432
+ function initializeFreshV3(scope) {
433
+ const sourceRoot = resolveSourceRoot();
434
+ applyAppContentRefresh(scope.roots.install_root, sourceRoot);
435
+ const skill = path.join(scope.roots.runtime_home, ".agents", "skills", "agentera");
436
+ assertContained(scope.roots.runtime_home, skill);
437
+ fs.rmSync(skill, { recursive: true, force: true });
438
+ fs.mkdirSync(path.dirname(skill), { recursive: true });
439
+ fs.symlinkSync(path.join(scope.roots.install_root, "skills", "agentera"), skill);
440
+ if (classifyProjectState(scope.roots.project, sourceRoot).state !== "fresh_uninitialized") {
441
+ throw new Error("canonical fresh-v3 project initialization did not produce fresh_uninitialized state");
442
+ }
443
+ }
444
+ export function applyProductV1Reset(options, authorization, dependencies = {}) {
445
+ const roots = resolvedRoots(options);
446
+ let { journal, retry } = loadResetJournal(options, authorization, dependencies, roots.project);
447
+ if (JSON.stringify(journal.preview.roots) !== JSON.stringify(roots)) {
448
+ throw new Error("product-v1 reset retry roots do not match the approved operation");
449
+ }
450
+ if (retry)
451
+ cleanSelectorStaging(journal.selector_states);
452
+ if (!retry) {
453
+ dependencies.afterEffect?.("journal");
454
+ }
455
+ const scope = validatedScope(journal.preview);
456
+ if (journal.stage !== "complete")
457
+ validateRetryState(journal);
458
+ if (journal.stage === "prepared")
459
+ journal = setJournalStage(roots.project, journal, "deleting");
460
+ if (journal.stage === "deleting") {
461
+ for (const deletion of scope.deletions)
462
+ for (const target of deletion.targets) {
463
+ const targetRoot = target.declared.startsWith("{project}") ? scope.roots.project
464
+ : target.declared.startsWith("{install_root}") ? scope.roots.install_root
465
+ : deletion.id === "runtime.resources" ? scope.roots.runtime_home : deletion.root;
466
+ if (target.path)
467
+ assertContained(targetRoot, target.path);
468
+ if (target.operation === "remove_path" && target.path) {
469
+ if (retry)
470
+ validateRemovePath(target, false);
471
+ fs.rmSync(target.path, { recursive: true, force: true });
472
+ dependencies.afterEffect?.(`delete:${deletion.id}:${target.declared}`);
473
+ }
474
+ }
475
+ for (const [target, approvedSelectors] of selectorGroups(scope)) {
476
+ if (retry)
477
+ validateSelectorState(journal.selector_states.find((state) => state.path === target), "deleting");
478
+ removeSelectors(target, approvedSelectors, dependencies.afterEffect);
479
+ dependencies.afterEffect?.(`selectors:${target}`);
480
+ }
481
+ journal = setJournalStage(roots.project, journal, "initializing");
482
+ }
483
+ if (journal.stage === "initializing") {
484
+ initializeFreshV3(scope);
485
+ journal = setJournalStage(roots.project, journal, "complete");
486
+ dependencies.afterEffect?.("initialize:fresh-v3");
487
+ }
488
+ cleanSelectorStaging(journal.selector_states);
489
+ fs.rmSync(journalStagingPath(roots.project), { force: true });
490
+ fs.rmSync(journalPath(roots.project), { force: true });
491
+ return {
492
+ schemaVersion: "agentera.productV1ResetApply.v1",
493
+ status: "complete",
494
+ authorization,
495
+ validated_scope: scope,
496
+ effects_performed: true,
497
+ };
498
+ }
499
+ export function authorizeProductV1Reset(options, authorization, dependencies = {}) {
500
+ const current = approvedProductV1ResetPreview(options, authorization, dependencies);
501
+ return {
502
+ schemaVersion: "agentera.productV1ResetAuthorization.v1",
503
+ status: "authorized",
504
+ authorization,
505
+ validated_scope: validatedScope(current),
506
+ effects_performed: false,
507
+ };
508
+ }
509
+ function approvedProductV1ResetPreview(options, authorization, dependencies) {
510
+ const current = previewProductV1Reset(options, dependencies);
511
+ if (authorization !== current.authorization) {
512
+ throw new Error("product-v1 reset scope changed after preview; run a new preview and review its authorization");
513
+ }
514
+ return current;
515
+ }
@@ -0,0 +1,139 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { resolveSourceRoot } from "../core/sourceRoot.js";
4
+ import { loadYamlMapping } from "../core/yaml.js";
5
+ import { loadRegistry } from "../registries/packageRegistry.js";
6
+ import { NATIVE_RESOURCE_CLEANUP_CONTRACT_RELATIVE_PATH } from "../runtime/lifecycleAuthority.js";
7
+ export const PRODUCT_V1_RESET_AUTHORITY_RELATIVE_PATH = "references/adapters/product-v1-reset.yaml";
8
+ const PACKAGE_REGISTRY_RELATIVE_PATH = "references/adapters/package-registry.yaml";
9
+ const RUNTIME_LIFECYCLE_RELATIVE_PATH = "references/adapters/runtime-lifecycle-authority.yaml";
10
+ const ALLOWED_ROOTS = new Set(["project", "profile_root", "install_root", "runtime_declared_roots"]);
11
+ function mapping(value, field) {
12
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
13
+ throw new Error(`${field} must be a mapping`);
14
+ }
15
+ return value;
16
+ }
17
+ function text(value, field) {
18
+ if (typeof value !== "string" || value.length === 0)
19
+ throw new Error(`${field} must be a non-empty string`);
20
+ return value;
21
+ }
22
+ function textList(value, field) {
23
+ if (!Array.isArray(value) || value.length === 0)
24
+ throw new Error(`${field} must be a non-empty string list`);
25
+ return value.map((item, index) => text(item, `${field}[${index}]`));
26
+ }
27
+ function relativePath(value, field) {
28
+ const result = text(value, field);
29
+ if (path.isAbsolute(result) || result.split("/").includes("..")) {
30
+ throw new Error(`${field} must stay within its declared root`);
31
+ }
32
+ return result;
33
+ }
34
+ export function loadProductV1ResetAuthority(authorityPath = path.join(resolveSourceRoot(), PRODUCT_V1_RESET_AUTHORITY_RELATIVE_PATH), sourceRoot = resolveSourceRoot()) {
35
+ const data = loadYamlMapping(fs.readFileSync(authorityPath, "utf8"));
36
+ if (data.schema_version !== "agentera.productV1ResetInventory.v1") {
37
+ throw new Error("product v1 reset authority has unsupported schema_version");
38
+ }
39
+ const policy = mapping(data.policy, "policy");
40
+ if (policy.trigger !== "any_declared_product_v1_generation_evidence"
41
+ || policy.schema_identifier_suffix_is_evidence !== false
42
+ || policy.discovery !== "declared_roots_only"
43
+ || policy.preserve_unlisted_state !== true) {
44
+ throw new Error("product v1 reset authority policy must require bounded product-generation evidence");
45
+ }
46
+ const sources = mapping(data.source_authorities, "source_authorities");
47
+ if (sources.retired_resources !== NATIVE_RESOURCE_CLEANUP_CONTRACT_RELATIVE_PATH
48
+ || sources.package_inventory !== PACKAGE_REGISTRY_RELATIVE_PATH
49
+ || sources.runtime_lifecycle !== RUNTIME_LIFECYCLE_RELATIVE_PATH) {
50
+ throw new Error("product v1 reset authority must reference the existing runtime and package authorities");
51
+ }
52
+ for (const authority of Object.values(sources)) {
53
+ if (typeof authority !== "string" || !fs.existsSync(path.join(sourceRoot, authority))) {
54
+ throw new Error(`product v1 reset source authority does not exist: ${String(authority)}`);
55
+ }
56
+ }
57
+ const triggers = mapping(data.trigger_evidence, "trigger_evidence");
58
+ if (!Array.isArray(triggers.project_artifacts) || triggers.project_artifacts.length === 0) {
59
+ throw new Error("trigger_evidence.project_artifacts must be non-empty");
60
+ }
61
+ const projectArtifacts = triggers.project_artifacts.map((value, index) => {
62
+ const item = mapping(value, `trigger_evidence.project_artifacts[${index}]`);
63
+ if (item.generation !== "product_v1") {
64
+ throw new Error(`trigger_evidence.project_artifacts[${index}].generation must be product_v1`);
65
+ }
66
+ if (typeof item.triggers_reset !== "boolean") {
67
+ throw new Error(`trigger_evidence.project_artifacts[${index}].triggers_reset must be boolean`);
68
+ }
69
+ return {
70
+ id: text(item.id, `trigger_evidence.project_artifacts[${index}].id`),
71
+ generation: "product_v1",
72
+ triggersReset: item.triggers_reset === true,
73
+ path: relativePath(item.path, `trigger_evidence.project_artifacts[${index}].path`),
74
+ currentPath: relativePath(item.current_path, `trigger_evidence.project_artifacts[${index}].current_path`),
75
+ };
76
+ });
77
+ const packageTrigger = mapping(triggers.installation_package, "trigger_evidence.installation_package");
78
+ const packageRecord = loadRegistry(path.join(sourceRoot, PACKAGE_REGISTRY_RELATIVE_PATH), sourceRoot).get("agentera");
79
+ if (packageTrigger.generation !== "product_v1"
80
+ || packageTrigger.triggers_reset !== true
81
+ || packageTrigger.authority !== PACKAGE_REGISTRY_RELATIVE_PATH
82
+ || packageTrigger.manifest !== packageRecord.version_authority.persisted_authority
83
+ || packageTrigger.selector !== packageRecord.version_authority.selector
84
+ || packageTrigger.predicate !== "semver_major_equals_1") {
85
+ throw new Error("installation package trigger must use the package inventory's version authority and product-v1 major");
86
+ }
87
+ const runtimeTrigger = mapping(triggers.runtime_resources, "trigger_evidence.runtime_resources");
88
+ if (runtimeTrigger.authority !== NATIVE_RESOURCE_CLEANUP_CONTRACT_RELATIVE_PATH
89
+ || runtimeTrigger.role !== "reset_scope_only") {
90
+ throw new Error("retired runtime identities must remain reset scope, not product-v1 trigger evidence");
91
+ }
92
+ if (!Array.isArray(data.scope_inventory) || data.scope_inventory.length === 0) {
93
+ throw new Error("scope_inventory must be non-empty");
94
+ }
95
+ const ids = new Set();
96
+ const scope = data.scope_inventory.map((value, index) => {
97
+ const item = mapping(value, `scope_inventory[${index}]`);
98
+ const id = text(item.id, `scope_inventory[${index}].id`);
99
+ const owner = text(item.owner, `scope_inventory[${index}].owner`);
100
+ const action = item.action;
101
+ const boundedRoot = item.bounded_root;
102
+ if (ids.has(id))
103
+ throw new Error(`scope_inventory has duplicate id: ${id}`);
104
+ if (action !== "delete" && action !== "recreate")
105
+ throw new Error(`scope_inventory[${index}].action is invalid`);
106
+ if (typeof boundedRoot !== "string" || !ALLOWED_ROOTS.has(boundedRoot)) {
107
+ throw new Error(`scope_inventory[${index}].bounded_root is invalid`);
108
+ }
109
+ ids.add(id);
110
+ return {
111
+ id,
112
+ action: action,
113
+ owner,
114
+ boundedRoot: boundedRoot,
115
+ targets: textList(item.targets, `scope_inventory[${index}].targets`).map((target, targetIndex) => relativePath(target, `scope_inventory[${index}].targets[${targetIndex}]`)),
116
+ };
117
+ });
118
+ return {
119
+ sourcePath: authorityPath,
120
+ projectArtifacts,
121
+ installationPackage: {
122
+ manifest: packageTrigger.manifest,
123
+ selector: packageTrigger.selector,
124
+ predicate: "semver_major_equals_1",
125
+ },
126
+ scope,
127
+ };
128
+ }
129
+ export function productV1ArtifactPairs() {
130
+ return loadProductV1ResetAuthority().projectArtifacts.map(({ path: legacyPath, currentPath }) => [legacyPath, currentPath]);
131
+ }
132
+ export function productV1ProjectTriggerPaths() {
133
+ return loadProductV1ResetAuthority().projectArtifacts
134
+ .filter(({ triggersReset }) => triggersReset)
135
+ .map(({ path: legacyPath }) => legacyPath);
136
+ }
137
+ export function isProductV1PackageVersion(version) {
138
+ return /^1\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(version);
139
+ }