trackops 1.0.1 → 2.0.0

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 (83) hide show
  1. package/README.md +292 -272
  2. package/bin/trackops.js +108 -50
  3. package/lib/config.js +267 -38
  4. package/lib/control.js +534 -480
  5. package/lib/env.js +244 -0
  6. package/lib/i18n.js +61 -53
  7. package/lib/init.js +170 -47
  8. package/lib/locale.js +63 -0
  9. package/lib/opera-bootstrap.js +1075 -0
  10. package/lib/opera.js +524 -125
  11. package/lib/preferences.js +74 -0
  12. package/lib/registry.js +27 -13
  13. package/lib/release.js +56 -0
  14. package/lib/resources.js +42 -0
  15. package/lib/runtime-state.js +144 -0
  16. package/lib/server.js +1004 -521
  17. package/lib/skills.js +148 -124
  18. package/lib/workspace.js +260 -0
  19. package/locales/en.json +418 -132
  20. package/locales/es.json +418 -132
  21. package/package.json +8 -9
  22. package/scripts/postinstall-locale.js +21 -0
  23. package/scripts/skills-marketplace-smoke.js +124 -0
  24. package/scripts/smoke-tests.js +570 -0
  25. package/scripts/sync-skill-version.js +21 -0
  26. package/scripts/validate-skill.js +89 -0
  27. package/skills/trackops/SKILL.md +89 -0
  28. package/skills/trackops/agents/openai.yaml +3 -0
  29. package/skills/trackops/references/activation.md +73 -0
  30. package/skills/trackops/references/troubleshooting.md +49 -0
  31. package/skills/trackops/references/workflow.md +26 -0
  32. package/skills/trackops/scripts/bootstrap-trackops.js +203 -0
  33. package/skills/trackops/skill.json +29 -0
  34. package/templates/opera/agent.md +10 -9
  35. package/templates/opera/architecture/dependency-graph.md +24 -0
  36. package/templates/opera/architecture/runtime-automation.md +24 -0
  37. package/templates/opera/architecture/runtime-operations.md +34 -0
  38. package/templates/opera/en/agent.md +27 -0
  39. package/templates/opera/en/architecture/dependency-graph.md +24 -0
  40. package/templates/opera/en/architecture/runtime-automation.md +24 -0
  41. package/templates/opera/en/architecture/runtime-operations.md +34 -0
  42. package/templates/opera/en/genesis.md +79 -0
  43. package/templates/opera/en/references/autonomy-and-recovery.md +23 -0
  44. package/templates/opera/en/references/opera-cycle.md +62 -0
  45. package/templates/opera/en/registry.md +28 -0
  46. package/templates/opera/en/reviews/delivery-audit.md +18 -0
  47. package/templates/opera/en/reviews/integration-audit.md +18 -0
  48. package/templates/opera/en/router.md +49 -0
  49. package/templates/opera/genesis.md +79 -94
  50. package/templates/opera/reviews/delivery-audit.md +18 -0
  51. package/templates/opera/reviews/integration-audit.md +18 -0
  52. package/templates/opera/router.md +15 -5
  53. package/templates/skills/changelog-updater/locales/en/SKILL.md +11 -0
  54. package/templates/skills/commiter/locales/en/SKILL.md +11 -0
  55. package/templates/skills/opera-contract-auditor/SKILL.md +38 -0
  56. package/templates/skills/opera-contract-auditor/locales/en/SKILL.md +38 -0
  57. package/templates/skills/opera-policy-guard/SKILL.md +26 -0
  58. package/templates/skills/opera-policy-guard/locales/en/SKILL.md +26 -0
  59. package/templates/skills/project-starter-skill/SKILL.md +89 -164
  60. package/templates/skills/project-starter-skill/locales/en/SKILL.md +104 -0
  61. package/ui/css/panels.css +956 -953
  62. package/ui/index.html +1 -1
  63. package/ui/js/api.js +211 -194
  64. package/ui/js/app.js +200 -199
  65. package/ui/js/i18n.js +14 -0
  66. package/ui/js/onboarding.js +439 -437
  67. package/ui/js/state.js +130 -129
  68. package/ui/js/utils.js +175 -172
  69. package/ui/js/views/board.js +255 -254
  70. package/ui/js/views/execution.js +256 -256
  71. package/ui/js/views/insights.js +340 -339
  72. package/ui/js/views/overview.js +366 -361
  73. package/ui/js/views/settings.js +340 -202
  74. package/ui/js/views/sidebar.js +131 -132
  75. package/ui/js/views/skills.js +163 -162
  76. package/ui/js/views/tasks.js +406 -405
  77. package/ui/js/views/topbar.js +239 -183
  78. package/templates/etapa/agent.md +0 -26
  79. package/templates/etapa/genesis.md +0 -94
  80. package/templates/etapa/references/autonomy-and-recovery.md +0 -117
  81. package/templates/etapa/references/etapa-cycle.md +0 -193
  82. package/templates/etapa/registry.md +0 -28
  83. package/templates/etapa/router.md +0 -39
