trackops 1.0.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/README.md +292 -272
  2. package/bin/trackops.js +108 -50
  3. package/lib/config.js +267 -38
  4. package/lib/control.js +534 -480
  5. package/lib/env.js +244 -0
  6. package/lib/i18n.js +61 -53
  7. package/lib/init.js +170 -47
  8. package/lib/locale.js +63 -0
  9. package/lib/opera-bootstrap.js +1075 -0
  10. package/lib/opera.js +524 -125
  11. package/lib/preferences.js +74 -0
  12. package/lib/registry.js +27 -13
  13. package/lib/release.js +56 -0
  14. package/lib/resources.js +42 -0
  15. package/lib/runtime-state.js +144 -0
  16. package/lib/server.js +1004 -521
  17. package/lib/skills.js +148 -124
  18. package/lib/workspace.js +260 -0
  19. package/locales/en.json +418 -132
  20. package/locales/es.json +418 -132
  21. package/package.json +8 -9
  22. package/scripts/postinstall-locale.js +21 -0
  23. package/scripts/skills-marketplace-smoke.js +124 -0
  24. package/scripts/smoke-tests.js +570 -0
  25. package/scripts/sync-skill-version.js +21 -0
  26. package/scripts/validate-skill.js +89 -0
  27. package/skills/trackops/SKILL.md +89 -0
  28. package/skills/trackops/agents/openai.yaml +3 -0
  29. package/skills/trackops/references/activation.md +73 -0
  30. package/skills/trackops/references/troubleshooting.md +49 -0
  31. package/skills/trackops/references/workflow.md +26 -0
  32. package/skills/trackops/scripts/bootstrap-trackops.js +203 -0
  33. package/skills/trackops/skill.json +29 -0
  34. package/templates/opera/agent.md +10 -9
  35. package/templates/opera/architecture/dependency-graph.md +24 -0
  36. package/templates/opera/architecture/runtime-automation.md +24 -0
  37. package/templates/opera/architecture/runtime-operations.md +34 -0
  38. package/templates/opera/en/agent.md +27 -0
  39. package/templates/opera/en/architecture/dependency-graph.md +24 -0
  40. package/templates/opera/en/architecture/runtime-automation.md +24 -0
  41. package/templates/opera/en/architecture/runtime-operations.md +34 -0
  42. package/templates/opera/en/genesis.md +79 -0
  43. package/templates/opera/en/references/autonomy-and-recovery.md +23 -0
  44. package/templates/opera/en/references/opera-cycle.md +62 -0
  45. package/templates/opera/en/registry.md +28 -0
  46. package/templates/opera/en/reviews/delivery-audit.md +18 -0
  47. package/templates/opera/en/reviews/integration-audit.md +18 -0
  48. package/templates/opera/en/router.md +49 -0
  49. package/templates/opera/genesis.md +79 -94
  50. package/templates/opera/reviews/delivery-audit.md +18 -0
  51. package/templates/opera/reviews/integration-audit.md +18 -0
  52. package/templates/opera/router.md +15 -5
  53. package/templates/skills/changelog-updater/locales/en/SKILL.md +11 -0
  54. package/templates/skills/commiter/locales/en/SKILL.md +11 -0
  55. package/templates/skills/opera-contract-auditor/SKILL.md +38 -0
  56. package/templates/skills/opera-contract-auditor/locales/en/SKILL.md +38 -0
  57. package/templates/skills/opera-policy-guard/SKILL.md +26 -0
  58. package/templates/skills/opera-policy-guard/locales/en/SKILL.md +26 -0
  59. package/templates/skills/project-starter-skill/SKILL.md +89 -164
  60. package/templates/skills/project-starter-skill/locales/en/SKILL.md +104 -0
  61. package/ui/css/panels.css +956 -953
  62. package/ui/index.html +1 -1
  63. package/ui/js/api.js +211 -194
  64. package/ui/js/app.js +200 -199
  65. package/ui/js/i18n.js +14 -0
  66. package/ui/js/onboarding.js +439 -437
  67. package/ui/js/state.js +130 -129
  68. package/ui/js/utils.js +175 -172
  69. package/ui/js/views/board.js +255 -254
  70. package/ui/js/views/execution.js +256 -256
  71. package/ui/js/views/insights.js +340 -339
  72. package/ui/js/views/overview.js +366 -361
  73. package/ui/js/views/settings.js +340 -202
  74. package/ui/js/views/sidebar.js +131 -132
  75. package/ui/js/views/skills.js +163 -162
  76. package/ui/js/views/tasks.js +406 -405
  77. package/ui/js/views/topbar.js +239 -183
  78. package/templates/etapa/agent.md +0 -26
  79. package/templates/etapa/genesis.md +0 -94
  80. package/templates/etapa/references/autonomy-and-recovery.md +0 -117
  81. package/templates/etapa/references/etapa-cycle.md +0 -193
  82. package/templates/etapa/registry.md +0 -28
  83. package/templates/etapa/router.md +0 -39
