trackops 1.0.1 → 1.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.
Files changed (57) hide show
  1. package/README.md +326 -270
  2. package/bin/trackops.js +102 -70
  3. package/lib/config.js +260 -35
  4. package/lib/control.js +517 -475
  5. package/lib/env.js +227 -0
  6. package/lib/i18n.js +61 -53
  7. package/lib/init.js +135 -46
  8. package/lib/locale.js +63 -0
  9. package/lib/opera-bootstrap.js +523 -0
  10. package/lib/opera.js +319 -170
  11. package/lib/registry.js +27 -13
  12. package/lib/release.js +56 -0
  13. package/lib/resources.js +42 -0
  14. package/lib/server.js +907 -554
  15. package/lib/skills.js +148 -124
  16. package/lib/workspace.js +260 -0
  17. package/locales/en.json +331 -139
  18. package/locales/es.json +331 -139
  19. package/package.json +7 -9
  20. package/scripts/skills-marketplace-smoke.js +124 -0
  21. package/scripts/smoke-tests.js +445 -0
  22. package/scripts/sync-skill-version.js +21 -0
  23. package/scripts/validate-skill.js +88 -0
  24. package/skills/trackops/SKILL.md +64 -0
  25. package/skills/trackops/agents/openai.yaml +3 -0
  26. package/skills/trackops/references/activation.md +39 -0
  27. package/skills/trackops/references/troubleshooting.md +34 -0
  28. package/skills/trackops/references/workflow.md +20 -0
  29. package/skills/trackops/scripts/bootstrap-trackops.js +201 -0
  30. package/skills/trackops/skill.json +29 -0
  31. package/templates/opera/en/agent.md +26 -0
  32. package/templates/opera/en/genesis.md +79 -0
  33. package/templates/opera/en/references/autonomy-and-recovery.md +23 -0
  34. package/templates/opera/en/references/opera-cycle.md +62 -0
  35. package/templates/opera/en/registry.md +28 -0
  36. package/templates/opera/en/router.md +39 -0
  37. package/templates/opera/genesis.md +79 -94
  38. package/templates/skills/changelog-updater/locales/en/SKILL.md +11 -0
  39. package/templates/skills/commiter/locales/en/SKILL.md +11 -0
  40. package/templates/skills/project-starter-skill/locales/en/SKILL.md +24 -0
  41. package/ui/css/panels.css +956 -953
  42. package/ui/index.html +1 -1
  43. package/ui/js/api.js +211 -194
  44. package/ui/js/app.js +200 -199
  45. package/ui/js/i18n.js +14 -0
  46. package/ui/js/onboarding.js +439 -437
  47. package/ui/js/state.js +130 -129
  48. package/ui/js/utils.js +175 -172
  49. package/ui/js/views/board.js +255 -254
  50. package/ui/js/views/execution.js +256 -256
  51. package/ui/js/views/insights.js +340 -339
  52. package/ui/js/views/overview.js +365 -364
  53. package/ui/js/views/settings.js +340 -202
  54. package/ui/js/views/sidebar.js +131 -132
  55. package/ui/js/views/skills.js +163 -162
  56. package/ui/js/views/tasks.js +406 -405
  57. package/ui/js/views/topbar.js +239 -183
