trackops 1.1.0 → 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 (51) hide show
  1. package/README.md +194 -230
  2. package/bin/trackops.js +54 -28
  3. package/lib/config.js +14 -10
  4. package/lib/control.js +44 -32
  5. package/lib/env.js +18 -1
  6. package/lib/init.js +40 -6
  7. package/lib/opera-bootstrap.js +825 -273
  8. package/lib/opera.js +360 -110
  9. package/lib/preferences.js +74 -0
  10. package/lib/runtime-state.js +144 -0
  11. package/lib/server.js +155 -25
  12. package/locales/en.json +136 -42
  13. package/locales/es.json +136 -42
  14. package/package.json +2 -1
  15. package/scripts/postinstall-locale.js +21 -0
  16. package/scripts/smoke-tests.js +130 -5
  17. package/scripts/validate-skill.js +2 -1
  18. package/skills/trackops/SKILL.md +57 -32
  19. package/skills/trackops/agents/openai.yaml +1 -1
  20. package/skills/trackops/references/activation.md +50 -16
  21. package/skills/trackops/references/troubleshooting.md +35 -20
  22. package/skills/trackops/references/workflow.md +18 -12
  23. package/skills/trackops/scripts/bootstrap-trackops.js +9 -7
  24. package/skills/trackops/skill.json +4 -4
  25. package/templates/opera/agent.md +10 -9
  26. package/templates/opera/architecture/dependency-graph.md +24 -0
  27. package/templates/opera/architecture/runtime-automation.md +24 -0
  28. package/templates/opera/architecture/runtime-operations.md +34 -0
  29. package/templates/opera/en/agent.md +21 -20
  30. package/templates/opera/en/architecture/dependency-graph.md +24 -0
  31. package/templates/opera/en/architecture/runtime-automation.md +24 -0
  32. package/templates/opera/en/architecture/runtime-operations.md +34 -0
  33. package/templates/opera/en/reviews/delivery-audit.md +18 -0
  34. package/templates/opera/en/reviews/integration-audit.md +18 -0
  35. package/templates/opera/en/router.md +19 -9
  36. package/templates/opera/reviews/delivery-audit.md +18 -0
  37. package/templates/opera/reviews/integration-audit.md +18 -0
  38. package/templates/opera/router.md +15 -5
  39. package/templates/skills/opera-contract-auditor/SKILL.md +38 -0
  40. package/templates/skills/opera-contract-auditor/locales/en/SKILL.md +38 -0
  41. package/templates/skills/opera-policy-guard/SKILL.md +26 -0
  42. package/templates/skills/opera-policy-guard/locales/en/SKILL.md +26 -0
  43. package/templates/skills/project-starter-skill/SKILL.md +89 -164
  44. package/templates/skills/project-starter-skill/locales/en/SKILL.md +104 -24
  45. package/ui/js/views/overview.js +16 -12
  46. package/templates/etapa/agent.md +0 -26
  47. package/templates/etapa/genesis.md +0 -94
  48. package/templates/etapa/references/autonomy-and-recovery.md +0 -117
  49. package/templates/etapa/references/etapa-cycle.md +0 -193
  50. package/templates/etapa/registry.md +0 -28
  51. package/templates/etapa/router.md +0 -39
package/lib/opera.js CHANGED
@@ -9,14 +9,20 @@ const { t, setLocale } = require("./i18n");
9
9
  const { promptForLocale, resolveLocale } = require("./locale");
10
10
  const { resolveLocalizedFile, resolveLocalizedDir, resolveSkillFile } = require("./resources");
11
11
  const bootstrap = require("./opera-bootstrap");
12
+ const runtimeState = require("./runtime-state");
12
13
 
13
14
  const TEMPLATES_DIR = path.join(__dirname, "..", "templates", "opera");
14
15
  const SKILLS_TEMPLATES_DIR = path.join(__dirname, "..", "templates", "skills");
15
16
  const OPERA_VERSION = require("../package.json").version;
17
+ const AUXILIARY_SKILLS = ["project-starter-skill", "opera-contract-auditor", "opera-policy-guard"];
16
18
 
17
- function nowIso() {
18
- return new Date().toISOString();
19
- }
19
+ function nowIso() {
20
+ return new Date().toISOString();
21
+ }
22
+
23
+ function formatLocaleSource(source) {
24
+ return t(`locale.source.${String(source || "").trim()}`) || source || t("locale.none");
25
+ }
20
26
 
