trackops 2.0.4 → 2.0.6

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 (92) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +660 -575
  3. package/bin/trackops.js +127 -106
  4. package/lib/cli-format.js +118 -0
  5. package/lib/config.js +352 -326
  6. package/lib/control.js +408 -246
  7. package/lib/env.js +234 -222
  8. package/lib/i18n.js +5 -4
  9. package/lib/init.js +390 -282
  10. package/lib/locale.js +41 -41
  11. package/lib/opera-bootstrap.js +1066 -880
  12. package/lib/opera.js +615 -444
  13. package/lib/preferences.js +74 -74
  14. package/lib/registry.js +214 -214
  15. package/lib/release.js +56 -56
  16. package/lib/runtime-state.js +144 -144
  17. package/lib/skills.js +114 -89
  18. package/lib/workspace.js +259 -248
  19. package/locales/en.json +311 -167
  20. package/locales/es.json +314 -170
  21. package/package.json +61 -58
  22. package/scripts/postinstall-locale.js +21 -21
  23. package/scripts/skills-marketplace-smoke.js +124 -124
  24. package/scripts/smoke-tests.js +563 -517
  25. package/scripts/sync-skill-version.js +21 -21
  26. package/scripts/validate-skill.js +103 -103
  27. package/skills/trackops/SKILL.md +126 -122
  28. package/skills/trackops/agents/openai.yaml +7 -7
  29. package/skills/trackops/locales/en/SKILL.md +126 -122
  30. package/skills/trackops/locales/en/references/activation.md +94 -90
  31. package/skills/trackops/locales/en/references/troubleshooting.md +73 -67
  32. package/skills/trackops/locales/en/references/workflow.md +55 -32
  33. package/skills/trackops/references/activation.md +94 -90
  34. package/skills/trackops/references/troubleshooting.md +73 -67
  35. package/skills/trackops/references/workflow.md +55 -32
  36. package/skills/trackops/skill.json +29 -29
  37. package/templates/hooks/post-checkout +2 -2
  38. package/templates/hooks/post-commit +2 -2
  39. package/templates/hooks/post-merge +2 -2
  40. package/templates/opera/agent.md +28 -27
  41. package/templates/opera/architecture/dependency-graph.md +24 -24
  42. package/templates/opera/architecture/runtime-automation.md +24 -24
  43. package/templates/opera/architecture/runtime-operations.md +34 -34
  44. package/templates/opera/en/agent.md +22 -21
  45. package/templates/opera/en/architecture/dependency-graph.md +24 -24
  46. package/templates/opera/en/architecture/runtime-automation.md +24 -24
  47. package/templates/opera/en/architecture/runtime-operations.md +34 -34
  48. package/templates/opera/en/reviews/delivery-audit.md +18 -18
  49. package/templates/opera/en/reviews/integration-audit.md +18 -18
  50. package/templates/opera/en/router.md +24 -19
  51. package/templates/opera/references/autonomy-and-recovery.md +117 -117
  52. package/templates/opera/references/opera-cycle.md +193 -193
  53. package/templates/opera/registry.md +28 -28
  54. package/templates/opera/reviews/delivery-audit.md +18 -18
  55. package/templates/opera/reviews/integration-audit.md +18 -18
  56. package/templates/opera/router.md +54 -49
  57. package/templates/skills/changelog-updater/SKILL.md +69 -69
  58. package/templates/skills/commiter/SKILL.md +99 -99
  59. package/templates/skills/opera-contract-auditor/SKILL.md +38 -38
  60. package/templates/skills/opera-contract-auditor/locales/en/SKILL.md +38 -38
  61. package/templates/skills/opera-policy-guard/SKILL.md +26 -26
  62. package/templates/skills/opera-policy-guard/locales/en/SKILL.md +26 -26
  63. package/templates/skills/opera-skill/SKILL.md +279 -0
  64. package/templates/skills/opera-skill/locales/en/SKILL.md +279 -0
  65. package/templates/skills/opera-skill/locales/en/references/phase-dod.md +138 -0
  66. package/templates/skills/opera-skill/references/phase-dod.md +138 -0
  67. package/templates/skills/project-starter-skill/SKILL.md +150 -131
  68. package/templates/skills/project-starter-skill/locales/en/SKILL.md +143 -105
  69. package/templates/skills/project-starter-skill/references/opera-cycle.md +195 -193
  70. package/ui/css/base.css +284 -284
  71. package/ui/css/charts.css +425 -425
  72. package/ui/css/components.css +1107 -1107
  73. package/ui/css/onboarding.css +133 -133
  74. package/ui/css/terminal.css +125 -125
  75. package/ui/css/timeline.css +58 -58
  76. package/ui/css/tokens.css +284 -284
  77. package/ui/favicon.svg +5 -5
  78. package/ui/index.html +99 -99
  79. package/ui/js/charts.js +526 -526
  80. package/ui/js/console-logger.js +172 -172
  81. package/ui/js/filters.js +247 -247
  82. package/ui/js/icons.js +129 -129
  83. package/ui/js/keyboard.js +229 -229
  84. package/ui/js/router.js +142 -142
  85. package/ui/js/theme.js +100 -100
  86. package/ui/js/time-tracker.js +248 -248
  87. package/ui/js/views/dashboard.js +870 -870
  88. package/ui/js/views/flash.js +47 -47
  89. package/ui/js/views/projects.js +745 -745
  90. package/ui/js/views/scrum.js +476 -476
  91. package/ui/js/views/settings.js +331 -331
  92. package/ui/js/views/timeline.js +265 -265
package/lib/opera.js CHANGED
@@ -3,26 +3,59 @@
3
3
  const fs = require("fs");
4
4
  const path = require("path");
5
5
 
6
- const config = require("./config");
7
- const env = require("./env");
8
- const { t, setLocale } = require("./i18n");
9
- const { promptForLocale, maybePromptForLocale, resolveLocale } = require("./locale");
10
- const { resolveLocalizedFile, resolveLocalizedDir, resolveSkillFile } = require("./resources");
11
- const bootstrap = require("./opera-bootstrap");
12
- const runtimeState = require("./runtime-state");
13
-
14
- const TEMPLATES_DIR = path.join(__dirname, "..", "templates", "opera");
15
- const SKILLS_TEMPLATES_DIR = path.join(__dirname, "..", "templates", "skills");
16
- const OPERA_VERSION = require("../package.json").version;
17
- const AUXILIARY_SKILLS = ["project-starter-skill", "opera-contract-auditor", "opera-policy-guard"];
6
+ const config = require("./config");
7
+ const env = require("./env");
8
+ const { t, setLocale } = require("./i18n");
9
+ const { promptForLocale, maybePromptForLocale, resolveLocale } = require("./locale");
10
+ const { resolveLocalizedFile, resolveLocalizedDir, resolveSkillFile } = require("./resources");
11
+ const bootstrap = require("./opera-bootstrap");
12
+ const runtimeState = require("./runtime-state");
13
+ const fmt = require("./cli-format");
14
+
15
+ const TEMPLATES_DIR = path.join(__dirname, "..", "templates", "opera");
16
+ const SKILLS_TEMPLATES_DIR = path.join(__dirname, "..", "templates", "skills");
17
+ const OPERA_VERSION = require("../package.json").version;
18
+ const AUXILIARY_SKILLS = ["opera-skill", "project-starter-skill", "opera-contract-auditor", "opera-policy-guard"];
19
+
20
+ function nowIso() {
21
+ return new Date().toISOString();
22
+ }
18
23
 
