trackops 2.0.5 → 2.1.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.
package/lib/opera.js CHANGED
@@ -10,19 +10,52 @@ const { promptForLocale, maybePromptForLocale, resolveLocale } = require("./loca
10
10
  const { resolveLocalizedFile, resolveLocalizedDir, resolveSkillFile } = require("./resources");
11
11
  const bootstrap = require("./opera-bootstrap");
12
12
  const runtimeState = require("./runtime-state");
13
+ const fmt = require("./cli-format");
13
14
 
14
15
  const TEMPLATES_DIR = path.join(__dirname, "..", "templates", "opera");
15
16
  const SKILLS_TEMPLATES_DIR = path.join(__dirname, "..", "templates", "skills");
16
17
  const OPERA_VERSION = require("../package.json").version;
17
- const AUXILIARY_SKILLS = ["opera-skill", "project-starter-skill", "opera-contract-auditor", "opera-policy-guard"];
18
+ const AUXILIARY_SKILLS = ["opera-skill", "opera-quality-guard", "project-starter-skill", "opera-contract-auditor", "opera-policy-guard"];
18
19
 
19
20
  function nowIso() {
20
21
  return new Date().toISOString();
21
22
  }
22
23
 
23
- function formatLocaleSource(source) {
24
- return t(`locale.source.${String(source || "").trim()}`) || source || t("locale.none");
25
- }
24
+ function formatLocaleSource(source) {
25
+ return t(`locale.source.${String(source || "").trim()}`) || source || t("locale.none");
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") : "";
@@ -88,7 +121,10 @@ function resolveOperaLocale(control, options = {}) {
88
121
 
89
122
  function seedAuxiliarySkill(skillName, locale, skillsDir, options = {}) {
90
123
  const templateDir = path.join(SKILLS_TEMPLATES_DIR, skillName);
91
- if (!fs.existsSync(templateDir)) return;
124
+ if (!fs.existsSync(templateDir)) {
125
+ process.stderr.write(`[opera] Warning: skill template not found: ${skillName} (${templateDir})\n`);
126
+ return;
127
+ }
92
128
  const targetDir = path.join(skillsDir, skillName);
93
129
  seedDirRecursive(templateDir, targetDir, {
94
130
  overwrite: options.rewriteLocalizedTemplates === true,
@@ -222,22 +258,36 @@ async function install(root, options = {}) {
222
258
  config.saveControl(context, control);
223
259
  env.syncEnvironment(context, control);
224
260
 
261
+ fmt.blank();
225
262
  if (!alreadyInstalled) {
226
- console.log(t("opera.installed", { version: OPERA_VERSION }));
263
+ fmt.success(t("opera.installed", { version: OPERA_VERSION }));
227
264
  } else {
228
- console.log(t("opera.alreadyInstalled", { version: config.getOperaVersion(control) || OPERA_VERSION }));
265
+ fmt.info(t("opera.alreadyInstalled", { version: config.getOperaVersion(control) || OPERA_VERSION }));
229
266
  }
230
267
 
231
268
  const skills = require("./skills");
232
269
  for (const skillName of ["commiter", "changelog-updater"]) {
233
270
  try {
234
271
  skills.installSkill(context, skillName, { locale });
235
- } catch (_error) {
236
- // ignore
272
+ } catch (error) {
273
+ fmt.warn(t("skill.installError", { name: skillName, error: error.message }));
237
274
  }
238
275
  }
239
276
  skills.updateRegistry(context);
240
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); });
283
+ });
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;
289
+ }
290
+ }
241
291
  if (options.bootstrap !== false) {
242
292
  await runBootstrap(context, {
243
293
  locale,
@@ -302,37 +352,85 @@ async function runBootstrap(root, options = {}) {
302
352
  if ((options.resume || options.forceResume) && control.meta?.opera?.bootstrap) {
303
353
  const resumed = bootstrap.resumeBootstrap(context, control);
304
354
  if (resumed.resumed) {
305
- const updatedControl = bootstrap.applyBootstrap(context, control, resumed.profile);
355
+ const profile = resumed.profile;
356
+ const updatedControl = bootstrap.applyBootstrap(context, control, profile);
306
357
  env.syncEnvironment(context, updatedControl, { requiredKeys: env.inferRequiredKeys(updatedControl, context) });
307
358
  const ops = require("./control");
308
359
  ops.syncDocs(context, updatedControl);
309
360
  ops.refreshRepoRuntime(context, { quiet: true });
310
- console.log(t("bootstrap.completed"));
311
- return resumed.profile;
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"));
312
394
  }
313
- console.log(t("bootstrap.awaitingAgent"));
395
+ fmt.hint(t("bootstrap.next.handoff"));
396
+ fmt.blank();
314
397
  return control.meta.opera.bootstrap;
315
398
  }
316
399
 
317
- const profile = await bootstrap.collectBootstrapProfile(context, control, options);
400
+ const profile = await bootstrap.collectBootstrapProfile(context, control, options);
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
+ }));
407
+ }
318
408
  const updatedControl = bootstrap.applyBootstrap(context, control, profile);