21
27
  function readText(filePath) {
22
28
  return fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf8") : "";
@@ -47,23 +53,38 @@ function copyTemplate(templatePath, targetPath, replacements, options = {}) {
47
53
  return true;
48
54
  }
49
55
 
50
- function copyDirRecursive(src, dest) {
51
- if (!src || !fs.existsSync(src)) return;
52
- fs.mkdirSync(dest, { recursive: true });
53
- for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
54
- const srcPath = path.join(src, entry.name);
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);
55
61
  const destPath = path.join(dest, entry.name);
56
62
  if (entry.isDirectory()) {
57
63
  copyDirRecursive(srcPath, destPath);
58
64
  } else {
59
65
  fs.copyFileSync(srcPath, destPath);
60
- }
61
- }
62
- }
66
+ }
67
+ }
68
+ }
69
+
70
+ function seedDirRecursive(src, dest, options = {}) {
71
+ if (!src || !fs.existsSync(src)) return;
72
+ fs.mkdirSync(dest, { recursive: true });
73
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
74
+ const srcPath = path.join(src, entry.name);
75
+ const destPath = path.join(dest, entry.name);
76
+ if (entry.isDirectory()) {
77
+ seedDirRecursive(srcPath, destPath, options);
78
+ } else if (options.overwrite || !fs.existsSync(destPath)) {
79
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
80
+ fs.copyFileSync(srcPath, destPath);
81
+ }
82
+ }
83
+ }
63
84
 
64
- function resolveOperaLocale(control, options = {}) {
65
- return resolveLocale(options.locale, config.getLocale(control));
66
- }
85
+ function resolveOperaLocale(control, options = {}) {
86
+ return resolveLocale(options.locale, config.getLocale(control) || runtimeState.getGlobalLocale());
87
+ }
67
88
 
68
89
  function installStructure(root, control, locale, options = {}) {
69
90
  const context = config.ensureContext(root);
@@ -89,6 +110,11 @@ function installStructure(root, control, locale, options = {}) {
89
110
 
90
111
  const agentHubDir = context.paths.agentHubDir;
91
112
  fs.mkdirSync(agentHubDir, { recursive: true });
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 });
92
118
 
93
119
  copyTemplate(
94
120
  resolveLocalizedFile(TEMPLATES_DIR, locale, "agent.md"),
@@ -118,20 +144,36 @@ function installStructure(root, control, locale, options = {}) {
118
144
  copyTemplate(genesisTemplatePath, genesisPath, replacements, { overwrite: true });
119
145
  }
120
146
 
121
- const refsTemplateDir = resolveLocalizedDir(TEMPLATES_DIR, locale, "references");
147
+ const refsTemplateDir = resolveLocalizedDir(TEMPLATES_DIR, locale, "references");
122
148
  const refsTargetDir = path.join(context.paths.skillsDir, "project-starter-skill", "references");
123
- copyDirRecursive(refsTemplateDir, refsTargetDir);
124
-
125
- const starterSkillTarget = path.join(context.paths.skillsDir, "project-starter-skill", "SKILL.md");
126
- const starterSkillTemplate = resolveSkillFile(SKILLS_TEMPLATES_DIR, "project-starter-skill", locale);
127
- if (starterSkillTemplate) {
128
- fs.mkdirSync(path.dirname(starterSkillTarget), { recursive: true });
129
- if (!fs.existsSync(starterSkillTarget) || options.rewriteLocalizedTemplates === true) {
130
- fs.copyFileSync(starterSkillTemplate, starterSkillTarget);
131
- }
132
- }
133
- }
134
-
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
+ }
172
+ }
173
+
174
+ bootstrap.writeAutonomyPolicy(context);
175
+ }
176
+
135
177
  async function install(root, options = {}) {
136
178
  const context = config.ensureContext(root);
137
179
  const controlFile = config.controlFilePath(context);
@@ -141,9 +183,12 @@ async function install(root, options = {}) {
141
183
 
142
184
  const control = config.loadControl(context);
143
185
  let locale = resolveOperaLocale(control, options);
144
- if (!options.locale && !control.meta?.locale) {
145
- locale = await promptForLocale(locale);
146
- }
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
+ }
191
+ }
147
192
  control.meta.locale = locale;
148
193
  setLocale(locale);