package/lib/locale.js ADDED
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env node
2
+
3
+ const readline = require("readline/promises");
4
+
5
+ const SUPPORTED_LOCALES = ["es", "en"];
6
+
7
+ function normalizeLocale(value) {
8
+ const raw = String(value || "").trim().toLowerCase();
9
+ if (!raw) return null;
10
+ if (raw.startsWith("es")) return "es";
11
+ if (raw.startsWith("en")) return "en";
12
+ return null;
13
+ }
14
+
15
+ function detectSystemLocale() {
16
+ const envLocale =
17
+ normalizeLocale(process.env.TRACKOPS_LOCALE) ||
18
+ normalizeLocale(process.env.LC_ALL) ||
19
+ normalizeLocale(process.env.LC_MESSAGES) ||
20
+ normalizeLocale(process.env.LANG);
21
+ if (envLocale) return envLocale;
22
+
23
+ try {
24
+ const locale = Intl.DateTimeFormat().resolvedOptions().locale;
25
+ return normalizeLocale(locale) || "es";
26
+ } catch (_error) {
27
+ return "es";
28
+ }
29
+ }
30
+
31
+ function isInteractive() {
32
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY);
33
+ }
34
+
35
+ async function promptForLocale(defaultLocale) {
36
+ const initial = normalizeLocale(defaultLocale) || detectSystemLocale();
37
+ if (!isInteractive()) return initial;
38
+
39
+ const rl = readline.createInterface({
40
+ input: process.stdin,
41
+ output: process.stdout,
42
+ });
43
+
44
+ try {
45
+ const answer = await rl.question(`Choose language / Elige idioma [es/en] (${initial}): `);
46
+ return normalizeLocale(answer) || initial;
47
+ } finally {
48
+ rl.close();
49
+ }
50
+ }
51
+
52
+ function resolveLocale(preferred, fallback) {
53
+ return normalizeLocale(preferred) || normalizeLocale(fallback) || detectSystemLocale();
54
+ }
55
+
56
+ module.exports = {
57
+ SUPPORTED_LOCALES,
58
+ normalizeLocale,
59
+ detectSystemLocale,
60
+ isInteractive,
61
+ promptForLocale,
62
+ resolveLocale,
63
+ };
@@ -0,0 +1,523 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const { spawnSync } = require("child_process");
6
+ const readline = require("readline/promises");
7
+
8
+ const config = require("./config");
9
+ const { t, setLocale } = require("./i18n");
10
+ const { isInteractive } = require("./locale");
11
+ const { resolveLocalizedFile } = require("./resources");
12
+
13
+ const TEMPLATES_DIR = path.join(__dirname, "..", "templates", "opera");
14
+
15
+ const SERVICE_HINTS = [
16
+ ["openai", "OpenAI"],
17
+ ["anthropic", "Anthropic"],
18
+ ["stripe", "Stripe"],
19
+ ["supabase", "Supabase"],
20
+ ["postgres", "PostgreSQL"],
21
+ ["mysql", "MySQL"],
22
+ ["redis", "Redis"],
23
+ ["slack", "Slack"],
24
+ ["s3", "Amazon S3"],
25
+ ["aws", "AWS"],
26
+ ["gcp", "Google Cloud"],
27
+ ["azure", "Azure"],
28
+ ];
29
+
30
+ function nowIso() {
31
+ return new Date().toISOString();
32
+ }
33
+
34
+ function git(args, root) {
35
+ const result = spawnSync("git", args, { cwd: root, encoding: "utf8" });
36
+ if (result.error || result.status !== 0) return "";
37
+ return result.stdout.trim();
38
+ }
39
+
40
+ function readText(filePath) {
41
+ return fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf8") : "";
42
+ }
43
+
44
+ function safeJson(text) {
45
+ try {
46
+ return JSON.parse(text);
47
+ } catch (_error) {
48
+ return null;
49
+ }
50
+ }
51
+
52
+ function unique(items) {
53
+ return [...new Set(items.filter(Boolean))];
54
+ }
55
+
56
+ function firstParagraph(text) {
57
+ return String(text || "")
58
+ .split(/\r?\n\r?\n/)
59
+ .map((chunk) => chunk.replace(/^#\s+/gm, "").trim())
60
+ .find(Boolean) || "";
61
+ }
62
+
63
+ function inferServicesFromEnv(text) {
64
+ const upper = String(text || "").toUpperCase();
65
+ return SERVICE_HINTS.filter(([needle]) => upper.includes(needle.toUpperCase())).map(([, label]) => label);
66
+ }
67
+
68
+ function inferServicesFromDependencies(pkg) {
69
+ const deps = {
70
+ ...(pkg?.dependencies || {}),
71
+ ...(pkg?.devDependencies || {}),
72
+ };
73
+ const names = Object.keys(deps);
74
+ return SERVICE_HINTS.filter(([needle]) => names.some((name) => name.includes(needle))).map(([, label]) => label);
75
+ }
76
+
77
+ function scanProject(root) {
78
+ const context = config.ensureContext(root);
79
+ const scanRoot = context.appRoot;
80
+ const envPaths = config.envFilePaths(context);
81
+ const pkgPath = path.join(scanRoot, "package.json");
82
+ const readmePath = fs.existsSync(path.join(scanRoot, "README.md"))
83
+ ? path.join(scanRoot, "README.md")
84
+ : fs.existsSync(path.join(scanRoot, "README.mdx"))
85
+ ? path.join(scanRoot, "README.mdx")
86
+ : "";
87
+ const envExamplePath = fs.existsSync(envPaths.exampleFile)
88
+ ? envPaths.exampleFile
89
+ : fs.existsSync(envPaths.rootFile)
90
+ ? envPaths.rootFile
91
+ : "";
92
+
93
+ const pkg = safeJson(readText(pkgPath));
94
+ const readme = readText(readmePath);
95
+ const envExample = readText(envExamplePath);
96
+ const workflowsDir = path.join(context.workspaceRoot, ".github", "workflows");
97
+ const workflowFiles = fs.existsSync(workflowsDir)
98
+ ? fs.readdirSync(workflowsDir).filter((file) => file.endsWith(".yml") || file.endsWith(".yaml"))
99
+ : [];
100
+ const files = fs.readdirSync(scanRoot);
101
+ const stacks = [];
102
+
103
+ if (pkg) stacks.push("node");
104
+ if (files.some((file) => file.startsWith("requirements") || file === "pyproject.toml")) stacks.push("python");
105
+ if (files.includes("go.mod")) stacks.push("go");
106
+ if (files.includes("Cargo.toml")) stacks.push("rust");
107
+ if (files.includes("pom.xml")) stacks.push("java");
108
+ if (files.includes("Dockerfile") || files.some((file) => file.startsWith("docker-compose"))) stacks.push("docker");
109
+
110
+ const description = pkg?.description || firstParagraph(readme);
111
+ const title = pkg?.displayName || pkg?.name || path.basename(scanRoot);
112
+ const testCommands = unique([
113
+ pkg?.scripts?.test ? "npm test" : "",
114
+ fs.existsSync(path.join(scanRoot, "pytest.ini")) ? "pytest" : "",
115
+ fs.existsSync(path.join(scanRoot, "Cargo.toml")) ? "cargo test" : "",
116
+ fs.existsSync(path.join(scanRoot, "go.mod")) ? "go test ./..." : "",
117
+ ]);
118
+ const buildCommands = unique([
119
+ pkg?.scripts?.build ? "npm run build" : "",
120
+ fs.existsSync(path.join(scanRoot, "Dockerfile")) ? "docker build ." : "",
121
+ ]);
122
+ const services = unique([
123
+ ...inferServicesFromDependencies(pkg),
124
+ ...inferServicesFromEnv(envExample),
125
+ ]);
126
+ const gitRemote = git(["remote", "get-url", "origin"], context.workspaceRoot) || null;
127
+ const ciProviders = workflowFiles.length ? ["github-actions"] : [];
128
+
129
+ return {
130
+ title,
131
+ description,
132
+ stacks: unique(stacks),
133
+ services,
134
+ testCommands,
135
+ buildCommands,
136
+ ciProviders,
137
+ gitRemote,
138
+ readmeSummary: firstParagraph(readme),
139
+ payloadHint: pkg?.homepage || "",
140
+ sourceOfTruthHint: envExample ? t("bootstrap.infer.envSourceHint") : "",
141
+ workflowFiles,
142
+ };
143
+ }
144
+
145
+ function normalizeList(value) {
146
+ if (Array.isArray(value)) return value.map((item) => String(item).trim()).filter(Boolean);
147
+ return String(value || "")
148
+ .split(/\r?\n|[,;]+/)
149
+ .map((item) => item.trim())
150
+ .filter(Boolean);
151
+ }
152
+
153
+ function parseJsonValue(value) {
154
+ if (!String(value || "").trim()) return {};
155
+ try {
156
+ return JSON.parse(value);
157
+ } catch (_error) {
158
+ return {};
159
+ }
160
+ }
161
+
162
+ async function askQuestion(rl, message, defaultValue) {
163
+ const suffix = defaultValue ? ` (${defaultValue})` : "";
164
+ const answer = await rl.question(`${message}${suffix}: `);
165
+ return String(answer || "").trim() || String(defaultValue || "").trim();
166
+ }
167
+
168
+ async function collectBootstrapProfile(root, control, options = {}) {
169
+ const context = config.ensureContext(root);
170
+ const locale = config.getLocale(control);
171
+ setLocale(locale);
172
+ const scan = scanProject(context);
173
+ const previous = control.meta?.opera?.bootstrap?.discovery || {};
174
+ const interactive = options.interactive !== false && isInteractive();
175
+
176
+ const defaults = {
177
+ desiredOutcome: options.answers?.desiredOutcome || previous.desiredOutcome || scan.description || "",
178
+ externalServices: normalizeList(options.answers?.externalServices || previous.externalServices || scan.services),
179
+ sourceOfTruth: options.answers?.sourceOfTruth || previous.sourceOfTruth || scan.sourceOfTruthHint || "",
180
+ payload: options.answers?.payload || previous.payload || scan.payloadHint || "",
181
+ behaviorRules: normalizeList(options.answers?.behaviorRules || previous.behaviorRules || ""),
182
+ inputSchema: options.answers?.inputSchema || previous.inputSchema || {},
183
+ outputSchema: options.answers?.outputSchema || previous.outputSchema || {},
184
+ architecturalInvariants: normalizeList(options.answers?.architecturalInvariants || previous.architecturalInvariants || ""),
185
+ pipeline: normalizeList(options.answers?.pipeline || previous.pipeline || ""),
186
+ templates: normalizeList(options.answers?.templates || previous.templates || ""),
187
+ repoTaskPolicy: options.answers?.repoTaskPolicy || control.meta?.opera?.bootstrap?.repoTaskPolicy || "optional_pending",
188
+ };
189
+
190
+ const answers = { ...defaults };
191
+
192
+ if (interactive) {
193
+ console.log("");
194
+ console.log(t("bootstrap.header"));
195
+ console.log(t("bootstrap.subtitle"));
196
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
197
+ try {
198
+ answers.desiredOutcome = await askQuestion(rl, t("bootstrap.question.desiredOutcome"), defaults.desiredOutcome);
199
+ answers.externalServices = normalizeList(await askQuestion(rl, t("bootstrap.question.externalServices"), defaults.externalServices.join(", ")));
200
+ answers.sourceOfTruth = await askQuestion(rl, t("bootstrap.question.sourceOfTruth"), defaults.sourceOfTruth);
201
+ answers.payload = await askQuestion(rl, t("bootstrap.question.payload"), defaults.payload);
202
+ answers.behaviorRules = normalizeList(await askQuestion(rl, t("bootstrap.question.behaviorRules"), defaults.behaviorRules.join("; ")));
203
+ answers.inputSchema = parseJsonValue(await askQuestion(rl, t("bootstrap.question.inputSchema"), JSON.stringify(defaults.inputSchema)));
204
+ answers.outputSchema = parseJsonValue(await askQuestion(rl, t("bootstrap.question.outputSchema"), JSON.stringify(defaults.outputSchema)));
205
+ answers.architecturalInvariants = normalizeList(await askQuestion(rl, t("bootstrap.question.invariants"), defaults.architecturalInvariants.join("; ")));
206
+ answers.pipeline = normalizeList(await askQuestion(rl, t("bootstrap.question.pipeline"), defaults.pipeline.join("; ")));
207
+ answers.templates = normalizeList(await askQuestion(rl, t("bootstrap.question.templates"), defaults.templates.join(", ")));
208
+ const repoDefault = defaults.repoTaskPolicy === "skip" ? "n" : "y";
209
+ const includeRepoTasks = await askQuestion(rl, t("bootstrap.question.repoTasks"), repoDefault);
210
+ answers.repoTaskPolicy = includeRepoTasks.toLowerCase().startsWith("n") ? "skip" : "optional_pending";
211
+ } finally {
212
+ rl.close();
213
+ }
214
+ }
215
+
216
+ const missingFields = [];
217
+ if (!answers.desiredOutcome) missingFields.push("desiredOutcome");
218
+ if (!answers.sourceOfTruth) missingFields.push("sourceOfTruth");
219
+ if (!answers.payload) missingFields.push("payload");
220
+ if (!Object.keys(answers.inputSchema || {}).length) missingFields.push("inputSchema");
221
+ if (!Object.keys(answers.outputSchema || {}).length) missingFields.push("outputSchema");
222
+
223
+ return {
224
+ version: 1,
225
+ localeAtBootstrap: locale,
226
+ status: missingFields.length ? "pending" : "completed",
227
+ mode: interactive ? "guided" : "non_interactive",
228
+ source: "hybrid",
229
+ startedAt: control.meta?.opera?.bootstrap?.startedAt || nowIso(),
230
+ completedAt: missingFields.length ? null : nowIso(),
231
+ missingFields,
232
+ repoTaskPolicy: answers.repoTaskPolicy || "optional_pending",
233
+ discovery: answers,
234
+ inference: scan,
235
+ };
236
+ }
237
+
238
+ function isVirginGenesis(content) {
239
+ const text = String(content || "");
240
+ return !text.trim() || /TODO:/i.test(text) || /The Constitution of the project/i.test(text) || /La Constitución del proyecto/i.test(text);
241
+ }
242
+
243
+ function renderGenesis(control, profile) {
244
+ const locale = config.getLocale(control);
245
+ const templatePath = resolveLocalizedFile(TEMPLATES_DIR, locale, "genesis.md");
246
+ let content = fs.readFileSync(templatePath, "utf8");
247
+ const rules = (profile.discovery.behaviorRules || []).map((item) => `- ${item}`).join("\n") || `- ${t("bootstrap.noneDefined")}`;
248
+ const invariants = (profile.discovery.architecturalInvariants || []).map((item) => `- ${item}`).join("\n") || `- ${t("bootstrap.noneDefined")}`;
249
+ const services = (profile.discovery.externalServices || []).length
250
+ ? profile.discovery.externalServices.map((item) => `| ${item} | ${t("bootstrap.servicePending")} | ${t("bootstrap.servicePending")} |`).join("\n")
251
+ : `| — | — | — |`;
252
+ const pipeline = (profile.discovery.pipeline || []).length
253
+ ? profile.discovery.pipeline.map((item) => `- ${item}`).join("\n")
254
+ : `- ${t("bootstrap.noneDefined")}`;
255
+ const templates = (profile.discovery.templates || []).length
256
+ ? profile.discovery.templates.map((item) => `- \`${item}\``).join("\n")
257
+ : `- ${t("bootstrap.noneDefined")}`;
258
+ const schema = JSON.stringify({
259
+ input: {
260
+ source: profile.discovery.sourceOfTruth,
261
+ schema: profile.discovery.inputSchema || {},
262
+ },
263
+ output: {
264
+ destination: profile.discovery.payload,
265
+ schema: profile.discovery.outputSchema || {},
266
+ },
267
+ }, null, 2);
268
+
269
+ const replacements = {
270
+ PROJECT_NAME: control.meta.projectName || "Project",
271
+ DESIRED_OUTCOME: profile.discovery.desiredOutcome || t("bootstrap.pendingValue"),
272
+ SERVICES_TABLE: services,
273
+ SOURCE_OF_TRUTH: profile.discovery.sourceOfTruth || t("bootstrap.pendingValue"),
274
+ PAYLOAD: profile.discovery.payload || t("bootstrap.pendingValue"),
275
+ BEHAVIOR_RULES: rules,
276
+ DATA_SCHEMA: schema,
277
+ ARCHITECTURAL_INVARIANTS: invariants,
278
+ PIPELINE_ITEMS: pipeline,
279
+ TEMPLATE_ITEMS: templates,
280
+ };
281
+
282
+ for (const [key, value] of Object.entries(replacements)) {
283
+ content = content.replace(new RegExp(`\\{\\{${key}\\}\\}`, "g"), value);
284
+ }
285
+ return content;
286
+ }
287
+
288
+ function buildSeedTasks(control, profile) {
289
+ const phaseOrder = config.getPhases(control);
290
+ const tasks = [
291
+ {
292
+ id: "opera-bootstrap",
293
+ origin: "bootstrap",
294
+ title: t("bootstrap.task.bootstrap.title"),
295
+ phase: phaseOrder[0]?.id || "O",
296
+ stream: "Operations",
297
+ priority: "P0",
298
+ status: profile.status === "completed" ? "completed" : "blocked",
299
+ required: true,
300
+ dependsOn: [],
301
+ summary: t("bootstrap.task.bootstrap.summary"),
302
+ acceptance: [
303
+ t("bootstrap.acceptance.discovery"),
304
+ t("bootstrap.acceptance.schema"),
305
+ t("bootstrap.acceptance.rules"),
306
+ t("bootstrap.acceptance.plan"),
307
+ ],
308
+ blocker: profile.status === "completed" ? undefined : t("bootstrap.blocker.missingData"),
309
+ history: [{ at: nowIso(), action: "create", note: t("bootstrap.history.seeded") }],
310
+ },
311
+ {
312
+ id: "opera-prove-integrations",
313
+ origin: "bootstrap",
314
+ title: t("bootstrap.task.prove.title"),
315
+ phase: phaseOrder[1]?.id || "P",
316
+ stream: "Operations",
317
+ priority: "P0",
318
+ status: "pending",
319
+ required: true,
320
+ dependsOn: ["opera-bootstrap"],
321
+ summary: t("bootstrap.task.prove.summary"),
322
+ acceptance: [
323
+ t("bootstrap.acceptance.env"),
324
+ t("bootstrap.acceptance.tests"),
325
+ t("bootstrap.acceptance.shape"),
326
+ t("bootstrap.acceptance.findings"),
327
+ ],
328
+ history: [{ at: nowIso(), action: "create", note: t("bootstrap.history.seeded") }],
329
+ },
330
+ {
331
+ id: "opera-structure-system",
332
+ origin: "bootstrap",
333
+ title: t("bootstrap.task.structure.title"),
334
+ phase: phaseOrder[2]?.id || "E",
335
+ stream: "Operations",
336
+ priority: "P1",
337
+ status: "pending",
338
+ required: true,
339
+ dependsOn: ["opera-prove-integrations"],
340
+ summary: t("bootstrap.task.structure.summary"),
341
+ acceptance: [
342
+ t("bootstrap.acceptance.sops"),
343
+ t("bootstrap.acceptance.tools"),
344
+ t("bootstrap.acceptance.graph"),
345
+ t("bootstrap.acceptance.integration"),
346
+ ],
347
+ history: [{ at: nowIso(), action: "create", note: t("bootstrap.history.seeded") }],
348
+ },
349
+ {
350
+ id: "opera-refine-delivery",
351
+ origin: "bootstrap",
352
+ title: t("bootstrap.task.refine.title"),
353
+ phase: phaseOrder[3]?.id || "R",
354
+ stream: "Operations",
355
+ priority: "P1",
356
+ status: "pending",
357
+ required: true,
358
+ dependsOn: ["opera-structure-system"],
359
+ summary: t("bootstrap.task.refine.summary"),
360
+ acceptance: [
361
+ t("bootstrap.acceptance.outputs"),
362
+ t("bootstrap.acceptance.delivery"),
363
+ t("bootstrap.acceptance.ui"),
364
+ ],
365
+ history: [{ at: nowIso(), action: "create", note: t("bootstrap.history.seeded") }],
366
+ },
367
+ {
368
+ id: "opera-automate-runtime",
369
+ origin: "bootstrap",
370
+ title: t("bootstrap.task.automate.title"),
371
+ phase: phaseOrder[4]?.id || "A",
372
+ stream: "Operations",
373
+ priority: "P2",
374
+ status: "pending",
375
+ required: true,
376
+ dependsOn: ["opera-refine-delivery"],
377
+ summary: t("bootstrap.task.automate.summary"),
378
+ acceptance: [
379
+ t("bootstrap.acceptance.tmp"),
380
+ t("bootstrap.acceptance.deploy"),
381
+ t("bootstrap.acceptance.triggers"),
382
+ t("bootstrap.acceptance.smoke"),
383
+ ],
384
+ history: [{ at: nowIso(), action: "create", note: t("bootstrap.history.seeded") }],
385
+ },
386
+ ];
387
+
388
+ if (profile.repoTaskPolicy !== "skip") {
389
+ tasks.push(
390
+ {
391
+ id: "repo-readme-align",
392
+ origin: "bootstrap",
393
+ title: t("bootstrap.task.repoReadme.title"),
394
+ phase: phaseOrder[0]?.id || "O",
395
+ stream: "Repository",
396
+ priority: "P2",
397
+ status: "pending",
398
+ required: false,
399
+ dependsOn: [],
400
+ summary: t("bootstrap.task.repoReadme.summary"),
401
+ acceptance: [],
402
+ history: [{ at: nowIso(), action: "create", note: t("bootstrap.history.seeded") }],
403
+ },
404
+ {
405
+ id: "repo-license-review",
406
+ origin: "bootstrap",
407
+ title: t("bootstrap.task.repoLicense.title"),
408
+ phase: phaseOrder[0]?.id || "O",
409
+ stream: "Repository",
410
+ priority: "P3",
411
+ status: "pending",
412
+ required: false,
413
+ dependsOn: [],
414
+ summary: t("bootstrap.task.repoLicense.summary"),
415
+ acceptance: [],
416
+ history: [{ at: nowIso(), action: "create", note: t("bootstrap.history.seeded") }],
417
+ },
418
+ {
419
+ id: "repo-changelog-policy",
420
+ origin: "bootstrap",
421
+ title: t("bootstrap.task.repoChangelog.title"),
422
+ phase: phaseOrder[0]?.id || "O",
423
+ stream: "Repository",
424
+ priority: "P3",
425
+ status: "pending",
426
+ required: false,
427
+ dependsOn: [],
428
+ summary: t("bootstrap.task.repoChangelog.summary"),
429
+ acceptance: [],
430
+ history: [{ at: nowIso(), action: "create", note: t("bootstrap.history.seeded") }],
431
+ },
432
+ );
433
+
434
+ if (profile.inference.gitRemote && profile.inference.gitRemote.includes("github")) {
435
+ tasks.push({
436
+ id: "repo-github-governance",
437
+ origin: "bootstrap",
438
+ title: t("bootstrap.task.repoGithub.title"),
439
+ phase: phaseOrder[0]?.id || "O",
440
+ stream: "Repository",
441
+ priority: "P3",
442
+ status: "pending",
443
+ required: false,
444
+ dependsOn: [],
445
+ summary: t("bootstrap.task.repoGithub.summary"),
446
+ acceptance: [],
447
+ history: [{ at: nowIso(), action: "create", note: t("bootstrap.history.seeded") }],
448
+ });
449
+ }
450
+ }
451
+
452
+ return tasks;
453
+ }
454
+
455
+ function applyBootstrap(root, control, profile) {
456
+ const context = config.ensureContext(root);
457
+ setLocale(config.getLocale(control));
458
+ control.meta.opera = control.meta.opera || {};
459
+ control.meta.opera.bootstrap = profile;
460
+ control.meta.currentFocus = profile.discovery.desiredOutcome || t("bootstrap.defaultFocus");
461
+ control.meta.deliveryTarget = profile.discovery.payload || t("bootstrap.defaultTarget");
462
+ control.meta.focusPhase = profile.status === "completed" ? "P" : "O";
463
+ control.meta.phases = config.getPhases(control);
464
+
465
+ const remainingTasks = (control.tasks || []).filter((task) => task.id !== "ops-bootstrap" && task.origin !== "bootstrap");
466
+ control.tasks = [...remainingTasks, ...buildSeedTasks(control, profile)];
467
+
468
+ control.decisionsPending = (profile.missingFields || []).map((field) => ({
469
+ owner: "user",
470
+ title: t(`bootstrap.field.${field}`),
471
+ impact: t("bootstrap.decisionImpact"),
472
+ }));
473
+
474
+ control.findings = control.findings || [];
475
+ const genesisPath = context.paths.genesisFile;
476
+ const existingGenesis = readText(genesisPath);
477
+ if (!existingGenesis || isVirginGenesis(existingGenesis)) {
478
+ fs.writeFileSync(genesisPath, `${renderGenesis(control, profile)}\n`, "utf8");
479
+ } else if (profile.status !== "completed") {
480
+ const duplicate = control.findings.some((finding) => finding.title === t("bootstrap.finding.genesisConflictTitle"));
481
+ if (!duplicate) {
482
+ control.findings.push({
483
+ status: "open",
484
+ severity: "medium",
485
+ title: t("bootstrap.finding.genesisConflictTitle"),
486
+ detail: t("bootstrap.finding.genesisConflictDetail"),
487
+ impact: t("bootstrap.finding.genesisConflictImpact"),
488
+ });
489
+ }
490
+ control.meta.opera.bootstrap.status = "needs_review";
491
+ }
492
+
493
+ config.saveControl(context, control);
494
+ return control;
495
+ }
496
+
497
+ function detectLegacyBootstrap(root, control) {
498
+ const context = config.ensureContext(root);
499
+ if (!config.isOperaInstalled(control)) return null;
500
+ if (control.meta?.opera?.bootstrap) return control.meta.opera.bootstrap;
501
+ const genesisContent = readText(context.paths.genesisFile);
502
+ return {
503
+ version: 1,
504
+ localeAtBootstrap: config.getLocale(control),
505
+ status: isVirginGenesis(genesisContent) ? "pending" : "needs_review",
506
+ mode: "legacy",
507
+ source: "legacy",
508
+ startedAt: control.meta?.opera?.installedAt || nowIso(),
509
+ completedAt: null,
510
+ missingFields: isVirginGenesis(genesisContent) ? ["desiredOutcome", "sourceOfTruth", "payload", "inputSchema", "outputSchema"] : [],
511
+ repoTaskPolicy: "optional_pending",
512
+ discovery: {},
513
+ inference: scanProject(context),
514
+ };
515
+ }
516
+
517
+ module.exports = {
518
+ scanProject,
519
+ collectBootstrapProfile,
520
+ applyBootstrap,
521
+ detectLegacyBootstrap,
522
+ isVirginGenesis,
523
+ };