319
409
  env.syncEnvironment(context, updatedControl, { requiredKeys: env.inferRequiredKeys(updatedControl, context) });
320
410
  const ops = require("./control");
321
411
  ops.syncDocs(context, updatedControl);
322
412
  ops.refreshRepoRuntime(context, { quiet: true });
323
413
 
414
+ fmt.header(t("bootstrap.resultHeader"));
415
+
324
416
  if (profile.mode === "agent_handoff") {
325
- console.log(t("bootstrap.awaitingAgent"));
326
- console.log(`${t("bootstrap.handoffFile")}: ${profile.handoffFiles?.markdown || bootstrap.bootstrapRelativePaths(context).markdown}`);
327
- console.log(t("bootstrap.next.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"));
328
423
  } else {
329
- console.log(profile.status === "completed" ? t("bootstrap.completed") : t("bootstrap.pending"));
330
- console.log(
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(
331
428
  profile.status === "completed"
332
429
  ? t("bootstrap.next.directCompleted")
333
430
  : t("bootstrap.next.directPending"),
334
431
  );
335
432
  }
433
+ fmt.blank();
336
434
  return profile;
337
435
  }
338
436
 
@@ -346,39 +444,59 @@ function status(root) {
346
444
  return;
347
445
  }
348
446
 
349
- const opera = control.meta.opera;
350
- const bootstrapState = opera.bootstrap || bootstrap.detectLegacyBootstrap(context, control);
351
- const localeDoctor = runtimeState.doctorLocale(control.meta?.locale || null);
352
- console.log(t("opera.status.version", { version: opera.version }));
353
- console.log(t("opera.status.installed", { value: opera.installedAt }));
354
- console.log(t("opera.status.skills", { value: (opera.skills || []).join(", ") || t("locale.none") }));
355
- console.log(t("opera.status.locale", { locale: config.getLocale(control), source: formatLocaleSource(localeDoctor.source) }));
356
- console.log(t("opera.status.legacy", { value: opera.legacyStatus || bootstrapState?.status || "supported" }));
357
- console.log(t("opera.status.contractVersion", { value: opera.contractVersion || t("locale.none") }));
358
- console.log(t("opera.status.contractReadiness", { value: opera.contractReadiness || "hypothesis" }));
359
-
360
- if (bootstrapState) {
361
- console.log(t("opera.status.bootstrap", { value: bootstrapState.status }));
362
- if (bootstrapState.mode) {
363
- console.log(t("opera.status.mode", { value: bootstrapState.mode }));
364
- }
365
- if (bootstrapState.routeReason) {
366
- console.log(t("opera.status.route", { value: bootstrapState.routeReason }));
367
- }
368
- if (bootstrapState.decisionOwnership) {
369
- console.log(t("opera.status.ownership", { value: bootstrapState.decisionOwnership }));
370
- }
371
- if ((bootstrapState.missingFields || []).length) {
372
- console.log(t("opera.status.missing", { value: bootstrapState.missingFields.join(", ") }));
373
- console.log(t("opera.status.resume"));
374
- }
375
- if (bootstrapState.handoffFiles?.markdown) {
376
- console.log(t("opera.status.handoff", { value: bootstrapState.handoffFiles.markdown }));
377
- }
378
- if (bootstrapState.reviewFiles?.qualityReport) {
379
- console.log(t("opera.status.qualityReport", { value: bootstrapState.reviewFiles.qualityReport }));
380
- }
381
- }
447
+ const opera = control.meta.opera;
448
+ const bootstrapState = opera.bootstrap || bootstrap.detectLegacyBootstrap(context, control);
449
+ const localeDoctor = runtimeState.doctorLocale(control.meta?.locale || null);
450
+ console.log(t("opera.status.version", { version: opera.version }));
451
+ console.log(t("opera.status.installed", { value: opera.installedAt }));
452
+ console.log(t("opera.status.skills", { value: (opera.skills || []).join(", ") || t("locale.none") }));
453
+ console.log(t("opera.status.locale", { locale: config.getLocale(control), source: formatLocaleSource(localeDoctor.source) }));
454
+ console.log(t("opera.status.legacy", { value: formatLegacyStatus(opera.legacyStatus || bootstrapState?.status || "supported") }));
455
+ console.log(t("opera.status.contractVersion", { value: opera.contractVersion || t("locale.none") }));
456
+ console.log(t("opera.status.contractReadiness", { value: formatContractReadiness(opera.contractReadiness || "hypothesis") }));
457
+
458
+ if (bootstrapState) {
459
+ console.log(t("opera.status.bootstrap", { value: formatBootstrapStatus(bootstrapState.status) }));
460
+ if (bootstrapState.mode) {
461
+ console.log(t("opera.status.mode", { value: formatBootstrapMode(bootstrapState.mode) }));
462
+ }
463
+ if (bootstrapState.routeReason) {
464
+ console.log(t("opera.status.route", { value: formatBootstrapReason(bootstrapState.routeReason) }));
465
+ }
466
+ if (bootstrapState.decisionOwnership) {
467
+ console.log(t("opera.status.ownership", { value: formatDecisionOwnership(bootstrapState.decisionOwnership) }));
468
+ }
469
+ if ((bootstrapState.missingFields || []).length) {
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"));
482
+ console.log(t("opera.status.resume"));
483
+ }
484
+ if (bootstrapState.mode === "agent_handoff" && relativePathExists(context, bootstrapState.handoffFiles?.markdown)) {
485
+ console.log(t("opera.status.handoff", { value: bootstrapState.handoffFiles.markdown }));
486
+ }
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)) {
497
+ console.log(t("opera.status.qualityReport", { value: bootstrapState.reviewFiles.qualityReport }));
498
+ }
499
+ }
382
500
 