149
194
 
@@ -151,12 +196,18 @@ async function install(root, options = {}) {
151
196
  installStructure(context, control, locale);
152
197
 
153
198
  control.meta.opera = {
154
- ...(control.meta.opera || {}),
155
- installed: true,
156
- version: OPERA_VERSION,
157
- installedAt: control.meta?.opera?.installedAt || nowIso(),
158
- skills: control.meta?.opera?.skills || [],
159
- };
199
+ ...(control.meta.opera || {}),
200
+ installed: true,
201
+ model: "v3",
202
+ stableTag: "stable",
203
+ version: OPERA_VERSION,
204
+ installedAt: control.meta?.opera?.installedAt || nowIso(),
205
+ skills: control.meta?.opera?.skills || [],
206
+ legacyStatus: "supported",
207
+ };
208
+ if (!control.meta.opera.bootstrap && options.bootstrap === false) {
209
+ control.meta.opera.bootstrap = bootstrap.createAwaitingBootstrapState(context);
210
+ }
160
211
  config.saveControl(context, control);
161
212
  env.syncEnvironment(context, control);
162
213
 
@@ -174,10 +225,53 @@ async function install(root, options = {}) {
174
225
  // ignore
175
226
  }
176
227
  }
228
+ skills.updateRegistry(context);
177
229
 
178
230
  if (options.bootstrap !== false) {
179
- await runBootstrap(context, { locale, answers: options.answers, interactive: options.interactive });
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
+ });
241
+ }
242
+ }
243
+
244
+ function removePath(targetPath) {
245
+ if (!targetPath || !fs.existsSync(targetPath)) return;
246
+ fs.rmSync(targetPath, { recursive: true, force: true });
247
+ }
248
+
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;
180
273
  }
274
+ return { backupRoot, copied };
181
275
  }
182
276
 
183
277
  async function runBootstrap(root, options = {}) {
@@ -194,17 +288,37 @@ async function runBootstrap(root, options = {}) {
194
288
  config.saveControl(context, control);
195
289
  }
196
290
 
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
+ }
305
+
197
306
  const profile = await bootstrap.collectBootstrapProfile(context, control, options);
198
307
  const updatedControl = bootstrap.applyBootstrap(context, control, profile);
199
308
  env.syncEnvironment(context, updatedControl, { requiredKeys: env.inferRequiredKeys(updatedControl, context) });
200
309
  const ops = require("./control");
201
310
  ops.syncDocs(context, updatedControl);
202
311
  ops.refreshRepoRuntime(context, { quiet: true });
203
-
204
- console.log(profile.status === "completed" ? t("bootstrap.completed") : t("bootstrap.pending"));
205
- return profile;
206
- }
207
-
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"));
318
+ }
319
+ return profile;
320
+ }
321
+
208
322
  function status(root) {
209
323
  const context = config.ensureContext(root);
210
324
  const control = config.loadControl(context);
@@ -214,30 +328,51 @@ function status(root) {
214
328
  console.log(t("opera.notInstalled"));
215
329
  return;
216
330
  }
217
-
218
- const opera = control.meta.opera;
331
+
332
+ const opera = control.meta.opera;
219
333
  const bootstrapState = opera.bootstrap || bootstrap.detectLegacyBootstrap(context, control);
220
- console.log(`OPERA v${opera.version}`);
221
- console.log(` Installed: ${opera.installedAt}`);
222
- console.log(` Skills: ${(opera.skills || []).join(", ") || "none"}`);
223
- console.log(` Locale: ${config.getLocale(control)}`);
224
-
225
- if (bootstrapState) {
226
- console.log(` Bootstrap: ${bootstrapState.status}`);
227
- if ((bootstrapState.missingFields || []).length) {
228
- console.log(` Missing: ${bootstrapState.missingFields.join(", ")}`);
229
- console.log(" Resume: trackops opera bootstrap --resume");
230
- }
231
- }
232
-
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 }));
363
+ }
364
+ }
365
+
233
366
  const checks = [
234
- [".agent/hub/agent.md", fs.existsSync(path.join(context.paths.agentHubDir, "agent.md"))],
235
- [".agent/hub/router.md", fs.existsSync(path.join(context.paths.agentHubDir, "router.md"))],
236
- [".agents/skills/_registry.md", fs.existsSync(context.paths.registryPath)],
237
- ["genesis.md", fs.existsSync(context.paths.genesisFile)],
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)],
238
373
  ];
