trackops 2.0.4 → 2.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +660 -575
  3. package/bin/trackops.js +127 -106
  4. package/lib/cli-format.js +118 -0
  5. package/lib/config.js +352 -326
  6. package/lib/control.js +408 -246
  7. package/lib/env.js +234 -222
  8. package/lib/i18n.js +5 -4
  9. package/lib/init.js +390 -282
  10. package/lib/locale.js +41 -41
  11. package/lib/opera-bootstrap.js +1066 -880
  12. package/lib/opera.js +615 -444
  13. package/lib/preferences.js +74 -74
  14. package/lib/registry.js +214 -214
  15. package/lib/release.js +56 -56
  16. package/lib/runtime-state.js +144 -144
  17. package/lib/skills.js +114 -89
  18. package/lib/workspace.js +259 -248
  19. package/locales/en.json +311 -167
  20. package/locales/es.json +314 -170
  21. package/package.json +61 -58
  22. package/scripts/postinstall-locale.js +21 -21
  23. package/scripts/skills-marketplace-smoke.js +124 -124
  24. package/scripts/smoke-tests.js +563 -517
  25. package/scripts/sync-skill-version.js +21 -21
  26. package/scripts/validate-skill.js +103 -103
  27. package/skills/trackops/SKILL.md +126 -122
  28. package/skills/trackops/agents/openai.yaml +7 -7
  29. package/skills/trackops/locales/en/SKILL.md +126 -122
  30. package/skills/trackops/locales/en/references/activation.md +94 -90
  31. package/skills/trackops/locales/en/references/troubleshooting.md +73 -67
  32. package/skills/trackops/locales/en/references/workflow.md +55 -32
  33. package/skills/trackops/references/activation.md +94 -90
  34. package/skills/trackops/references/troubleshooting.md +73 -67
  35. package/skills/trackops/references/workflow.md +55 -32
  36. package/skills/trackops/skill.json +29 -29
  37. package/templates/hooks/post-checkout +2 -2
  38. package/templates/hooks/post-commit +2 -2
  39. package/templates/hooks/post-merge +2 -2
  40. package/templates/opera/agent.md +28 -27
  41. package/templates/opera/architecture/dependency-graph.md +24 -24
  42. package/templates/opera/architecture/runtime-automation.md +24 -24
  43. package/templates/opera/architecture/runtime-operations.md +34 -34
  44. package/templates/opera/en/agent.md +22 -21
  45. package/templates/opera/en/architecture/dependency-graph.md +24 -24
  46. package/templates/opera/en/architecture/runtime-automation.md +24 -24
  47. package/templates/opera/en/architecture/runtime-operations.md +34 -34
  48. package/templates/opera/en/reviews/delivery-audit.md +18 -18
  49. package/templates/opera/en/reviews/integration-audit.md +18 -18
  50. package/templates/opera/en/router.md +24 -19
  51. package/templates/opera/references/autonomy-and-recovery.md +117 -117
  52. package/templates/opera/references/opera-cycle.md +193 -193
  53. package/templates/opera/registry.md +28 -28
  54. package/templates/opera/reviews/delivery-audit.md +18 -18
  55. package/templates/opera/reviews/integration-audit.md +18 -18
  56. package/templates/opera/router.md +54 -49
  57. package/templates/skills/changelog-updater/SKILL.md +69 -69
  58. package/templates/skills/commiter/SKILL.md +99 -99
  59. package/templates/skills/opera-contract-auditor/SKILL.md +38 -38
  60. package/templates/skills/opera-contract-auditor/locales/en/SKILL.md +38 -38
  61. package/templates/skills/opera-policy-guard/SKILL.md +26 -26
  62. package/templates/skills/opera-policy-guard/locales/en/SKILL.md +26 -26
  63. package/templates/skills/opera-skill/SKILL.md +279 -0
  64. package/templates/skills/opera-skill/locales/en/SKILL.md +279 -0
  65. package/templates/skills/opera-skill/locales/en/references/phase-dod.md +138 -0
  66. package/templates/skills/opera-skill/references/phase-dod.md +138 -0
  67. package/templates/skills/project-starter-skill/SKILL.md +150 -131
  68. package/templates/skills/project-starter-skill/locales/en/SKILL.md +143 -105
  69. package/templates/skills/project-starter-skill/references/opera-cycle.md +195 -193
  70. package/ui/css/base.css +284 -284
  71. package/ui/css/charts.css +425 -425
  72. package/ui/css/components.css +1107 -1107
  73. package/ui/css/onboarding.css +133 -133
  74. package/ui/css/terminal.css +125 -125
  75. package/ui/css/timeline.css +58 -58
  76. package/ui/css/tokens.css +284 -284
  77. package/ui/favicon.svg +5 -5
  78. package/ui/index.html +99 -99
  79. package/ui/js/charts.js +526 -526
  80. package/ui/js/console-logger.js +172 -172
  81. package/ui/js/filters.js +247 -247
  82. package/ui/js/icons.js +129 -129
  83. package/ui/js/keyboard.js +229 -229
  84. package/ui/js/router.js +142 -142
  85. package/ui/js/theme.js +100 -100
  86. package/ui/js/time-tracker.js +248 -248
  87. package/ui/js/views/dashboard.js +870 -870
  88. package/ui/js/views/flash.js +47 -47
  89. package/ui/js/views/projects.js +745 -745
  90. package/ui/js/views/scrum.js +476 -476
  91. package/ui/js/views/settings.js +331 -331
  92. package/ui/js/views/timeline.js +265 -265
@@ -9,10 +9,11 @@ const config = require("./config");
9
9
  const { t, setLocale } = require("./i18n");
10
10
  const { isInteractive } = require("./locale");
11
11
  const { resolveLocalizedFile } = require("./resources");
12
+ const fmt = require("./cli-format");
12
13
 
13
14
  const TEMPLATES_DIR = path.join(__dirname, "..", "templates", "opera");
14
15
 