19
- function nowIso() {
20
- return new Date().toISOString();
21
- }
22
-
23
24
  function formatLocaleSource(source) {
24
25
  return t(`locale.source.${String(source || "").trim()}`) || source || t("locale.none");
25
26
  }
27
+
28
+ function formatBootstrapStatus(status) {
29
+ return t(`bootstrap.status.${String(status || "").trim()}`) || status || t("locale.none");
30
+ }
31
+
32
+ function formatBootstrapMode(mode) {
33
+ return t(`bootstrap.mode.${String(mode || "").trim()}`) || mode || t("locale.none");
34
+ }
35
+
36
+ function formatBootstrapReason(reason) {
37
+ return t(`bootstrap.reason.${String(reason || "").trim()}`) || reason || t("locale.none");
38
+ }
39
+
40
+ function formatDecisionOwnership(value) {
41
+ return t(`bootstrap.ownership.${String(value || "").trim()}`) || value || t("locale.none");
42
+ }
43
+
44
+ function formatContractReadiness(value) {
45
+ return t(`bootstrap.readiness.${String(value || "").trim()}`) || value || t("locale.none");
46
+ }
47
+
48
+ function formatLegacyStatus(value) {
49
+ return t(`bootstrap.legacy.${String(value || "").trim()}`) || value || t("locale.none");
50
+ }
51
+
52
+ function formatMissingFields(fields) {
53
+ return (fields || []).map((field) => t(`bootstrap.field.${field}`) || field).join(", ");
54
+ }
55
+
56
+ function relativePathExists(context, relativePath) {
57
+ return Boolean(relativePath) && fs.existsSync(path.join(context.workspaceRoot, relativePath));
58
+ }
26
59
 
27
60
  function readText(filePath) {
28
61
  return fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf8") : "";
@@ -53,42 +86,61 @@ function copyTemplate(templatePath, targetPath, replacements, options = {}) {
53
86
  return true;
54
87
  }
55
88
 
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);
89
+ function copyDirRecursive(src, dest) {
90
+ if (!src || !fs.existsSync(src)) return;
91
+ fs.mkdirSync(dest, { recursive: true });
92
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
93
+ const srcPath = path.join(src, entry.name);
61
94
  const destPath = path.join(dest, entry.name);
62
95
  if (entry.isDirectory()) {
63
96
  copyDirRecursive(srcPath, destPath);
64
97
  } else {
65
98
  fs.copyFileSync(srcPath, destPath);
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
- }
99
+ }
100
+ }
101
+ }
84
102
 
85
- function resolveOperaLocale(control, options = {}) {
86
- return resolveLocale(options.locale, config.getLocale(control) || runtimeState.getGlobalLocale());
87
- }
103
+ function seedDirRecursive(src, dest, options = {}) {
104
+ if (!src || !fs.existsSync(src)) return;
105
+ fs.mkdirSync(dest, { recursive: true });
106
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
107
+ const srcPath = path.join(src, entry.name);
108
+ const destPath = path.join(dest, entry.name);
109
+ if (entry.isDirectory()) {
110
+ seedDirRecursive(srcPath, destPath, options);
111
+ } else if (options.overwrite || !fs.existsSync(destPath)) {
112
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
113
+ fs.copyFileSync(srcPath, destPath);
114
+ }
115
+ }
116
+ }
117
+
118
+ function resolveOperaLocale(control, options = {}) {
119
+ return resolveLocale(options.locale, config.getLocale(control) || runtimeState.getGlobalLocale());
120
+ }
121
+
122
+ function seedAuxiliarySkill(skillName, locale, skillsDir, options = {}) {
123
+ const templateDir = path.join(SKILLS_TEMPLATES_DIR, skillName);
124
+ if (!fs.existsSync(templateDir)) {
125
+ process.stderr.write(`[opera] Warning: skill template not found: ${skillName} (${templateDir})\n`);
126
+ return;
127
+ }
128
+ const targetDir = path.join(skillsDir, skillName);
129
+ seedDirRecursive(templateDir, targetDir, {
130
+ overwrite: options.rewriteLocalizedTemplates === true,
131
+ });
132
+ const localizedSkill = resolveSkillFile(SKILLS_TEMPLATES_DIR, skillName, locale);
133
+ if (localizedSkill) {
134
+ const targetSkill = path.join(targetDir, "SKILL.md");
135
+ if (!fs.existsSync(targetSkill) || options.rewriteLocalizedTemplates === true) {
136
+ fs.copyFileSync(localizedSkill, targetSkill);
137
+ }
138
+ }
139
+ }
88
140
 