239
374
 
240
- console.log(" Structure:");
375
+ console.log(t("opera.status.structure"));
241
376
  for (const [file, exists] of checks) {
242
377
  console.log(` ${exists ? "\u2705" : "\u274C"} ${file}`);
243
378
  }
@@ -255,13 +390,13 @@ function configure(root, args) {
255
390
  control.meta.locale = nextLocale;
256
391
  i += 1;
257
392
  }
258
- if (args[i] === "--phases" && args[i + 1]) {
259
- try {
260
- control.meta.phases = JSON.parse(args[i + 1]);
261
- } catch (_e) {
262
- console.error("Invalid phases JSON.");
263
- return;
264
- }
393
+ if (args[i] === "--phases" && args[i + 1]) {
394
+ try {
395
+ control.meta.phases = JSON.parse(args[i + 1]);
396
+ } catch (_e) {
397
+ console.error(t("opera.configure.invalidPhases"));
398
+ return;
399
+ }
265
400
  i += 1;
266
401
  }
267
402
  }
@@ -273,34 +408,82 @@ function configure(root, args) {
273
408
  const ops = require("./control");
274
409
  ops.syncDocs(context, control);
275
410
  }
276
- console.log("Configuration updated.");
411
+ console.log(t("opera.configure.updated"));
277
412
  }
278
413
 
279
- function upgrade(root) {
414
+ function upgrade(root, args = []) {
280
415
  const context = config.ensureContext(root);
281
416
  const control = config.loadControl(context);
282
417
  const locale = config.getLocale(control);
283
- setLocale(locale);
284
-
285
- if (!config.isOperaInstalled(control)) {
286
- console.log(t("opera.notInstalled"));
287
- console.log("Run 'trackops opera install' first.");
288
- return;
289
- }
290
-
418
+ setLocale(locale);
419
+ const wantsStable = (args || []).includes("--stable");
420
+ const wantsReset = (args || []).includes("--reset");
421
+
422
+ if (!config.isOperaInstalled(control)) {
423
+ console.log(t("opera.notInstalled"));
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"));
440
+ return;
441
+ }
442
+
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);
451
+ }
452
+
291
453
  installStructure(context, control, locale, { rewriteLocalizedTemplates: true });
292
- control.meta.opera.version = OPERA_VERSION;
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
+ };
293
469
  config.saveControl(context, control);
294
470
  env.syncEnvironment(context, control);
471
+ require("./skills").updateRegistry(context);
472
+ console.log(t("opera.upgrade.backup", { path: path.relative(context.workspaceRoot, backup.backupRoot) }));
295
473
  console.log(t("opera.upgraded", { version: OPERA_VERSION }));
296
474
  }