15
- const SERVICE_HINTS = [
16
+ const SERVICE_HINTS = [
16
17
  ["openai", "OpenAI"],
17
18
  ["anthropic", "Anthropic"],
18
19
  ["stripe", "Stripe"],
@@ -24,60 +25,60 @@ const SERVICE_HINTS = [
24
25
  ["s3", "Amazon S3"],
25
26
  ["aws", "AWS"],
26
27
  ["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
- const ENUM_ALIASES = {
39
- low: "low",
40
- bajo: "low",
41
- basic: "low",
42
- beginner: "low",
43
- medium: "medium",
44
- medio: "medium",
45
- mid: "medium",
46
- high: "high",
47
- alto: "high",
48
- advanced: "high",
49
- senior: "senior",
50
- idea: "idea",
51
- ideacion: "idea",
52
- ideation: "idea",
53
- draft: "draft",
54
- borrador: "draft",
55
- existing_repo: "existing_repo",
56
- existingrepo: "existing_repo",
57
- existing: "existing_repo",
58
- repo_existente: "existing_repo",
59
- "repo existente": "existing_repo",
60
- advanced_project: "advanced",
61
- avanzado: "advanced",
62
- advancedrepo: "advanced",
63
- none: "none",
64
- ninguna: "none",
65
- notes: "notes",
66
- notas: "notes",
67
- sos: "sos",
68
- spec_dossier: "spec_dossier",
69
- spec: "spec_dossier",
70
- dossier: "spec_dossier",
71
- repo_docs: "repo_docs",
72
- repodocs: "repo_docs",
73
- docs_repo: "repo_docs",
74
- user: "user",
75
- usuario: "user",
76
- shared: "shared",
77
- compartido: "shared",
78
- agent: "agent",
79
- agente: "agent",
80
- };
28
+ ["azure", "Azure"],
29
+ ];
30
+
31
+ const TECHNICAL_LEVELS = ["low", "medium", "high", "senior"];
32
+ const PROJECT_STATES = ["idea", "draft", "existing_repo", "advanced"];
33
+ const DOC_STATES = ["none", "notes", "sos", "spec_dossier", "repo_docs"];
34
+ const DECISION_OWNERSHIPS = ["user", "shared", "agent"];
35
+ const BOOTSTRAP_MODES = ["auto", "direct", "handoff"];
36
+ const QUALITY_STATUSES = ["ready", "needs_review", "blocked"];
37
+ const CONTRACT_READINESS = ["hypothesis", "provisional", "verified", "locked"];
38
+ const CONTRACT_VERSION = 3;
39
+ const ENUM_ALIASES = {
40
+ low: "low",
41
+ bajo: "low",
42
+ basic: "low",
43
+ beginner: "low",
44
+ medium: "medium",
45
+ medio: "medium",
46
+ mid: "medium",
47
+ high: "high",
48
+ alto: "high",
49
+ advanced: "high",
50
+ senior: "senior",
51
+ idea: "idea",
52
+ ideacion: "idea",
53
+ ideation: "idea",
54
+ draft: "draft",
55
+ borrador: "draft",
56
+ existing_repo: "existing_repo",
57
+ existingrepo: "existing_repo",
58
+ existing: "existing_repo",
59
+ repo_existente: "existing_repo",
60
+ "repo existente": "existing_repo",
61
+ advanced_project: "advanced",
62
+ avanzado: "advanced",
63
+ advancedrepo: "advanced",
64
+ none: "none",
65
+ ninguna: "none",
66
+ notes: "notes",
67
+ notas: "notes",
68
+ sos: "sos",
69
+ spec_dossier: "spec_dossier",
70
+ spec: "spec_dossier",
71
+ dossier: "spec_dossier",
72
+ repo_docs: "repo_docs",
73
+ repodocs: "repo_docs",
74
+ docs_repo: "repo_docs",
75
+ user: "user",
76
+ usuario: "user",
77
+ shared: "shared",
78
+ compartido: "shared",
79
+ agent: "agent",
80
+ agente: "agent",
81
+ };
81
82
 
82
83
  function nowIso() {
83
84
  return new Date().toISOString();
@@ -89,27 +90,27 @@ function git(args, root) {
89
90
  return result.stdout.trim();
90
91
  }
91
92
 
92
- function readText(filePath) {
93
- return fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf8") : "";
94
- }
95
-
96
- function readJson(filePath) {
97
- if (!fs.existsSync(filePath)) return null;
98
- try {
99
- return JSON.parse(fs.readFileSync(filePath, "utf8"));
100
- } catch (_error) {
101
- return null;
102
- }
103
- }
104
-
105
- function writeText(filePath, content) {
106
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
107
- fs.writeFileSync(filePath, content.replace(/\r?\n/g, "\n"), "utf8");
108
- }
109
-
110
- function writeJson(filePath, data) {
111
- writeText(filePath, `${JSON.stringify(data, null, 2)}\n`);
112
- }
93
+ function readText(filePath) {
94
+ return fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf8") : "";
95
+ }
96
+
97
+ function readJson(filePath) {
98
+ if (!fs.existsSync(filePath)) return null;
99
+ try {
100
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
101
+ } catch (_error) {
102
+ return null;
103
+ }
104
+ }
105
+
106
+ function writeText(filePath, content) {
107
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
108
+ fs.writeFileSync(filePath, content.replace(/\r?\n/g, "\n"), "utf8");
109
+ }
110
+
111
+ function writeJson(filePath, data) {
112
+ writeText(filePath, `${JSON.stringify(data, null, 2)}\n`);
113
+ }
113
114
 
114
115
  function safeJson(text) {
115
116
  try {
@@ -119,75 +120,75 @@ function safeJson(text) {
119
120
  }
120
121
  }
121
122
 
122
- function unique(items) {
123
- return [...new Set((items || []).map((item) => String(item || "").trim()).filter(Boolean))];
124
- }
125
-
126
- function normalizeEnum(value, allowed) {
127
- const normalized = String(value || "").trim().toLowerCase().replace(/\s+/g, "_");
128
- const alias = ENUM_ALIASES[normalized] || normalized;
129
- return allowed.includes(alias) ? alias : null;
130
- }
131
-
132
- function choiceHint(allowed) {
133
- if (allowed === TECHNICAL_LEVELS) return "low|medium|high|senior (o bajo|medio|alto)";
134
- if (allowed === PROJECT_STATES) return "idea|draft|existing_repo|advanced";
135
- if (allowed === DOC_STATES) return "none|notes|sos|spec_dossier|repo_docs";
136
- if (allowed === DECISION_OWNERSHIPS) return "user|shared|agent (o usuario|compartido|agente)";
137
- return Array.isArray(allowed) ? allowed.join("|") : null;
138
- }
139
-
140
- function explanationModeFor(technicalLevel) {
141
- if (technicalLevel === "low") return "guided";
142
- if (technicalLevel === "medium") return "balanced";
143
- return "expert";
144
- }
145
-
146
- function bootstrapRelativePaths(context) {
147
- if (context.layout === "split") {
148
- return {
149
- markdown: "ops/bootstrap/agent-handoff.md",
150
- json: "ops/bootstrap/agent-handoff.json",
151
- intakeJson: "ops/bootstrap/intake.json",
152
- specDossier: "ops/bootstrap/spec-dossier.md",
153
- openQuestions: "ops/bootstrap/open-questions.md",
154
- qualityReport: "ops/bootstrap/quality-report.json",
155
- };
156
- }
157
-
158
- return {
159
- markdown: "bootstrap/agent-handoff.md",
160
- json: "bootstrap/agent-handoff.json",
161
- intakeJson: "bootstrap/intake.json",
162
- specDossier: "bootstrap/spec-dossier.md",
163
- openQuestions: "bootstrap/open-questions.md",
164
- qualityReport: "bootstrap/quality-report.json",
165
- };
166
- }
167
-
168
- function bootstrapFilePaths(context) {
169
- return {
170
- dir: context.paths.bootstrapDir,
171
- markdown: path.join(context.paths.bootstrapDir, "agent-handoff.md"),
172
- json: path.join(context.paths.bootstrapDir, "agent-handoff.json"),
173
- intakeJson: path.join(context.paths.bootstrapDir, "intake.json"),
174
- specDossier: path.join(context.paths.bootstrapDir, "spec-dossier.md"),
175
- openQuestions: path.join(context.paths.bootstrapDir, "open-questions.md"),
176
- qualityReport: path.join(context.paths.bootstrapDir, "quality-report.json"),
177
- };
178
- }
179
-
180
- function contractRelativePath(context) {
181
- return context.layout === "split"
182
- ? "ops/contract/operating-contract.json"
183
- : "contract/operating-contract.json";
184
- }
185
-
186
- function policyRelativePath(context) {
187
- return context.layout === "split"
188
- ? "ops/policy/autonomy.json"
189
- : "policy/autonomy.json";
190
- }
123
+ function unique(items) {
124
+ return [...new Set((items || []).map((item) => String(item || "").trim()).filter(Boolean))];
125
+ }
126
+
127
+ function normalizeEnum(value, allowed) {
128
+ const normalized = String(value || "").trim().toLowerCase().replace(/\s+/g, "_");
129
+ const alias = ENUM_ALIASES[normalized] || normalized;
130
+ return allowed.includes(alias) ? alias : null;
131
+ }
132
+
133
+ function choiceHint(allowed) {
134
+ if (allowed === TECHNICAL_LEVELS) return "low|medium|high|senior (o bajo|medio|alto)";
135
+ if (allowed === PROJECT_STATES) return "idea|draft|existing_repo|advanced";
136
+ if (allowed === DOC_STATES) return "none|notes|sos|spec_dossier|repo_docs";
137
+ if (allowed === DECISION_OWNERSHIPS) return "user|shared|agent (o usuario|compartido|agente)";
138
+ return Array.isArray(allowed) ? allowed.join("|") : null;
139
+ }
140
+
141
+ function explanationModeFor(technicalLevel) {
142
+ if (technicalLevel === "low") return "guided";
143
+ if (technicalLevel === "medium") return "balanced";
144
+ return "expert";
145
+ }
146
+
147
+ function bootstrapRelativePaths(context) {
148
+ if (context.layout === "split") {
149
+ return {
150
+ markdown: "ops/bootstrap/agent-handoff.md",
151
+ json: "ops/bootstrap/agent-handoff.json",
152
+ intakeJson: "ops/bootstrap/intake.json",
153
+ specDossier: "ops/bootstrap/spec-dossier.md",
154
+ openQuestions: "ops/bootstrap/open-questions.md",
155
+ qualityReport: "ops/bootstrap/quality-report.json",
156
+ };
157
+ }
158
+
159
+ return {
160
+ markdown: "bootstrap/agent-handoff.md",
161
+ json: "bootstrap/agent-handoff.json",
162
+ intakeJson: "bootstrap/intake.json",
163
+ specDossier: "bootstrap/spec-dossier.md",
164
+ openQuestions: "bootstrap/open-questions.md",
165
+ qualityReport: "bootstrap/quality-report.json",
166
+ };
167
+ }
168
+
169
+ function bootstrapFilePaths(context) {
170
+ return {
171
+ dir: context.paths.bootstrapDir,
172
+ markdown: path.join(context.paths.bootstrapDir, "agent-handoff.md"),
173
+ json: path.join(context.paths.bootstrapDir, "agent-handoff.json"),
174
+ intakeJson: path.join(context.paths.bootstrapDir, "intake.json"),
175
+ specDossier: path.join(context.paths.bootstrapDir, "spec-dossier.md"),
176
+ openQuestions: path.join(context.paths.bootstrapDir, "open-questions.md"),
177
+ qualityReport: path.join(context.paths.bootstrapDir, "quality-report.json"),
178
+ };
179
+ }
180
+
181
+ function contractRelativePath(context) {
182
+ return context.layout === "split"
183
+ ? "ops/contract/operating-contract.json"
184
+ : "contract/operating-contract.json";
185
+ }
186
+
187
+ function policyRelativePath(context) {
188
+ return context.layout === "split"
189
+ ? "ops/policy/autonomy.json"
190
+ : "policy/autonomy.json";
191
+ }
191
192
 
192
193
  function firstParagraph(text) {
193
194
  return String(text || "")
@@ -210,30 +211,30 @@ function inferServicesFromDependencies(pkg) {
210
211
  return SERVICE_HINTS.filter(([needle]) => names.some((name) => name.includes(needle))).map(([, label]) => label);
211
212
  }
212
213
 
213
- function scanProject(root) {
214
- const context = config.ensureContext(root);
215
- const scanRoot = context.appRoot;
216
- const envPaths = config.envFilePaths(context);
217
- const pkgPath = path.join(scanRoot, "package.json");
218
- const readmePath = fs.existsSync(path.join(scanRoot, "README.md"))
219
- ? path.join(scanRoot, "README.md")
220
- : fs.existsSync(path.join(scanRoot, "README.mdx"))
221
- ? path.join(scanRoot, "README.mdx")
222
- : "";
223
- const envExamplePath = fs.existsSync(envPaths.exampleFile)
224
- ? envPaths.exampleFile
225
- : fs.existsSync(envPaths.rootFile)
226
- ? envPaths.rootFile
227
- : "";
214
+ function scanProject(root) {
215
+ const context = config.ensureContext(root);
216
+ const scanRoot = context.appRoot;
217
+ const envPaths = config.envFilePaths(context);
218
+ const pkgPath = path.join(scanRoot, "package.json");
219
+ const readmePath = fs.existsSync(path.join(scanRoot, "README.md"))
220
+ ? path.join(scanRoot, "README.md")
221
+ : fs.existsSync(path.join(scanRoot, "README.mdx"))
222
+ ? path.join(scanRoot, "README.mdx")
223
+ : "";
224
+ const envExamplePath = fs.existsSync(envPaths.exampleFile)
225
+ ? envPaths.exampleFile
226
+ : fs.existsSync(envPaths.rootFile)
227
+ ? envPaths.rootFile
228
+ : "";
228
229
 
229
230
  const pkg = safeJson(readText(pkgPath));
230
231
  const readme = readText(readmePath);
231
232
  const envExample = readText(envExamplePath);
232
- const workflowsDir = path.join(context.workspaceRoot, ".github", "workflows");
233
+ const workflowsDir = path.join(context.workspaceRoot, ".github", "workflows");
233
234
  const workflowFiles = fs.existsSync(workflowsDir)
234
235
  ? fs.readdirSync(workflowsDir).filter((file) => file.endsWith(".yml") || file.endsWith(".yaml"))
235
236
  : [];
236
- const files = fs.readdirSync(scanRoot);
237
+ const files = fs.readdirSync(scanRoot);
237
238
  const stacks = [];
238
239
 
239
240
  if (pkg) stacks.push("node");
@@ -244,22 +245,22 @@ function scanProject(root) {
244
245
  if (files.includes("Dockerfile") || files.some((file) => file.startsWith("docker-compose"))) stacks.push("docker");
245
246
 
246
247
  const description = pkg?.description || firstParagraph(readme);
247
- const title = pkg?.displayName || pkg?.name || path.basename(scanRoot);
248
- const testCommands = unique([
249
- pkg?.scripts?.test ? "npm test" : "",
250
- fs.existsSync(path.join(scanRoot, "pytest.ini")) ? "pytest" : "",
251
- fs.existsSync(path.join(scanRoot, "Cargo.toml")) ? "cargo test" : "",
252
- fs.existsSync(path.join(scanRoot, "go.mod")) ? "go test ./..." : "",
253
- ]);
254
- const buildCommands = unique([
255
- pkg?.scripts?.build ? "npm run build" : "",
256
- fs.existsSync(path.join(scanRoot, "Dockerfile")) ? "docker build ." : "",
257
- ]);
248
+ const title = pkg?.displayName || pkg?.name || path.basename(scanRoot);
249
+ const testCommands = unique([
250
+ pkg?.scripts?.test ? "npm test" : "",
251
+ fs.existsSync(path.join(scanRoot, "pytest.ini")) ? "pytest" : "",
252
+ fs.existsSync(path.join(scanRoot, "Cargo.toml")) ? "cargo test" : "",
253
+ fs.existsSync(path.join(scanRoot, "go.mod")) ? "go test ./..." : "",
254
+ ]);
255
+ const buildCommands = unique([
256
+ pkg?.scripts?.build ? "npm run build" : "",
257
+ fs.existsSync(path.join(scanRoot, "Dockerfile")) ? "docker build ." : "",
258
+ ]);
258
259
  const services = unique([
259
260
  ...inferServicesFromDependencies(pkg),
260
261
  ...inferServicesFromEnv(envExample),
261
262
  ]);
262
- const gitRemote = git(["remote", "get-url", "origin"], context.workspaceRoot) || null;
263
+ const gitRemote = git(["remote", "get-url", "origin"], context.workspaceRoot) || null;
263
264
  const ciProviders = workflowFiles.length ? ["github-actions"] : [];
264
265
 
265
266
  return {
@@ -286,499 +287,620 @@ function normalizeList(value) {
286
287
  .filter(Boolean);
287
288
  }
288
289
 
289
- function parseJsonValue(value) {
290
- if (!String(value || "").trim()) return {};
291
- try {
292
- return JSON.parse(value);
293
- } catch (_error) {
294
- return {};
295
- }
296
- }
297
-
298
- function getBootstrapState(control, contextOrRoot) {
299
- const context = config.ensureContext(contextOrRoot);
300
- const bootstrap = control.meta?.opera?.bootstrap;
301
- if (!bootstrap) return null;
302
- const relative = bootstrapRelativePaths(context);
303
- return {
304
- ...bootstrap,
305
- handoffFiles: bootstrap.handoffFiles || {
306
- markdown: relative.markdown,
307
- json: relative.json,
308
- },
309
- intakeFiles: bootstrap.intakeFiles || {
310
- json: relative.intakeJson,
311
- specDossier: relative.specDossier,
312
- },
313
- reviewFiles: bootstrap.reviewFiles || {
314
- openQuestions: relative.openQuestions,
315
- qualityReport: relative.qualityReport,
316
- },
317
- };
318
- }
319
-
320
- function buildAvailableArtifacts(context, docsState) {
321
- const artifacts = [];
322
- if (docsState === "repo_docs") {
323
- const readmePath = path.join(context.appRoot, "README.md");
324
- if (fs.existsSync(readmePath)) {
325
- artifacts.push({
326
- kind: "readme",
327
- location: "repo",
328
- path: path.relative(context.workspaceRoot, readmePath).replace(/\\/g, "/"),
329
- });
330
- }
331
- }
332
- return artifacts;
333
- }
334
-
335
- function determineBootstrapMode(intake, requestedMode) {
336
- const normalizedRequested = normalizeEnum(requestedMode || "auto", BOOTSTRAP_MODES) || "auto";
337
-
338
- if (normalizedRequested === "handoff") {
339
- return { requestedMode: normalizedRequested, mode: "agent_handoff", routeReason: "forced_handoff" };
340
- }
341
-
342
- if (normalizedRequested === "direct") {
343
- const hasCore = intake.technicalLevel && intake.projectState && intake.documentationState && intake.decisionOwnership;
344
- return {
345
- requestedMode: normalizedRequested,
346
- mode: hasCore ? "direct_cli" : "agent_handoff",
347
- routeReason: hasCore ? "forced_direct" : "insufficient_docs",
348
- };
349
- }
350
-
351
- if (!["high", "senior"].includes(intake.technicalLevel)) {
352
- return { requestedMode: normalizedRequested, mode: "agent_handoff", routeReason: "non_technical_user" };
353
- }
354
- if (!["existing_repo", "advanced"].includes(intake.projectState)) {
355
- return { requestedMode: normalizedRequested, mode: "agent_handoff", routeReason: "idea_stage" };
290
+ function parseJsonValue(value) {
291
+ if (!String(value || "").trim()) return {};
292
+ try {
293
+ return JSON.parse(value);
294
+ } catch (_error) {
295
+ return {};
296
+ }
297
+ }
298
+
299
+ function getBootstrapState(control, contextOrRoot) {
300
+ const context = config.ensureContext(contextOrRoot);
301
+ const bootstrap = control.meta?.opera?.bootstrap;
302
+ if (!bootstrap) return null;
303
+ const relative = bootstrapRelativePaths(context);
304
+ return {
305
+ ...bootstrap,
306
+ handoffFiles: bootstrap.handoffFiles || {
307
+ markdown: relative.markdown,
308
+ json: relative.json,
309
+ },
310
+ intakeFiles: bootstrap.intakeFiles || {
311
+ json: relative.intakeJson,
312
+ specDossier: relative.specDossier,
313
+ },
314
+ reviewFiles: bootstrap.reviewFiles || {
315
+ openQuestions: relative.openQuestions,
316
+ qualityReport: relative.qualityReport,
317
+ },
318
+ };
319
+ }
320
+
321
+ function buildAvailableArtifacts(context, docsState) {
322
+ const artifacts = [];
323
+ if (docsState === "repo_docs") {
324
+ const readmePath = path.join(context.appRoot, "README.md");
325
+ if (fs.existsSync(readmePath)) {
326
+ artifacts.push({
327
+ kind: "readme",
328
+ location: "repo",
329
+ path: path.relative(context.workspaceRoot, readmePath).replace(/\\/g, "/"),
330
+ });
331
+ }
332
+ }
333
+ return artifacts;
334
+ }
335
+
336
+ function determineBootstrapMode(intake, requestedMode) {
337
+ const normalizedRequested = normalizeEnum(requestedMode || "auto", BOOTSTRAP_MODES) || "auto";
338
+
339
+ if (normalizedRequested === "handoff") {
340
+ return { requestedMode: normalizedRequested, mode: "agent_handoff", routeReason: "forced_handoff" };
341
+ }
342
+
343
+ if (normalizedRequested === "direct") {
344
+ const hasCore = intake.technicalLevel && intake.projectState && intake.documentationState && intake.decisionOwnership;
345
+ return {
346
+ requestedMode: normalizedRequested,
347
+ mode: hasCore ? "direct_cli" : "agent_handoff",
348
+ routeReason: hasCore ? "forced_direct" : "insufficient_docs",
349
+ };
350
+ }
351
+
352
+ if (!["high", "senior"].includes(intake.technicalLevel)) {
353
+ return { requestedMode: normalizedRequested, mode: "agent_handoff", routeReason: "non_technical_user" };
354
+ }
355
+ if (!["existing_repo", "advanced"].includes(intake.projectState)) {
356
+ return { requestedMode: normalizedRequested, mode: "agent_handoff", routeReason: "idea_stage" };
357
+ }
358
+ if (!["sos", "spec_dossier", "repo_docs"].includes(intake.documentationState)) {
359
+ return { requestedMode: normalizedRequested, mode: "agent_handoff", routeReason: "insufficient_docs" };
360
+ }
361
+ if (!["user", "shared"].includes(intake.decisionOwnership)) {
362
+ return { requestedMode: normalizedRequested, mode: "agent_handoff", routeReason: "agent_owned_decisions" };
363
+ }
364
+ return { requestedMode: normalizedRequested, mode: "direct_cli", routeReason: "technical_existing_project" };
365
+ }
366
+
367
+ function directMissingFields(discovery) {
368
+ const missing = [];
369
+ if (!discovery.problemStatement) missing.push("problemStatement");
370
+ if (!discovery.targetUser) missing.push("targetUser");
371
+ if (!discovery.singularDesiredOutcome) missing.push("singularDesiredOutcome");
372
+ if (!discovery.sourceOfTruth) missing.push("sourceOfTruth");
373
+ if (!discovery.payload) missing.push("payload");
374
+ if (!Object.keys(discovery.inputSchema || {}).length) missing.push("inputSchema");
375
+ if (!Object.keys(discovery.outputSchema || {}).length) missing.push("outputSchema");
376
+ if (!discovery.decisionOwnership) missing.push("decisionOwnership");
377
+ return missing;
378
+ }
379
+
380
+ function buildHandoffPayload(control, profile, context) {
381
+ const relative = bootstrapRelativePaths(context);
382
+ const locale = config.getLocale(control);
383
+ return {
384
+ version: 1,
385
+ skill: "project-starter-skill",
386
+ locale,
387
+ technicalLevel: profile.technicalLevel,
388
+ explanationMode: explanationModeFor(profile.technicalLevel),
389
+ decisionOwnership: profile.discovery?.decisionOwnership || profile.decisionOwnership || null,
390
+ projectState: profile.projectState,
391
+ documentationState: profile.documentationState,
392
+ availableArtifacts: control.meta?.discovery?.availableArtifacts || [],
393
+ problemStatement: profile.discovery?.problemStatement || "",
394
+ targetUser: profile.discovery?.targetUser || "",
395
+ singularDesiredOutcome: profile.discovery?.singularDesiredOutcome || "",
396
+ files: {
397
+ intakeJson: relative.intakeJson,
398
+ specDossier: relative.specDossier,
399
+ openQuestions: relative.openQuestions,
400
+ },
401
+ };
402
+ }
403
+
404
+ function buildHandoffPrompt(control, profile) {
405
+ const locale = config.getLocale(control);
406
+ const languageName = locale === "es" ? "Spanish" : "English";
407
+ const lines = [
408
+ `# ${t("handoff.title")}`,
409
+ "",
410
+ t("handoff.skillInstruction"),
411
+ "",
412
+ `## ${t("handoff.section.userProfile")}`,
413
+ `- ${t("handoff.label.technicalLevel")}: ${profile.technicalLevel || "unknown"}`,
414
+ `- ${t("handoff.label.explanationMode")}: ${explanationModeFor(profile.technicalLevel)}`,
415
+ `- ${t("handoff.label.decisionOwnership")}: ${profile.discovery?.decisionOwnership || profile.decisionOwnership || "unknown"}`,
416
+ `- ${t("handoff.label.preferredLanguage")}: ${languageName} (${locale})`,
417
+ "",
418
+ `## ${t("handoff.section.projectState")}`,
419
+ `- ${t("handoff.label.projectState")}: ${profile.projectState || "unknown"}`,
420
+ `- ${t("handoff.label.documentationState")}: ${profile.documentationState || "unknown"}`,
421
+ "",
422
+ `## ${t("handoff.section.whatToDo")}`,
423
+ `- ${t("handoff.instruction.startFromUser")}`,
424
+ `- ${t("handoff.instruction.adaptDepth")}`,
425
+ `- ${t("handoff.instruction.readDocs")}`,
426
+ `- ${t("handoff.instruction.helpSpec")}`,
427
+ `- ${t("handoff.instruction.writeIntake")}`,
428
+ `- ${t("handoff.instruction.writeSpec")}`,
429
+ `- ${t("handoff.instruction.writeQuestions")}`,
430
+ `- ${t("handoff.instruction.includeFields")}`,
431
+ `- ${t("handoff.instruction.schemaFormat")}`,
432
+ `- ${t("handoff.instruction.decisionOwnership")}`,
433
+ `- ${t("handoff.instruction.respondInLanguage", { language: languageName })}`,
434
+ `- **${t("handoff.instruction.neverRename")}**`,
435
+ `- **${t("handoff.instruction.closure")}**`,
436
+ ];
437
+
438
+ lines.push(
439
+ "",
440
+ `## ${t("handoff.section.intakeSchema")}`,
441
+ "",
442
+ t("handoff.instruction.intakeRequired"),
443
+ "",
444
+ "```json",
445
+ JSON.stringify({
446
+ problemStatement: "string — core problem the project solves",
447
+ targetUser: "string — primary user persona",
448
+ singularDesiredOutcome: "string — single most important outcome",
449
+ sourceOfTruth: "string — authoritative data source",
450
+ payload: "string — what gets delivered (NOT 'deliveryTarget')",
451
+ decisionOwnership: "user | shared | agent",
452
+ inputSchema: { "fieldName": "type — at least one key required" },
453
+ outputSchema: { "fieldName": "type — at least one key required" },
454
+ }, null, 2),
455
+ "```",
456
+ );
457
+
458
+ if (profile.discovery?.singularDesiredOutcome) {
459
+ lines.push("", `${t("handoff.label.knownIntention")}: ${profile.discovery.singularDesiredOutcome}`);
460
+ }
461
+
462
+ return `${lines.join("\n")}\n`;
463
+ }
464
+
465
+ async function askQuestion(rl, message, defaultValue) {
466
+ const suffix = defaultValue ? ` (${defaultValue})` : "";
467
+ const answer = await rl.question(`${message}${suffix}: `);
468
+ return String(answer || "").trim() || String(defaultValue || "").trim();
469
+ }
470
+
471
+ async function askEnumQuestion(rl, message, defaultValue, allowed) {
472
+ const hint = choiceHint(allowed);
473
+ const suffix = defaultValue ? ` (${defaultValue})` : "";
474
+ const answer = await rl.question(`${message}${hint ? ` [${hint}]` : ""}${suffix}: `);
475
+ const normalized = normalizeEnum(answer || defaultValue, allowed);
476
+ return normalized || normalizeEnum(defaultValue, allowed);
477
+ }
478
+
479
+ async function collectBootstrapProfile(root, control, options = {}) {
480
+ const context = config.ensureContext(root);
481
+ const locale = config.getLocale(control);
482
+ setLocale(locale);
483
+ const scan = scanProject(context);
484
+ const previousUser = control.meta?.userProfile || {};
485
+ const previousDiscovery = control.meta?.discovery || {};
486
+ const previousBootstrap = getBootstrapState(control, context);
487
+ const previous = previousBootstrap?.discovery || {};
488
+ const interactive = options.interactive !== false && isInteractive();
489
+
490
+ const defaults = {
491
+ technicalLevel: normalizeEnum(options.technicalLevel || options.answers?.technicalLevel || previousUser.technicalLevel, TECHNICAL_LEVELS),
492
+ projectState: normalizeEnum(options.projectState || options.answers?.projectState || previousDiscovery.projectState, PROJECT_STATES),
493
+ documentationState: normalizeEnum(options.docsState || options.answers?.documentationState || previousDiscovery.documentationState, DOC_STATES),
494
+ decisionOwnership: normalizeEnum(options.decisionOwnership || options.answers?.decisionOwnership || previous.decisionOwnership, DECISION_OWNERSHIPS),
495
+ problemStatement: options.answers?.problemStatement || previous.problemStatement || "",
496
+ targetUser: options.answers?.targetUser || previous.targetUser || "",
497
+ singularDesiredOutcome:
498
+ options.answers?.singularDesiredOutcome ||
499
+ options.answers?.desiredOutcome ||
500
+ previous.singularDesiredOutcome ||
501
+ previous.desiredOutcome ||
502
+ scan.description ||
503
+ "",
504
+ userLanguage: options.answers?.userLanguage || previous.userLanguage || locale,
505
+ needsPlainLanguage: options.answers?.needsPlainLanguage ?? previous.needsPlainLanguage ?? false,
506
+ recommendedStack: normalizeList(options.answers?.recommendedStack || previous.recommendedStack || scan.stacks),
507
+ externalServices: normalizeList(options.answers?.externalServices || previous.externalServices || scan.services),
508
+ sourceOfTruth: options.answers?.sourceOfTruth || previous.sourceOfTruth || scan.sourceOfTruthHint || "",
509
+ payload: options.answers?.payload || previous.payload || scan.payloadHint || "",
510
+ behaviorRules: normalizeList(options.answers?.behaviorRules || previous.behaviorRules || ""),
511
+ inputSchema: options.answers?.inputSchema || previous.inputSchema || {},
512
+ outputSchema: options.answers?.outputSchema || previous.outputSchema || {},
513
+ architecturalInvariants: normalizeList(options.answers?.architecturalInvariants || previous.architecturalInvariants || ""),
514
+ pipeline: normalizeList(options.answers?.pipeline || previous.pipeline || ""),
515
+ templates: normalizeList(options.answers?.templates || previous.templates || ""),
516
+ availableArtifacts: Array.isArray(previousDiscovery.availableArtifacts) ? previousDiscovery.availableArtifacts : [],
517
+ };
518
+
519
+ const answers = { ...defaults };
520
+
521
+ if (interactive) {
522
+ fmt.header(t("bootstrap.header"));
523
+ fmt.blank();
524
+ fmt.info(t("bootstrap.subtitle"));
525
+ fmt.blank();
526
+ fmt.info(t("bootstrap.instructions"));
527
+ fmt.blank();
528
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
529
+ try {
530
+ answers.technicalLevel = await askEnumQuestion(rl, t("bootstrap.question.technicalLevel"), defaults.technicalLevel || "medium", TECHNICAL_LEVELS) || "medium";
531
+ answers.projectState = await askEnumQuestion(rl, t("bootstrap.question.projectState"), defaults.projectState || "idea", PROJECT_STATES) || "idea";
532
+ answers.documentationState = await askEnumQuestion(rl, t("bootstrap.question.docsState"), defaults.documentationState || "none", DOC_STATES) || "none";
533
+ answers.decisionOwnership = await askEnumQuestion(rl, t("bootstrap.question.decisionOwnership"), defaults.decisionOwnership || "shared", DECISION_OWNERSHIPS) || "shared";
534
+ } finally {
535
+ rl.close();
536
+ }
537
+ }
538
+
539
+ answers.availableArtifacts = answers.availableArtifacts.length
540
+ ? answers.availableArtifacts
541
+ : buildAvailableArtifacts(context, answers.documentationState);
542
+
543
+ const routing = determineBootstrapMode(answers, options.bootstrapMode);
544
+
545
+ if (routing.mode === "agent_handoff") {
546
+ return {
547
+ version: 2,
548
+ status: "awaiting_agent",
549
+ mode: "agent_handoff",
550
+ routeReason: routing.routeReason,
551
+ technicalLevel: answers.technicalLevel,
552
+ projectState: answers.projectState,
553
+ documentationState: answers.documentationState,
554
+ decisionOwnership: answers.decisionOwnership,
555
+ startedAt: previousBootstrap?.startedAt || nowIso(),
556
+ completedAt: null,
557
+ missingFields: ["intakeJson", "specDossier"],
558
+ discovery: answers,
559
+ inference: scan,
560
+ };
561
+ }
562
+
563
+ if (interactive) {
564
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
565
+ try {
566
+ answers.problemStatement = await askQuestion(rl, t("bootstrap.question.problemStatement"), defaults.problemStatement);
567
+ answers.targetUser = await askQuestion(rl, t("bootstrap.question.targetUser"), defaults.targetUser);
568
+ answers.singularDesiredOutcome = await askQuestion(rl, t("bootstrap.question.desiredOutcome"), defaults.singularDesiredOutcome);
569
+ answers.externalServices = normalizeList(await askQuestion(rl, t("bootstrap.question.externalServices"), defaults.externalServices.join(", ")));
570
+ answers.sourceOfTruth = await askQuestion(rl, t("bootstrap.question.sourceOfTruth"), defaults.sourceOfTruth);
571
+ answers.payload = await askQuestion(rl, t("bootstrap.question.payload"), defaults.payload);
572
+ answers.behaviorRules = normalizeList(await askQuestion(rl, t("bootstrap.question.behaviorRules"), defaults.behaviorRules.join("; ")));
573
+ answers.inputSchema = parseJsonValue(await askQuestion(rl, t("bootstrap.question.inputSchema"), JSON.stringify(defaults.inputSchema)));
574
+ answers.outputSchema = parseJsonValue(await askQuestion(rl, t("bootstrap.question.outputSchema"), JSON.stringify(defaults.outputSchema)));
575
+ answers.architecturalInvariants = normalizeList(await askQuestion(rl, t("bootstrap.question.invariants"), defaults.architecturalInvariants.join("; ")));
576
+ answers.pipeline = normalizeList(await askQuestion(rl, t("bootstrap.question.pipeline"), defaults.pipeline.join("; ")));
577
+ answers.templates = normalizeList(await askQuestion(rl, t("bootstrap.question.templates"), defaults.templates.join(", ")));
578
+ } finally {
579
+ rl.close();
580
+ }
581
+ }
582
+
583
+ const missingFields = directMissingFields(answers);
584
+ return {
585
+ version: 2,
586
+ status: missingFields.length ? "awaiting_intake" : "completed",
587
+ mode: "direct_cli",
588
+ routeReason: routing.routeReason,
589
+ technicalLevel: answers.technicalLevel,
590
+ projectState: answers.projectState,
591
+ documentationState: answers.documentationState,
592
+ decisionOwnership: answers.decisionOwnership,
593
+ startedAt: previousBootstrap?.startedAt || nowIso(),
594
+ completedAt: missingFields.length ? null : nowIso(),
595
+ missingFields,
596
+ discovery: answers,
597
+ inference: scan,
598
+ };
599
+ }
600
+
601
+ function isVirginGenesis(content) {
602
+ const text = String(content || "");
603
+ return !text.trim() || /TODO:/i.test(text) || /The Constitution of the project/i.test(text) || /La Constitución del proyecto/i.test(text);
604
+ }
605
+
606
+ function parseSpecSections(specText) {
607
+ const sections = {};
608
+ const lines = String(specText || "").split(/\r?\n/);
609
+ let current = null;
610
+ for (const line of lines) {
611
+ const heading = line.match(/^##\s+(.+?)\s*$/);
612
+ if (heading) {
613
+ current = heading[1].trim().toLowerCase();
614
+ sections[current] = [];
615
+ continue;
616
+ }
617
+ if (current) sections[current].push(line);
618
+ }
619
+ return Object.fromEntries(
620
+ Object.entries(sections).map(([key, value]) => [key, value.join("\n").trim()]),
621
+ );
622
+ }
623
+
624
+ function buildOpenQuestions(missingFields, contradictions) {
625
+ const lines = [`# ${t("bootstrap.scaffold.openQuestionsTitle")}`, ""];
626
+ if (missingFields.length) {
627
+ lines.push(`## ${t("bootstrap.scaffold.missingFields")}`, "");
628
+ missingFields.forEach((field) => lines.push(`- ${field}`));
629
+ lines.push("");
356
630
  }
357
- if (!["sos", "spec_dossier", "repo_docs"].includes(intake.documentationState)) {
358
- return { requestedMode: normalizedRequested, mode: "agent_handoff", routeReason: "insufficient_docs" };
631
+ if (contradictions.length) {
632
+ lines.push(`## ${t("bootstrap.scaffold.contradictions")}`, "");
633
+ contradictions.forEach((item) => lines.push(`- ${item}`));
634
+ lines.push("");
359
635
  }
360
- if (!["user", "shared"].includes(intake.decisionOwnership)) {
361
- return { requestedMode: normalizedRequested, mode: "agent_handoff", routeReason: "agent_owned_decisions" };
636
+ if (!missingFields.length && !contradictions.length) {
637
+ lines.push(`- ${t("bootstrap.scaffold.none")}`);
362
638
  }
363
- return { requestedMode: normalizedRequested, mode: "direct_cli", routeReason: "technical_existing_project" };
364
- }
365
-
366
- function directMissingFields(discovery) {
367
- const missing = [];
368
- if (!discovery.problemStatement) missing.push("problemStatement");
369
- if (!discovery.targetUser) missing.push("targetUser");
370
- if (!discovery.singularDesiredOutcome) missing.push("singularDesiredOutcome");
371
- if (!discovery.sourceOfTruth) missing.push("sourceOfTruth");
372
- if (!discovery.payload) missing.push("payload");
373
- if (!Object.keys(discovery.inputSchema || {}).length) missing.push("inputSchema");
374
- if (!Object.keys(discovery.outputSchema || {}).length) missing.push("outputSchema");
375
- return missing;
639
+ return `${lines.join("\n")}\n`;
376
640
  }
377
641
 
378
- function buildHandoffPayload(control, profile, context) {
379
- const relative = bootstrapRelativePaths(context);
642
+ function buildDirectIntakeTemplate(profile) {
380
643
  return {
381
644
  version: 1,
382
- skill: "project-starter-skill",
383
- technicalLevel: profile.technicalLevel,
384
- explanationMode: explanationModeFor(profile.technicalLevel),
645
+ technicalLevel: profile.technicalLevel || null,
646
+ projectState: profile.projectState || null,
647
+ documentationState: profile.documentationState || null,
385
648
  decisionOwnership: profile.discovery?.decisionOwnership || profile.decisionOwnership || null,
386
- projectState: profile.projectState,
387
- documentationState: profile.documentationState,
388
- availableArtifacts: control.meta?.discovery?.availableArtifacts || [],
389
649
  problemStatement: profile.discovery?.problemStatement || "",
390
650
  targetUser: profile.discovery?.targetUser || "",
391
651
  singularDesiredOutcome: profile.discovery?.singularDesiredOutcome || "",
392
- files: {
393
- intakeJson: relative.intakeJson,
394
- specDossier: relative.specDossier,
395
- openQuestions: relative.openQuestions,
396
- },
652
+ userLanguage: profile.discovery?.userLanguage || null,
653
+ needsPlainLanguage: Boolean(profile.discovery?.needsPlainLanguage),
654
+ recommendedStack: normalizeList(profile.discovery?.recommendedStack || []),
655
+ externalServices: normalizeList(profile.discovery?.externalServices || []),
656
+ sourceOfTruth: profile.discovery?.sourceOfTruth || "",
657
+ payload: profile.discovery?.payload || "",
658
+ behaviorRules: normalizeList(profile.discovery?.behaviorRules || []),
659
+ inputSchema: profile.discovery?.inputSchema || {},
660
+ outputSchema: profile.discovery?.outputSchema || {},
661
+ architecturalInvariants: normalizeList(profile.discovery?.architecturalInvariants || []),
662
+ pipeline: normalizeList(profile.discovery?.pipeline || []),
663
+ templates: normalizeList(profile.discovery?.templates || []),
397
664
  };
398
665
  }
399
666
 
400
- function buildHandoffPrompt(control, profile) {
401
- const lines = [
402
- "# TrackOps agent handoff",
667
+ function buildSpecDossierScaffold(profile) {
668
+ return [
669
+ `# ${t("bootstrap.scaffold.specTitle")}`,
403
670
  "",
404
- "Use `project-starter-skill` as a discovery and structuring skill for this project.",
671
+ `## ${t("bootstrap.scaffold.problemStatement")}`,
672
+ profile.discovery?.problemStatement || "",
405
673
  "",
406
- "## User profile",
407
- `- Technical level: ${profile.technicalLevel || "unknown"}`,
408
- `- Explanation mode: ${explanationModeFor(profile.technicalLevel)}`,
409
- `- Decision ownership: ${profile.discovery?.decisionOwnership || profile.decisionOwnership || "unknown"}`,
674
+ `## ${t("bootstrap.scaffold.targetUser")}`,
675
+ profile.discovery?.targetUser || "",
410
676
  "",
411
- "## Project state",
412
- `- Project state: ${profile.projectState || "unknown"}`,
413
- `- Documentation state: ${profile.documentationState || "unknown"}`,
677
+ `## ${t("bootstrap.scaffold.singularDesiredOutcome")}`,
678
+ profile.discovery?.singularDesiredOutcome || "",
414
679
  "",
415
- "## What to do",
416
- "- Start from the user, not from architecture assumptions.",
417
- "- Adapt depth and language to the user's technical level.",
418
- "- If documentation exists, read it, summarize it, and consolidate it.",
419
- "- If documentation does not exist, help the user turn the idea into a workable project specification.",
420
- "- Write `ops/bootstrap/intake.json` with the structured discovery output.",
421
- "- Write `ops/bootstrap/spec-dossier.md` with the narrative or technical specification that OPERA will ingest.",
422
- "- Write `ops/bootstrap/open-questions.md` if important uncertainties remain.",
423
- "- Include explicit fields for problem statement, target user, desired outcome, source of truth, delivery target, and schemas.",
424
- ];
425
-
426
- if (profile.discovery?.singularDesiredOutcome) {
427
- lines.push("", `Known project intention: ${profile.discovery.singularDesiredOutcome}`);
428
- }
429
-
430
- return `${lines.join("\n")}\n`;
431
- }
432
-
433
- async function askQuestion(rl, message, defaultValue) {
434
- const suffix = defaultValue ? ` (${defaultValue})` : "";
435
- const answer = await rl.question(`${message}${suffix}: `);
436
- return String(answer || "").trim() || String(defaultValue || "").trim();
437
- }
438
-
439
- async function askEnumQuestion(rl, message, defaultValue, allowed) {
440
- const hint = choiceHint(allowed);
441
- const suffix = defaultValue ? ` (${defaultValue})` : "";
442
- const answer = await rl.question(`${message}${hint ? ` [${hint}]` : ""}${suffix}: `);
443
- const normalized = normalizeEnum(answer || defaultValue, allowed);
444
- return normalized || normalizeEnum(defaultValue, allowed);
680
+ `## ${t("bootstrap.scaffold.deliveryTarget")}`,
681
+ profile.discovery?.payload || "",
682
+ "",
683
+ `## ${t("bootstrap.scaffold.sourceOfTruth")}`,
684
+ profile.discovery?.sourceOfTruth || "",
685
+ "",
686
+ ].join("\n");
445
687
  }
446
688
 
447
- async function collectBootstrapProfile(root, control, options = {}) {
448
- const context = config.ensureContext(root);
449
- const locale = config.getLocale(control);
450
- setLocale(locale);
451
- const scan = scanProject(context);
452
- const previousUser = control.meta?.userProfile || {};
453
- const previousDiscovery = control.meta?.discovery || {};
454
- const previousBootstrap = getBootstrapState(control, context);
455
- const previous = previousBootstrap?.discovery || {};
456
- const interactive = options.interactive !== false && isInteractive();
457
-
458
- const defaults = {
459
- technicalLevel: normalizeEnum(options.technicalLevel || options.answers?.technicalLevel || previousUser.technicalLevel, TECHNICAL_LEVELS),
460
- projectState: normalizeEnum(options.projectState || options.answers?.projectState || previousDiscovery.projectState, PROJECT_STATES),
461
- documentationState: normalizeEnum(options.docsState || options.answers?.documentationState || previousDiscovery.documentationState, DOC_STATES),
462
- decisionOwnership: normalizeEnum(options.decisionOwnership || options.answers?.decisionOwnership || previous.decisionOwnership, DECISION_OWNERSHIPS),
463
- problemStatement: options.answers?.problemStatement || previous.problemStatement || "",
464
- targetUser: options.answers?.targetUser || previous.targetUser || "",
465
- singularDesiredOutcome:
466
- options.answers?.singularDesiredOutcome ||
467
- options.answers?.desiredOutcome ||
468
- previous.singularDesiredOutcome ||
469
- previous.desiredOutcome ||
470
- scan.description ||
471
- "",
472
- userLanguage: options.answers?.userLanguage || previous.userLanguage || locale,
473
- needsPlainLanguage: options.answers?.needsPlainLanguage ?? previous.needsPlainLanguage ?? false,
474
- recommendedStack: normalizeList(options.answers?.recommendedStack || previous.recommendedStack || scan.stacks),
475
- externalServices: normalizeList(options.answers?.externalServices || previous.externalServices || scan.services),
476
- sourceOfTruth: options.answers?.sourceOfTruth || previous.sourceOfTruth || scan.sourceOfTruthHint || "",
477
- payload: options.answers?.payload || previous.payload || scan.payloadHint || "",
478
- behaviorRules: normalizeList(options.answers?.behaviorRules || previous.behaviorRules || ""),
479
- inputSchema: options.answers?.inputSchema || previous.inputSchema || {},
480
- outputSchema: options.answers?.outputSchema || previous.outputSchema || {},
481
- architecturalInvariants: normalizeList(options.answers?.architecturalInvariants || previous.architecturalInvariants || ""),
482
- pipeline: normalizeList(options.answers?.pipeline || previous.pipeline || ""),
483
- templates: normalizeList(options.answers?.templates || previous.templates || ""),
484
- availableArtifacts: Array.isArray(previousDiscovery.availableArtifacts) ? previousDiscovery.availableArtifacts : [],
485
- };
486
-
487
- const answers = { ...defaults };
488
-
489
- if (interactive) {
490
- console.log("");
491
- console.log(t("bootstrap.header"));
492
- console.log(t("bootstrap.subtitle"));
493
- console.log(t("bootstrap.instructions"));
494
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
495
- try {
496
- answers.technicalLevel = await askEnumQuestion(rl, t("bootstrap.question.technicalLevel"), defaults.technicalLevel || "medium", TECHNICAL_LEVELS) || "medium";
497
- answers.projectState = await askEnumQuestion(rl, t("bootstrap.question.projectState"), defaults.projectState || "idea", PROJECT_STATES) || "idea";
498
- answers.documentationState = await askEnumQuestion(rl, t("bootstrap.question.docsState"), defaults.documentationState || "none", DOC_STATES) || "none";
499
- answers.decisionOwnership = await askEnumQuestion(rl, t("bootstrap.question.decisionOwnership"), defaults.decisionOwnership || "shared", DECISION_OWNERSHIPS) || "shared";
500
- } finally {
501
- rl.close();
502
- }
689
+ function ensureDirectBootstrapArtifacts(context, profile) {
690
+ const files = bootstrapFilePaths(context);
691
+ const existingIntake = readJson(files.intakeJson);
692
+ const intake = existingIntake && typeof existingIntake === "object"
693
+ ? existingIntake
694
+ : buildDirectIntakeTemplate(profile);
695
+ if (!existingIntake) {
696
+ writeJson(files.intakeJson, intake);
503
697
  }
504
698
 
505
- answers.availableArtifacts = answers.availableArtifacts.length
506
- ? answers.availableArtifacts
507
- : buildAvailableArtifacts(context, answers.documentationState);
508
-
509
- const routing = determineBootstrapMode(answers, options.bootstrapMode);
510
-
511
- if (routing.mode === "agent_handoff") {
512
- return {
513
- version: 2,
514
- status: "awaiting_agent",
515
- mode: "agent_handoff",
516
- routeReason: routing.routeReason,
517
- technicalLevel: answers.technicalLevel,
518
- projectState: answers.projectState,
519
- documentationState: answers.documentationState,
520
- decisionOwnership: answers.decisionOwnership,
521
- startedAt: previousBootstrap?.startedAt || nowIso(),
522
- completedAt: null,
523
- missingFields: ["intakeJson", "specDossier"],
524
- discovery: answers,
525
- inference: scan,
526
- };
699
+ let specText = readText(files.specDossier);
700
+ if (!specText.trim()) {
701
+ specText = buildSpecDossierScaffold({ ...profile, discovery: { ...profile.discovery, ...intake } });
702
+ writeText(files.specDossier, specText);
527
703
  }
528
704
 
529
- if (interactive) {
530
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
531
- try {
532
- answers.problemStatement = await askQuestion(rl, t("bootstrap.question.problemStatement"), defaults.problemStatement);
533
- answers.targetUser = await askQuestion(rl, t("bootstrap.question.targetUser"), defaults.targetUser);
534
- answers.singularDesiredOutcome = await askQuestion(rl, t("bootstrap.question.desiredOutcome"), defaults.singularDesiredOutcome);
535
- answers.externalServices = normalizeList(await askQuestion(rl, t("bootstrap.question.externalServices"), defaults.externalServices.join(", ")));
536
- answers.sourceOfTruth = await askQuestion(rl, t("bootstrap.question.sourceOfTruth"), defaults.sourceOfTruth);
537
- answers.payload = await askQuestion(rl, t("bootstrap.question.payload"), defaults.payload);
538
- answers.behaviorRules = normalizeList(await askQuestion(rl, t("bootstrap.question.behaviorRules"), defaults.behaviorRules.join("; ")));
539
- answers.inputSchema = parseJsonValue(await askQuestion(rl, t("bootstrap.question.inputSchema"), JSON.stringify(defaults.inputSchema)));
540
- answers.outputSchema = parseJsonValue(await askQuestion(rl, t("bootstrap.question.outputSchema"), JSON.stringify(defaults.outputSchema)));
541
- answers.architecturalInvariants = normalizeList(await askQuestion(rl, t("bootstrap.question.invariants"), defaults.architecturalInvariants.join("; ")));
542
- answers.pipeline = normalizeList(await askQuestion(rl, t("bootstrap.question.pipeline"), defaults.pipeline.join("; ")));
543
- answers.templates = normalizeList(await askQuestion(rl, t("bootstrap.question.templates"), defaults.templates.join(", ")));
544
- } finally {
545
- rl.close();
546
- }
547
- }
705
+ const mergedProfile = {
706
+ ...profile,
707
+ discovery: {
708
+ ...(profile.discovery || {}),
709
+ ...intake,
710
+ externalServices: normalizeList(intake.externalServices || profile.discovery?.externalServices || []),
711
+ behaviorRules: normalizeList(intake.behaviorRules || profile.discovery?.behaviorRules || []),
712
+ architecturalInvariants: normalizeList(intake.architecturalInvariants || profile.discovery?.architecturalInvariants || []),
713
+ pipeline: normalizeList(intake.pipeline || profile.discovery?.pipeline || []),
714
+ templates: normalizeList(intake.templates || profile.discovery?.templates || []),
715
+ inputSchema: intake.inputSchema || profile.discovery?.inputSchema || {},
716
+ outputSchema: intake.outputSchema || profile.discovery?.outputSchema || {},
717
+ decisionOwnership: intake.decisionOwnership || profile.discovery?.decisionOwnership || profile.decisionOwnership || null,
718
+ },
719
+ };
720
+ const qualityReport = buildQualityReport(context, mergedProfile, specText);
721
+ writeText(files.openQuestions, buildOpenQuestions(qualityReport.missingFields, qualityReport.contradictions));
722
+ writeJson(files.qualityReport, qualityReport);
548
723
 
549
- const missingFields = directMissingFields(answers);
550
724
  return {
551
- version: 2,
552
- status: missingFields.length ? "awaiting_intake" : "completed",
553
- mode: "direct_cli",
554
- routeReason: routing.routeReason,
555
- technicalLevel: answers.technicalLevel,
556
- projectState: answers.projectState,
557
- documentationState: answers.documentationState,
558
- decisionOwnership: answers.decisionOwnership,
559
- startedAt: previousBootstrap?.startedAt || nowIso(),
560
- completedAt: missingFields.length ? null : nowIso(),
561
- missingFields,
562
- discovery: answers,
563
- inference: scan,
725
+ intake,
726
+ specText,
727
+ qualityReport,
728
+ profile: mergedProfile,
564
729
  };
565
730
  }
566
731
 
567
- function isVirginGenesis(content) {
568
- const text = String(content || "");
569
- return !text.trim() || /TODO:/i.test(text) || /The Constitution of the project/i.test(text) || /La Constitución del proyecto/i.test(text);
570
- }
571
-
572
- function parseSpecSections(specText) {
573
- const sections = {};
574
- const lines = String(specText || "").split(/\r?\n/);
575
- let current = null;
576
- for (const line of lines) {
577
- const heading = line.match(/^##\s+(.+?)\s*$/);
578
- if (heading) {
579
- current = heading[1].trim().toLowerCase();
580
- sections[current] = [];
581
- continue;
582
- }
583
- if (current) sections[current].push(line);
584
- }
585
- return Object.fromEntries(
586
- Object.entries(sections).map(([key, value]) => [key, value.join("\n").trim()]),
587
- );
588
- }
589
-
590
- function buildOpenQuestions(missingFields, contradictions) {
591
- const lines = ["# Open questions", ""];
592
- if (missingFields.length) {
593
- lines.push("## Missing fields", "");
594
- missingFields.forEach((field) => lines.push(`- ${field}`));
595
- lines.push("");
596
- }
597
- if (contradictions.length) {
598
- lines.push("## Contradictions", "");
599
- contradictions.forEach((item) => lines.push(`- ${item}`));
600
- lines.push("");
601
- }
602
- if (!missingFields.length && !contradictions.length) {
603
- lines.push("- None.");
604
- }
605
- return `${lines.join("\n")}\n`;
606
- }
607
-
608
732
  function qualityStatusFor(missingFields, contradictions) {
609
- if (missingFields.length >= 2) return "blocked";
610
- if (missingFields.length || contradictions.length) return "needs_review";
611
- return "ready";
612
- }
613
-
614
- function contractReadinessFor(profile, qualityStatus) {
615
- if (qualityStatus === "blocked") return "hypothesis";
616
- if (qualityStatus === "needs_review") return "provisional";
617
- if (
618
- ["sos", "spec_dossier"].includes(profile.documentationState) &&
619
- profile.discovery?.decisionOwnership === "user" &&
620
- profile.mode === "direct_cli"
621
- ) {
622
- return "locked";
623
- }
624
- return "verified";
625
- }
626
-
733
+ if (missingFields.length >= 2) return "blocked";
734
+ if (missingFields.length || contradictions.length) return "needs_review";
735
+ return "ready";
736
+ }
737
+
738
+ function contractReadinessFor(profile, qualityStatus) {
739
+ if (qualityStatus === "blocked") return "hypothesis";
740
+ if (qualityStatus === "needs_review") return "provisional";
741
+ if (
742
+ ["sos", "spec_dossier"].includes(profile.documentationState) &&
743
+ profile.discovery?.decisionOwnership === "user" &&
744
+ profile.mode === "direct_cli"
745
+ ) {
746
+ return "locked";
747
+ }
748
+ return "verified";
749
+ }
750
+
627
751
  function buildQualityReport(context, profile, specText) {
628
752
  const sections = parseSpecSections(specText);
629
753
  const missingFields = directMissingFields(profile.discovery);
630
- if (!profile.discovery.decisionOwnership) missingFields.push("decisionOwnership");
631
- if (!profile.discovery.problemStatement) missingFields.push("problemStatement");
632
- if (!profile.discovery.targetUser) missingFields.push("targetUser");
633
754
 
634
755
  const contradictions = [];
635
756
  const mappings = [
636
- ["problem statement", "problemStatement", profile.discovery.problemStatement],
637
- ["target user", "targetUser", profile.discovery.targetUser],
638
- ["singular desired outcome", "singularDesiredOutcome", profile.discovery.singularDesiredOutcome],
639
- ["delivery target", "payload", profile.discovery.payload],
640
- ["source of truth", "sourceOfTruth", profile.discovery.sourceOfTruth],
757
+ ["problem statement", "problemStatement", profile.discovery.problemStatement, "problema principal"],
758
+ ["target user", "targetUser", profile.discovery.targetUser, "usuario objetivo", "usuario objetivo principal"],
759
+ ["singular desired outcome", "singularDesiredOutcome", profile.discovery.singularDesiredOutcome, "resultado singular deseado", "resultado deseado"],
760
+ ["payload", "payload", profile.discovery.payload, "delivery target", "objetivo de entrega"],
761
+ ["source of truth", "sourceOfTruth", profile.discovery.sourceOfTruth, "fuente de la verdad"],
641
762
  ];
642
- for (const [sectionName, fieldName, expected] of mappings) {
643
- const actual = sections[sectionName];
644
- if (!actual) {
645
- missingFields.push(fieldName);
646
- continue;
647
- }
648
- if (expected && actual && normalizeText(actual) !== normalizeText(expected)) {
649
- contradictions.push(`${sectionName}: spec dossier and intake disagree`);
650
- }
651
- }
652
-
653
- const status = qualityStatusFor(unique(missingFields), contradictions);
654
- const contractReadiness = contractReadinessFor(profile, status);
655
- const report = {
656
- version: 1,
657
- status,
658
- missingFields: unique(missingFields),
659
- contradictions,
660
- contractReadiness,
661
- validatedAt: nowIso(),
662
- handoffMode: profile.mode,
663
- };
664
- const files = bootstrapFilePaths(context);
665
- writeJson(files.qualityReport, report);
666
- writeText(files.openQuestions, buildOpenQuestions(report.missingFields, report.contradictions));
667
- return report;
668
- }
669
-
670
- function normalizeText(value) {
671
- return String(value || "").trim().replace(/\s+/g, " ").toLowerCase();
672
- }
673
-
674
- function buildOperatingContract(control, profile, qualityReport, context) {
675
- const discovery = profile.discovery || {};
676
- return {
677
- version: CONTRACT_VERSION,
678
- intent: {
679
- problemStatement: discovery.problemStatement || "",
680
- targetUser: discovery.targetUser || "",
681
- singularDesiredOutcome: discovery.singularDesiredOutcome || "",
682
- deliveryTarget: discovery.payload || "",
683
- },
684
- userModel: {
685
- technicalLevel: profile.technicalLevel || null,
686
- explanationMode: explanationModeFor(profile.technicalLevel),
687
- decisionOwnership: discovery.decisionOwnership || profile.decisionOwnership || null,
688
- language: discovery.userLanguage || config.getLocale(control),
689
- needsPlainLanguage: Boolean(discovery.needsPlainLanguage || profile.technicalLevel === "low"),
690
- },
691
- evidence: {
692
- projectState: profile.projectState || null,
693
- documentationState: profile.documentationState || null,
694
- sourceArtifacts: discovery.availableArtifacts || [],
695
- repoScan: profile.inference || {},
696
- },
697
- system: {
698
- sourceOfTruth: discovery.sourceOfTruth || "",
699
- externalServices: discovery.externalServices || [],
700
- inputSchema: discovery.inputSchema || {},
701
- outputSchema: discovery.outputSchema || {},
702
- behaviorRules: discovery.behaviorRules || [],
703
- architecturalInvariants: discovery.architecturalInvariants || [],
704
- },
705
- execution: {
706
- pipeline: discovery.pipeline || [],
707
- templates: discovery.templates || [],
708
- phaseModel: "opera-v3",
709
- taskSeeds: buildSeedTasks(control, profile).map((task) => task.id),
710
- },
711
- governance: {
712
- policyFile: policyRelativePath(context),
713
- riskProfile: "standard",
714
- approvalRules: [
715
- "destructive_changes_require_approval",
716
- "production_deploy_requires_approval",
717
- "external_side_effects_require_approval",
718
- ],
719
- },
720
- quality: {
721
- contractReadiness: qualityReport.contractReadiness,
722
- openQuestions: qualityReport.missingFields.concat(qualityReport.contradictions),
723
- lastValidatedAt: qualityReport.validatedAt,
724
- },
725
- };
726
- }
727
-
728
- function writeAutonomyPolicy(context) {
729
- const payload = {
730
- version: 1,
731
- defaultRiskProfile: "standard",
732
- levels: {
733
- green: ["read_files", "run_tests", "update_operational_docs", "write_tmp_files"],
734
- yellow: ["install_dependencies", "change_structure", "modify_pipeline"],
735
- red: ["delete_persistent_data", "deploy_to_production", "external_side_effects", "security_changes"],
736
- },
737
- approvalRules: {
738
- destructive_changes_require_approval: true,
739
- production_deploy_requires_approval: true,
740
- external_side_effects_require_approval: true,
741
- },
742
- };
743
- writeJson(context.paths.autonomyPolicyFile, payload);
744
- }
745
-
746
- function renderGenesis(control, contract) {
747
- const locale = config.getLocale(control);
748
- const templatePath = resolveLocalizedFile(TEMPLATES_DIR, locale, "genesis.md");
749
- let content = fs.readFileSync(templatePath, "utf8");
750
- const rules = (contract.system.behaviorRules || []).map((item) => `- ${item}`).join("\n") || `- ${t("bootstrap.noneDefined")}`;
751
- const invariants = (contract.system.architecturalInvariants || []).map((item) => `- ${item}`).join("\n") || `- ${t("bootstrap.noneDefined")}`;
752
- const services = (contract.system.externalServices || []).length
753
- ? contract.system.externalServices.map((item) => `| ${item} | ${t("bootstrap.servicePending")} | ${t("bootstrap.servicePending")} |`).join("\n")
754
- : `| | | |`;
755
- const pipeline = (contract.execution.pipeline || []).length
756
- ? contract.execution.pipeline.map((item) => `- ${item}`).join("\n")
757
- : `- ${t("bootstrap.noneDefined")}`;
758
- const templates = (contract.execution.templates || []).length
759
- ? contract.execution.templates.map((item) => `- \`${item}\``).join("\n")
760
- : `- ${t("bootstrap.noneDefined")}`;
761
- const schema = JSON.stringify({
762
- input: {
763
- source: contract.system.sourceOfTruth,
764
- schema: contract.system.inputSchema || {},
765
- },
766
- output: {
767
- destination: contract.intent.deliveryTarget,
768
- schema: contract.system.outputSchema || {},
769
- },
770
- }, null, 2);
771
-
772
- const replacements = {
773
- PROJECT_NAME: control.meta.projectName || "Project",
774
- DESIRED_OUTCOME: contract.intent.singularDesiredOutcome || t("bootstrap.pendingValue"),
775
- SERVICES_TABLE: services,
776
- SOURCE_OF_TRUTH: contract.system.sourceOfTruth || t("bootstrap.pendingValue"),
777
- PAYLOAD: contract.intent.deliveryTarget || t("bootstrap.pendingValue"),
778
- BEHAVIOR_RULES: rules,
779
- DATA_SCHEMA: schema,
780
- ARCHITECTURAL_INVARIANTS: invariants,
781
- PIPELINE_ITEMS: pipeline,
763
+ for (const mapping of mappings) {
764
+ const [sectionName, fieldName, expected, ...aliases] = mapping;
765
+ const actual = sections[sectionName] || aliases.reduce((found, alias) => found || sections[alias], null);
766
+ if (!actual) {
767
+ missingFields.push(fieldName);
768
+ continue;
769
+ }
770
+ if (expected && actual && normalizeText(actual) !== normalizeText(expected)) {
771
+ contradictions.push(`${sectionName}: spec dossier and intake disagree`);
772
+ }
773
+ }
774
+
775
+ const status = qualityStatusFor(unique(missingFields), contradictions);
776
+ const contractReadiness = contractReadinessFor(profile, status);
777
+ const report = {
778
+ version: 1,
779
+ status,
780
+ missingFields: unique(missingFields),
781
+ contradictions,
782
+ contractReadiness,
783
+ validatedAt: nowIso(),
784
+ handoffMode: profile.mode,
785
+ };
786
+ const files = bootstrapFilePaths(context);
787
+ writeJson(files.qualityReport, report);
788
+ writeText(files.openQuestions, buildOpenQuestions(report.missingFields, report.contradictions));
789
+ return report;
790
+ }
791
+
792
+ function normalizeText(value) {
793
+ return String(value || "").trim().replace(/\s+/g, " ").toLowerCase();
794
+ }
795
+
796
+ function buildOperatingContract(control, profile, qualityReport, context) {
797
+ const discovery = profile.discovery || {};
798
+ return {
799
+ version: CONTRACT_VERSION,
800
+ intent: {
801
+ problemStatement: discovery.problemStatement || "",
802
+ targetUser: discovery.targetUser || "",
803
+ singularDesiredOutcome: discovery.singularDesiredOutcome || "",
804
+ deliveryTarget: discovery.payload || "",
805
+ },
806
+ userModel: {
807
+ technicalLevel: profile.technicalLevel || null,
808
+ explanationMode: explanationModeFor(profile.technicalLevel),
809
+ decisionOwnership: discovery.decisionOwnership || profile.decisionOwnership || null,
810
+ language: discovery.userLanguage || config.getLocale(control),
811
+ needsPlainLanguage: Boolean(discovery.needsPlainLanguage || profile.technicalLevel === "low"),
812
+ },
813
+ evidence: {
814
+ projectState: profile.projectState || null,
815
+ documentationState: profile.documentationState || null,
816
+ sourceArtifacts: discovery.availableArtifacts || [],
817
+ repoScan: profile.inference || {},
818
+ },
819
+ system: {
820
+ sourceOfTruth: discovery.sourceOfTruth || "",
821
+ externalServices: discovery.externalServices || [],
822
+ inputSchema: discovery.inputSchema || {},
823
+ outputSchema: discovery.outputSchema || {},
824
+ behaviorRules: discovery.behaviorRules || [],
825
+ architecturalInvariants: discovery.architecturalInvariants || [],
826
+ },
827
+ execution: {
828
+ pipeline: discovery.pipeline || [],
829
+ templates: discovery.templates || [],
830
+ phaseModel: "opera-v3",
831
+ taskSeeds: buildSeedTasks(control, profile).map((task) => task.id),
832
+ },
833
+ governance: {
834
+ policyFile: policyRelativePath(context),
835
+ riskProfile: "standard",
836
+ approvalRules: [
837
+ "destructive_changes_require_approval",
838
+ "production_deploy_requires_approval",
839
+ "external_side_effects_require_approval",
840
+ ],
841
+ },
842
+ quality: {
843
+ contractReadiness: qualityReport.contractReadiness,
844
+ openQuestions: qualityReport.missingFields.concat(qualityReport.contradictions),
845
+ lastValidatedAt: qualityReport.validatedAt,
846
+ },
847
+ };
848
+ }
849
+
850
+ function writeAutonomyPolicy(context) {
851
+ const payload = {
852
+ version: 1,
853
+ defaultRiskProfile: "standard",
854
+ levels: {
855
+ green: ["read_files", "run_tests", "update_operational_docs", "write_tmp_files"],
856
+ yellow: ["install_dependencies", "change_structure", "modify_pipeline"],
857
+ red: ["delete_persistent_data", "deploy_to_production", "external_side_effects", "security_changes"],
858
+ },
859
+ approvalRules: {
860
+ destructive_changes_require_approval: true,
861
+ production_deploy_requires_approval: true,
862
+ external_side_effects_require_approval: true,
863
+ },
864
+ };
865
+ writeJson(context.paths.autonomyPolicyFile, payload);
866
+ }
867
+
868
+ function renderGenesis(control, contract) {
869
+ const locale = config.getLocale(control);
870
+ const templatePath = resolveLocalizedFile(TEMPLATES_DIR, locale, "genesis.md");
871
+ let content = fs.readFileSync(templatePath, "utf8");
872
+ const rules = (contract.system.behaviorRules || []).map((item) => `- ${item}`).join("\n") || `- ${t("bootstrap.noneDefined")}`;
873
+ const invariants = (contract.system.architecturalInvariants || []).map((item) => `- ${item}`).join("\n") || `- ${t("bootstrap.noneDefined")}`;
874
+ const services = (contract.system.externalServices || []).length
875
+ ? contract.system.externalServices.map((item) => `| ${item} | ${t("bootstrap.servicePending")} | ${t("bootstrap.servicePending")} |`).join("\n")
876
+ : `| | | — |`;
877
+ const pipeline = (contract.execution.pipeline || []).length
878
+ ? contract.execution.pipeline.map((item) => `- ${item}`).join("\n")
879
+ : `- ${t("bootstrap.noneDefined")}`;
880
+ const templates = (contract.execution.templates || []).length
881
+ ? contract.execution.templates.map((item) => `- \`${item}\``).join("\n")
882
+ : `- ${t("bootstrap.noneDefined")}`;
883
+ const schema = JSON.stringify({
884
+ input: {
885
+ source: contract.system.sourceOfTruth,
886
+ schema: contract.system.inputSchema || {},
887
+ },
888
+ output: {
889
+ destination: contract.intent.deliveryTarget,
890
+ schema: contract.system.outputSchema || {},
891
+ },
892
+ }, null, 2);
893
+
894
+ const replacements = {
895
+ PROJECT_NAME: control.meta.projectName || "Project",
896
+ DESIRED_OUTCOME: contract.intent.singularDesiredOutcome || t("bootstrap.pendingValue"),
897
+ SERVICES_TABLE: services,
898
+ SOURCE_OF_TRUTH: contract.system.sourceOfTruth || t("bootstrap.pendingValue"),
899
+ PAYLOAD: contract.intent.deliveryTarget || t("bootstrap.pendingValue"),
900
+ BEHAVIOR_RULES: rules,
901
+ DATA_SCHEMA: schema,
902
+ ARCHITECTURAL_INVARIANTS: invariants,
903
+ PIPELINE_ITEMS: pipeline,
782
904
  TEMPLATE_ITEMS: templates,
783
905
  };
784
906
 
@@ -788,49 +910,52 @@ function renderGenesis(control, contract) {
788
910
  return content;
789
911
  }
790
912
 
791
- function buildSeedTasks(control, profile) {
792
- const phaseOrder = config.getPhases(control);
793
- const firstTaskStatus =
794
- profile.status === "completed"
795
- ? "completed"
796
- : ["blocked", "needs_review"].includes(profile.status)
797
- ? "blocked"
798
- : profile.mode === "agent_handoff"
799
- ? (profile.status === "awaiting_agent" ? "blocked" : "pending")
800
- : "blocked";
801
- const tasks = [
802
- {
803
- id: "opera-bootstrap",
804
- origin: "bootstrap",
805
- title: t("bootstrap.task.bootstrap.title"),
806
- phase: phaseOrder[0]?.id || "O",
807
- stream: "Operations",
808
- priority: "P0",
809
- status: firstTaskStatus,
810
- required: true,
811
- dependsOn: [],
812
- summary: profile.mode === "agent_handoff"
813
- ? t("bootstrap.task.bootstrap.handoffSummary")
814
- : t("bootstrap.task.bootstrap.summary"),
815
- acceptance: profile.mode === "agent_handoff"
816
- ? [
817
- t("bootstrap.acceptance.intake"),
818
- t("bootstrap.acceptance.specDossier"),
819
- t("bootstrap.acceptance.resume"),
820
- ]
821
- : [
822
- t("bootstrap.acceptance.discovery"),
823
- t("bootstrap.acceptance.schema"),
824
- t("bootstrap.acceptance.rules"),
825
- t("bootstrap.acceptance.plan"),
826
- ],
827
- blocker: profile.status === "completed"
828
- ? undefined
829
- : profile.mode === "agent_handoff"
830
- ? t("bootstrap.blocker.awaitingAgent")
831
- : t("bootstrap.blocker.missingData"),
832
- history: [{ at: nowIso(), action: "create", note: t("bootstrap.history.seeded") }],
833
- },
913
+ function buildSeedTasks(control, profile) {
914
+ let phaseOrder = config.getPhases(control);
915
+ if (!phaseOrder || !phaseOrder.length) {
916
+ phaseOrder = config.buildDefaultPhases ? config.buildDefaultPhases(config.getLocale(control)) : config.DEFAULT_PHASES;
917
+ }
918
+ const firstTaskStatus =
919
+ profile.status === "completed"
920
+ ? "completed"
921
+ : ["blocked", "needs_review"].includes(profile.status)
922
+ ? "blocked"
923
+ : profile.mode === "agent_handoff"
924
+ ? (profile.status === "awaiting_agent" ? "blocked" : "pending")
925
+ : "blocked";
926
+ const tasks = [
927
+ {
928
+ id: "opera-bootstrap",
929
+ origin: "bootstrap",
930
+ title: t("bootstrap.task.bootstrap.title"),
931
+ phase: phaseOrder[0]?.id || "O",
932
+ stream: "Operations",
933
+ priority: "P0",
934
+ status: firstTaskStatus,
935
+ required: true,
936
+ dependsOn: [],
937
+ summary: profile.mode === "agent_handoff"
938
+ ? t("bootstrap.task.bootstrap.handoffSummary")
939
+ : t("bootstrap.task.bootstrap.summary"),
940
+ acceptance: profile.mode === "agent_handoff"
941
+ ? [
942
+ t("bootstrap.acceptance.intake"),
943
+ t("bootstrap.acceptance.specDossier"),
944
+ t("bootstrap.acceptance.resume"),
945
+ ]
946
+ : [
947
+ t("bootstrap.acceptance.discovery"),
948
+ t("bootstrap.acceptance.schema"),
949
+ t("bootstrap.acceptance.rules"),
950
+ t("bootstrap.acceptance.plan"),
951
+ ],
952
+ blocker: profile.status === "completed"
953
+ ? undefined
954
+ : profile.mode === "agent_handoff"
955
+ ? t("bootstrap.blocker.awaitingAgent")
956
+ : t("bootstrap.blocker.missingData"),
957
+ history: [{ at: nowIso(), action: "create", note: t("bootstrap.history.seeded") }],
958
+ },
834
959
  {
835
960
  id: "opera-prove-integrations",
836
961
  origin: "bootstrap",
@@ -908,229 +1033,290 @@ function buildSeedTasks(control, profile) {
908
1033
  },
909
1034
  ];
910
1035
 
911
- return tasks;
912
- }
913
-
914
- function createAwaitingBootstrapState(context) {
915
- return {
916
- version: CONTRACT_VERSION,
917
- mode: null,
918
- status: "awaiting_intake",
919
- routeReason: null,
920
- technicalLevel: null,
921
- projectState: null,
922
- documentationState: null,
923
- decisionOwnership: null,
924
- startedAt: nowIso(),
925
- completedAt: null,
926
- missingFields: [],
927
- discovery: {},
928
- handoffFiles: {
929
- markdown: bootstrapRelativePaths(context).markdown,
930
- json: bootstrapRelativePaths(context).json,
931
- },
932
- intakeFiles: {
933
- json: bootstrapRelativePaths(context).intakeJson,
934
- specDossier: bootstrapRelativePaths(context).specDossier,
935
- },
936
- reviewFiles: {
937
- openQuestions: bootstrapRelativePaths(context).openQuestions,
938
- qualityReport: bootstrapRelativePaths(context).qualityReport,
939
- },
940
- inference: scanProject(context),
941
- };
942
- }
943
-
944
- function applyBootstrap(root, control, profile) {
945
- const context = config.ensureContext(root);
946
- setLocale(config.getLocale(control));
947
- control.meta = control.meta || {};
948
- control.meta.opera = control.meta.opera || {};
949
- control.meta.opera.model = "v3";
950
- writeAutonomyPolicy(context);
951
- control.meta.userProfile = {
952
- technicalLevel: profile.technicalLevel || null,
953
- explanationMode: explanationModeFor(profile.technicalLevel),
954
- capturedAt: nowIso(),
955
- };
956
- control.meta.discovery = {
957
- projectState: profile.projectState || null,
958
- documentationState: profile.documentationState || null,
959
- availableArtifacts: Array.isArray(profile.discovery?.availableArtifacts) ? profile.discovery.availableArtifacts : [],
960
- };
961
- const relative = bootstrapRelativePaths(context);
962
- control.meta.opera.bootstrap = {
963
- ...profile,
964
- handoffFiles: {
965
- markdown: relative.markdown,
966
- json: relative.json,
967
- },
968
- intakeFiles: {
969
- json: relative.intakeJson,
970
- specDossier: relative.specDossier,
971
- },
972
- reviewFiles: {
973
- openQuestions: relative.openQuestions,
974
- qualityReport: relative.qualityReport,
975
- },
976
- };
977
- control.meta.currentFocus = profile.discovery.singularDesiredOutcome || profile.discovery.desiredOutcome || t("bootstrap.defaultFocus");
978
- control.meta.deliveryTarget = profile.discovery.payload || t("bootstrap.defaultTarget");
979
- control.meta.focusPhase = profile.status === "completed" ? "P" : "O";
980
- control.meta.phases = config.getPhases(control);
981
- control.meta.opera.legacyStatus = "supported";
982
-
983
- const remainingTasks = (control.tasks || []).filter((task) => task.id !== "ops-bootstrap" && task.origin !== "bootstrap");
984
- control.tasks = [...remainingTasks, ...buildSeedTasks(control, profile)];
985
-
986
- control.decisionsPending = (profile.missingFields || [])
987
- .filter((field) => !["intakeJson", "specDossier"].includes(field))
988
- .map((field) => ({
989
- owner: "user",
990
- title: t(`bootstrap.field.${field}`),
991
- impact: t("bootstrap.decisionImpact"),
992
- }));
993
- if (profile.mode === "agent_handoff" && profile.status !== "completed") {
994
- control.decisionsPending.unshift({
995
- owner: "user",
996
- title: t("bootstrap.pendingDecision.handoff"),
997
- impact: t("bootstrap.pendingDecision.handoffImpact"),
998
- });
999
- }
1000
-
1001
- control.findings = control.findings || [];
1036
+ return tasks;
1037
+ }
1038
+
1039
+ function createAwaitingBootstrapState(context) {
1040
+ return {
1041
+ version: CONTRACT_VERSION,
1042
+ mode: null,
1043
+ status: "awaiting_intake",
1044
+ routeReason: null,
1045
+ technicalLevel: null,
1046
+ projectState: null,
1047
+ documentationState: null,
1048
+ decisionOwnership: null,
1049
+ startedAt: nowIso(),
1050
+ completedAt: null,
1051
+ missingFields: [],
1052
+ discovery: {},
1053
+ handoffFiles: {
1054
+ markdown: bootstrapRelativePaths(context).markdown,
1055
+ json: bootstrapRelativePaths(context).json,
1056
+ },
1057
+ intakeFiles: {
1058
+ json: bootstrapRelativePaths(context).intakeJson,
1059
+ specDossier: bootstrapRelativePaths(context).specDossier,
1060
+ },
1061
+ reviewFiles: {
1062
+ openQuestions: bootstrapRelativePaths(context).openQuestions,
1063
+ qualityReport: bootstrapRelativePaths(context).qualityReport,
1064
+ },
1065
+ inference: scanProject(context),
1066
+ };
1067
+ }
1068
+
1069
+ function applyBootstrap(root, control, profile) {
1070
+ const context = config.ensureContext(root);
1071
+ setLocale(config.getLocale(control));
1072
+ control.meta = control.meta || {};
1073
+ control.meta.opera = control.meta.opera || {};
1074
+ control.meta.opera.model = "v3";
1075
+ writeAutonomyPolicy(context);
1076
+ control.meta.userProfile = {
1077
+ technicalLevel: profile.technicalLevel || null,
1078
+ explanationMode: explanationModeFor(profile.technicalLevel),
1079
+ capturedAt: nowIso(),
1080
+ };
1081
+ control.meta.discovery = {
1082
+ projectState: profile.projectState || null,
1083
+ documentationState: profile.documentationState || null,
1084
+ availableArtifacts: Array.isArray(profile.discovery?.availableArtifacts) ? profile.discovery.availableArtifacts : [],
1085
+ };
1086
+ const relative = bootstrapRelativePaths(context);
1087
+ control.meta.opera.bootstrap = {
1088
+ ...profile,
1089
+ handoffFiles: {
1090
+ markdown: relative.markdown,
1091
+ json: relative.json,
1092
+ },
1093
+ intakeFiles: {
1094
+ json: relative.intakeJson,
1095
+ specDossier: relative.specDossier,
1096
+ },
1097
+ reviewFiles: {
1098
+ openQuestions: relative.openQuestions,
1099
+ qualityReport: relative.qualityReport,
1100
+ },
1101
+ };
1102
+ control.meta.currentFocus = profile.discovery.singularDesiredOutcome || profile.discovery.desiredOutcome || t("bootstrap.defaultFocus");
1103
+ control.meta.deliveryTarget = profile.discovery.payload || t("bootstrap.defaultTarget");
1104
+ control.meta.focusPhase = profile.status === "completed" ? "P" : "O";
1105
+ control.meta.phases = config.getPhases(control);
1106
+ control.meta.opera.legacyStatus = "supported";
1107
+
1108
+ const existingBootstrap = new Map(
1109
+ (control.tasks || []).filter((task) => task.origin === "bootstrap").map((task) => [task.id, task])
1110
+ );
1111
+ const remainingTasks = (control.tasks || []).filter((task) => task.id !== "ops-bootstrap" && task.origin !== "bootstrap");
1112
+ const newSeeds = buildSeedTasks(control, profile).map((seed) => {
1113
+ const existing = existingBootstrap.get(seed.id);
1114
+ if (!existing) return seed;
1115
+ return {
1116
+ ...seed,
1117
+ status: existing.status === "completed" ? "completed" : seed.status,
1118
+ history: existing.history || seed.history,
1119
+ blocker: existing.status === "blocked" ? existing.blocker : seed.blocker,
1120
+ };
1121
+ });
1122
+ control.tasks = [...remainingTasks, ...newSeeds];
1123
+
1124
+ control.decisionsPending = (profile.missingFields || [])
1125
+ .filter((field) => !["intakeJson", "specDossier"].includes(field))
1126
+ .map((field) => ({
1127
+ owner: "user",
1128
+ title: t(`bootstrap.field.${field}`),
1129
+ impact: t("bootstrap.decisionImpact"),
1130
+ }));
1131
+ if (profile.mode === "agent_handoff" && profile.status !== "completed") {
1132
+ control.decisionsPending.unshift({
1133
+ owner: "user",
1134
+ title: t("bootstrap.pendingDecision.handoff"),
1135
+ impact: t("bootstrap.pendingDecision.handoffImpact"),
1136
+ });
1137
+ }
1138
+
1139
+ control.findings = control.findings || [];
1002
1140
  const files = bootstrapFilePaths(context);
1003
1141
  if (profile.mode === "agent_handoff") {
1004
1142
  writeJson(files.json, buildHandoffPayload(control, profile, context));
1005
1143
  writeText(files.markdown, buildHandoffPrompt(control, profile));
1006
1144
  if (!fs.existsSync(files.specDossier)) {
1007
- writeText(files.specDossier, "# Spec dossier\n\nUse this file to consolidate the project specification before OPERA ingest.\n");
1145
+ writeText(
1146
+ files.specDossier,
1147
+ `# ${t("bootstrap.scaffold.specTitle")}\n\n${t("bootstrap.scaffold.specPlaceholder")}\n`,
1148
+ );
1008
1149
  }
1009
1150
  writeText(files.openQuestions, buildOpenQuestions(["intakeJson", "specDossier"], []));
1010
1151
  }
1152
+ let directArtifacts = null;
1153
+ if (profile.mode === "direct_cli") {
1154
+ directArtifacts = ensureDirectBootstrapArtifacts(context, profile);
1155
+ profile = directArtifacts.profile;
1156
+ control.meta.opera.bootstrap = {
1157
+ ...control.meta.opera.bootstrap,
1158
+ ...profile,
1159
+ handoffFiles: null,
1160
+ };
1161
+ }
1011
1162
  const genesisPath = context.paths.genesisFile;
1012
1163
  if (profile.status === "completed") {
1013
- const specText = fs.existsSync(files.specDossier) && readText(files.specDossier).trim()
1014
- ? readText(files.specDossier)
1015
- : `## 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`;
1016
- const qualityReport = profile.qualityReport || buildQualityReport(context, profile, specText);
1164
+ const specText = directArtifacts?.specText || (
1165
+ fs.existsSync(files.specDossier) && readText(files.specDossier).trim()
1166
+ ? readText(files.specDossier)
1167
+ : `## 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`
1168
+ );
1169
+ const qualityReport = directArtifacts?.qualityReport || profile.qualityReport || buildQualityReport(context, profile, specText);
1017
1170
  const contract = buildOperatingContract(control, profile, qualityReport, context);
1018
1171
  writeJson(context.paths.contractFile, contract);
1019
1172
  fs.writeFileSync(genesisPath, `${renderGenesis(control, contract)}\n`, "utf8");
1020
1173
  control.meta.opera.contractVersion = CONTRACT_VERSION;
1021
- control.meta.opera.contractReadiness = qualityReport.contractReadiness;
1022
- control.meta.opera.contractFile = contractRelativePath(context);
1023
- control.meta.opera.qualityStatus = qualityReport.status;
1174
+ control.meta.opera.contractReadiness = qualityReport.contractReadiness;
1175
+ control.meta.opera.contractFile = contractRelativePath(context);
1176
+ control.meta.opera.qualityStatus = qualityReport.status;
1024
1177
  } else if (["needs_review", "blocked"].includes(profile.status)) {
1178
+ const qualityReport = directArtifacts?.qualityReport || profile.qualityReport || null;
1025
1179
  control.meta.opera.contractVersion = null;
1026
- control.meta.opera.contractReadiness = profile.qualityReport?.contractReadiness || "hypothesis";
1027
- control.meta.opera.qualityStatus = profile.qualityReport?.status || profile.status;
1180
+ control.meta.opera.contractReadiness = qualityReport?.contractReadiness || "hypothesis";
1181
+ control.meta.opera.qualityStatus = qualityReport?.status || profile.status;
1028
1182
  } else {
1183
+ const qualityReport = directArtifacts?.qualityReport || profile.qualityReport || null;
1029
1184
  control.meta.opera.contractVersion = null;
1030
- control.meta.opera.contractReadiness = "hypothesis";
1031
- control.meta.opera.qualityStatus = profile.status;
1032
- }
1033
-
1034
- config.saveControl(context, control);
1035
- return control;
1036
- }
1037
-
1038
- function resumeBootstrap(root, control) {
1039
- const context = config.ensureContext(root);
1040
- const bootstrap = getBootstrapState(control, context);
1041
- if (!bootstrap) {
1042
- return { resumed: false, status: "awaiting_intake", reason: "no_bootstrap_state" };
1043
- }
1044
-
1045
- const files = bootstrapFilePaths(context);
1046
- const intake = readJson(files.intakeJson);
1047
- const specDossier = readText(files.specDossier);
1048
- if (!intake || !specDossier.trim()) {
1049
- return { resumed: false, status: "awaiting_agent", reason: "missing_agent_artifacts" };
1050
- }
1051
-
1052
- const discovery = {
1053
- ...bootstrap.discovery,
1054
- ...intake,
1055
- singularDesiredOutcome: intake.singularDesiredOutcome || bootstrap.discovery?.singularDesiredOutcome || "",
1056
- externalServices: normalizeList(intake.externalServices || bootstrap.discovery?.externalServices || []),
1057
- behaviorRules: normalizeList(intake.behaviorRules || bootstrap.discovery?.behaviorRules || []),
1058
- architecturalInvariants: normalizeList(intake.architecturalInvariants || bootstrap.discovery?.architecturalInvariants || []),
1059
- pipeline: normalizeList(intake.pipeline || bootstrap.discovery?.pipeline || []),
1060
- templates: normalizeList(intake.templates || bootstrap.discovery?.templates || []),
1061
- inputSchema: intake.inputSchema || bootstrap.discovery?.inputSchema || {},
1062
- outputSchema: intake.outputSchema || bootstrap.discovery?.outputSchema || {},
1063
- };
1064
- const missingFields = directMissingFields(discovery);
1065
- const profile = {
1066
- ...bootstrap,
1067
- mode: bootstrap.mode || "agent_handoff",
1068
- status: missingFields.length ? "needs_review" : "completed",
1069
- technicalLevel: intake.technicalLevel || bootstrap.technicalLevel || null,
1070
- projectState: intake.projectState || bootstrap.projectState || null,
1071
- documentationState: intake.documentationState || bootstrap.documentationState || null,
1072
- decisionOwnership: intake.decisionOwnership || bootstrap.decisionOwnership || null,
1073
- completedAt: missingFields.length ? null : nowIso(),
1074
- missingFields,
1075
- discovery,
1076
- inference: scanProject(context),
1077
- };
1078
- const qualityReport = buildQualityReport(context, profile, specDossier);
1079
- profile.status = qualityReport.status === "ready" ? "completed" : qualityReport.status;
1080
- profile.completedAt = qualityReport.status === "ready" ? nowIso() : null;
1081
- profile.qualityReport = qualityReport;
1082
- return { resumed: true, profile };
1083
- }
1084
-
1085
- function detectLegacyBootstrap(root, control) {
1086
- const context = config.ensureContext(root);
1087
- if (!config.isOperaInstalled(control)) return null;
1088
- if (control.meta?.opera?.bootstrap) return getBootstrapState(control, context);
1089
- if (control.meta?.opera?.model === "v3") {
1090
- return createAwaitingBootstrapState(context);
1185
+ control.meta.opera.contractReadiness = qualityReport?.contractReadiness || "hypothesis";
1186
+ control.meta.opera.qualityStatus = qualityReport?.status || profile.status;
1091
1187
  }
1092
- if (!fs.existsSync(context.paths.contractFile)) {
1093
- return {
1094
- version: CONTRACT_VERSION,
1095
- status: "legacy_unsupported",
1096
- mode: null,
1097
- routeReason: "legacy_unsupported",
1098
- technicalLevel: null,
1099
- projectState: null,
1100
- documentationState: null,
1101
- decisionOwnership: null,
1102
- startedAt: control.meta?.opera?.installedAt || nowIso(),
1103
- completedAt: null,
1104
- missingFields: [],
1105
- discovery: {},
1106
- inference: scanProject(context),
1107
- };
1108
- }
1109
- return null;
1110
- }
1111
-
1112
- module.exports = {
1113
- TECHNICAL_LEVELS,
1114
- PROJECT_STATES,
1115
- DOC_STATES,
1116
- DECISION_OWNERSHIPS,
1117
- BOOTSTRAP_MODES,
1118
- QUALITY_STATUSES,
1119
- CONTRACT_READINESS,
1120
- CONTRACT_VERSION,
1121
- bootstrapFilePaths,
1122
- bootstrapRelativePaths,
1123
- contractRelativePath,
1124
- policyRelativePath,
1125
- scanProject,
1126
- collectBootstrapProfile,
1127
- applyBootstrap,
1128
- resumeBootstrap,
1129
- detectLegacyBootstrap,
1130
- getBootstrapState,
1131
- buildQualityReport,
1132
- buildOperatingContract,
1133
- writeAutonomyPolicy,
1134
- createAwaitingBootstrapState,
1135
- isVirginGenesis,
1136
- };
1188
+
1189
+ config.saveControl(context, control);
1190
+ return control;
1191
+ }
1192
+
1193
+ function resumeBootstrap(root, control) {
1194
+ const context = config.ensureContext(root);
1195
+ const bootstrap = getBootstrapState(control, context);
1196
+ if (!bootstrap) {
1197
+ return { resumed: false, status: "awaiting_intake", reason: "no_bootstrap_state" };
1198
+ }
1199
+
1200
+ const files = bootstrapFilePaths(context);
1201
+ const intake = readJson(files.intakeJson);
1202
+ const specDossier = readText(files.specDossier);
1203
+ if (!intake || typeof intake !== "object") {
1204
+ return { resumed: false, status: "awaiting_agent", reason: "missing_agent_artifacts" };
1205
+ }
1206
+ const meaningfulKeys = ["problemStatement", "targetUser", "singularDesiredOutcome", "payload", "sourceOfTruth"];
1207
+ const hasContent = meaningfulKeys.some((k) => intake[k] && String(intake[k]).trim());
1208
+ if (!hasContent && !specDossier.trim()) {
1209
+ return { resumed: false, status: "awaiting_agent", reason: "empty_intake_and_spec" };
1210
+ }
1211
+
1212
+ const discovery = {
1213
+ ...bootstrap.discovery,
1214
+ ...intake,
1215
+ singularDesiredOutcome: intake.singularDesiredOutcome || bootstrap.discovery?.singularDesiredOutcome || "",
1216
+ externalServices: normalizeList(intake.externalServices || bootstrap.discovery?.externalServices || []),
1217
+ behaviorRules: normalizeList(intake.behaviorRules || bootstrap.discovery?.behaviorRules || []),
1218
+ architecturalInvariants: normalizeList(intake.architecturalInvariants || bootstrap.discovery?.architecturalInvariants || []),
1219
+ pipeline: normalizeList(intake.pipeline || bootstrap.discovery?.pipeline || []),
1220
+ templates: normalizeList(intake.templates || bootstrap.discovery?.templates || []),
1221
+ inputSchema: intake.inputSchema || bootstrap.discovery?.inputSchema || {},
1222
+ outputSchema: intake.outputSchema || bootstrap.discovery?.outputSchema || {},
1223
+ };
1224
+ const missingFields = directMissingFields(discovery);
1225
+ const profile = {
1226
+ ...bootstrap,
1227
+ mode: bootstrap.mode || "agent_handoff",
1228
+ status: missingFields.length ? "needs_review" : "completed",
1229
+ technicalLevel: intake.technicalLevel || bootstrap.technicalLevel || null,
1230
+ projectState: intake.projectState || bootstrap.projectState || null,
1231
+ documentationState: intake.documentationState || bootstrap.documentationState || null,
1232
+ decisionOwnership: intake.decisionOwnership || bootstrap.decisionOwnership || null,
1233
+ completedAt: missingFields.length ? null : nowIso(),
1234
+ missingFields,
1235
+ discovery,
1236
+ inference: scanProject(context),
1237
+ };
1238
+ const qualityReport = buildQualityReport(context, profile, specDossier);
1239
+ profile.status = qualityReport.status === "ready" ? "completed" : qualityReport.status;
1240
+ profile.completedAt = qualityReport.status === "ready" ? nowIso() : null;
1241
+ profile.qualityReport = qualityReport;
1242
+ return { resumed: true, profile };
1243
+ }
1244
+
1245
+ function detectLegacyBootstrap(root, control) {
1246
+ const context = config.ensureContext(root);
1247
+ if (!config.isOperaInstalled(control)) return null;
1248
+ if (control.meta?.opera?.bootstrap) return getBootstrapState(control, context);
1249
+ if (control.meta?.opera?.model === "v3") {
1250
+ return createAwaitingBootstrapState(context);
1251
+ }
1252
+ if (!fs.existsSync(context.paths.contractFile)) {
1253
+ return {
1254
+ version: CONTRACT_VERSION,
1255
+ status: "legacy_unsupported",
1256
+ mode: null,
1257
+ routeReason: "legacy_unsupported",
1258
+ technicalLevel: null,
1259
+ projectState: null,
1260
+ documentationState: null,
1261
+ decisionOwnership: null,
1262
+ startedAt: control.meta?.opera?.installedAt || nowIso(),
1263
+ completedAt: null,
1264
+ missingFields: [],
1265
+ discovery: {},
1266
+ inference: scanProject(context),
1267
+ };
1268
+ }
1269
+ return null;
1270
+ }
1271
+
1272
+ function revalidateContract(root, control) {
1273
+ const context = config.ensureContext(root);
1274
+ if (!config.isOperaInstalled(control)) return { changed: false };
1275
+ if (!fs.existsSync(context.paths.contractFile)) return { changed: false };
1276
+ const files = bootstrapFilePaths(context);
1277
+ const intake = readJson(files.intakeJson);
1278
+ const specDossier = readText(files.specDossier);
1279
+ if (!intake) return { changed: false };
1280
+ const bootstrapState = getBootstrapState(control, context);
1281
+ if (!bootstrapState || !bootstrapState.discovery) return { changed: false };
1282
+ const discovery = { ...bootstrapState.discovery, ...intake };
1283
+ const profile = { ...bootstrapState, discovery };
1284
+ const qualityReport = buildQualityReport(context, profile, specDossier);
1285
+ const newContract = buildOperatingContract(control, profile, qualityReport, context);
1286
+ const existing = readJson(context.paths.contractFile);
1287
+ const changed = JSON.stringify(existing) !== JSON.stringify(newContract);
1288
+ if (changed) {
1289
+ writeJson(context.paths.contractFile, newContract);
1290
+ control.meta.opera.contractReadiness = qualityReport.contractReadiness;
1291
+ control.meta.opera.qualityStatus = qualityReport.status === "ready" ? "completed" : qualityReport.status;
1292
+ config.saveControl(context, control);
1293
+ }
1294
+ return { changed, contractReadiness: qualityReport.contractReadiness };
1295
+ }
1296
+
1297
+ module.exports = {
1298
+ TECHNICAL_LEVELS,
1299
+ PROJECT_STATES,
1300
+ DOC_STATES,
1301
+ DECISION_OWNERSHIPS,
1302
+ BOOTSTRAP_MODES,
1303
+ QUALITY_STATUSES,
1304
+ CONTRACT_READINESS,
1305
+ CONTRACT_VERSION,
1306
+ bootstrapFilePaths,
1307
+ bootstrapRelativePaths,
1308
+ contractRelativePath,
1309
+ policyRelativePath,
1310
+ scanProject,
1311
+ collectBootstrapProfile,
1312
+ applyBootstrap,
1313
+ resumeBootstrap,
1314
+ revalidateContract,
1315
+ detectLegacyBootstrap,
1316
+ getBootstrapState,
1317
+ buildQualityReport,
1318
+ buildOperatingContract,
1319
+ writeAutonomyPolicy,
1320
+ createAwaitingBootstrapState,
1321
+ isVirginGenesis,
1322
+ };