package/lib/opera.js CHANGED
@@ -1,202 +1,601 @@
1
- #!/usr/bin/env node
2
-
3
- const fs = require("fs");
4
- const path = require("path");
5
-
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+
6
6
  const config = require("./config");
7
+ const env = require("./env");
7
8
  const { t, setLocale } = require("./i18n");
9
+ const { promptForLocale, resolveLocale } = require("./locale");
10
+ const { resolveLocalizedFile, resolveLocalizedDir, resolveSkillFile } = require("./resources");
11
+ const bootstrap = require("./opera-bootstrap");
12
+ const runtimeState = require("./runtime-state");
8
13
 
9
14
  const TEMPLATES_DIR = path.join(__dirname, "..", "templates", "opera");
10
15
  const SKILLS_TEMPLATES_DIR = path.join(__dirname, "..", "templates", "skills");
11
16
  const OPERA_VERSION = require("../package.json").version;
12
-
17
+ const AUXILIARY_SKILLS = ["project-starter-skill", "opera-contract-auditor", "opera-policy-guard"];
18
+
13
19
  function nowIso() {
14
20
  return new Date().toISOString();
15
21
  }
16
22
 
17
- function copyTemplate(templatePath, targetPath, replacements) {
18
- fs.mkdirSync(path.dirname(targetPath), { recursive: true });
19
- let content = fs.readFileSync(templatePath, "utf8");
20
- if (replacements) {
21
- for (const [key, value] of Object.entries(replacements)) {
22
- content = content.replace(new RegExp(`\\{\\{${key}\\}\\}`, "g"), value);
23
+ function formatLocaleSource(source) {
24
+ return t(`locale.source.${String(source || "").trim()}`) || source || t("locale.none");
25
+ }
26
+
27
+ function readText(filePath) {
28
+ return fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf8") : "";
29
+ }
30
+
31
+ function renderTemplate(templatePath, replacements) {
32
+ let content = fs.readFileSync(templatePath, "utf8");
33
+ for (const [key, value] of Object.entries(replacements || {})) {
34
+ content = content.replace(new RegExp(`\\{\\{${key}\\}\\}`, "g"), value);
35
+ }
36
+ return content;
37
+ }
38
+
39
+ function matchesKnownTemplate(filePath, templatePath, replacements) {
40
+ if (!fs.existsSync(filePath) || !fs.existsSync(templatePath)) return false;
41
+ return readText(filePath).replace(/\r\n/g, "\n") === renderTemplate(templatePath, replacements).replace(/\r\n/g, "\n");
42
+ }
43
+
44
+ function copyTemplate(templatePath, targetPath, replacements, options = {}) {
45
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
46
+ const shouldWrite =
47
+ !fs.existsSync(targetPath) ||
48
+ options.overwrite === true ||
49
+ (options.overwriteIfTemplate && matchesKnownTemplate(targetPath, templatePath, replacements));
50
+
51
+ if (!shouldWrite) return false;
52
+ fs.writeFileSync(targetPath, renderTemplate(templatePath, replacements), "utf8");
53
+ return true;
54
+ }
55
+
56
+ function copyDirRecursive(src, dest) {
57
+ if (!src || !fs.existsSync(src)) return;
58
+ fs.mkdirSync(dest, { recursive: true });
59
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
60
+ const srcPath = path.join(src, entry.name);
61
+ const destPath = path.join(dest, entry.name);
62
+ if (entry.isDirectory()) {
63
+ copyDirRecursive(srcPath, destPath);
64
+ } else {
65
+ fs.copyFileSync(srcPath, destPath);
23
66
  }
24
67
  }
25
- fs.writeFileSync(targetPath, content, "utf8");
26
68
  }
27
69
 
28
- function copyDirRecursive(src, dest) {
70
+ function seedDirRecursive(src, dest, options = {}) {
71
+ if (!src || !fs.existsSync(src)) return;
29
72
  fs.mkdirSync(dest, { recursive: true });
30
73
  for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
31
74
  const srcPath = path.join(src, entry.name);
32
75
  const destPath = path.join(dest, entry.name);
33
76
  if (entry.isDirectory()) {
34
- copyDirRecursive(srcPath, destPath);
35
- } else {
77
+ seedDirRecursive(srcPath, destPath, options);
78
+ } else if (options.overwrite || !fs.existsSync(destPath)) {
79
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
36
80
  fs.copyFileSync(srcPath, destPath);
37
81
  }
38
82
  }
39
83
  }
40
-
41
- function install(root, options) {
42
- const controlFile = config.controlFilePath(root);
43
- if (!fs.existsSync(controlFile)) {
44
- throw new Error("project_control.json not found. Run 'trackops init' first.");
45
- }
46
-
47
- const control = config.loadControl(root);
48
- setLocale(config.getLocale(control));
49
-
50
- if (config.isOperaInstalled(control)) {
51
- console.log(t("opera.alreadyInstalled", { version: config.getOperaVersion(control) }));
52
- return;
53
- }
54
-
84
+
85
+ function resolveOperaLocale(control, options = {}) {
86
+ return resolveLocale(options.locale, config.getLocale(control) || runtimeState.getGlobalLocale());
87
+ }
88
+
89
+ function installStructure(root, control, locale, options = {}) {
90
+ const context = config.ensureContext(root);
55
91
  const projectName = control.meta.projectName || "Project";
56
-
57
- // Create .agent/hub/ with agent.md and router.md
58
- const agentHubDir = path.join(root, ".agent", "hub");
92
+ const replacements = {
93
+ PROJECT_NAME: projectName,
94
+ DESIRED_OUTCOME: t("bootstrap.pendingValue"),
95
+ SERVICES_TABLE: "| — | — | — |",
96
+ SOURCE_OF_TRUTH: t("bootstrap.pendingValue"),
97
+ PAYLOAD: t("bootstrap.pendingValue"),
98
+ BEHAVIOR_RULES: `- ${t("bootstrap.noneDefined")}`,
99
+ DATA_SCHEMA: JSON.stringify({
100
+ input: { source: "", schema: {} },
101
+ output: { destination: "", schema: {} },
102
+ }, null, 2),
103
+ ARCHITECTURAL_INVARIANTS: `- ${t("bootstrap.noneDefined")}`,
104
+ PIPELINE_ITEMS: `- ${t("bootstrap.noneDefined")}`,
105
+ TEMPLATE_ITEMS: `- ${t("bootstrap.noneDefined")}`,
106
+ };
107
+ const overwriteOptions = {
108
+ overwriteIfTemplate: options.rewriteLocalizedTemplates === true,
109
+ };
110
+
111
+ const agentHubDir = context.paths.agentHubDir;
59
112
  fs.mkdirSync(agentHubDir, { recursive: true });
60
-
61
- const agentTemplatePath = path.join(TEMPLATES_DIR, "agent.md");
62
- const routerTemplatePath = path.join(TEMPLATES_DIR, "router.md");
63
-
64
- if (fs.existsSync(agentTemplatePath) && !fs.existsSync(path.join(agentHubDir, "agent.md"))) {
65
- copyTemplate(agentTemplatePath, path.join(agentHubDir, "agent.md"), { PROJECT_NAME: projectName });
66
- }
67
- if (fs.existsSync(routerTemplatePath) && !fs.existsSync(path.join(agentHubDir, "router.md"))) {
68
- copyTemplate(routerTemplatePath, path.join(agentHubDir, "router.md"), { PROJECT_NAME: projectName });
69
- }
70
-
71
- // Create .agents/skills/_registry.md
72
- const skillsRegistryDir = path.join(root, ".agents", "skills");
113
+ fs.mkdirSync(context.paths.architectureDir, { recursive: true });
114
+ fs.mkdirSync(context.paths.contractDir, { recursive: true });
115
+ fs.mkdirSync(context.paths.policyDir, { recursive: true });
116
+ fs.mkdirSync(context.paths.bootstrapDir, { recursive: true });
117
+ fs.mkdirSync(context.paths.reviewsDir, { recursive: true });
118
+
119
+ copyTemplate(
120
+ resolveLocalizedFile(TEMPLATES_DIR, locale, "agent.md"),
121
+ path.join(agentHubDir, "agent.md"),
122
+ replacements,
123
+ overwriteOptions,
124
+ );
125
+ copyTemplate(
126
+ resolveLocalizedFile(TEMPLATES_DIR, locale, "router.md"),
127
+ path.join(agentHubDir, "router.md"),
128
+ replacements,
129
+ overwriteOptions,
130
+ );
131
+
132
+ const skillsRegistryDir = context.paths.skillsDir;
73
133
  fs.mkdirSync(skillsRegistryDir, { recursive: true });
74
- const registryPath = path.join(skillsRegistryDir, "_registry.md");
75
- const registryTemplatePath = path.join(TEMPLATES_DIR, "registry.md");
76
- if (fs.existsSync(registryTemplatePath) && !fs.existsSync(registryPath)) {
77
- copyTemplate(registryTemplatePath, registryPath, { PROJECT_NAME: projectName });
134
+ copyTemplate(
135
+ resolveLocalizedFile(TEMPLATES_DIR, locale, "registry.md"),
136
+ path.join(skillsRegistryDir, "_registry.md"),
137
+ replacements,
138
+ overwriteOptions,
139
+ );
140
+
141
+ const genesisTemplatePath = resolveLocalizedFile(TEMPLATES_DIR, locale, "genesis.md");
142
+ const genesisPath = context.paths.genesisFile;
143
+ if (!fs.existsSync(genesisPath) || bootstrap.isVirginGenesis(readText(genesisPath))) {
144
+ copyTemplate(genesisTemplatePath, genesisPath, replacements, { overwrite: true });
145
+ }
146
+
147
+ const refsTemplateDir = resolveLocalizedDir(TEMPLATES_DIR, locale, "references");
148
+ const refsTargetDir = path.join(context.paths.skillsDir, "project-starter-skill", "references");
149
+ seedDirRecursive(refsTemplateDir, refsTargetDir, {
150
+ overwrite: options.rewriteLocalizedTemplates === true,
151
+ });
152
+
153
+ const architectureTemplateDir = resolveLocalizedDir(TEMPLATES_DIR, locale, "architecture");
154
+ seedDirRecursive(architectureTemplateDir, context.paths.architectureDir, {
155
+ overwrite: options.rewriteLocalizedTemplates === true,
156
+ });
157
+
158
+ const reviewsTemplateDir = resolveLocalizedDir(TEMPLATES_DIR, locale, "reviews");
159
+ seedDirRecursive(reviewsTemplateDir, context.paths.reviewsDir, {
160
+ overwrite: options.rewriteLocalizedTemplates === true,
161
+ });
162
+
163
+ for (const skillName of AUXILIARY_SKILLS) {
164
+ const skillTarget = path.join(context.paths.skillsDir, skillName, "SKILL.md");
165
+ const skillTemplate = resolveSkillFile(SKILLS_TEMPLATES_DIR, skillName, locale);
166
+ if (skillTemplate) {
167
+ fs.mkdirSync(path.dirname(skillTarget), { recursive: true });
168
+ if (!fs.existsSync(skillTarget) || options.rewriteLocalizedTemplates === true) {
169
+ fs.copyFileSync(skillTemplate, skillTarget);
170
+ }
171
+ }
78
172
  }
79
173
 
80
- // Create genesis.md if not exists
81
- const genesisTemplatePath = path.join(TEMPLATES_DIR, "genesis.md");
82
- const genesisPath = path.join(root, "genesis.md");
83
- if (fs.existsSync(genesisTemplatePath) && !fs.existsSync(genesisPath)) {
84
- copyTemplate(genesisTemplatePath, genesisPath, { PROJECT_NAME: projectName });
85
- }
174
+ bootstrap.writeAutonomyPolicy(context);
175
+ }
86
176
 
87
- // Copy OPERA references
88
- const refsTemplateDir = path.join(TEMPLATES_DIR, "references");
89
- if (fs.existsSync(refsTemplateDir)) {
90
- const starterSkillDir = path.join(root, ".agents", "skills", "project-starter-skill", "references");
91
- copyDirRecursive(refsTemplateDir, starterSkillDir);
177
+ async function install(root, options = {}) {
178
+ const context = config.ensureContext(root);
179
+ const controlFile = config.controlFilePath(context);
180
+ if (!fs.existsSync(controlFile)) {
181
+ throw new Error("project_control.json not found. Run 'trackops init' first.");
92
182
  }
93
183
 
94
- // Install project-starter-skill SKILL.md
95
- const starterSkillTemplate = path.join(SKILLS_TEMPLATES_DIR, "project-starter-skill", "SKILL.md");
96
- const starterSkillTarget = path.join(root, ".agents", "skills", "project-starter-skill", "SKILL.md");
97
- if (fs.existsSync(starterSkillTemplate) && !fs.existsSync(starterSkillTarget)) {
98
- fs.mkdirSync(path.dirname(starterSkillTarget), { recursive: true });
99
- fs.copyFileSync(starterSkillTemplate, starterSkillTarget);
184
+ const control = config.loadControl(context);
185
+ let locale = resolveOperaLocale(control, options);
186
+ if (!options.locale && !control.meta?.locale && !runtimeState.getGlobalLocale()) {
187
+ locale = await promptForLocale(locale);
188
+ if (!runtimeState.getGlobalLocale()) {
189
+ await runtimeState.ensureGlobalLocale({ preferredLocale: locale, interactive: false });
190
+ }
100
191
  }
192
+ control.meta.locale = locale;
193
+ setLocale(locale);
194
+
195
+ const alreadyInstalled = config.isOperaInstalled(control);
196
+ installStructure(context, control, locale);
101
197
 
102
- // Mark as installed in control
103
198
  control.meta.opera = {
199
+ ...(control.meta.opera || {}),
104
200
  installed: true,
201
+ model: "v3",
202
+ stableTag: "stable",
105
203
  version: OPERA_VERSION,
106
- installedAt: nowIso(),
107
- skills: [],
204
+ installedAt: control.meta?.opera?.installedAt || nowIso(),
205
+ skills: control.meta?.opera?.skills || [],
206
+ legacyStatus: "supported",
108
207
  };
109
- config.saveControl(root, control);
110
-
111
- console.log(t("opera.installed", { version: OPERA_VERSION }));
112
-
113
- // Auto-install base skills (commiter, changelog-updater)
208
+ if (!control.meta.opera.bootstrap && options.bootstrap === false) {
209
+ control.meta.opera.bootstrap = bootstrap.createAwaitingBootstrapState(context);
210
+ }
211
+ config.saveControl(context, control);
212
+ env.syncEnvironment(context, control);
213
+
214
+ if (!alreadyInstalled) {
215
+ console.log(t("opera.installed", { version: OPERA_VERSION }));
216
+ } else {
217
+ console.log(t("opera.alreadyInstalled", { version: config.getOperaVersion(control) || OPERA_VERSION }));
218
+ }
219
+
114
220
  const skills = require("./skills");
115
221
  for (const skillName of ["commiter", "changelog-updater"]) {
116
- try { skills.installSkill(root, skillName); }
117
- catch (_e) { /* already installed or not in catalog — skip silently */ }
222
+ try {
223
+ skills.installSkill(context, skillName, { locale });
224
+ } catch (_error) {
225
+ // ignore
226
+ }
227
+ }
228
+ skills.updateRegistry(context);
229
+
230
+ if (options.bootstrap !== false) {
231
+ await runBootstrap(context, {
232
+ locale,
233
+ answers: options.answers,
234
+ interactive: options.interactive,
235
+ bootstrapMode: options.bootstrapMode,
236
+ technicalLevel: options.technicalLevel,
237
+ projectState: options.projectState,
238
+ docsState: options.docsState,
239
+ decisionOwnership: options.decisionOwnership,
240
+ });
118
241
  }
119
242
  }
120
243
 
121
- function status(root) {
122
- const control = config.loadControl(root);
123
- setLocale(config.getLocale(control));
244
+ function removePath(targetPath) {
245
+ if (!targetPath || !fs.existsSync(targetPath)) return;
246
+ fs.rmSync(targetPath, { recursive: true, force: true });
247
+ }
124
248
 
125
- if (!config.isOperaInstalled(control)) {
126
- console.log(t("opera.notInstalled"));
127
- return;
249
+ function backupManagedArtifacts(context) {
250
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
251
+ const backupRoot = path.join(context.paths.tmpDir, "upgrade-backups", timestamp);
252
+ const items = [
253
+ context.paths.agentHubDir,
254
+ context.paths.skillsDir,
255
+ context.paths.genesisFile,
256
+ context.paths.contractFile,
257
+ context.paths.autonomyPolicyFile,
258
+ context.paths.bootstrapDir,
259
+ ];
260
+ fs.mkdirSync(backupRoot, { recursive: true });
261
+ let copied = 0;
262
+ for (const item of items) {
263
+ if (!fs.existsSync(item)) continue;
264
+ const relative = path.relative(context.workspaceRoot, item);
265
+ const destination = path.join(backupRoot, relative);
266
+ if (fs.statSync(item).isDirectory()) {
267
+ copyDirRecursive(item, destination);
268
+ } else {
269
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
270
+ fs.copyFileSync(item, destination);
271
+ }
272
+ copied += 1;
128
273
  }
274
+ return { backupRoot, copied };
275
+ }
129
276
 
130
- const opera = control.meta.opera;
131
- console.log(`OPERA v${opera.version}`);
132
- console.log(` Installed: ${opera.installedAt}`);
133
- console.log(` Skills: ${(opera.skills || []).join(", ") || "none"}`);
277
+ async function runBootstrap(root, options = {}) {
278
+ const context = config.ensureContext(root);
279
+ const control = config.loadControl(context);
280
+ const locale = resolveOperaLocale(control, options);
281
+ control.meta.locale = locale;
282
+ setLocale(locale);
283
+
284
+ const legacyBootstrap = bootstrap.detectLegacyBootstrap(context, control);
285
+ if (legacyBootstrap && !control.meta.opera?.bootstrap) {
286
+ control.meta.opera = control.meta.opera || {};
287
+ control.meta.opera.bootstrap = legacyBootstrap;
288
+ config.saveControl(context, control);
289
+ }
134
290
 
135
- // Check structural integrity
136
- const checks = [
137
- [".agent/hub/agent.md", fs.existsSync(path.join(root, ".agent", "hub", "agent.md"))],
138
- [".agent/hub/router.md", fs.existsSync(path.join(root, ".agent", "hub", "router.md"))],
139
- [".agents/skills/_registry.md", fs.existsSync(path.join(root, ".agents", "skills", "_registry.md"))],
140
- ["genesis.md", fs.existsSync(path.join(root, "genesis.md"))],
141
- ];
291
+ if ((options.resume || options.forceResume) && control.meta?.opera?.bootstrap) {
292
+ const resumed = bootstrap.resumeBootstrap(context, control);
293
+ if (resumed.resumed) {
294
+ const updatedControl = bootstrap.applyBootstrap(context, control, resumed.profile);
295
+ env.syncEnvironment(context, updatedControl, { requiredKeys: env.inferRequiredKeys(updatedControl, context) });
296
+ const ops = require("./control");
297
+ ops.syncDocs(context, updatedControl);
298
+ ops.refreshRepoRuntime(context, { quiet: true });
299
+ console.log(t("bootstrap.completed"));
300
+ return resumed.profile;
301
+ }
302
+ console.log(t("bootstrap.awaitingAgent"));
303
+ return control.meta.opera.bootstrap;
304
+ }
142
305
 
143
- console.log(" Structure:");
144
- for (const [file, exists] of checks) {
145
- console.log(` ${exists ? "\u2705" : "\u274C"} ${file}`);
306
+ const profile = await bootstrap.collectBootstrapProfile(context, control, options);
307
+ const updatedControl = bootstrap.applyBootstrap(context, control, profile);
308
+ env.syncEnvironment(context, updatedControl, { requiredKeys: env.inferRequiredKeys(updatedControl, context) });
309
+ const ops = require("./control");
310
+ ops.syncDocs(context, updatedControl);
311
+ ops.refreshRepoRuntime(context, { quiet: true });
312
+
313
+ if (profile.mode === "agent_handoff") {
314
+ console.log(t("bootstrap.awaitingAgent"));
315
+ console.log(`${t("bootstrap.handoffFile")}: ${profile.handoffFiles?.markdown || bootstrap.bootstrapRelativePaths(context).markdown}`);
316
+ } else {
317
+ console.log(profile.status === "completed" ? t("bootstrap.completed") : t("bootstrap.pending"));
146
318
  }
319
+ return profile;
147
320
  }
148
321
 
149
- function configure(root, args) {
150
- const control = config.loadControl(root);
322
+ function status(root) {
323
+ const context = config.ensureContext(root);
324
+ const control = config.loadControl(context);
151
325
  setLocale(config.getLocale(control));
326
+
327
+ if (!config.isOperaInstalled(control)) {
328
+ console.log(t("opera.notInstalled"));
329
+ return;
330
+ }
152
331
 
153
- for (let i = 0; i < args.length; i += 1) {
154
- if (args[i] === "--locale" && args[i + 1]) {
155
- control.meta.locale = args[i + 1];
156
- i += 1;
332
+ const opera = control.meta.opera;
333
+ const bootstrapState = opera.bootstrap || bootstrap.detectLegacyBootstrap(context, control);
334
+ const localeDoctor = runtimeState.doctorLocale(control.meta?.locale || null);
335
+ console.log(t("opera.status.version", { version: opera.version }));
336
+ console.log(t("opera.status.installed", { value: opera.installedAt }));
337
+ console.log(t("opera.status.skills", { value: (opera.skills || []).join(", ") || t("locale.none") }));
338
+ console.log(t("opera.status.locale", { locale: config.getLocale(control), source: formatLocaleSource(localeDoctor.source) }));
339
+ console.log(t("opera.status.legacy", { value: opera.legacyStatus || bootstrapState?.status || "supported" }));
340
+ console.log(t("opera.status.contractVersion", { value: opera.contractVersion || t("locale.none") }));
341
+ console.log(t("opera.status.contractReadiness", { value: opera.contractReadiness || "hypothesis" }));
342
+
343
+ if (bootstrapState) {
344
+ console.log(t("opera.status.bootstrap", { value: bootstrapState.status }));
345
+ if (bootstrapState.mode) {
346
+ console.log(t("opera.status.mode", { value: bootstrapState.mode }));
347
+ }
348
+ if (bootstrapState.routeReason) {
349
+ console.log(t("opera.status.route", { value: bootstrapState.routeReason }));
350
+ }
351
+ if (bootstrapState.decisionOwnership) {
352
+ console.log(t("opera.status.ownership", { value: bootstrapState.decisionOwnership }));
353
+ }
354
+ if ((bootstrapState.missingFields || []).length) {
355
+ console.log(t("opera.status.missing", { value: bootstrapState.missingFields.join(", ") }));
356
+ console.log(t("opera.status.resume"));
357
+ }
358
+ if (bootstrapState.handoffFiles?.markdown) {
359
+ console.log(t("opera.status.handoff", { value: bootstrapState.handoffFiles.markdown }));
360
+ }
361
+ if (bootstrapState.reviewFiles?.qualityReport) {
362
+ console.log(t("opera.status.qualityReport", { value: bootstrapState.reviewFiles.qualityReport }));
157
363
  }
364
+ }
365
+
366
+ const checks = [
367
+ [context.layout === "split" ? "ops/.agent/hub/agent.md" : ".agent/hub/agent.md", fs.existsSync(path.join(context.paths.agentHubDir, "agent.md"))],
368
+ [context.layout === "split" ? "ops/.agent/hub/router.md" : ".agent/hub/router.md", fs.existsSync(path.join(context.paths.agentHubDir, "router.md"))],
369
+ [context.layout === "split" ? "ops/.agents/skills/_registry.md" : ".agents/skills/_registry.md", fs.existsSync(context.paths.registryPath)],
370
+ [context.layout === "split" ? "ops/genesis.md" : "genesis.md", fs.existsSync(context.paths.genesisFile)],
371
+ [context.layout === "split" ? "ops/contract/operating-contract.json" : "contract/operating-contract.json", fs.existsSync(context.paths.contractFile)],
372
+ [context.layout === "split" ? "ops/policy/autonomy.json" : "policy/autonomy.json", fs.existsSync(context.paths.autonomyPolicyFile)],
373
+ ];
374
+
375
+ console.log(t("opera.status.structure"));
376
+ for (const [file, exists] of checks) {
377
+ console.log(` ${exists ? "\u2705" : "\u274C"} ${file}`);
378
+ }
379
+ }
380
+
381
+ function configure(root, args) {
382
+ const context = config.ensureContext(root);
383
+ const control = config.loadControl(context);
384
+ setLocale(config.getLocale(control));
385
+ let nextLocale = config.getLocale(control);
386
+
387
+ for (let i = 0; i < args.length; i += 1) {
388
+ if (args[i] === "--locale" && args[i + 1]) {
389
+ nextLocale = resolveLocale(args[i + 1], nextLocale);
390
+ control.meta.locale = nextLocale;
391
+ i += 1;
392
+ }
158
393
  if (args[i] === "--phases" && args[i + 1]) {
159
394
  try {
160
395
  control.meta.phases = JSON.parse(args[i + 1]);
161
396
  } catch (_e) {
162
- console.error("Invalid phases JSON.");
397
+ console.error(t("opera.configure.invalidPhases"));
163
398
  return;
164
399
  }
165
- i += 1;
166
- }
400
+ i += 1;
401
+ }
402
+ }
403
+
404
+ config.saveControl(context, control);
405
+ if (config.isOperaInstalled(control)) {
406
+ installStructure(context, control, nextLocale, { rewriteLocalizedTemplates: true });
407
+ env.syncEnvironment(context, control);
408
+ const ops = require("./control");
409
+ ops.syncDocs(context, control);
167
410
  }
168
-
169
- config.saveControl(root, control);
170
- console.log("Configuration updated.");
411
+ console.log(t("opera.configure.updated"));
171
412
  }
172
413
 
173
- function upgrade(root) {
174
- const control = config.loadControl(root);
175
- setLocale(config.getLocale(control));
414
+ function upgrade(root, args = []) {
415
+ const context = config.ensureContext(root);
416
+ const control = config.loadControl(context);
417
+ const locale = config.getLocale(control);
418
+ setLocale(locale);
419
+ const wantsStable = (args || []).includes("--stable");
420
+ const wantsReset = (args || []).includes("--reset");
176
421
 
177
422
  if (!config.isOperaInstalled(control)) {
178
423
  console.log(t("opera.notInstalled"));
179
- console.log("Run 'trackops opera install' first.");
424
+ console.log(t("opera.upgrade.runInstallFirst"));
425
+ return;
426
+ }
427
+
428
+ if (!wantsStable) {
429
+ console.log(t("opera.upgrade.usage"));
430
+ return;
431
+ }
432
+
433
+ const legacy = bootstrap.detectLegacyBootstrap(context, control);
434
+ const isLegacyUnsupported = legacy?.status === "legacy_unsupported";
435
+ if (isLegacyUnsupported && !wantsReset) {
436
+ control.meta.opera = control.meta.opera || {};
437
+ control.meta.opera.legacyStatus = "legacy_unsupported";
438
+ config.saveControl(context, control);
439
+ console.log(t("opera.upgrade.legacyUnsupported"));
180
440
  return;
181
441
  }
182
442
 
183
- // Re-copy reference files
184
- const refsTemplateDir = path.join(TEMPLATES_DIR, "references");
185
- if (fs.existsSync(refsTemplateDir)) {
186
- const starterSkillDir = path.join(root, ".agents", "skills", "project-starter-skill", "references");
187
- copyDirRecursive(refsTemplateDir, starterSkillDir);
443
+ const backup = backupManagedArtifacts(context);
444
+ if (wantsReset) {
445
+ removePath(context.paths.agentHubDir);
446
+ removePath(context.paths.skillsDir);
447
+ removePath(context.paths.genesisFile);
448
+ removePath(context.paths.contractFile);
449
+ removePath(context.paths.autonomyPolicyFile);
450
+ removePath(context.paths.bootstrapDir);
188
451
  }
189
452
 
190
- control.meta.opera.version = OPERA_VERSION;
191
- config.saveControl(root, control);
453
+ installStructure(context, control, locale, { rewriteLocalizedTemplates: true });
454
+ control.meta.opera = {
455
+ ...(control.meta.opera || {}),
456
+ installed: true,
457
+ model: "v3",
458
+ stableTag: "stable",
459
+ version: OPERA_VERSION,
460
+ legacyStatus: "supported",
461
+ contractVersion: fs.existsSync(context.paths.contractFile) ? bootstrap.CONTRACT_VERSION : null,
462
+ contractReadiness: fs.existsSync(context.paths.contractFile)
463
+ ? (control.meta?.opera?.contractReadiness || "verified")
464
+ : "hypothesis",
465
+ bootstrap: wantsReset
466
+ ? bootstrap.createAwaitingBootstrapState(context)
467
+ : (control.meta?.opera?.bootstrap || bootstrap.createAwaitingBootstrapState(context)),
468
+ };
469
+ config.saveControl(context, control);
470
+ env.syncEnvironment(context, control);
471
+ require("./skills").updateRegistry(context);
472
+ console.log(t("opera.upgrade.backup", { path: path.relative(context.workspaceRoot, backup.backupRoot) }));
192
473
  console.log(t("opera.upgraded", { version: OPERA_VERSION }));
193
474
  }
194
475
 
195
- /* ── CLI commands ── */
476
+ function cmdInstall(root, args) {
477
+ const options = {
478
+ bootstrap: true,
479
+ answers: {},
480
+ interactive: true,
481
+ locale: null,
482
+ bootstrapMode: "auto",
483
+ technicalLevel: null,
484
+ projectState: null,
485
+ docsState: null,
486
+ decisionOwnership: null,
487
+ };
488
+ for (let i = 0; i < (args || []).length; i += 1) {
489
+ if (args[i] === "--locale" && args[i + 1]) {
490
+ options.locale = args[i + 1];
491
+ i += 1;
492
+ } else if (args[i] === "--no-bootstrap") {
493
+ options.bootstrap = false;
494
+ } else if (args[i] === "--non-interactive") {
495
+ options.interactive = false;
496
+ } else if (args[i] === "--bootstrap-mode" && args[i + 1]) {
497
+ options.bootstrapMode = args[i + 1];
498
+ i += 1;
499
+ } else if (args[i] === "--technical-level" && args[i + 1]) {
500
+ options.technicalLevel = args[i + 1];
501
+ i += 1;
502
+ } else if (args[i] === "--project-state" && args[i + 1]) {
503
+ options.projectState = args[i + 1];
504
+ i += 1;
505
+ } else if (args[i] === "--docs-state" && args[i + 1]) {
506
+ options.docsState = args[i + 1];
507
+ i += 1;
508
+ } else if (args[i] === "--decision-ownership" && args[i + 1]) {
509
+ options.decisionOwnership = args[i + 1];
510
+ i += 1;
511
+ }
512
+ }
513
+ return install(root, options);
514
+ }
196
515
 
197
- function cmdInstall(root, args) { install(root, {}); }
198
516
  function cmdStatus(root) { status(root); }
199
517
  function cmdConfigure(root, args) { configure(root, args); }
200
- function cmdUpgrade(root) { upgrade(root); }
518
+ function cmdUpgrade(root, args) { upgrade(root, args); }
519
+
520
+ async function cmdBootstrap(root, args) {
521
+ const options = {
522
+ locale: null,
523
+ interactive: true,
524
+ answers: {},
525
+ resume: false,
526
+ bootstrapMode: "auto",
527
+ technicalLevel: null,
528
+ projectState: null,
529
+ docsState: null,
530
+ decisionOwnership: null,
531
+ };
532
+ for (let i = 0; i < (args || []).length; i += 1) {
533
+ if (args[i] === "--locale" && args[i + 1]) {
534
+ options.locale = args[i + 1];
535
+ i += 1;
536
+ } else if (args[i] === "--non-interactive") {
537
+ options.interactive = false;
538
+ } else if (args[i] === "--resume") {
539
+ options.resume = true;
540
+ } else if (args[i] === "--bootstrap-mode" && args[i + 1]) {
541
+ options.bootstrapMode = args[i + 1];
542
+ i += 1;
543
+ } else if (args[i] === "--technical-level" && args[i + 1]) {
544
+ options.technicalLevel = args[i + 1];
545
+ i += 1;
546
+ } else if (args[i] === "--project-state" && args[i + 1]) {
547
+ options.projectState = args[i + 1];
548
+ i += 1;
549
+ } else if (args[i] === "--docs-state" && args[i + 1]) {
550
+ options.docsState = args[i + 1];
551
+ i += 1;
552
+ } else if (args[i] === "--decision-ownership" && args[i + 1]) {
553
+ options.decisionOwnership = args[i + 1];
554
+ i += 1;
555
+ }
556
+ }
557
+ return runBootstrap(root, options);
558
+ }
559
+
560
+ function cmdHandoff(root, args) {
561
+ const context = config.ensureContext(root);
562
+ const control = config.loadControl(context);
563
+ const state = bootstrap.getBootstrapState(control, context) || bootstrap.detectLegacyBootstrap(context, control);
564
+ if (!state) {
565
+ throw new Error("OPERA bootstrap is not initialized.");
566
+ }
567
+ const files = bootstrap.bootstrapFilePaths(context);
568
+ const printMode = (args || []).includes("--print");
569
+ const jsonMode = (args || []).includes("--json");
570
+ if (jsonMode) {
571
+ const payload = readText(files.json);
572
+ process.stdout.write(payload || "{}\n");
573
+ return;
574
+ }
575
+ if (printMode) {
576
+ process.stdout.write(readText(files.markdown) || "");
577
+ return;
578
+ }
579
+ console.log(`Bootstrap: ${state.status}`);
580
+ console.log(`Mode: ${state.mode}`);
581
+ console.log(`Markdown handoff: ${state.handoffFiles?.markdown || bootstrap.bootstrapRelativePaths(context).markdown}`);
582
+ console.log(`JSON handoff: ${state.handoffFiles?.json || bootstrap.bootstrapRelativePaths(context).json}`);
583
+ if (state.reviewFiles?.openQuestions) {
584
+ console.log(`Open questions: ${state.reviewFiles.openQuestions}`);
585
+ }
586
+ }
201
587
 
202
- module.exports = { install, status, configure, upgrade, cmdInstall, cmdStatus, cmdConfigure, cmdUpgrade };
588
+ module.exports = {
589
+ installStructure,
590
+ install,
591
+ runBootstrap,
592
+ status,
593
+ configure,
594
+ upgrade,
595
+ cmdInstall,
596
+ cmdStatus,
597
+ cmdConfigure,
598
+ cmdUpgrade,
599
+ cmdBootstrap,
600
+ cmdHandoff,
601
+ };