297
-
475
+
298
476
  function cmdInstall(root, args) {
299
477
  const options = {
300
478
  bootstrap: true,
301
479
  answers: {},
302
480
  interactive: true,
303
481
  locale: null,
482
+ bootstrapMode: "auto",
483
+ technicalLevel: null,
484
+ projectState: null,
485
+ docsState: null,
486
+ decisionOwnership: null,
304
487
  };
305
488
  for (let i = 0; i < (args || []).length; i += 1) {
306
489
  if (args[i] === "--locale" && args[i + 1]) {
@@ -310,42 +493,109 @@ function cmdInstall(root, args) {
310
493
  options.bootstrap = false;
311
494
  } else if (args[i] === "--non-interactive") {
312
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;
313
511
  }
314
512
  }
315
513
  return install(root, options);
316
514
  }
515
+
516
+ function cmdStatus(root) { status(root); }
517
+ function cmdConfigure(root, args) { configure(root, args); }
518
+ function cmdUpgrade(root, args) { upgrade(root, args); }
317
519
 
318
- function cmdStatus(root) { status(root); }
319
- function cmdConfigure(root, args) { configure(root, args); }
320
- function cmdUpgrade(root) { upgrade(root); }
321
-
322
- async function cmdBootstrap(root, args) {
323
- const options = { locale: null, interactive: true, answers: {} };
324
- for (let i = 0; i < (args || []).length; i += 1) {
325
- if (args[i] === "--locale" && args[i + 1]) {
326
- options.locale = args[i + 1];
327
- i += 1;
328
- } else if (args[i] === "--non-interactive") {
329
- options.interactive = false;
330
- } else if (args[i] === "--skip-repo-tasks") {
331
- options.answers.repoTaskPolicy = "skip";
332
- } else if (args[i] === "--include-repo-tasks") {
333
- options.answers.repoTaskPolicy = "optional_pending";
334
- }
335
- }
336
- return runBootstrap(root, options);
337
- }
338
-
339
- module.exports = {
340
- installStructure,
341
- install,
342
- runBootstrap,
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
+ }
587
+
588
+ module.exports = {
589
+ installStructure,
590
+ install,
591
+ runBootstrap,
343
592
  status,
344
593
  configure,
345
594
  upgrade,
346
595
  cmdInstall,
347
596
  cmdStatus,
348
- cmdConfigure,
349
- cmdUpgrade,
350
- cmdBootstrap,
351
- };
597
+ cmdConfigure,
598
+ cmdUpgrade,
599
+ cmdBootstrap,
600
+ cmdHandoff,
601
+ };
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env node
2
+
3
+ const config = require("./config");
4
+ const runtimeState = require("./runtime-state");
5
+ const { setLocale, t } = require("./i18n");
6
+ const { normalizeLocale } = require("./locale");
7
+
8
+ function formatLocaleSource(source) {
9
+ return t(`locale.source.${String(source || "").trim()}`) || source || t("locale.none");
10
+ }
11
+
12
+ function resolveProjectLocale(root) {
13
+ const context = config.resolveWorkspaceContext(root || process.cwd());
14
+ if (!context) return null;
15
+ try {
16
+ return config.getLocale(config.loadControl(context));
17
+ } catch (_error) {
18
+ return null;
19
+ }
20
+ }
21
+
22
+ function cmdLocale(args = [], root) {
23
+ const sub = args[0];
24
+ const projectLocale = resolveProjectLocale(root);
25
+ const doctor = runtimeState.doctorLocale(projectLocale);
26
+ setLocale(doctor.effectiveLocale);
27
+
28
+ if (sub === "get" || !sub) {
29
+ console.log(`${t("locale.effective")}: ${doctor.effectiveLocale}`);
30
+ console.log(`${t("locale.source")}: ${formatLocaleSource(doctor.source)}`);
31
+ console.log(`${t("locale.global")}: ${doctor.globalLocale || t("locale.none")}`);
32
+ if (doctor.projectLocale) {
33
+ console.log(`${t("locale.project")}: ${doctor.projectLocale}`);
34
+ }
35
+ return;
36
+ }
37
+
38
+ if (sub === "set") {
39
+ const nextLocale = normalizeLocale(args[1]);
40
+ if (!nextLocale) {
41
+ throw new Error(t("locale.invalid", { value: String(args[1] || "") }));
42
+ }
43
+ runtimeState.writeRuntimeState({ locale: nextLocale, localeSource: "manual" });
44
+ setLocale(nextLocale);
45
+ console.log(t("locale.updated", { locale: nextLocale }));
46
+ return;
47
+ }
48
+
49
+ console.log(t("cli.usage.locale"));
50
+ }
51
+
52
+ function cmdDoctor(args = [], root) {
53
+ const sub = args[0];
54
+ if (sub !== "locale") {
55
+ console.log(t("cli.usage.doctor"));
56
+ return;
57
+ }
58
+
59
+ const projectLocale = resolveProjectLocale(root);
60
+ const doctor = runtimeState.doctorLocale(projectLocale);
61
+ setLocale(doctor.effectiveLocale);
62
+ console.log(`${t("locale.effective")}: ${doctor.effectiveLocale}`);
63
+ console.log(`${t("locale.source")}: ${formatLocaleSource(doctor.source)}`);
64
+ console.log(`${t("locale.global")}: ${doctor.globalLocale || t("locale.none")}`);
65
+ console.log(`${t("locale.project")}: ${doctor.projectLocale || t("locale.none")}`);
66
+ console.log(`${t("locale.env")}: ${doctor.envLocale || t("locale.none")}`);
67
+ console.log(`${t("locale.system")}: ${doctor.systemLocale}`);
68
+ console.log(`${t("locale.runtimeFile")}: ${doctor.runtimeFile}`);
69
+ }
70
+
71
+ module.exports = {
72
+ cmdLocale,
73
+ cmdDoctor,
74
+ };