@@ -0,0 +1,1075 @@
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
+ const TECHNICAL_LEVELS = ["low", "medium", "high", "senior"];
31
+ const PROJECT_STATES = ["idea", "draft", "existing_repo", "advanced"];
32
+ const DOC_STATES = ["none", "notes", "sos", "spec_dossier", "repo_docs"];
33
+ const DECISION_OWNERSHIPS = ["user", "shared", "agent"];
34
+ const BOOTSTRAP_MODES = ["auto", "direct", "handoff"];
35
+ const QUALITY_STATUSES = ["ready", "needs_review", "blocked"];
36
+ const CONTRACT_READINESS = ["hypothesis", "provisional", "verified", "locked"];
37
+ const CONTRACT_VERSION = 3;
38
+
39
+ function nowIso() {
40
+ return new Date().toISOString();
41
+ }
42
+
43
+ function git(args, root) {
44
+ const result = spawnSync("git", args, { cwd: root, encoding: "utf8" });
45
+ if (result.error || result.status !== 0) return "";
46
+ return result.stdout.trim();
47
+ }
48
+
49
+ function readText(filePath) {
50
+ return fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf8") : "";
51
+ }
52
+
53
+ function readJson(filePath) {
54
+ if (!fs.existsSync(filePath)) return null;
55
+ try {
56
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
57
+ } catch (_error) {
58
+ return null;
59
+ }
60
+ }
61
+
62
+ function writeText(filePath, content) {
63
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
64
+ fs.writeFileSync(filePath, content.replace(/\r?\n/g, "\n"), "utf8");
65
+ }
66
+
67
+ function writeJson(filePath, data) {
68
+ writeText(filePath, `${JSON.stringify(data, null, 2)}\n`);
69
+ }
70
+
71
+ function safeJson(text) {
72
+ try {
73
+ return JSON.parse(text);
74
+ } catch (_error) {
75
+ return null;
76
+ }
77
+ }
78
+
79
+ function unique(items) {
80
+ return [...new Set((items || []).map((item) => String(item || "").trim()).filter(Boolean))];
81
+ }
82
+
83
+ function normalizeEnum(value, allowed) {
84
+ const normalized = String(value || "").trim().toLowerCase();
85
+ return allowed.includes(normalized) ? normalized : null;
86
+ }
87
+
88
+ function explanationModeFor(technicalLevel) {
89
+ if (technicalLevel === "low") return "guided";
90
+ if (technicalLevel === "medium") return "balanced";
91
+ return "expert";
92
+ }
93
+
94
+ function bootstrapRelativePaths(context) {
95
+ if (context.layout === "split") {
96
+ return {
97
+ markdown: "ops/bootstrap/agent-handoff.md",
98
+ json: "ops/bootstrap/agent-handoff.json",
99
+ intakeJson: "ops/bootstrap/intake.json",
100
+ specDossier: "ops/bootstrap/spec-dossier.md",
101
+ openQuestions: "ops/bootstrap/open-questions.md",
102
+ qualityReport: "ops/bootstrap/quality-report.json",
103
+ };
104
+ }
105
+
106
+ return {
107
+ markdown: "bootstrap/agent-handoff.md",
108
+ json: "bootstrap/agent-handoff.json",
109
+ intakeJson: "bootstrap/intake.json",
110
+ specDossier: "bootstrap/spec-dossier.md",
111
+ openQuestions: "bootstrap/open-questions.md",
112
+ qualityReport: "bootstrap/quality-report.json",
113
+ };
114
+ }
115
+
116
+ function bootstrapFilePaths(context) {
117
+ return {
118
+ dir: context.paths.bootstrapDir,
119
+ markdown: path.join(context.paths.bootstrapDir, "agent-handoff.md"),
120
+ json: path.join(context.paths.bootstrapDir, "agent-handoff.json"),
121
+ intakeJson: path.join(context.paths.bootstrapDir, "intake.json"),
122
+ specDossier: path.join(context.paths.bootstrapDir, "spec-dossier.md"),
123
+ openQuestions: path.join(context.paths.bootstrapDir, "open-questions.md"),
124
+ qualityReport: path.join(context.paths.bootstrapDir, "quality-report.json"),
125
+ };
126
+ }
127
+
128
+ function contractRelativePath(context) {
129
+ return context.layout === "split"
130
+ ? "ops/contract/operating-contract.json"
131
+ : "contract/operating-contract.json";
132
+ }
133
+
134
+ function policyRelativePath(context) {
135
+ return context.layout === "split"
136
+ ? "ops/policy/autonomy.json"
137
+ : "policy/autonomy.json";
138
+ }
139
+
140
+ function firstParagraph(text) {
141
+ return String(text || "")
142
+ .split(/\r?\n\r?\n/)
143
+ .map((chunk) => chunk.replace(/^#\s+/gm, "").trim())
144
+ .find(Boolean) || "";
145
+ }
146
+
147
+ function inferServicesFromEnv(text) {
148
+ const upper = String(text || "").toUpperCase();
149
+ return SERVICE_HINTS.filter(([needle]) => upper.includes(needle.toUpperCase())).map(([, label]) => label);
150
+ }
151
+
152
+ function inferServicesFromDependencies(pkg) {
153
+ const deps = {
154
+ ...(pkg?.dependencies || {}),
155
+ ...(pkg?.devDependencies || {}),
156
+ };
157
+ const names = Object.keys(deps);
158
+ return SERVICE_HINTS.filter(([needle]) => names.some((name) => name.includes(needle))).map(([, label]) => label);
159
+ }
160
+
161
+ function scanProject(root) {
162
+ const context = config.ensureContext(root);
163
+ const scanRoot = context.appRoot;
164
+ const envPaths = config.envFilePaths(context);
165
+ const pkgPath = path.join(scanRoot, "package.json");
166
+ const readmePath = fs.existsSync(path.join(scanRoot, "README.md"))
167
+ ? path.join(scanRoot, "README.md")
168
+ : fs.existsSync(path.join(scanRoot, "README.mdx"))
169
+ ? path.join(scanRoot, "README.mdx")
170
+ : "";
171
+ const envExamplePath = fs.existsSync(envPaths.exampleFile)
172
+ ? envPaths.exampleFile
173
+ : fs.existsSync(envPaths.rootFile)
174
+ ? envPaths.rootFile
175
+ : "";
176
+
177
+ const pkg = safeJson(readText(pkgPath));
178
+ const readme = readText(readmePath);
179
+ const envExample = readText(envExamplePath);
180
+ const workflowsDir = path.join(context.workspaceRoot, ".github", "workflows");
181
+ const workflowFiles = fs.existsSync(workflowsDir)
182
+ ? fs.readdirSync(workflowsDir).filter((file) => file.endsWith(".yml") || file.endsWith(".yaml"))
183
+ : [];
184
+ const files = fs.readdirSync(scanRoot);
185
+ const stacks = [];
186
+
187
+ if (pkg) stacks.push("node");
188
+ if (files.some((file) => file.startsWith("requirements") || file === "pyproject.toml")) stacks.push("python");
189
+ if (files.includes("go.mod")) stacks.push("go");
190
+ if (files.includes("Cargo.toml")) stacks.push("rust");
191
+ if (files.includes("pom.xml")) stacks.push("java");
192
+ if (files.includes("Dockerfile") || files.some((file) => file.startsWith("docker-compose"))) stacks.push("docker");
193
+
194
+ const description = pkg?.description || firstParagraph(readme);
195
+ const title = pkg?.displayName || pkg?.name || path.basename(scanRoot);
196
+ const testCommands = unique([
197
+ pkg?.scripts?.test ? "npm test" : "",
198
+ fs.existsSync(path.join(scanRoot, "pytest.ini")) ? "pytest" : "",
199
+ fs.existsSync(path.join(scanRoot, "Cargo.toml")) ? "cargo test" : "",
200
+ fs.existsSync(path.join(scanRoot, "go.mod")) ? "go test ./..." : "",
201
+ ]);
202
+ const buildCommands = unique([
203
+ pkg?.scripts?.build ? "npm run build" : "",
204
+ fs.existsSync(path.join(scanRoot, "Dockerfile")) ? "docker build ." : "",
205
+ ]);
206
+ const services = unique([
207
+ ...inferServicesFromDependencies(pkg),
208
+ ...inferServicesFromEnv(envExample),
209
+ ]);
210
+ const gitRemote = git(["remote", "get-url", "origin"], context.workspaceRoot) || null;
211
+ const ciProviders = workflowFiles.length ? ["github-actions"] : [];
212
+
213
+ return {
214
+ title,
215
+ description,
216
+ stacks: unique(stacks),
217
+ services,
218
+ testCommands,
219
+ buildCommands,
220
+ ciProviders,
221
+ gitRemote,
222
+ readmeSummary: firstParagraph(readme),
223
+ payloadHint: pkg?.homepage || "",
224
+ sourceOfTruthHint: envExample ? t("bootstrap.infer.envSourceHint") : "",
225
+ workflowFiles,
226
+ };
227
+ }
228
+
229
+ function normalizeList(value) {
230
+ if (Array.isArray(value)) return value.map((item) => String(item).trim()).filter(Boolean);
231
+ return String(value || "")
232
+ .split(/\r?\n|[,;]+/)
233
+ .map((item) => item.trim())
234
+ .filter(Boolean);
235
+ }
236
+
237
+ function parseJsonValue(value) {
238
+ if (!String(value || "").trim()) return {};
239
+ try {
240
+ return JSON.parse(value);
241
+ } catch (_error) {
242
+ return {};
243
+ }
244
+ }
245
+
246
+ function getBootstrapState(control, contextOrRoot) {
247
+ const context = config.ensureContext(contextOrRoot);
248
+ const bootstrap = control.meta?.opera?.bootstrap;
249
+ if (!bootstrap) return null;
250
+ const relative = bootstrapRelativePaths(context);
251
+ return {
252
+ ...bootstrap,
253
+ handoffFiles: bootstrap.handoffFiles || {
254
+ markdown: relative.markdown,
255
+ json: relative.json,
256
+ },
257
+ intakeFiles: bootstrap.intakeFiles || {
258
+ json: relative.intakeJson,
259
+ specDossier: relative.specDossier,
260
+ },
261
+ reviewFiles: bootstrap.reviewFiles || {
262
+ openQuestions: relative.openQuestions,
263
+ qualityReport: relative.qualityReport,
264
+ },
265
+ };
266
+ }
267
+
268
+ function buildAvailableArtifacts(context, docsState) {
269
+ const artifacts = [];
270
+ if (docsState === "repo_docs") {
271
+ const readmePath = path.join(context.appRoot, "README.md");
272
+ if (fs.existsSync(readmePath)) {
273
+ artifacts.push({
274
+ kind: "readme",
275
+ location: "repo",
276
+ path: path.relative(context.workspaceRoot, readmePath).replace(/\\/g, "/"),
277
+ });
278
+ }
279
+ }
280
+ return artifacts;
281
+ }
282
+
283
+ function determineBootstrapMode(intake, requestedMode) {
284
+ const normalizedRequested = normalizeEnum(requestedMode || "auto", BOOTSTRAP_MODES) || "auto";
285
+
286
+ if (normalizedRequested === "handoff") {
287
+ return { requestedMode: normalizedRequested, mode: "agent_handoff", routeReason: "forced_handoff" };
288
+ }
289
+
290
+ if (normalizedRequested === "direct") {
291
+ const hasCore = intake.technicalLevel && intake.projectState && intake.documentationState && intake.decisionOwnership;
292
+ return {
293
+ requestedMode: normalizedRequested,
294
+ mode: hasCore ? "direct_cli" : "agent_handoff",
295
+ routeReason: hasCore ? "forced_direct" : "insufficient_docs",
296
+ };
297
+ }
298
+
299
+ if (!["high", "senior"].includes(intake.technicalLevel)) {
300
+ return { requestedMode: normalizedRequested, mode: "agent_handoff", routeReason: "non_technical_user" };
301
+ }
302
+ if (!["existing_repo", "advanced"].includes(intake.projectState)) {
303
+ return { requestedMode: normalizedRequested, mode: "agent_handoff", routeReason: "idea_stage" };
304
+ }
305
+ if (!["sos", "spec_dossier", "repo_docs"].includes(intake.documentationState)) {
306
+ return { requestedMode: normalizedRequested, mode: "agent_handoff", routeReason: "insufficient_docs" };
307
+ }
308
+ if (!["user", "shared"].includes(intake.decisionOwnership)) {
309
+ return { requestedMode: normalizedRequested, mode: "agent_handoff", routeReason: "agent_owned_decisions" };
310
+ }
311
+ return { requestedMode: normalizedRequested, mode: "direct_cli", routeReason: "technical_existing_project" };
312
+ }
313
+
314
+ function directMissingFields(discovery) {
315
+ const missing = [];
316
+ if (!discovery.problemStatement) missing.push("problemStatement");
317
+ if (!discovery.targetUser) missing.push("targetUser");
318
+ if (!discovery.singularDesiredOutcome) missing.push("singularDesiredOutcome");
319
+ if (!discovery.sourceOfTruth) missing.push("sourceOfTruth");
320
+ if (!discovery.payload) missing.push("payload");
321
+ if (!Object.keys(discovery.inputSchema || {}).length) missing.push("inputSchema");
322
+ if (!Object.keys(discovery.outputSchema || {}).length) missing.push("outputSchema");
323
+ return missing;
324
+ }
325
+
326
+ function buildHandoffPayload(control, profile, context) {
327
+ const relative = bootstrapRelativePaths(context);
328
+ return {
329
+ version: 1,
330
+ skill: "project-starter-skill",
331
+ technicalLevel: profile.technicalLevel,
332
+ explanationMode: explanationModeFor(profile.technicalLevel),
333
+ decisionOwnership: profile.discovery?.decisionOwnership || profile.decisionOwnership || null,
334
+ projectState: profile.projectState,
335
+ documentationState: profile.documentationState,
336
+ availableArtifacts: control.meta?.discovery?.availableArtifacts || [],
337
+ problemStatement: profile.discovery?.problemStatement || "",
338
+ targetUser: profile.discovery?.targetUser || "",
339
+ singularDesiredOutcome: profile.discovery?.singularDesiredOutcome || "",
340
+ files: {
341
+ intakeJson: relative.intakeJson,
342
+ specDossier: relative.specDossier,
343
+ openQuestions: relative.openQuestions,
344
+ },
345
+ };
346
+ }
347
+
348
+ function buildHandoffPrompt(control, profile) {
349
+ const lines = [
350
+ "# TrackOps agent handoff",
351
+ "",
352
+ "Use `project-starter-skill` as a discovery and structuring skill for this project.",
353
+ "",
354
+ "## User profile",
355
+ `- Technical level: ${profile.technicalLevel || "unknown"}`,
356
+ `- Explanation mode: ${explanationModeFor(profile.technicalLevel)}`,
357
+ `- Decision ownership: ${profile.discovery?.decisionOwnership || profile.decisionOwnership || "unknown"}`,
358
+ "",
359
+ "## Project state",
360
+ `- Project state: ${profile.projectState || "unknown"}`,
361
+ `- Documentation state: ${profile.documentationState || "unknown"}`,
362
+ "",
363
+ "## What to do",
364
+ "- Start from the user, not from architecture assumptions.",
365
+ "- Adapt depth and language to the user's technical level.",
366
+ "- If documentation exists, read it, summarize it, and consolidate it.",
367
+ "- If documentation does not exist, help the user turn the idea into a workable project specification.",
368
+ "- Write `ops/bootstrap/intake.json` with the structured discovery output.",
369
+ "- Write `ops/bootstrap/spec-dossier.md` with the narrative or technical specification that OPERA will ingest.",
370
+ "- Write `ops/bootstrap/open-questions.md` if important uncertainties remain.",
371
+ "- Include explicit fields for problem statement, target user, desired outcome, source of truth, delivery target, and schemas.",
372
+ ];
373
+
374
+ if (profile.discovery?.singularDesiredOutcome) {
375
+ lines.push("", `Known project intention: ${profile.discovery.singularDesiredOutcome}`);
376
+ }
377
+
378
+ return `${lines.join("\n")}\n`;
379
+ }
380
+
381
+ async function askQuestion(rl, message, defaultValue) {
382
+ const suffix = defaultValue ? ` (${defaultValue})` : "";
383
+ const answer = await rl.question(`${message}${suffix}: `);
384
+ return String(answer || "").trim() || String(defaultValue || "").trim();
385
+ }
386
+
387
+ async function collectBootstrapProfile(root, control, options = {}) {
388
+ const context = config.ensureContext(root);
389
+ const locale = config.getLocale(control);
390
+ setLocale(locale);
391
+ const scan = scanProject(context);
392
+ const previousUser = control.meta?.userProfile || {};
393
+ const previousDiscovery = control.meta?.discovery || {};
394
+ const previousBootstrap = getBootstrapState(control, context);
395
+ const previous = previousBootstrap?.discovery || {};
396
+ const interactive = options.interactive !== false && isInteractive();
397
+
398
+ const defaults = {
399
+ technicalLevel: normalizeEnum(options.technicalLevel || options.answers?.technicalLevel || previousUser.technicalLevel, TECHNICAL_LEVELS),
400
+ projectState: normalizeEnum(options.projectState || options.answers?.projectState || previousDiscovery.projectState, PROJECT_STATES),
401
+ documentationState: normalizeEnum(options.docsState || options.answers?.documentationState || previousDiscovery.documentationState, DOC_STATES),
402
+ decisionOwnership: normalizeEnum(options.decisionOwnership || options.answers?.decisionOwnership || previous.decisionOwnership, DECISION_OWNERSHIPS),
403
+ problemStatement: options.answers?.problemStatement || previous.problemStatement || "",
404
+ targetUser: options.answers?.targetUser || previous.targetUser || "",
405
+ singularDesiredOutcome:
406
+ options.answers?.singularDesiredOutcome ||
407
+ options.answers?.desiredOutcome ||
408
+ previous.singularDesiredOutcome ||
409
+ previous.desiredOutcome ||
410
+ scan.description ||
411
+ "",
412
+ userLanguage: options.answers?.userLanguage || previous.userLanguage || locale,
413
+ needsPlainLanguage: options.answers?.needsPlainLanguage ?? previous.needsPlainLanguage ?? false,
414
+ recommendedStack: normalizeList(options.answers?.recommendedStack || previous.recommendedStack || scan.stacks),
415
+ externalServices: normalizeList(options.answers?.externalServices || previous.externalServices || scan.services),
416
+ sourceOfTruth: options.answers?.sourceOfTruth || previous.sourceOfTruth || scan.sourceOfTruthHint || "",
417
+ payload: options.answers?.payload || previous.payload || scan.payloadHint || "",
418
+ behaviorRules: normalizeList(options.answers?.behaviorRules || previous.behaviorRules || ""),
419
+ inputSchema: options.answers?.inputSchema || previous.inputSchema || {},
420
+ outputSchema: options.answers?.outputSchema || previous.outputSchema || {},
421
+ architecturalInvariants: normalizeList(options.answers?.architecturalInvariants || previous.architecturalInvariants || ""),
422
+ pipeline: normalizeList(options.answers?.pipeline || previous.pipeline || ""),
423
+ templates: normalizeList(options.answers?.templates || previous.templates || ""),
424
+ availableArtifacts: Array.isArray(previousDiscovery.availableArtifacts) ? previousDiscovery.availableArtifacts : [],
425
+ };
426
+
427
+ const answers = { ...defaults };
428
+
429
+ if (interactive) {
430
+ console.log("");
431
+ console.log(t("bootstrap.header"));
432
+ console.log(t("bootstrap.subtitle"));
433
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
434
+ try {
435
+ answers.technicalLevel = normalizeEnum(await askQuestion(rl, t("bootstrap.question.technicalLevel"), defaults.technicalLevel || "medium"), TECHNICAL_LEVELS) || "medium";
436
+ answers.projectState = normalizeEnum(await askQuestion(rl, t("bootstrap.question.projectState"), defaults.projectState || "idea"), PROJECT_STATES) || "idea";
437
+ answers.documentationState = normalizeEnum(await askQuestion(rl, t("bootstrap.question.docsState"), defaults.documentationState || "none"), DOC_STATES) || "none";
438
+ answers.decisionOwnership = normalizeEnum(await askQuestion(rl, t("bootstrap.question.decisionOwnership"), defaults.decisionOwnership || "shared"), DECISION_OWNERSHIPS) || "shared";
439
+ } finally {
440
+ rl.close();
441
+ }
442
+ }
443
+
444
+ answers.availableArtifacts = answers.availableArtifacts.length
445
+ ? answers.availableArtifacts
446
+ : buildAvailableArtifacts(context, answers.documentationState);
447
+
448
+ const routing = determineBootstrapMode(answers, options.bootstrapMode);
449
+
450
+ if (routing.mode === "agent_handoff") {
451
+ return {
452
+ version: 2,
453
+ status: "awaiting_agent",
454
+ mode: "agent_handoff",
455
+ routeReason: routing.routeReason,
456
+ technicalLevel: answers.technicalLevel,
457
+ projectState: answers.projectState,
458
+ documentationState: answers.documentationState,
459
+ decisionOwnership: answers.decisionOwnership,
460
+ startedAt: previousBootstrap?.startedAt || nowIso(),
461
+ completedAt: null,
462
+ missingFields: ["intakeJson", "specDossier"],
463
+ discovery: answers,
464
+ inference: scan,
465
+ };
466
+ }
467
+
468
+ if (interactive) {
469
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
470
+ try {
471
+ answers.problemStatement = await askQuestion(rl, t("bootstrap.question.problemStatement"), defaults.problemStatement);
472
+ answers.targetUser = await askQuestion(rl, t("bootstrap.question.targetUser"), defaults.targetUser);
473
+ answers.singularDesiredOutcome = await askQuestion(rl, t("bootstrap.question.desiredOutcome"), defaults.singularDesiredOutcome);
474
+ answers.externalServices = normalizeList(await askQuestion(rl, t("bootstrap.question.externalServices"), defaults.externalServices.join(", ")));
475
+ answers.sourceOfTruth = await askQuestion(rl, t("bootstrap.question.sourceOfTruth"), defaults.sourceOfTruth);
476
+ answers.payload = await askQuestion(rl, t("bootstrap.question.payload"), defaults.payload);
477
+ answers.behaviorRules = normalizeList(await askQuestion(rl, t("bootstrap.question.behaviorRules"), defaults.behaviorRules.join("; ")));
478
+ answers.inputSchema = parseJsonValue(await askQuestion(rl, t("bootstrap.question.inputSchema"), JSON.stringify(defaults.inputSchema)));
479
+ answers.outputSchema = parseJsonValue(await askQuestion(rl, t("bootstrap.question.outputSchema"), JSON.stringify(defaults.outputSchema)));
480
+ answers.architecturalInvariants = normalizeList(await askQuestion(rl, t("bootstrap.question.invariants"), defaults.architecturalInvariants.join("; ")));
481
+ answers.pipeline = normalizeList(await askQuestion(rl, t("bootstrap.question.pipeline"), defaults.pipeline.join("; ")));
482
+ answers.templates = normalizeList(await askQuestion(rl, t("bootstrap.question.templates"), defaults.templates.join(", ")));
483
+ } finally {
484
+ rl.close();
485
+ }
486
+ }
487
+
488
+ const missingFields = directMissingFields(answers);
489
+ return {
490
+ version: 2,
491
+ status: missingFields.length ? "awaiting_intake" : "completed",
492
+ mode: "direct_cli",
493
+ routeReason: routing.routeReason,
494
+ technicalLevel: answers.technicalLevel,
495
+ projectState: answers.projectState,
496
+ documentationState: answers.documentationState,
497
+ decisionOwnership: answers.decisionOwnership,
498
+ startedAt: previousBootstrap?.startedAt || nowIso(),
499
+ completedAt: missingFields.length ? null : nowIso(),
500
+ missingFields,
501
+ discovery: answers,
502
+ inference: scan,
503
+ };
504
+ }
505
+
506
+ function isVirginGenesis(content) {
507
+ const text = String(content || "");
508
+ return !text.trim() || /TODO:/i.test(text) || /The Constitution of the project/i.test(text) || /La Constitución del proyecto/i.test(text);
509
+ }
510
+
511
+ function parseSpecSections(specText) {
512
+ const sections = {};
513
+ const lines = String(specText || "").split(/\r?\n/);
514
+ let current = null;
515
+ for (const line of lines) {
516
+ const heading = line.match(/^##\s+(.+?)\s*$/);
517
+ if (heading) {
518
+ current = heading[1].trim().toLowerCase();
519
+ sections[current] = [];
520
+ continue;
521
+ }
522
+ if (current) sections[current].push(line);
523
+ }
524
+ return Object.fromEntries(
525
+ Object.entries(sections).map(([key, value]) => [key, value.join("\n").trim()]),
526
+ );
527
+ }
528
+
529
+ function buildOpenQuestions(missingFields, contradictions) {
530
+ const lines = ["# Open questions", ""];
531
+ if (missingFields.length) {
532
+ lines.push("## Missing fields", "");
533
+ missingFields.forEach((field) => lines.push(`- ${field}`));
534
+ lines.push("");
535
+ }
536
+ if (contradictions.length) {
537
+ lines.push("## Contradictions", "");
538
+ contradictions.forEach((item) => lines.push(`- ${item}`));
539
+ lines.push("");
540
+ }
541
+ if (!missingFields.length && !contradictions.length) {
542
+ lines.push("- None.");
543
+ }
544
+ return `${lines.join("\n")}\n`;
545
+ }
546
+
547
+ function qualityStatusFor(missingFields, contradictions) {
548
+ if (missingFields.length >= 2) return "blocked";
549
+ if (missingFields.length || contradictions.length) return "needs_review";
550
+ return "ready";
551
+ }
552
+
553
+ function contractReadinessFor(profile, qualityStatus) {
554
+ if (qualityStatus === "blocked") return "hypothesis";
555
+ if (qualityStatus === "needs_review") return "provisional";
556
+ if (
557
+ ["sos", "spec_dossier"].includes(profile.documentationState) &&
558
+ profile.discovery?.decisionOwnership === "user" &&
559
+ profile.mode === "direct_cli"
560
+ ) {
561
+ return "locked";
562
+ }
563
+ return "verified";
564
+ }
565
+
566
+ function buildQualityReport(context, profile, specText) {
567
+ const sections = parseSpecSections(specText);
568
+ const missingFields = directMissingFields(profile.discovery);
569
+ if (!profile.discovery.decisionOwnership) missingFields.push("decisionOwnership");
570
+ if (!profile.discovery.problemStatement) missingFields.push("problemStatement");
571
+ if (!profile.discovery.targetUser) missingFields.push("targetUser");
572
+
573
+ const contradictions = [];
574
+ const mappings = [
575
+ ["problem statement", "problemStatement", profile.discovery.problemStatement],
576
+ ["target user", "targetUser", profile.discovery.targetUser],
577
+ ["singular desired outcome", "singularDesiredOutcome", profile.discovery.singularDesiredOutcome],
578
+ ["delivery target", "payload", profile.discovery.payload],
579
+ ["source of truth", "sourceOfTruth", profile.discovery.sourceOfTruth],
580
+ ];
581
+ for (const [sectionName, fieldName, expected] of mappings) {
582
+ const actual = sections[sectionName];
583
+ if (!actual) {
584
+ missingFields.push(fieldName);
585
+ continue;
586
+ }
587
+ if (expected && actual && normalizeText(actual) !== normalizeText(expected)) {
588
+ contradictions.push(`${sectionName}: spec dossier and intake disagree`);
589
+ }
590
+ }
591
+
592
+ const status = qualityStatusFor(unique(missingFields), contradictions);
593
+ const contractReadiness = contractReadinessFor(profile, status);
594
+ const report = {
595
+ version: 1,
596
+ status,
597
+ missingFields: unique(missingFields),
598
+ contradictions,
599
+ contractReadiness,
600
+ validatedAt: nowIso(),
601
+ handoffMode: profile.mode,
602
+ };
603
+ const files = bootstrapFilePaths(context);
604
+ writeJson(files.qualityReport, report);
605
+ writeText(files.openQuestions, buildOpenQuestions(report.missingFields, report.contradictions));
606
+ return report;
607
+ }
608
+
609
+ function normalizeText(value) {
610
+ return String(value || "").trim().replace(/\s+/g, " ").toLowerCase();
611
+ }
612
+
613
+ function buildOperatingContract(control, profile, qualityReport, context) {
614
+ const discovery = profile.discovery || {};
615
+ return {
616
+ version: CONTRACT_VERSION,
617
+ intent: {
618
+ problemStatement: discovery.problemStatement || "",
619
+ targetUser: discovery.targetUser || "",
620
+ singularDesiredOutcome: discovery.singularDesiredOutcome || "",
621
+ deliveryTarget: discovery.payload || "",
622
+ },
623
+ userModel: {
624
+ technicalLevel: profile.technicalLevel || null,
625
+ explanationMode: explanationModeFor(profile.technicalLevel),
626
+ decisionOwnership: discovery.decisionOwnership || profile.decisionOwnership || null,
627
+ language: discovery.userLanguage || config.getLocale(control),
628
+ needsPlainLanguage: Boolean(discovery.needsPlainLanguage || profile.technicalLevel === "low"),
629
+ },
630
+ evidence: {
631
+ projectState: profile.projectState || null,
632
+ documentationState: profile.documentationState || null,
633
+ sourceArtifacts: discovery.availableArtifacts || [],
634
+ repoScan: profile.inference || {},
635
+ },
636
+ system: {
637
+ sourceOfTruth: discovery.sourceOfTruth || "",
638
+ externalServices: discovery.externalServices || [],
639
+ inputSchema: discovery.inputSchema || {},
640
+ outputSchema: discovery.outputSchema || {},
641
+ behaviorRules: discovery.behaviorRules || [],
642
+ architecturalInvariants: discovery.architecturalInvariants || [],
643
+ },
644
+ execution: {
645
+ pipeline: discovery.pipeline || [],
646
+ templates: discovery.templates || [],
647
+ phaseModel: "opera-v3",
648
+ taskSeeds: buildSeedTasks(control, profile).map((task) => task.id),
649
+ },
650
+ governance: {
651
+ policyFile: policyRelativePath(context),
652
+ riskProfile: "standard",
653
+ approvalRules: [
654
+ "destructive_changes_require_approval",
655
+ "production_deploy_requires_approval",
656
+ "external_side_effects_require_approval",
657
+ ],
658
+ },
659
+ quality: {
660
+ contractReadiness: qualityReport.contractReadiness,
661
+ openQuestions: qualityReport.missingFields.concat(qualityReport.contradictions),
662
+ lastValidatedAt: qualityReport.validatedAt,
663
+ },
664
+ };
665
+ }
666
+
667
+ function writeAutonomyPolicy(context) {
668
+ const payload = {
669
+ version: 1,
670
+ defaultRiskProfile: "standard",
671
+ levels: {
672
+ green: ["read_files", "run_tests", "update_operational_docs", "write_tmp_files"],
673
+ yellow: ["install_dependencies", "change_structure", "modify_pipeline"],
674
+ red: ["delete_persistent_data", "deploy_to_production", "external_side_effects", "security_changes"],
675
+ },
676
+ approvalRules: {
677
+ destructive_changes_require_approval: true,
678
+ production_deploy_requires_approval: true,
679
+ external_side_effects_require_approval: true,
680
+ },
681
+ };
682
+ writeJson(context.paths.autonomyPolicyFile, payload);
683
+ }
684
+
685
+ function renderGenesis(control, contract) {
686
+ const locale = config.getLocale(control);
687
+ const templatePath = resolveLocalizedFile(TEMPLATES_DIR, locale, "genesis.md");
688
+ let content = fs.readFileSync(templatePath, "utf8");
689
+ const rules = (contract.system.behaviorRules || []).map((item) => `- ${item}`).join("\n") || `- ${t("bootstrap.noneDefined")}`;
690
+ const invariants = (contract.system.architecturalInvariants || []).map((item) => `- ${item}`).join("\n") || `- ${t("bootstrap.noneDefined")}`;
691
+ const services = (contract.system.externalServices || []).length
692
+ ? contract.system.externalServices.map((item) => `| ${item} | ${t("bootstrap.servicePending")} | ${t("bootstrap.servicePending")} |`).join("\n")
693
+ : `| — | — | — |`;
694
+ const pipeline = (contract.execution.pipeline || []).length
695
+ ? contract.execution.pipeline.map((item) => `- ${item}`).join("\n")
696
+ : `- ${t("bootstrap.noneDefined")}`;
697
+ const templates = (contract.execution.templates || []).length
698
+ ? contract.execution.templates.map((item) => `- \`${item}\``).join("\n")
699
+ : `- ${t("bootstrap.noneDefined")}`;
700
+ const schema = JSON.stringify({
701
+ input: {
702
+ source: contract.system.sourceOfTruth,
703
+ schema: contract.system.inputSchema || {},
704
+ },
705
+ output: {
706
+ destination: contract.intent.deliveryTarget,
707
+ schema: contract.system.outputSchema || {},
708
+ },
709
+ }, null, 2);
710
+
711
+ const replacements = {
712
+ PROJECT_NAME: control.meta.projectName || "Project",
713
+ DESIRED_OUTCOME: contract.intent.singularDesiredOutcome || t("bootstrap.pendingValue"),
714
+ SERVICES_TABLE: services,
715
+ SOURCE_OF_TRUTH: contract.system.sourceOfTruth || t("bootstrap.pendingValue"),
716
+ PAYLOAD: contract.intent.deliveryTarget || t("bootstrap.pendingValue"),
717
+ BEHAVIOR_RULES: rules,
718
+ DATA_SCHEMA: schema,
719
+ ARCHITECTURAL_INVARIANTS: invariants,
720
+ PIPELINE_ITEMS: pipeline,
721
+ TEMPLATE_ITEMS: templates,
722
+ };
723
+
724
+ for (const [key, value] of Object.entries(replacements)) {
725
+ content = content.replace(new RegExp(`\\{\\{${key}\\}\\}`, "g"), value);
726
+ }
727
+ return content;
728
+ }
729
+
730
+ function buildSeedTasks(control, profile) {
731
+ const phaseOrder = config.getPhases(control);
732
+ const firstTaskStatus =
733
+ profile.status === "completed"
734
+ ? "completed"
735
+ : ["blocked", "needs_review"].includes(profile.status)
736
+ ? "blocked"
737
+ : profile.mode === "agent_handoff"
738
+ ? (profile.status === "awaiting_agent" ? "blocked" : "pending")
739
+ : "blocked";
740
+ const tasks = [
741
+ {
742
+ id: "opera-bootstrap",
743
+ origin: "bootstrap",
744
+ title: t("bootstrap.task.bootstrap.title"),
745
+ phase: phaseOrder[0]?.id || "O",
746
+ stream: "Operations",
747
+ priority: "P0",
748
+ status: firstTaskStatus,
749
+ required: true,
750
+ dependsOn: [],
751
+ summary: profile.mode === "agent_handoff"
752
+ ? t("bootstrap.task.bootstrap.handoffSummary")
753
+ : t("bootstrap.task.bootstrap.summary"),
754
+ acceptance: profile.mode === "agent_handoff"
755
+ ? [
756
+ t("bootstrap.acceptance.intake"),
757
+ t("bootstrap.acceptance.specDossier"),
758
+ t("bootstrap.acceptance.resume"),
759
+ ]
760
+ : [
761
+ t("bootstrap.acceptance.discovery"),
762
+ t("bootstrap.acceptance.schema"),
763
+ t("bootstrap.acceptance.rules"),
764
+ t("bootstrap.acceptance.plan"),
765
+ ],
766
+ blocker: profile.status === "completed"
767
+ ? undefined
768
+ : profile.mode === "agent_handoff"
769
+ ? t("bootstrap.blocker.awaitingAgent")
770
+ : t("bootstrap.blocker.missingData"),
771
+ history: [{ at: nowIso(), action: "create", note: t("bootstrap.history.seeded") }],
772
+ },
773
+ {
774
+ id: "opera-prove-integrations",
775
+ origin: "bootstrap",
776
+ title: t("bootstrap.task.prove.title"),
777
+ phase: phaseOrder[1]?.id || "P",
778
+ stream: "Operations",
779
+ priority: "P0",
780
+ status: "pending",
781
+ required: true,
782
+ dependsOn: ["opera-bootstrap"],
783
+ summary: t("bootstrap.task.prove.summary"),
784
+ acceptance: [
785
+ t("bootstrap.acceptance.env"),
786
+ t("bootstrap.acceptance.tests"),
787
+ t("bootstrap.acceptance.shape"),
788
+ t("bootstrap.acceptance.findings"),
789
+ ],
790
+ history: [{ at: nowIso(), action: "create", note: t("bootstrap.history.seeded") }],
791
+ },
792
+ {
793
+ id: "opera-structure-system",
794
+ origin: "bootstrap",
795
+ title: t("bootstrap.task.structure.title"),
796
+ phase: phaseOrder[2]?.id || "E",
797
+ stream: "Operations",
798
+ priority: "P1",
799
+ status: "pending",
800
+ required: true,
801
+ dependsOn: ["opera-prove-integrations"],
802
+ summary: t("bootstrap.task.structure.summary"),
803
+ acceptance: [
804
+ t("bootstrap.acceptance.sops"),
805
+ t("bootstrap.acceptance.tools"),
806
+ t("bootstrap.acceptance.graph"),
807
+ t("bootstrap.acceptance.integration"),
808
+ ],
809
+ history: [{ at: nowIso(), action: "create", note: t("bootstrap.history.seeded") }],
810
+ },
811
+ {
812
+ id: "opera-refine-delivery",
813
+ origin: "bootstrap",
814
+ title: t("bootstrap.task.refine.title"),
815
+ phase: phaseOrder[3]?.id || "R",
816
+ stream: "Operations",
817
+ priority: "P1",
818
+ status: "pending",
819
+ required: true,
820
+ dependsOn: ["opera-structure-system"],
821
+ summary: t("bootstrap.task.refine.summary"),
822
+ acceptance: [
823
+ t("bootstrap.acceptance.outputs"),
824
+ t("bootstrap.acceptance.delivery"),
825
+ t("bootstrap.acceptance.ui"),
826
+ ],
827
+ history: [{ at: nowIso(), action: "create", note: t("bootstrap.history.seeded") }],
828
+ },
829
+ {
830
+ id: "opera-automate-runtime",
831
+ origin: "bootstrap",
832
+ title: t("bootstrap.task.automate.title"),
833
+ phase: phaseOrder[4]?.id || "A",
834
+ stream: "Operations",
835
+ priority: "P2",
836
+ status: "pending",
837
+ required: true,
838
+ dependsOn: ["opera-refine-delivery"],
839
+ summary: t("bootstrap.task.automate.summary"),
840
+ acceptance: [
841
+ t("bootstrap.acceptance.tmp"),
842
+ t("bootstrap.acceptance.deploy"),
843
+ t("bootstrap.acceptance.triggers"),
844
+ t("bootstrap.acceptance.smoke"),
845
+ ],
846
+ history: [{ at: nowIso(), action: "create", note: t("bootstrap.history.seeded") }],
847
+ },
848
+ ];
849
+
850
+ return tasks;
851
+ }
852
+
853
+ function createAwaitingBootstrapState(context) {
854
+ return {
855
+ version: CONTRACT_VERSION,
856
+ mode: null,
857
+ status: "awaiting_intake",
858
+ routeReason: null,
859
+ technicalLevel: null,
860
+ projectState: null,
861
+ documentationState: null,
862
+ decisionOwnership: null,
863
+ startedAt: nowIso(),
864
+ completedAt: null,
865
+ missingFields: [],
866
+ discovery: {},
867
+ handoffFiles: {
868
+ markdown: bootstrapRelativePaths(context).markdown,
869
+ json: bootstrapRelativePaths(context).json,
870
+ },
871
+ intakeFiles: {
872
+ json: bootstrapRelativePaths(context).intakeJson,
873
+ specDossier: bootstrapRelativePaths(context).specDossier,
874
+ },
875
+ reviewFiles: {
876
+ openQuestions: bootstrapRelativePaths(context).openQuestions,
877
+ qualityReport: bootstrapRelativePaths(context).qualityReport,
878
+ },
879
+ inference: scanProject(context),
880
+ };
881
+ }
882
+
883
+ function applyBootstrap(root, control, profile) {
884
+ const context = config.ensureContext(root);
885
+ setLocale(config.getLocale(control));
886
+ control.meta = control.meta || {};
887
+ control.meta.opera = control.meta.opera || {};
888
+ control.meta.opera.model = "v3";
889
+ writeAutonomyPolicy(context);
890
+ control.meta.userProfile = {
891
+ technicalLevel: profile.technicalLevel || null,
892
+ explanationMode: explanationModeFor(profile.technicalLevel),
893
+ capturedAt: nowIso(),
894
+ };
895
+ control.meta.discovery = {
896
+ projectState: profile.projectState || null,
897
+ documentationState: profile.documentationState || null,
898
+ availableArtifacts: Array.isArray(profile.discovery?.availableArtifacts) ? profile.discovery.availableArtifacts : [],
899
+ };
900
+ const relative = bootstrapRelativePaths(context);
901
+ control.meta.opera.bootstrap = {
902
+ ...profile,
903
+ handoffFiles: {
904
+ markdown: relative.markdown,
905
+ json: relative.json,
906
+ },
907
+ intakeFiles: {
908
+ json: relative.intakeJson,
909
+ specDossier: relative.specDossier,
910
+ },
911
+ reviewFiles: {
912
+ openQuestions: relative.openQuestions,
913
+ qualityReport: relative.qualityReport,
914
+ },
915
+ };
916
+ control.meta.currentFocus = profile.discovery.singularDesiredOutcome || profile.discovery.desiredOutcome || t("bootstrap.defaultFocus");
917
+ control.meta.deliveryTarget = profile.discovery.payload || t("bootstrap.defaultTarget");
918
+ control.meta.focusPhase = profile.status === "completed" ? "P" : "O";
919
+ control.meta.phases = config.getPhases(control);
920
+ control.meta.opera.legacyStatus = "supported";
921
+
922
+ const remainingTasks = (control.tasks || []).filter((task) => task.id !== "ops-bootstrap" && task.origin !== "bootstrap");
923
+ control.tasks = [...remainingTasks, ...buildSeedTasks(control, profile)];
924
+
925
+ control.decisionsPending = (profile.missingFields || [])
926
+ .filter((field) => !["intakeJson", "specDossier"].includes(field))
927
+ .map((field) => ({
928
+ owner: "user",
929
+ title: t(`bootstrap.field.${field}`),
930
+ impact: t("bootstrap.decisionImpact"),
931
+ }));
932
+ if (profile.mode === "agent_handoff" && profile.status !== "completed") {
933
+ control.decisionsPending.unshift({
934
+ owner: "user",
935
+ title: t("bootstrap.pendingDecision.handoff"),
936
+ impact: t("bootstrap.pendingDecision.handoffImpact"),
937
+ });
938
+ }
939
+
940
+ control.findings = control.findings || [];
941
+ const files = bootstrapFilePaths(context);
942
+ if (profile.mode === "agent_handoff") {
943
+ writeJson(files.json, buildHandoffPayload(control, profile, context));
944
+ writeText(files.markdown, buildHandoffPrompt(control, profile));
945
+ if (!fs.existsSync(files.specDossier)) {
946
+ writeText(files.specDossier, "# Spec dossier\n\nUse this file to consolidate the project specification before OPERA ingest.\n");
947
+ }
948
+ writeText(files.openQuestions, buildOpenQuestions(["intakeJson", "specDossier"], []));
949
+ }
950
+ const genesisPath = context.paths.genesisFile;
951
+ if (profile.status === "completed") {
952
+ const specText = fs.existsSync(files.specDossier) && readText(files.specDossier).trim()
953
+ ? readText(files.specDossier)
954
+ : `## Problem statement\n${profile.discovery.problemStatement || ""}\n\n## Target user\n${profile.discovery.targetUser || ""}\n\n## Singular desired outcome\n${profile.discovery.singularDesiredOutcome || ""}\n\n## Delivery target\n${profile.discovery.payload || ""}\n\n## Source of truth\n${profile.discovery.sourceOfTruth || ""}\n`;
955
+ const qualityReport = profile.qualityReport || buildQualityReport(context, profile, specText);
956
+ const contract = buildOperatingContract(control, profile, qualityReport, context);
957
+ writeJson(context.paths.contractFile, contract);
958
+ fs.writeFileSync(genesisPath, `${renderGenesis(control, contract)}\n`, "utf8");
959
+ control.meta.opera.contractVersion = CONTRACT_VERSION;
960
+ control.meta.opera.contractReadiness = qualityReport.contractReadiness;
961
+ control.meta.opera.contractFile = contractRelativePath(context);
962
+ control.meta.opera.qualityStatus = qualityReport.status;
963
+ } else if (["needs_review", "blocked"].includes(profile.status)) {
964
+ control.meta.opera.contractVersion = null;
965
+ control.meta.opera.contractReadiness = profile.qualityReport?.contractReadiness || "hypothesis";
966
+ control.meta.opera.qualityStatus = profile.qualityReport?.status || profile.status;
967
+ } else {
968
+ control.meta.opera.contractVersion = null;
969
+ control.meta.opera.contractReadiness = "hypothesis";
970
+ control.meta.opera.qualityStatus = profile.status;
971
+ }
972
+
973
+ config.saveControl(context, control);
974
+ return control;
975
+ }
976
+
977
+ function resumeBootstrap(root, control) {
978
+ const context = config.ensureContext(root);
979
+ const bootstrap = getBootstrapState(control, context);
980
+ if (!bootstrap) {
981
+ return { resumed: false, status: "awaiting_intake", reason: "no_bootstrap_state" };
982
+ }
983
+
984
+ const files = bootstrapFilePaths(context);
985
+ const intake = readJson(files.intakeJson);
986
+ const specDossier = readText(files.specDossier);
987
+ if (!intake || !specDossier.trim()) {
988
+ return { resumed: false, status: "awaiting_agent", reason: "missing_agent_artifacts" };
989
+ }
990
+
991
+ const discovery = {
992
+ ...bootstrap.discovery,
993
+ ...intake,
994
+ singularDesiredOutcome: intake.singularDesiredOutcome || bootstrap.discovery?.singularDesiredOutcome || "",
995
+ externalServices: normalizeList(intake.externalServices || bootstrap.discovery?.externalServices || []),
996
+ behaviorRules: normalizeList(intake.behaviorRules || bootstrap.discovery?.behaviorRules || []),
997
+ architecturalInvariants: normalizeList(intake.architecturalInvariants || bootstrap.discovery?.architecturalInvariants || []),
998
+ pipeline: normalizeList(intake.pipeline || bootstrap.discovery?.pipeline || []),
999
+ templates: normalizeList(intake.templates || bootstrap.discovery?.templates || []),
1000
+ inputSchema: intake.inputSchema || bootstrap.discovery?.inputSchema || {},
1001
+ outputSchema: intake.outputSchema || bootstrap.discovery?.outputSchema || {},
1002
+ };
1003
+ const missingFields = directMissingFields(discovery);
1004
+ const profile = {
1005
+ ...bootstrap,
1006
+ mode: bootstrap.mode || "agent_handoff",
1007
+ status: missingFields.length ? "needs_review" : "completed",
1008
+ technicalLevel: intake.technicalLevel || bootstrap.technicalLevel || null,
1009
+ projectState: intake.projectState || bootstrap.projectState || null,
1010
+ documentationState: intake.documentationState || bootstrap.documentationState || null,
1011
+ decisionOwnership: intake.decisionOwnership || bootstrap.decisionOwnership || null,
1012
+ completedAt: missingFields.length ? null : nowIso(),
1013
+ missingFields,
1014
+ discovery,
1015
+ inference: scanProject(context),
1016
+ };
1017
+ const qualityReport = buildQualityReport(context, profile, specDossier);
1018
+ profile.status = qualityReport.status === "ready" ? "completed" : qualityReport.status;
1019
+ profile.completedAt = qualityReport.status === "ready" ? nowIso() : null;
1020
+ profile.qualityReport = qualityReport;
1021
+ return { resumed: true, profile };
1022
+ }
1023
+
1024
+ function detectLegacyBootstrap(root, control) {
1025
+ const context = config.ensureContext(root);
1026
+ if (!config.isOperaInstalled(control)) return null;
1027
+ if (control.meta?.opera?.bootstrap) return getBootstrapState(control, context);
1028
+ if (control.meta?.opera?.model === "v3") {
1029
+ return createAwaitingBootstrapState(context);
1030
+ }
1031
+ if (!fs.existsSync(context.paths.contractFile)) {
1032
+ return {
1033
+ version: CONTRACT_VERSION,
1034
+ status: "legacy_unsupported",
1035
+ mode: null,
1036
+ routeReason: "legacy_unsupported",
1037
+ technicalLevel: null,
1038
+ projectState: null,
1039
+ documentationState: null,
1040
+ decisionOwnership: null,
1041
+ startedAt: control.meta?.opera?.installedAt || nowIso(),
1042
+ completedAt: null,
1043
+ missingFields: [],
1044
+ discovery: {},
1045
+ inference: scanProject(context),
1046
+ };
1047
+ }
1048
+ return null;
1049
+ }
1050
+
1051
+ module.exports = {
1052
+ TECHNICAL_LEVELS,
1053
+ PROJECT_STATES,
1054
+ DOC_STATES,
1055
+ DECISION_OWNERSHIPS,
1056
+ BOOTSTRAP_MODES,
1057
+ QUALITY_STATUSES,
1058
+ CONTRACT_READINESS,
1059
+ CONTRACT_VERSION,
1060
+ bootstrapFilePaths,
1061
+ bootstrapRelativePaths,
1062
+ contractRelativePath,
1063
+ policyRelativePath,
1064
+ scanProject,
1065
+ collectBootstrapProfile,
1066
+ applyBootstrap,
1067
+ resumeBootstrap,
1068
+ detectLegacyBootstrap,
1069
+ getBootstrapState,
1070
+ buildQualityReport,
1071
+ buildOperatingContract,
1072
+ writeAutonomyPolicy,
1073
+ createAwaitingBootstrapState,
1074
+ isVirginGenesis,
1075
+ };