trackops 2.0.3 → 2.0.5

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