89
- function installStructure(root, control, locale, options = {}) {
90
- const context = config.ensureContext(root);
91
- const projectName = control.meta.projectName || "Project";
141
+ function installStructure(root, control, locale, options = {}) {
142
+ const context = config.ensureContext(root);
143
+ const projectName = control.meta.projectName || "Project";
92
144
  const replacements = {
93
145
  PROJECT_NAME: projectName,
94
146
  DESIRED_OUTCOME: t("bootstrap.pendingValue"),
@@ -108,13 +160,13 @@ function installStructure(root, control, locale, options = {}) {
108
160
  overwriteIfTemplate: options.rewriteLocalizedTemplates === true,
109
161
  };
110
162
 
111
- const agentHubDir = context.paths.agentHubDir;
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 });
163
+ const agentHubDir = context.paths.agentHubDir;
164
+ fs.mkdirSync(agentHubDir, { recursive: true });
165
+ fs.mkdirSync(context.paths.architectureDir, { recursive: true });
166
+ fs.mkdirSync(context.paths.contractDir, { recursive: true });
167
+ fs.mkdirSync(context.paths.policyDir, { recursive: true });
168
+ fs.mkdirSync(context.paths.bootstrapDir, { recursive: true });
169
+ fs.mkdirSync(context.paths.reviewsDir, { recursive: true });
118
170
 
119
171
  copyTemplate(
120
172
  resolveLocalizedFile(TEMPLATES_DIR, locale, "agent.md"),
@@ -129,8 +181,8 @@ function installStructure(root, control, locale, options = {}) {
129
181
  overwriteOptions,
130
182
  );
131
183
 
132
- const skillsRegistryDir = context.paths.skillsDir;
133
- fs.mkdirSync(skillsRegistryDir, { recursive: true });
184
+ const skillsRegistryDir = context.paths.skillsDir;
185
+ fs.mkdirSync(skillsRegistryDir, { recursive: true });
134
186
  copyTemplate(
135
187
  resolveLocalizedFile(TEMPLATES_DIR, locale, "registry.md"),
136
188
  path.join(skillsRegistryDir, "_registry.md"),
@@ -139,204 +191,259 @@ function installStructure(root, control, locale, options = {}) {
139
191
  );
140
192
 
141
193
  const genesisTemplatePath = resolveLocalizedFile(TEMPLATES_DIR, locale, "genesis.md");
142
- const genesisPath = context.paths.genesisFile;
194
+ const genesisPath = context.paths.genesisFile;
143
195
  if (!fs.existsSync(genesisPath) || bootstrap.isVirginGenesis(readText(genesisPath))) {
144
196
  copyTemplate(genesisTemplatePath, genesisPath, replacements, { overwrite: true });
145
197
  }
146
198
 
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
- }
172
- }
173
-
174
- bootstrap.writeAutonomyPolicy(context);
175
- }
176
-
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.");
182
- }
183
-
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
- }
191
- } else if (!options.locale) {
192
- locale = await maybePromptForLocale(locale, { promptMode: "always" });
193
- }
194
- control.meta.locale = locale;
195
- setLocale(locale);
196
-
197
- const alreadyInstalled = config.isOperaInstalled(control);
198
- installStructure(context, control, locale);
199
-
200
- control.meta.opera = {
201
- ...(control.meta.opera || {}),
202
- installed: true,
203
- model: "v3",
204
- stableTag: "stable",
205
- version: OPERA_VERSION,
206
- installedAt: control.meta?.opera?.installedAt || nowIso(),
207
- skills: control.meta?.opera?.skills || [],
208
- legacyStatus: "supported",
209
- };
210
- if (!control.meta.opera.bootstrap && options.bootstrap === false) {
211
- control.meta.opera.bootstrap = bootstrap.createAwaitingBootstrapState(context);
212
- }
213
- config.saveControl(context, control);
214
- env.syncEnvironment(context, control);
199
+ const refsTemplateDir = resolveLocalizedDir(TEMPLATES_DIR, locale, "references");
200
+ const refsTargetDir = path.join(context.paths.skillsDir, "project-starter-skill", "references");
201
+ seedDirRecursive(refsTemplateDir, refsTargetDir, {
202
+ overwrite: options.rewriteLocalizedTemplates === true,
203
+ });
204
+
205
+ const architectureTemplateDir = resolveLocalizedDir(TEMPLATES_DIR, locale, "architecture");
206
+ seedDirRecursive(architectureTemplateDir, context.paths.architectureDir, {
207
+ overwrite: options.rewriteLocalizedTemplates === true,
208
+ });
209
+
210
+ const reviewsTemplateDir = resolveLocalizedDir(TEMPLATES_DIR, locale, "reviews");
211
+ seedDirRecursive(reviewsTemplateDir, context.paths.reviewsDir, {
212
+ overwrite: options.rewriteLocalizedTemplates === true,
213
+ });
214
+
215
+ for (const skillName of AUXILIARY_SKILLS) {
216
+ seedAuxiliarySkill(skillName, locale, context.paths.skillsDir, options);
217
+ }
218
+
219
+ bootstrap.writeAutonomyPolicy(context);
220
+ }
221
+
222
+ async function install(root, options = {}) {
223
+ const context = config.ensureContext(root);
224
+ const controlFile = config.controlFilePath(context);
225
+ if (!fs.existsSync(controlFile)) {
226
+ throw new Error("project_control.json not found. Run 'trackops init' first.");
227
+ }
215
228
 
229
+ const control = config.loadControl(context);
230
+ let locale = resolveOperaLocale(control, options);
231
+ if (!options.locale && !control.meta?.locale && !runtimeState.getGlobalLocale()) {
232
+ locale = await promptForLocale(locale);
233
+ if (!runtimeState.getGlobalLocale()) {
234
+ await runtimeState.ensureGlobalLocale({ preferredLocale: locale, interactive: false });
235
+ }
236
+ } else if (!options.locale) {
237
+ locale = await maybePromptForLocale(locale, { promptMode: "always" });
238
+ }
239
+ control.meta.locale = locale;
240
+ setLocale(locale);
241
+
242
+ const alreadyInstalled = config.isOperaInstalled(control);
243
+ installStructure(context, control, locale);
244
+
245
+ control.meta.opera = {
246
+ ...(control.meta.opera || {}),
247
+ installed: true,
248
+ model: "v3",
249
+ stableTag: "stable",
250
+ version: OPERA_VERSION,
251
+ installedAt: control.meta?.opera?.installedAt || nowIso(),
252
+ skills: control.meta?.opera?.skills || [],
253
+ legacyStatus: "supported",
254
+ };
255
+ if (!control.meta.opera.bootstrap && options.bootstrap === false) {
256
+ control.meta.opera.bootstrap = bootstrap.createAwaitingBootstrapState(context);
257
+ }
258
+ config.saveControl(context, control);
259
+ env.syncEnvironment(context, control);
260
+
261
+ fmt.blank();
216
262
  if (!alreadyInstalled) {
217
- console.log(t("opera.installed", { version: OPERA_VERSION }));
263
+ fmt.success(t("opera.installed", { version: OPERA_VERSION }));
218
264
  } else {
219
- console.log(t("opera.alreadyInstalled", { version: config.getOperaVersion(control) || OPERA_VERSION }));
265
+ fmt.info(t("opera.alreadyInstalled", { version: config.getOperaVersion(control) || OPERA_VERSION }));
220
266
  }
221
267
 
222
- const skills = require("./skills");
223
- for (const skillName of ["commiter", "changelog-updater"]) {
224
- try {
225
- skills.installSkill(context, skillName, { locale });
226
- } catch (_error) {
227
- // ignore
228
- }
229
- }
230
- skills.updateRegistry(context);
231
-
232
- if (options.bootstrap !== false) {
233
- await runBootstrap(context, {
234
- locale,
235
- answers: options.answers,
236
- interactive: options.interactive,
237
- bootstrapMode: options.bootstrapMode,
238
- technicalLevel: options.technicalLevel,
239
- projectState: options.projectState,
240
- docsState: options.docsState,
241
- decisionOwnership: options.decisionOwnership,
268
+ const skills = require("./skills");
269
+ for (const skillName of ["commiter", "changelog-updater"]) {
270
+ try {
271
+ skills.installSkill(context, skillName, { locale });
272
+ } catch (error) {
273
+ fmt.warn(t("skill.installError", { name: skillName, error: error.message }));
274
+ }
275
+ }
276
+ skills.updateRegistry(context);
277
+
278
+ if (options.bootstrap !== false && options.interactive !== false) {
279
+ const readline = require("readline");
280
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
281
+ const answer = await new Promise((resolve) => {
282
+ rl.question(` ${t("opera.install.bootstrapConfirm")} `, (ans) => { rl.close(); resolve(ans); });
242
283
  });
243
- }
244
- }
245
-
246
- function removePath(targetPath) {
247
- if (!targetPath || !fs.existsSync(targetPath)) return;
248
- fs.rmSync(targetPath, { recursive: true, force: true });
249
- }
250
-
251
- function backupManagedArtifacts(context) {
252
- const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
253
- const backupRoot = path.join(context.paths.tmpDir, "upgrade-backups", timestamp);
254
- const items = [
255
- context.paths.agentHubDir,
256
- context.paths.skillsDir,
257
- context.paths.genesisFile,
258
- context.paths.contractFile,
259
- context.paths.autonomyPolicyFile,
260
- context.paths.bootstrapDir,
261
- ];
262
- fs.mkdirSync(backupRoot, { recursive: true });
263
- let copied = 0;
264
- for (const item of items) {
265
- if (!fs.existsSync(item)) continue;
266
- const relative = path.relative(context.workspaceRoot, item);
267
- const destination = path.join(backupRoot, relative);
268
- if (fs.statSync(item).isDirectory()) {
269
- copyDirRecursive(item, destination);
270
- } else {
271
- fs.mkdirSync(path.dirname(destination), { recursive: true });
272
- fs.copyFileSync(item, destination);
284
+ const normalizedAnswer = String(answer || "").trim().toLowerCase();
285
+ if (!["y", "yes", "s", "si"].includes(normalizedAnswer)) {
286
+ fmt.info(t("opera.install.bootstrapSkipped"));
287
+ fmt.blank();
288
+ return;
273
289
  }
274
- copied += 1;
275
290
  }
276
- return { backupRoot, copied };
277
- }
278
-
279
- async function runBootstrap(root, options = {}) {
280
- const context = config.ensureContext(root);
281
- const control = config.loadControl(context);
282
- const locale = resolveOperaLocale(control, options);
291
+ if (options.bootstrap !== false) {
292
+ await runBootstrap(context, {
293
+ locale,
294
+ answers: options.answers,
295
+ interactive: options.interactive,
296
+ bootstrapMode: options.bootstrapMode,
297
+ technicalLevel: options.technicalLevel,
298
+ projectState: options.projectState,
299
+ docsState: options.docsState,
300
+ decisionOwnership: options.decisionOwnership,
301
+ });
302
+ }
303
+ }
304
+
305
+ function removePath(targetPath) {
306
+ if (!targetPath || !fs.existsSync(targetPath)) return;
307
+ fs.rmSync(targetPath, { recursive: true, force: true });
308
+ }
309
+
310
+ function backupManagedArtifacts(context) {
311
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
312
+ const backupRoot = path.join(context.paths.tmpDir, "upgrade-backups", timestamp);
313
+ const items = [
314
+ context.paths.agentHubDir,
315
+ context.paths.skillsDir,
316
+ context.paths.genesisFile,
317
+ context.paths.contractFile,
318
+ context.paths.autonomyPolicyFile,
319
+ context.paths.bootstrapDir,
320
+ ];
321
+ fs.mkdirSync(backupRoot, { recursive: true });
322
+ let copied = 0;
323
+ for (const item of items) {
324
+ if (!fs.existsSync(item)) continue;
325
+ const relative = path.relative(context.workspaceRoot, item);
326
+ const destination = path.join(backupRoot, relative);
327
+ if (fs.statSync(item).isDirectory()) {
328
+ copyDirRecursive(item, destination);
329
+ } else {
330
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
331
+ fs.copyFileSync(item, destination);
332
+ }
333
+ copied += 1;
334
+ }
335
+ return { backupRoot, copied };
336
+ }
337
+
338
+ async function runBootstrap(root, options = {}) {
339
+ const context = config.ensureContext(root);
340
+ const control = config.loadControl(context);
341
+ const locale = resolveOperaLocale(control, options);
283
342
  control.meta.locale = locale;
284
343
  setLocale(locale);
285
344
 
286
- const legacyBootstrap = bootstrap.detectLegacyBootstrap(context, control);
287
- if (legacyBootstrap && !control.meta.opera?.bootstrap) {
288
- control.meta.opera = control.meta.opera || {};
289
- control.meta.opera.bootstrap = legacyBootstrap;
290
- config.saveControl(context, control);
291
- }
292
-
293
- if ((options.resume || options.forceResume) && control.meta?.opera?.bootstrap) {
294
- const resumed = bootstrap.resumeBootstrap(context, control);
295
- if (resumed.resumed) {
296
- const updatedControl = bootstrap.applyBootstrap(context, control, resumed.profile);
297
- env.syncEnvironment(context, updatedControl, { requiredKeys: env.inferRequiredKeys(updatedControl, context) });
298
- const ops = require("./control");
299
- ops.syncDocs(context, updatedControl);
300
- ops.refreshRepoRuntime(context, { quiet: true });
301
- console.log(t("bootstrap.completed"));
302
- return resumed.profile;
303
- }
304
- console.log(t("bootstrap.awaitingAgent"));
305
- return control.meta.opera.bootstrap;
306
- }
307
-
345
+ const legacyBootstrap = bootstrap.detectLegacyBootstrap(context, control);
346
+ if (legacyBootstrap && !control.meta.opera?.bootstrap) {
347
+ control.meta.opera = control.meta.opera || {};
348
+ control.meta.opera.bootstrap = legacyBootstrap;
349
+ config.saveControl(context, control);
350
+ }
351
+
352
+ if ((options.resume || options.forceResume) && control.meta?.opera?.bootstrap) {
353
+ const resumed = bootstrap.resumeBootstrap(context, control);
354
+ if (resumed.resumed) {
355
+ const profile = resumed.profile;
356
+ const updatedControl = bootstrap.applyBootstrap(context, control, profile);
357
+ env.syncEnvironment(context, updatedControl, { requiredKeys: env.inferRequiredKeys(updatedControl, context) });
358
+ const ops = require("./control");
359
+ ops.syncDocs(context, updatedControl);
360
+ ops.refreshRepoRuntime(context, { quiet: true });
361
+ fmt.blank();
362
+
363
+ if (profile.status === "completed") {
364
+ fmt.success(t("bootstrap.completed"));
365
+ fmt.blank();
366
+ fmt.step(t("bootstrap.next.label"));
367
+ fmt.hint(t("bootstrap.next.directCompleted"));
368
+ } else if (profile.status === "blocked") {
369
+ fmt.warn(t("bootstrap.resumeBlocked"));
370
+ for (const field of profile.missingFields || []) {
371
+ fmt.bullet(t(`bootstrap.field.${field}`) || field);
372
+ }
373
+ fmt.blank();
374
+ fmt.hint(t("bootstrap.next.directPending"));
375
+ } else if (profile.status === "needs_review") {
376
+ fmt.info(t("bootstrap.resumeNeedsReview"));
377
+ for (const field of profile.missingFields || []) {
378
+ fmt.bullet(t(`bootstrap.field.${field}`) || field);
379
+ }
380
+ fmt.blank();
381
+ fmt.hint(t("bootstrap.next.directPending"));
382
+ }
383
+
384
+ fmt.blank();
385
+ return profile;
386
+ }
387
+ fmt.blank();
388
+ if (resumed.reason === "missing_agent_artifacts") {
389
+ fmt.warn(t("bootstrap.resumeAwaitingArtifacts"));
390
+ } else if (resumed.reason === "empty_intake_and_spec") {
391
+ fmt.warn(t("bootstrap.resumeEmptyArtifacts"));
392
+ } else {
393
+ fmt.info(t("bootstrap.awaitingAgent"));
394
+ }
395
+ fmt.hint(t("bootstrap.next.handoff"));
396
+ fmt.blank();
397
+ return control.meta.opera.bootstrap;
398
+ }
399
+
308
400
  const profile = await bootstrap.collectBootstrapProfile(context, control, options);
309
- const updatedControl = bootstrap.applyBootstrap(context, control, profile);
310
- env.syncEnvironment(context, updatedControl, { requiredKeys: env.inferRequiredKeys(updatedControl, context) });
311
- const ops = require("./control");
312
- ops.syncDocs(context, updatedControl);
313
- ops.refreshRepoRuntime(context, { quiet: true });
314
-
315
- if (profile.mode === "agent_handoff") {
316
- console.log(t("bootstrap.awaitingAgent"));
317
- console.log(`${t("bootstrap.handoffFile")}: ${profile.handoffFiles?.markdown || bootstrap.bootstrapRelativePaths(context).markdown}`);
318
- console.log(t("bootstrap.next.handoff"));
319
- } else {
320
- console.log(profile.status === "completed" ? t("bootstrap.completed") : t("bootstrap.pending"));
321
- console.log(
322
- profile.status === "completed"
323
- ? t("bootstrap.next.directCompleted")
324
- : t("bootstrap.next.directPending"),
325
- );
401
+ if (profile.mode && profile.routeReason) {
402
+ fmt.blank();
403
+ fmt.info(t("bootstrap.routingMode", {
404
+ mode: formatBootstrapMode(profile.mode),
405
+ reason: formatBootstrapReason(profile.routeReason),
406
+ }));
326
407
  }
327
- return profile;
328
- }
329
-
330
- function status(root) {
331
- const context = config.ensureContext(root);
332
- const control = config.loadControl(context);
333
- setLocale(config.getLocale(control));
408
+ const updatedControl = bootstrap.applyBootstrap(context, control, profile);
409
+ env.syncEnvironment(context, updatedControl, { requiredKeys: env.inferRequiredKeys(updatedControl, context) });
410
+ const ops = require("./control");
411
+ ops.syncDocs(context, updatedControl);
412
+ ops.refreshRepoRuntime(context, { quiet: true });
413
+
414
+ fmt.header(t("bootstrap.resultHeader"));
415
+
416
+ if (profile.mode === "agent_handoff") {
417
+ fmt.info(t("bootstrap.awaitingAgent"));
418
+ fmt.blank();
419
+ fmt.step(t("bootstrap.handoffFile"), profile.handoffFiles?.markdown || bootstrap.bootstrapRelativePaths(context).markdown);
420
+ fmt.blank();
421
+ fmt.step(t("bootstrap.next.label"));
422
+ fmt.hint(t("bootstrap.next.handoff"));
423
+ } else {
424
+ fmt.success(profile.status === "completed" ? t("bootstrap.completed") : t("bootstrap.pending"));
425
+ fmt.blank();
426
+ fmt.step(t("bootstrap.next.label"));
427
+ fmt.hint(
428
+ profile.status === "completed"
429
+ ? t("bootstrap.next.directCompleted")
430
+ : t("bootstrap.next.directPending"),
431
+ );
432
+ }
433
+ fmt.blank();
434
+ return profile;
435
+ }
436
+
437
+ function status(root) {
438
+ const context = config.ensureContext(root);
439
+ const control = config.loadControl(context);
440
+ setLocale(config.getLocale(control));
334
441
 
335
442
  if (!config.isOperaInstalled(control)) {
336
443
  console.log(t("opera.notInstalled"));
337
444
  return;
338
445
  }
339
-
446
+
340
447
  const opera = control.meta.opera;
341
448
  const bootstrapState = opera.bootstrap || bootstrap.detectLegacyBootstrap(context, control);
342
449
  const localeDoctor = runtimeState.doctorLocale(control.meta?.locale || null);
@@ -344,51 +451,71 @@ function status(root) {
344
451
  console.log(t("opera.status.installed", { value: opera.installedAt }));
345
452
  console.log(t("opera.status.skills", { value: (opera.skills || []).join(", ") || t("locale.none") }));
346
453
  console.log(t("opera.status.locale", { locale: config.getLocale(control), source: formatLocaleSource(localeDoctor.source) }));
347
- console.log(t("opera.status.legacy", { value: opera.legacyStatus || bootstrapState?.status || "supported" }));
454
+ console.log(t("opera.status.legacy", { value: formatLegacyStatus(opera.legacyStatus || bootstrapState?.status || "supported") }));
348
455
  console.log(t("opera.status.contractVersion", { value: opera.contractVersion || t("locale.none") }));
349
- console.log(t("opera.status.contractReadiness", { value: opera.contractReadiness || "hypothesis" }));
456
+ console.log(t("opera.status.contractReadiness", { value: formatContractReadiness(opera.contractReadiness || "hypothesis") }));
350
457
 
351
458
  if (bootstrapState) {
352
- console.log(t("opera.status.bootstrap", { value: bootstrapState.status }));
459
+ console.log(t("opera.status.bootstrap", { value: formatBootstrapStatus(bootstrapState.status) }));
353
460
  if (bootstrapState.mode) {
354
- console.log(t("opera.status.mode", { value: bootstrapState.mode }));
461
+ console.log(t("opera.status.mode", { value: formatBootstrapMode(bootstrapState.mode) }));
355
462
  }
356
463
  if (bootstrapState.routeReason) {
357
- console.log(t("opera.status.route", { value: bootstrapState.routeReason }));
464
+ console.log(t("opera.status.route", { value: formatBootstrapReason(bootstrapState.routeReason) }));
358
465
  }
359
466
  if (bootstrapState.decisionOwnership) {
360
- console.log(t("opera.status.ownership", { value: bootstrapState.decisionOwnership }));
467
+ console.log(t("opera.status.ownership", { value: formatDecisionOwnership(bootstrapState.decisionOwnership) }));
361
468
  }
362
469
  if ((bootstrapState.missingFields || []).length) {
363
- console.log(t("opera.status.missing", { value: bootstrapState.missingFields.join(", ") }));
470
+ console.log(t("opera.status.missing", { value: formatMissingFields(bootstrapState.missingFields) }));
471
+ if (bootstrapState.mode === "agent_handoff") {
472
+ console.log(t("opera.status.awaitingAgentExplanation"));
473
+ console.log(t("opera.status.awaitingAgentAction"));
474
+ } else {
475
+ console.log(t("opera.status.directExplanation"));
476
+ console.log(t("opera.status.directAction", {
477
+ intake: bootstrapState.intakeFiles?.json || bootstrap.bootstrapRelativePaths(context).intakeJson,
478
+ spec: bootstrapState.intakeFiles?.specDossier || bootstrap.bootstrapRelativePaths(context).specDossier,
479
+ }));
480
+ }
481
+ console.log(t("opera.status.contractNotGenerated"));
364
482
  console.log(t("opera.status.resume"));
365
483
  }
366
- if (bootstrapState.handoffFiles?.markdown) {
484
+ if (bootstrapState.mode === "agent_handoff" && relativePathExists(context, bootstrapState.handoffFiles?.markdown)) {
367
485
  console.log(t("opera.status.handoff", { value: bootstrapState.handoffFiles.markdown }));
368
486
  }
369
- if (bootstrapState.reviewFiles?.qualityReport) {
487
+ if (relativePathExists(context, bootstrapState.intakeFiles?.json)) {
488
+ console.log(t("opera.status.intake", { value: bootstrapState.intakeFiles.json }));
489
+ }
490
+ if (relativePathExists(context, bootstrapState.intakeFiles?.specDossier)) {
491
+ console.log(t("opera.status.specDossier", { value: bootstrapState.intakeFiles.specDossier }));
492
+ }
493
+ if (relativePathExists(context, bootstrapState.reviewFiles?.openQuestions)) {
494
+ console.log(t("opera.status.openQuestions", { value: bootstrapState.reviewFiles.openQuestions }));
495
+ }
496
+ if (relativePathExists(context, bootstrapState.reviewFiles?.qualityReport)) {
370
497
  console.log(t("opera.status.qualityReport", { value: bootstrapState.reviewFiles.qualityReport }));
371
498
  }
372
499
  }
373
-
374
- const checks = [
375
- [context.layout === "split" ? "ops/.agent/hub/agent.md" : ".agent/hub/agent.md", fs.existsSync(path.join(context.paths.agentHubDir, "agent.md"))],
376
- [context.layout === "split" ? "ops/.agent/hub/router.md" : ".agent/hub/router.md", fs.existsSync(path.join(context.paths.agentHubDir, "router.md"))],
377
- [context.layout === "split" ? "ops/.agents/skills/_registry.md" : ".agents/skills/_registry.md", fs.existsSync(context.paths.registryPath)],
378
- [context.layout === "split" ? "ops/genesis.md" : "genesis.md", fs.existsSync(context.paths.genesisFile)],
379
- [context.layout === "split" ? "ops/contract/operating-contract.json" : "contract/operating-contract.json", fs.existsSync(context.paths.contractFile)],
380
- [context.layout === "split" ? "ops/policy/autonomy.json" : "policy/autonomy.json", fs.existsSync(context.paths.autonomyPolicyFile)],
381
- ];
500
+
501
+ const checks = [
502
+ [context.layout === "split" ? "ops/.agent/hub/agent.md" : ".agent/hub/agent.md", fs.existsSync(path.join(context.paths.agentHubDir, "agent.md"))],
503
+ [context.layout === "split" ? "ops/.agent/hub/router.md" : ".agent/hub/router.md", fs.existsSync(path.join(context.paths.agentHubDir, "router.md"))],
504
+ [context.layout === "split" ? "ops/.agents/skills/_registry.md" : ".agents/skills/_registry.md", fs.existsSync(context.paths.registryPath)],
505
+ [context.layout === "split" ? "ops/genesis.md" : "genesis.md", fs.existsSync(context.paths.genesisFile)],
506
+ [context.layout === "split" ? "ops/contract/operating-contract.json" : "contract/operating-contract.json", fs.existsSync(context.paths.contractFile)],
507
+ [context.layout === "split" ? "ops/policy/autonomy.json" : "policy/autonomy.json", fs.existsSync(context.paths.autonomyPolicyFile)],
508
+ ];
382
509
 
383
510
  console.log(t("opera.status.structure"));
384
- for (const [file, exists] of checks) {
385
- console.log(` ${exists ? "\u2705" : "\u274C"} ${file}`);
386
- }
387
- }
511
+ for (const [file, exists] of checks) {
512
+ console.log(` ${fmt.boolToken(exists)} ${file}`);
513
+ }
514
+ }
388
515
 
389
- function configure(root, args) {
390
- const context = config.ensureContext(root);
391
- const control = config.loadControl(context);
516
+ function configure(root, args) {
517
+ const context = config.ensureContext(root);
518
+ const control = config.loadControl(context);
392
519
  setLocale(config.getLocale(control));
393
520
  let nextLocale = config.getLocale(control);
394
521
 
@@ -398,212 +525,256 @@ function configure(root, args) {
398
525
  control.meta.locale = nextLocale;
399
526
  i += 1;
400
527
  }
401
- if (args[i] === "--phases" && args[i + 1]) {
402
- try {
403
- control.meta.phases = JSON.parse(args[i + 1]);
404
- } catch (_e) {
405
- console.error(t("opera.configure.invalidPhases"));
406
- return;
407
- }
528
+ if (args[i] === "--phases" && args[i + 1]) {
529
+ try {
530
+ control.meta.phases = JSON.parse(args[i + 1]);
531
+ } catch (_e) {
532
+ console.error(t("opera.configure.invalidPhases"));
533
+ return;
534
+ }
408
535
  i += 1;
409
536
  }
410
537
  }
411
538
 
412
- config.saveControl(context, control);
413
- if (config.isOperaInstalled(control)) {
414
- installStructure(context, control, nextLocale, { rewriteLocalizedTemplates: true });
415
- env.syncEnvironment(context, control);
416
- const ops = require("./control");
417
- ops.syncDocs(context, control);
418
- }
419
- console.log(t("opera.configure.updated"));
420
- }
421
-
422
- function upgrade(root, args = []) {
423
- const context = config.ensureContext(root);
424
- const control = config.loadControl(context);
425
- const locale = config.getLocale(control);
426
- setLocale(locale);
427
- const wantsStable = (args || []).includes("--stable");
428
- const wantsReset = (args || []).includes("--reset");
429
-
430
- if (!config.isOperaInstalled(control)) {
431
- console.log(t("opera.notInstalled"));
432
- console.log(t("opera.upgrade.runInstallFirst"));
433
- return;
434
- }
435
-
436
- if (!wantsStable) {
437
- console.log(t("opera.upgrade.usage"));
438
- return;
439
- }
440
-
441
- const legacy = bootstrap.detectLegacyBootstrap(context, control);
442
- const isLegacyUnsupported = legacy?.status === "legacy_unsupported";
443
- if (isLegacyUnsupported && !wantsReset) {
444
- control.meta.opera = control.meta.opera || {};
445
- control.meta.opera.legacyStatus = "legacy_unsupported";
446
- config.saveControl(context, control);
447
- console.log(t("opera.upgrade.legacyUnsupported"));
448
- return;
449
- }
450
-
451
- const backup = backupManagedArtifacts(context);
452
- if (wantsReset) {
453
- removePath(context.paths.agentHubDir);
454
- removePath(context.paths.skillsDir);
455
- removePath(context.paths.genesisFile);
456
- removePath(context.paths.contractFile);
457
- removePath(context.paths.autonomyPolicyFile);
458
- removePath(context.paths.bootstrapDir);
459
- }
460
-
461
- installStructure(context, control, locale, { rewriteLocalizedTemplates: true });
462
- control.meta.opera = {
463
- ...(control.meta.opera || {}),
464
- installed: true,
465
- model: "v3",
466
- stableTag: "stable",
467
- version: OPERA_VERSION,
468
- legacyStatus: "supported",
469
- contractVersion: fs.existsSync(context.paths.contractFile) ? bootstrap.CONTRACT_VERSION : null,
470
- contractReadiness: fs.existsSync(context.paths.contractFile)
471
- ? (control.meta?.opera?.contractReadiness || "verified")
472
- : "hypothesis",
473
- bootstrap: wantsReset
474
- ? bootstrap.createAwaitingBootstrapState(context)
475
- : (control.meta?.opera?.bootstrap || bootstrap.createAwaitingBootstrapState(context)),
476
- };
477
- config.saveControl(context, control);
478
- env.syncEnvironment(context, control);
479
- require("./skills").updateRegistry(context);
480
- console.log(t("opera.upgrade.backup", { path: path.relative(context.workspaceRoot, backup.backupRoot) }));
481
- console.log(t("opera.upgraded", { version: OPERA_VERSION }));
482
- }
483
-
484
- function cmdInstall(root, args) {
485
- const options = {
486
- bootstrap: true,
487
- answers: {},
488
- interactive: true,
489
- locale: null,
490
- bootstrapMode: "auto",
491
- technicalLevel: null,
492
- projectState: null,
493
- docsState: null,
494
- decisionOwnership: null,
495
- };
496
- for (let i = 0; i < (args || []).length; i += 1) {
497
- if (args[i] === "--locale" && args[i + 1]) {
498
- options.locale = args[i + 1];
499
- i += 1;
500
- } else if (args[i] === "--no-bootstrap") {
501
- options.bootstrap = false;
502
- } else if (args[i] === "--non-interactive") {
503
- options.interactive = false;
504
- } else if (args[i] === "--bootstrap-mode" && args[i + 1]) {
505
- options.bootstrapMode = args[i + 1];
506
- i += 1;
507
- } else if (args[i] === "--technical-level" && args[i + 1]) {
508
- options.technicalLevel = args[i + 1];
509
- i += 1;
510
- } else if (args[i] === "--project-state" && args[i + 1]) {
511
- options.projectState = args[i + 1];
512
- i += 1;
513
- } else if (args[i] === "--docs-state" && args[i + 1]) {
514
- options.docsState = args[i + 1];
515
- i += 1;
516
- } else if (args[i] === "--decision-ownership" && args[i + 1]) {
517
- options.decisionOwnership = args[i + 1];
518
- i += 1;
519
- }
520
- }
521
- return install(root, options);
522
- }
523
-
524
- function cmdStatus(root) { status(root); }
525
- function cmdConfigure(root, args) { configure(root, args); }
526
- function cmdUpgrade(root, args) { upgrade(root, args); }
527
-
528
- async function cmdBootstrap(root, args) {
529
- const options = {
530
- locale: null,
531
- interactive: true,
532
- answers: {},
533
- resume: false,
534
- bootstrapMode: "auto",
535
- technicalLevel: null,
536
- projectState: null,
537
- docsState: null,
538
- decisionOwnership: null,
539
- };
540
- for (let i = 0; i < (args || []).length; i += 1) {
541
- if (args[i] === "--locale" && args[i + 1]) {
542
- options.locale = args[i + 1];
543
- i += 1;
544
- } else if (args[i] === "--non-interactive") {
545
- options.interactive = false;
546
- } else if (args[i] === "--resume") {
547
- options.resume = true;
548
- } else if (args[i] === "--bootstrap-mode" && args[i + 1]) {
549
- options.bootstrapMode = args[i + 1];
550
- i += 1;
551
- } else if (args[i] === "--technical-level" && args[i + 1]) {
552
- options.technicalLevel = args[i + 1];
553
- i += 1;
554
- } else if (args[i] === "--project-state" && args[i + 1]) {
555
- options.projectState = args[i + 1];
556
- i += 1;
557
- } else if (args[i] === "--docs-state" && args[i + 1]) {
558
- options.docsState = args[i + 1];
559
- i += 1;
560
- } else if (args[i] === "--decision-ownership" && args[i + 1]) {
561
- options.decisionOwnership = args[i + 1];
562
- i += 1;
563
- }
564
- }
565
- return runBootstrap(root, options);
566
- }
567
-
539
+ config.saveControl(context, control);
540
+ if (config.isOperaInstalled(control)) {
541
+ installStructure(context, control, nextLocale, { rewriteLocalizedTemplates: true });
542
+ env.syncEnvironment(context, control);
543
+ const ops = require("./control");
544
+ ops.syncDocs(context, control);
545
+ }
546
+ console.log(t("opera.configure.updated"));
547
+ }
548
+
549
+ function upgrade(root, args = []) {
550
+ const context = config.ensureContext(root);
551
+ const control = config.loadControl(context);
552
+ const locale = config.getLocale(control);
553
+ setLocale(locale);
554
+ const wantsStable = (args || []).includes("--stable");
555
+ const wantsReset = (args || []).includes("--reset");
556
+
557
+ if (!config.isOperaInstalled(control)) {
558
+ console.log(t("opera.notInstalled"));
559
+ console.log(t("opera.upgrade.runInstallFirst"));
560
+ return;
561
+ }
562
+
563
+ if (!wantsStable) {
564
+ console.log(t("opera.upgrade.usage"));
565
+ return;
566
+ }
567
+
568
+ const legacy = bootstrap.detectLegacyBootstrap(context, control);
569
+ const isLegacyUnsupported = legacy?.status === "legacy_unsupported";
570
+ if (isLegacyUnsupported && !wantsReset) {
571
+ control.meta.opera = control.meta.opera || {};
572
+ control.meta.opera.legacyStatus = "legacy_unsupported";
573
+ config.saveControl(context, control);
574
+ console.log(t("opera.upgrade.legacyUnsupported"));
575
+ return;
576
+ }
577
+
578
+ const backup = backupManagedArtifacts(context);
579
+ if (wantsReset) {
580
+ removePath(context.paths.agentHubDir);
581
+ removePath(context.paths.skillsDir);
582
+ removePath(context.paths.genesisFile);
583
+ removePath(context.paths.contractFile);
584
+ removePath(context.paths.autonomyPolicyFile);
585
+ removePath(context.paths.bootstrapDir);
586
+ }
587
+
588
+ installStructure(context, control, locale, { rewriteLocalizedTemplates: true });
589
+ control.meta.opera = {
590
+ ...(control.meta.opera || {}),
591
+ installed: true,
592
+ model: "v3",
593
+ stableTag: "stable",
594
+ version: OPERA_VERSION,
595
+ legacyStatus: "supported",
596
+ contractVersion: fs.existsSync(context.paths.contractFile) ? bootstrap.CONTRACT_VERSION : null,
597
+ contractReadiness: fs.existsSync(context.paths.contractFile)
598
+ ? (control.meta?.opera?.contractReadiness || "verified")
599
+ : "hypothesis",
600
+ bootstrap: wantsReset
601
+ ? bootstrap.createAwaitingBootstrapState(context)
602
+ : (control.meta?.opera?.bootstrap || bootstrap.createAwaitingBootstrapState(context)),
603
+ };
604
+ config.saveControl(context, control);
605
+ env.syncEnvironment(context, control);
606
+ require("./skills").updateRegistry(context);
607
+ console.log(t("opera.upgrade.backup", { path: path.relative(context.workspaceRoot, backup.backupRoot) }));
608
+ console.log(t("opera.upgraded", { version: OPERA_VERSION }));
609
+ }
610
+
611
+ function cmdInstall(root, args) {
612
+ const options = {
613
+ bootstrap: true,
614
+ answers: {},
615
+ interactive: true,
616
+ locale: null,
617
+ bootstrapMode: "auto",
618
+ technicalLevel: null,
619
+ projectState: null,
620
+ docsState: null,
621
+ decisionOwnership: null,
622
+ };
623
+ for (let i = 0; i < (args || []).length; i += 1) {
624
+ if (args[i] === "--locale" && args[i + 1]) {
625
+ options.locale = args[i + 1];
626
+ i += 1;
627
+ } else if (args[i] === "--no-bootstrap") {
628
+ options.bootstrap = false;
629
+ } else if (args[i] === "--non-interactive") {
630
+ options.interactive = false;
631
+ } else if (args[i] === "--bootstrap-mode" && args[i + 1]) {
632
+ options.bootstrapMode = args[i + 1];
633
+ i += 1;
634
+ } else if (args[i] === "--technical-level" && args[i + 1]) {
635
+ options.technicalLevel = args[i + 1];
636
+ i += 1;
637
+ } else if (args[i] === "--project-state" && args[i + 1]) {
638
+ options.projectState = args[i + 1];
639
+ i += 1;
640
+ } else if (args[i] === "--docs-state" && args[i + 1]) {
641
+ options.docsState = args[i + 1];
642
+ i += 1;
643
+ } else if (args[i] === "--decision-ownership" && args[i + 1]) {
644
+ options.decisionOwnership = args[i + 1];
645
+ i += 1;
646
+ }
647
+ }
648
+ return install(root, options);
649
+ }
650
+
651
+ function cmdStatus(root) { status(root); }
652
+ function cmdConfigure(root, args) { configure(root, args); }
653
+ function cmdUpgrade(root, args) { upgrade(root, args); }
654
+
655
+ async function cmdBootstrap(root, args) {
656
+ const options = {
657
+ locale: null,
658
+ interactive: true,
659
+ answers: {},
660
+ resume: false,
661
+ bootstrapMode: "auto",
662
+ technicalLevel: null,
663
+ projectState: null,
664
+ docsState: null,
665
+ decisionOwnership: null,
666
+ };
667
+ for (let i = 0; i < (args || []).length; i += 1) {
668
+ if (args[i] === "--locale" && args[i + 1]) {
669
+ options.locale = args[i + 1];
670
+ i += 1;
671
+ } else if (args[i] === "--non-interactive") {
672
+ options.interactive = false;
673
+ } else if (args[i] === "--resume") {
674
+ options.resume = true;
675
+ } else if (args[i] === "--bootstrap-mode" && args[i + 1]) {
676
+ options.bootstrapMode = args[i + 1];
677
+ i += 1;
678
+ } else if (args[i] === "--technical-level" && args[i + 1]) {
679
+ options.technicalLevel = args[i + 1];
680
+ i += 1;
681
+ } else if (args[i] === "--project-state" && args[i + 1]) {
682
+ options.projectState = args[i + 1];
683
+ i += 1;
684
+ } else if (args[i] === "--docs-state" && args[i + 1]) {
685
+ options.docsState = args[i + 1];
686
+ i += 1;
687
+ } else if (args[i] === "--decision-ownership" && args[i + 1]) {
688
+ options.decisionOwnership = args[i + 1];
689
+ i += 1;
690
+ }
691
+ }
692
+ return runBootstrap(root, options);
693
+ }
694
+
568
695
  function cmdHandoff(root, args) {
569
- const context = config.ensureContext(root);
570
- const control = config.loadControl(context);
571
- const state = bootstrap.getBootstrapState(control, context) || bootstrap.detectLegacyBootstrap(context, control);
572
- if (!state) {
573
- throw new Error("OPERA bootstrap is not initialized.");
574
- }
696
+ const context = config.ensureContext(root);
697
+ const control = config.loadControl(context);
698
+ const state = bootstrap.getBootstrapState(control, context) || bootstrap.detectLegacyBootstrap(context, control);
699
+ if (!state) {
700
+ throw new Error("OPERA bootstrap is not initialized.");
701
+ }
575
702
  const files = bootstrap.bootstrapFilePaths(context);
576
703
  const printMode = (args || []).includes("--print");
577
704
  const jsonMode = (args || []).includes("--json");
705
+ if (state.mode !== "agent_handoff") {
706
+ const payload = {
707
+ mode: state.mode,
708
+ status: state.status,
709
+ files: {
710
+ intakeJson: state.intakeFiles?.json || bootstrap.bootstrapRelativePaths(context).intakeJson,
711
+ specDossier: state.intakeFiles?.specDossier || bootstrap.bootstrapRelativePaths(context).specDossier,
712
+ openQuestions: state.reviewFiles?.openQuestions || bootstrap.bootstrapRelativePaths(context).openQuestions,
713
+ qualityReport: state.reviewFiles?.qualityReport || bootstrap.bootstrapRelativePaths(context).qualityReport,
714
+ },
715
+ nextStep: t("opera.handoff.directNext"),
716
+ };
717
+ if (jsonMode) {
718
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
719
+ return;
720
+ }
721
+ if (printMode) {
722
+ process.stdout.write([
723
+ `# ${t("opera.handoff.directTitle")}`,
724
+ "",
725
+ `- ${t("opera.handoff.directStatus")}: ${formatBootstrapStatus(state.status)}`,
726
+ `- ${t("opera.handoff.directIntake")}: ${payload.files.intakeJson}`,
727
+ `- ${t("opera.handoff.directSpec")}: ${payload.files.specDossier}`,
728
+ `- ${t("opera.handoff.directQuestions")}: ${payload.files.openQuestions}`,
729
+ `- ${t("opera.handoff.directQuality")}: ${payload.files.qualityReport}`,
730
+ "",
731
+ t("opera.handoff.directNext"),
732
+ "",
733
+ ].join("\n"));
734
+ return;
735
+ }
736
+ console.log(t("opera.handoff.directSummary", { status: formatBootstrapStatus(state.status) }));
737
+ console.log(t("opera.status.intake", { value: payload.files.intakeJson }));
738
+ console.log(t("opera.status.specDossier", { value: payload.files.specDossier }));
739
+ if (relativePathExists(context, payload.files.openQuestions)) {
740
+ console.log(t("opera.status.openQuestions", { value: payload.files.openQuestions }));
741
+ }
742
+ if (relativePathExists(context, payload.files.qualityReport)) {
743
+ console.log(t("opera.status.qualityReport", { value: payload.files.qualityReport }));
744
+ }
745
+ console.log(t("opera.status.resume"));
746
+ console.log(t("opera.handoff.directNext"));
747
+ return;
748
+ }
578
749
  if (jsonMode) {
579
750
  const payload = readText(files.json);
580
751
  process.stdout.write(payload || "{}\n");
581
752
  return;
582
753
  }
583
- if (printMode) {
584
- process.stdout.write(readText(files.markdown) || "");
585
- return;
586
- }
587
- console.log(`Bootstrap: ${state.status}`);
588
- console.log(`Mode: ${state.mode}`);
589
- console.log(`Markdown handoff: ${state.handoffFiles?.markdown || bootstrap.bootstrapRelativePaths(context).markdown}`);
590
- console.log(`JSON handoff: ${state.handoffFiles?.json || bootstrap.bootstrapRelativePaths(context).json}`);
591
- if (state.reviewFiles?.openQuestions) {
592
- console.log(`Open questions: ${state.reviewFiles.openQuestions}`);
754
+ if (printMode) {
755
+ process.stdout.write(readText(files.markdown) || "");
756
+ return;
757
+ }
758
+ console.log(t("opera.handoff.summary", { status: formatBootstrapStatus(state.status) }));
759
+ console.log(t("opera.handoff.mode", { mode: formatBootstrapMode(state.mode) }));
760
+ console.log(t("opera.handoff.markdown", { value: state.handoffFiles?.markdown || bootstrap.bootstrapRelativePaths(context).markdown }));
761
+ console.log(t("opera.handoff.json", { value: state.handoffFiles?.json || bootstrap.bootstrapRelativePaths(context).json }));
762
+ if (relativePathExists(context, state.reviewFiles?.openQuestions)) {
763
+ console.log(t("opera.status.openQuestions", { value: state.reviewFiles.openQuestions }));
593
764
  }
594
765
  }
595
-
596
- module.exports = {
597
- installStructure,
598
- install,
599
- runBootstrap,
766
+
767
+ module.exports = {
768
+ installStructure,
769
+ install,
770
+ runBootstrap,
600
771
  status,
601
772
  configure,
602
773
  upgrade,
603
774
  cmdInstall,
604
775
  cmdStatus,
605
- cmdConfigure,
606
- cmdUpgrade,
607
- cmdBootstrap,
608
- cmdHandoff,
609
- };
776
+ cmdConfigure,
777
+ cmdUpgrade,
778
+ cmdBootstrap,
779
+ cmdHandoff,
780
+ };