383
501
  const checks = [
384
502
  [context.layout === "split" ? "ops/.agent/hub/agent.md" : ".agent/hub/agent.md", fs.existsSync(path.join(context.paths.agentHubDir, "agent.md"))],
@@ -389,11 +507,11 @@ function status(root) {
389
507
  [context.layout === "split" ? "ops/policy/autonomy.json" : "policy/autonomy.json", fs.existsSync(context.paths.autonomyPolicyFile)],
390
508
  ];
391
509
 
392
- console.log(t("opera.status.structure"));
393
- for (const [file, exists] of checks) {
394
- console.log(` ${exists ? "\u2705" : "\u274C"} ${file}`);
395
- }
396
- }
510
+ console.log(t("opera.status.structure"));
511
+ for (const [file, exists] of checks) {
512
+ console.log(` ${fmt.boolToken(exists)} ${file}`);
513
+ }
514
+ }
397
515
 
398
516
  function configure(root, args) {
399
517
  const context = config.ensureContext(root);
@@ -467,10 +585,13 @@ function upgrade(root, args = []) {
467
585
  removePath(context.paths.bootstrapDir);
468
586
  }
469
587
 
470
- installStructure(context, control, locale, { rewriteLocalizedTemplates: true });
471
- control.meta.opera = {
472
- ...(control.meta.opera || {}),
473
- installed: true,
588
+ installStructure(context, control, locale, { rewriteLocalizedTemplates: true });
589
+ require("./quality").backfillBootstrapMetadata(context, control, {
590
+ persist: true,
591
+ });
592
+ control.meta.opera = {
593
+ ...(control.meta.opera || {}),
594
+ installed: true,
474
595
  model: "v3",
475
596
  stableTag: "stable",
476
597
  version: OPERA_VERSION,
@@ -574,33 +695,77 @@ async function cmdBootstrap(root, args) {
574
695
  return runBootstrap(root, options);
575
696
  }
576
697
 
577
- function cmdHandoff(root, args) {
698
+ function cmdHandoff(root, args) {
578
699
  const context = config.ensureContext(root);
579
700
  const control = config.loadControl(context);
580
701
  const state = bootstrap.getBootstrapState(control, context) || bootstrap.detectLegacyBootstrap(context, control);
581
702
  if (!state) {
582
703
  throw new Error("OPERA bootstrap is not initialized.");
583
704
  }
584
- const files = bootstrap.bootstrapFilePaths(context);
585
- const printMode = (args || []).includes("--print");
586
- const jsonMode = (args || []).includes("--json");
587
- if (jsonMode) {
588
- const payload = readText(files.json);
589
- process.stdout.write(payload || "{}\n");
590
- return;
591
- }
705
+ const files = bootstrap.bootstrapFilePaths(context);
706
+ const printMode = (args || []).includes("--print");
707
+ const jsonMode = (args || []).includes("--json");
708
+ if (state.mode !== "agent_handoff") {
709
+ const payload = {
710
+ mode: state.mode,
711
+ status: state.status,
712
+ files: {
713
+ intakeJson: state.intakeFiles?.json || bootstrap.bootstrapRelativePaths(context).intakeJson,
714
+ specDossier: state.intakeFiles?.specDossier || bootstrap.bootstrapRelativePaths(context).specDossier,
715
+ openQuestions: state.reviewFiles?.openQuestions || bootstrap.bootstrapRelativePaths(context).openQuestions,
716
+ qualityReport: state.reviewFiles?.qualityReport || bootstrap.bootstrapRelativePaths(context).qualityReport,
717
+ },
718
+ nextStep: t("opera.handoff.directNext"),
719
+ };
720
+ if (jsonMode) {
721
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
722
+ return;
723
+ }
724
+ if (printMode) {
725
+ process.stdout.write([
726
+ `# ${t("opera.handoff.directTitle")}`,
727
+ "",
728
+ `- ${t("opera.handoff.directStatus")}: ${formatBootstrapStatus(state.status)}`,
729
+ `- ${t("opera.handoff.directIntake")}: ${payload.files.intakeJson}`,
730
+ `- ${t("opera.handoff.directSpec")}: ${payload.files.specDossier}`,
731
+ `- ${t("opera.handoff.directQuestions")}: ${payload.files.openQuestions}`,
732
+ `- ${t("opera.handoff.directQuality")}: ${payload.files.qualityReport}`,
733
+ "",
734
+ t("opera.handoff.directNext"),
735
+ "",
736
+ ].join("\n"));
737
+ return;
738
+ }
739
+ console.log(t("opera.handoff.directSummary", { status: formatBootstrapStatus(state.status) }));
740
+ console.log(t("opera.status.intake", { value: payload.files.intakeJson }));
741
+ console.log(t("opera.status.specDossier", { value: payload.files.specDossier }));
742
+ if (relativePathExists(context, payload.files.openQuestions)) {
743
+ console.log(t("opera.status.openQuestions", { value: payload.files.openQuestions }));
744
+ }
745
+ if (relativePathExists(context, payload.files.qualityReport)) {
746
+ console.log(t("opera.status.qualityReport", { value: payload.files.qualityReport }));
747
+ }
748
+ console.log(t("opera.status.resume"));
749
+ console.log(t("opera.handoff.directNext"));
750
+ return;
751
+ }
752
+ if (jsonMode) {
753
+ const payload = readText(files.json);
754
+ process.stdout.write(payload || "{}\n");
755
+ return;
756
+ }
592
757
  if (printMode) {
593
758
  process.stdout.write(readText(files.markdown) || "");
594
759
  return;
595
760
  }
596
- console.log(`Bootstrap: ${state.status}`);
597
- console.log(`Mode: ${state.mode}`);
598
- console.log(`Markdown handoff: ${state.handoffFiles?.markdown || bootstrap.bootstrapRelativePaths(context).markdown}`);
599
- console.log(`JSON handoff: ${state.handoffFiles?.json || bootstrap.bootstrapRelativePaths(context).json}`);
600
- if (state.reviewFiles?.openQuestions) {
601
- console.log(`Open questions: ${state.reviewFiles.openQuestions}`);
602
- }
603
- }
761
+ console.log(t("opera.handoff.summary", { status: formatBootstrapStatus(state.status) }));
762
+ console.log(t("opera.handoff.mode", { mode: formatBootstrapMode(state.mode) }));
763
+ console.log(t("opera.handoff.markdown", { value: state.handoffFiles?.markdown || bootstrap.bootstrapRelativePaths(context).markdown }));
764
+ console.log(t("opera.handoff.json", { value: state.handoffFiles?.json || bootstrap.bootstrapRelativePaths(context).json }));
765
+ if (relativePathExists(context, state.reviewFiles?.openQuestions)) {
766
+ console.log(t("opera.status.openQuestions", { value: state.reviewFiles.openQuestions }));
767
+ }
768
+ }
604
769
 
605
770
  module.exports = {
606
771
  installStructure,