trackops 1.1.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +194 -230
  2. package/bin/trackops.js +54 -28
  3. package/lib/config.js +14 -10
  4. package/lib/control.js +44 -32
  5. package/lib/env.js +18 -1
  6. package/lib/init.js +40 -6
  7. package/lib/opera-bootstrap.js +825 -273
  8. package/lib/opera.js +360 -110
  9. package/lib/preferences.js +74 -0
  10. package/lib/runtime-state.js +144 -0
  11. package/lib/server.js +155 -25
  12. package/locales/en.json +136 -42
  13. package/locales/es.json +136 -42
  14. package/package.json +2 -1
  15. package/scripts/postinstall-locale.js +21 -0
  16. package/scripts/smoke-tests.js +130 -5
  17. package/scripts/validate-skill.js +2 -1
  18. package/skills/trackops/SKILL.md +57 -32
  19. package/skills/trackops/agents/openai.yaml +1 -1
  20. package/skills/trackops/references/activation.md +50 -16
  21. package/skills/trackops/references/troubleshooting.md +35 -20
  22. package/skills/trackops/references/workflow.md +18 -12
  23. package/skills/trackops/scripts/bootstrap-trackops.js +9 -7
  24. package/skills/trackops/skill.json +4 -4
  25. package/templates/opera/agent.md +10 -9
  26. package/templates/opera/architecture/dependency-graph.md +24 -0
  27. package/templates/opera/architecture/runtime-automation.md +24 -0
  28. package/templates/opera/architecture/runtime-operations.md +34 -0
  29. package/templates/opera/en/agent.md +21 -20
  30. package/templates/opera/en/architecture/dependency-graph.md +24 -0
  31. package/templates/opera/en/architecture/runtime-automation.md +24 -0
  32. package/templates/opera/en/architecture/runtime-operations.md +34 -0
  33. package/templates/opera/en/reviews/delivery-audit.md +18 -0
  34. package/templates/opera/en/reviews/integration-audit.md +18 -0
  35. package/templates/opera/en/router.md +19 -9
  36. package/templates/opera/reviews/delivery-audit.md +18 -0
  37. package/templates/opera/reviews/integration-audit.md +18 -0
  38. package/templates/opera/router.md +15 -5
  39. package/templates/skills/opera-contract-auditor/SKILL.md +38 -0
  40. package/templates/skills/opera-contract-auditor/locales/en/SKILL.md +38 -0
  41. package/templates/skills/opera-policy-guard/SKILL.md +26 -0
  42. package/templates/skills/opera-policy-guard/locales/en/SKILL.md +26 -0
  43. package/templates/skills/project-starter-skill/SKILL.md +89 -164
  44. package/templates/skills/project-starter-skill/locales/en/SKILL.md +104 -24
  45. package/ui/js/views/overview.js +16 -12
  46. package/templates/etapa/agent.md +0 -26
  47. package/templates/etapa/genesis.md +0 -94
  48. package/templates/etapa/references/autonomy-and-recovery.md +0 -117
  49. package/templates/etapa/references/etapa-cycle.md +0 -193
  50. package/templates/etapa/registry.md +0 -28
  51. package/templates/etapa/router.md +0 -39
@@ -12,7 +12,7 @@ const { resolveLocalizedFile } = require("./resources");
12
12
 
13
13
  const TEMPLATES_DIR = path.join(__dirname, "..", "templates", "opera");
14
14
 
15
- const SERVICE_HINTS = [
15
+ const SERVICE_HINTS = [
16
16
  ["openai", "OpenAI"],
17
17
  ["anthropic", "Anthropic"],
18
18
  ["stripe", "Stripe"],
@@ -24,8 +24,17 @@ const SERVICE_HINTS = [
24
24
  ["s3", "Amazon S3"],
25
25
  ["aws", "AWS"],
26
26
  ["gcp", "Google Cloud"],
27
- ["azure", "Azure"],
28
- ];
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;
29
38
 
30
39
  function nowIso() {
31
40
  return new Date().toISOString();
@@ -37,9 +46,27 @@ function git(args, root) {
37
46
  return result.stdout.trim();
38
47
  }
39
48
 
40
- function readText(filePath) {
41
- return fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf8") : "";
42
- }
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
+ }
43
70
 
44
71
  function safeJson(text) {
45
72
  try {
@@ -49,9 +76,66 @@ function safeJson(text) {
49
76
  }
50
77
  }
51
78
 
52
- function unique(items) {
53
- return [...new Set(items.filter(Boolean))];
54
- }
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
+ }
55
139
 
56
140
  function firstParagraph(text) {
57
141
  return String(text || "")
@@ -150,132 +234,490 @@ function normalizeList(value) {
150
234
  .filter(Boolean);
151
235
  }
152
236
 
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
- }
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
+ }
167
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
+
168
387
  async function collectBootstrapProfile(root, control, options = {}) {
169
388
  const context = config.ensureContext(root);
170
389
  const locale = config.getLocale(control);
171
390
  setLocale(locale);
172
391
  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) {
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) {
193
430
  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);
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
+ }
268
505
 
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,
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,
279
721
  TEMPLATE_ITEMS: templates,
280
722
  };
281
723
 
@@ -285,29 +727,49 @@ function renderGenesis(control, profile) {
285
727
  return content;
286
728
  }
287
729
 
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
- },
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
+ },
311
773
  {
312
774
  id: "opera-prove-integrations",
313
775
  origin: "bootstrap",
@@ -385,139 +847,229 @@ function buildSeedTasks(control, profile) {
385
847
  },
386
848
  ];
387
849
 
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
-
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
+
455
883
  function applyBootstrap(root, control, profile) {
456
884
  const context = config.ensureContext(root);
457
885
  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
- }));
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)];
473
924
 
474
- control.findings = control.findings || [];
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
+ }
475
950
  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
-
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
+
493
973
  config.saveControl(context, control);
494
974
  return control;
495
975
  }
496
976
 
497
- function detectLegacyBootstrap(root, control) {
977
+ function resumeBootstrap(root, control) {
498
978
  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: {},
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,
513
1015
  inference: scanProject(context),
514
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 };
515
1022
  }
516
-
517
- module.exports = {
518
- scanProject,
519
- collectBootstrapProfile,
520
- applyBootstrap,
521
- detectLegacyBootstrap,
522
- isVirginGenesis,
523
- };
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
+ };