trackops 1.0.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/README.md +292 -272
  2. package/bin/trackops.js +108 -50
  3. package/lib/config.js +267 -38
  4. package/lib/control.js +534 -480
  5. package/lib/env.js +244 -0
  6. package/lib/i18n.js +61 -53
  7. package/lib/init.js +170 -47
  8. package/lib/locale.js +63 -0
  9. package/lib/opera-bootstrap.js +1075 -0
  10. package/lib/opera.js +524 -125
  11. package/lib/preferences.js +74 -0
  12. package/lib/registry.js +27 -13
  13. package/lib/release.js +56 -0
  14. package/lib/resources.js +42 -0
  15. package/lib/runtime-state.js +144 -0
  16. package/lib/server.js +1004 -521
  17. package/lib/skills.js +148 -124
  18. package/lib/workspace.js +260 -0
  19. package/locales/en.json +418 -132
  20. package/locales/es.json +418 -132
  21. package/package.json +8 -9
  22. package/scripts/postinstall-locale.js +21 -0
  23. package/scripts/skills-marketplace-smoke.js +124 -0
  24. package/scripts/smoke-tests.js +570 -0
  25. package/scripts/sync-skill-version.js +21 -0
  26. package/scripts/validate-skill.js +89 -0
  27. package/skills/trackops/SKILL.md +89 -0
  28. package/skills/trackops/agents/openai.yaml +3 -0
  29. package/skills/trackops/references/activation.md +73 -0
  30. package/skills/trackops/references/troubleshooting.md +49 -0
  31. package/skills/trackops/references/workflow.md +26 -0
  32. package/skills/trackops/scripts/bootstrap-trackops.js +203 -0
  33. package/skills/trackops/skill.json +29 -0
  34. package/templates/opera/agent.md +10 -9
  35. package/templates/opera/architecture/dependency-graph.md +24 -0
  36. package/templates/opera/architecture/runtime-automation.md +24 -0
  37. package/templates/opera/architecture/runtime-operations.md +34 -0
  38. package/templates/opera/en/agent.md +27 -0
  39. package/templates/opera/en/architecture/dependency-graph.md +24 -0
  40. package/templates/opera/en/architecture/runtime-automation.md +24 -0
  41. package/templates/opera/en/architecture/runtime-operations.md +34 -0
  42. package/templates/opera/en/genesis.md +79 -0
  43. package/templates/opera/en/references/autonomy-and-recovery.md +23 -0
  44. package/templates/opera/en/references/opera-cycle.md +62 -0
  45. package/templates/opera/en/registry.md +28 -0
  46. package/templates/opera/en/reviews/delivery-audit.md +18 -0
  47. package/templates/opera/en/reviews/integration-audit.md +18 -0
  48. package/templates/opera/en/router.md +49 -0
  49. package/templates/opera/genesis.md +79 -94
  50. package/templates/opera/reviews/delivery-audit.md +18 -0
  51. package/templates/opera/reviews/integration-audit.md +18 -0
  52. package/templates/opera/router.md +15 -5
  53. package/templates/skills/changelog-updater/locales/en/SKILL.md +11 -0
  54. package/templates/skills/commiter/locales/en/SKILL.md +11 -0
  55. package/templates/skills/opera-contract-auditor/SKILL.md +38 -0
  56. package/templates/skills/opera-contract-auditor/locales/en/SKILL.md +38 -0
  57. package/templates/skills/opera-policy-guard/SKILL.md +26 -0
  58. package/templates/skills/opera-policy-guard/locales/en/SKILL.md +26 -0
  59. package/templates/skills/project-starter-skill/SKILL.md +89 -164
  60. package/templates/skills/project-starter-skill/locales/en/SKILL.md +104 -0
  61. package/ui/css/panels.css +956 -953
  62. package/ui/index.html +1 -1
  63. package/ui/js/api.js +211 -194
  64. package/ui/js/app.js +200 -199
  65. package/ui/js/i18n.js +14 -0
  66. package/ui/js/onboarding.js +439 -437
  67. package/ui/js/state.js +130 -129
  68. package/ui/js/utils.js +175 -172
  69. package/ui/js/views/board.js +255 -254
  70. package/ui/js/views/execution.js +256 -256
  71. package/ui/js/views/insights.js +340 -339
  72. package/ui/js/views/overview.js +366 -361
  73. package/ui/js/views/settings.js +340 -202
  74. package/ui/js/views/sidebar.js +131 -132
  75. package/ui/js/views/skills.js +163 -162
  76. package/ui/js/views/tasks.js +406 -405
  77. package/ui/js/views/topbar.js +239 -183
  78. package/templates/etapa/agent.md +0 -26
  79. package/templates/etapa/genesis.md +0 -94
  80. package/templates/etapa/references/autonomy-and-recovery.md +0 -117
  81. package/templates/etapa/references/etapa-cycle.md +0 -193
  82. package/templates/etapa/registry.md +0 -28
  83. package/templates/etapa/router.md +0 -39
package/lib/control.js CHANGED
@@ -1,576 +1,630 @@
1
- #!/usr/bin/env node
2
-
1
+ #!/usr/bin/env node
2
+
3
3
  const fs = require("fs");
4
4
  const path = require("path");
5
5
  const { spawnSync } = require("child_process");
6
6
 
7
7
  const config = require("./config");
8
+ const env = require("./env");
8
9
  const { t, setLocale, getLocale } = require("./i18n");
9
-
10
- const PRIORITY_ORDER = ["P0", "P1", "P2", "P3"];
11
- const STATUS_ORDER = ["in_progress", "in_review", "pending", "blocked", "completed", "cancelled"];
12
- const STATUS_ICONS = {
13
- pending: "\u23F3",
14
- in_progress: "\uD83D\uDEA7",
15
- in_review: "\uD83D\uDC40",
16
- blocked: "\u26D4",
17
- completed: "\u2705",
18
- cancelled: "\uD83D\uDDD1\uFE0F",
19
- };
20
- const CHECK_ICONS = {
21
- pass: "\u2705",
22
- warn: "\u26A0\uFE0F",
23
- fail: "\u274C",
24
- pending: "\u23F3",
25
- };
26
-
27
- /* ── helpers ── */
28
-
29
- function writeText(filePath, content) {
30
- fs.writeFileSync(filePath, content.replace(/\r?\n/g, "\n"), "utf8");
31
- }
32
-
33
- function writeJson(filePath, data) {
34
- writeText(filePath, `${JSON.stringify(data, null, 2)}\n`);
35
- }
36
-
37
- function nowIso() {
38
- return new Date().toISOString();
39
- }
40
-
41
- function git(args, root) {
42
- const result = spawnSync("git", args, { cwd: root, encoding: "utf8" });
43
- if (result.error || result.status !== 0) return null;
44
- return result.stdout.replace(/\s+$/, "");
45
- }
46
-
47
- function statusLabel(status) {
48
- return t(`status.${status}`);
49
- }
50
-
51
- /* ── repo snapshot ── */
52
-
53
- function getRepoSnapshot(root) {
54
- const branch = git(["branch", "--show-current"], root) || "detached";
55
- const status = git(["status", "--short"], root) || "";
10
+
11
+ const PRIORITY_ORDER = ["P0", "P1", "P2", "P3"];
12
+ const STATUS_ORDER = ["in_progress", "in_review", "pending", "blocked", "completed", "cancelled"];
13
+ const STATUS_ICONS = {
14
+ pending: "\u23F3",
15
+ in_progress: "\uD83D\uDEA7",
16
+ in_review: "\uD83D\uDC40",
17
+ blocked: "\u26D4",
18
+ completed: "\u2705",
19
+ cancelled: "\uD83D\uDDD1\uFE0F",
20
+ };
21
+ const CHECK_ICONS = {
22
+ pass: "\u2705",
23
+ warn: "\u26A0\uFE0F",
24
+ fail: "\u274C",
25
+ pending: "\u23F3",
26
+ };
27
+
28
+ /* ── helpers ── */
29
+
30
+ function writeText(filePath, content) {
31
+ fs.writeFileSync(filePath, content.replace(/\r?\n/g, "\n"), "utf8");
32
+ }
33
+
34
+ function writeJson(filePath, data) {
35
+ writeText(filePath, `${JSON.stringify(data, null, 2)}\n`);
36
+ }
37
+
38
+ function nowIso() {
39
+ return new Date().toISOString();
40
+ }
41
+
42
+ function git(args, root) {
43
+ const result = spawnSync("git", args, { cwd: root, encoding: "utf8" });
44
+ if (result.error || result.status !== 0) return null;
45
+ return result.stdout.replace(/\s+$/, "");
46
+ }
47
+
48
+ function statusLabel(status) {
49
+ return t(`status.${status}`);
50
+ }
51
+
52
+ /* ── repo snapshot ── */
53
+
54
+ function getRepoSnapshot(contextOrRoot) {
55
+ const context = config.ensureContext(contextOrRoot);
56
+ const repoRoot = context.workspaceRoot;
57
+ const branch = git(["branch", "--show-current"], repoRoot) || "detached";
58
+ const status = git(["status", "--short"], repoRoot) || "";
56
59
  const lines = status.split(/\r?\n/).filter(Boolean);
57
- const lastCommitRaw = git(["log", "-1", "--pretty=format:%H%n%cs%n%s"], root);
58
- const divergenceRaw = git(["rev-list", "--left-right", "--count", "@{upstream}...HEAD"], root);
59
-
60
- let staged = 0;
61
- let unstaged = 0;
62
- let untracked = 0;
63
-
64
- lines.forEach((line) => {
65
- if (line.startsWith("??")) { untracked += 1; return; }
66
- if (line[0] && line[0] !== " ") staged += 1;
67
- if (line[1] && line[1] !== " ") unstaged += 1;
68
- });
69
-
70
- let lastCommit = null;
71
- if (lastCommitRaw) {
72
- const [hash, date, subject] = lastCommitRaw.split(/\r?\n/);
73
- lastCommit = { hash, shortHash: hash ? hash.slice(0, 7) : null, date, subject };
74
- }
75
-
76
- let ahead = 0;
77
- let behind = 0;
78
- if (divergenceRaw) {
79
- const [left, right] = divergenceRaw.split(/\s+/).map(Number);
80
- behind = Number.isFinite(left) ? left : 0;
81
- ahead = Number.isFinite(right) ? right : 0;
82
- }
83
-
84
- return { generatedAt: nowIso(), branch, clean: lines.length === 0, staged, unstaged, untracked, ahead, behind, lastCommit };
85
- }
86
-
60
+ const lastCommitRaw = git(["log", "-1", "--pretty=format:%H%n%cs%n%s"], repoRoot);
61
+ const divergenceRaw = git(["rev-list", "--left-right", "--count", "@{upstream}...HEAD"], repoRoot);
62
+
63
+ let staged = 0;
64
+ let unstaged = 0;
65
+ let untracked = 0;
66
+
67
+ lines.forEach((line) => {
68
+ if (line.startsWith("??")) { untracked += 1; return; }
69
+ if (line[0] && line[0] !== " ") staged += 1;
70
+ if (line[1] && line[1] !== " ") unstaged += 1;
71
+ });
72
+
73
+ let lastCommit = null;
74
+ if (lastCommitRaw) {
75
+ const [hash, date, subject] = lastCommitRaw.split(/\r?\n/);
76
+ lastCommit = { hash, shortHash: hash ? hash.slice(0, 7) : null, date, subject };
77
+ }
78
+
79
+ let ahead = 0;
80
+ let behind = 0;
81
+ if (divergenceRaw) {
82
+ const [left, right] = divergenceRaw.split(/\s+/).map(Number);
83
+ behind = Number.isFinite(left) ? left : 0;
84
+ ahead = Number.isFinite(right) ? right : 0;
85
+ }
86
+
87
+ return { generatedAt: nowIso(), branch, clean: lines.length === 0, staged, unstaged, untracked, ahead, behind, lastCommit };
88
+ }
89
+
87
90
  function refreshRepoRuntime(root, options = {}) {
88
- const runtimeFile = config.runtimeFilePath(root);
91
+ const context = config.ensureContext(root);
92
+ const runtimeFile = config.runtimeFilePath(context);
89
93
  fs.mkdirSync(path.dirname(runtimeFile), { recursive: true });
90
- const snapshot = getRepoSnapshot(root);
94
+ const snapshot = getRepoSnapshot(context);
91
95
  writeJson(runtimeFile, snapshot);
92
96
  if (!options.quiet) {
93
- console.log(t("cli.runtimeUpdated", { path: path.relative(root, runtimeFile) }));
97
+ console.log(t("cli.runtimeUpdated", { path: path.relative(context.workspaceRoot, runtimeFile) }));
94
98
  }
95
99
  return snapshot;
96
100
  }
97
-
98
- /* ── derive ── */
99
-
100
- function getPhaseInfo(phaseId, phases) {
101
- return phases.find((p) => p.id === phaseId) || { id: phaseId, label: phaseId, index: 99 };
102
- }
103
-
104
- function compareTasks(a, b, phases) {
105
- const phaseDelta = getPhaseInfo(a.phase, phases).index - getPhaseInfo(b.phase, phases).index;
106
- if (phaseDelta !== 0) return phaseDelta;
107
- const priorityDelta = PRIORITY_ORDER.indexOf(a.priority) - PRIORITY_ORDER.indexOf(b.priority);
108
- if (priorityDelta !== 0) return priorityDelta;
109
- const statusDelta = STATUS_ORDER.indexOf(a.status) - STATUS_ORDER.indexOf(b.status);
110
- if (statusDelta !== 0) return statusDelta;
111
- return a.title.localeCompare(b.title, getLocale());
112
- }
113
-
101
+
102
+ /* ── derive ── */
103
+
104
+ function getPhaseInfo(phaseId, phases) {
105
+ return phases.find((p) => p.id === phaseId) || { id: phaseId, label: phaseId, index: 99 };
106
+ }
107
+
108
+ function compareTasks(a, b, phases) {
109
+ const phaseDelta = getPhaseInfo(a.phase, phases).index - getPhaseInfo(b.phase, phases).index;
110
+ if (phaseDelta !== 0) return phaseDelta;
111
+ const priorityDelta = PRIORITY_ORDER.indexOf(a.priority) - PRIORITY_ORDER.indexOf(b.priority);
112
+ if (priorityDelta !== 0) return priorityDelta;
113
+ const statusDelta = STATUS_ORDER.indexOf(a.status) - STATUS_ORDER.indexOf(b.status);
114
+ if (statusDelta !== 0) return statusDelta;
115
+ return a.title.localeCompare(b.title, getLocale());
116
+ }
117
+
114
118
  function derive(control) {
115
119
  const phases = config.getPhases(control);
116
120
  const tasks = [...control.tasks].sort((a, b) => compareTasks(a, b, phases));
117
121
  const completedIds = new Set(tasks.filter((t) => t.status === "completed").map((t) => t.id));
118
-
119
- const readyTasks = tasks
120
- .filter((task) => {
121
- if (task.status !== "pending") return false;
122
- return (task.dependsOn || []).every((dep) => completedIds.has(dep));
123
- })
124
- .sort((a, b) => {
125
- const focusPhase = control.meta.focusPhase || "";
126
- const aFocused = a.phase === focusPhase ? 0 : 1;
127
- const bFocused = b.phase === focusPhase ? 0 : 1;
128
- if (aFocused !== bFocused) return aFocused - bFocused;
129
- return compareTasks(a, b, phases);
130
- });
131
-
132
- const blockers = tasks.filter((t) => t.status === "blocked");
133
- const activeTasks = tasks.filter((t) => t.status === "in_progress");
134
- const reviewTasks = tasks.filter((t) => t.status === "in_review");
135
- const openTasks = tasks.filter((t) => !["completed", "cancelled"].includes(t.status));
136
- const requiredOpenTasks = tasks.filter((t) => t.required !== false && !["completed", "cancelled"].includes(t.status));
137
-
138
- const activePhase =
139
- phases.find((p) => requiredOpenTasks.some((t) => t.phase === p.id)) ||
140
- phases[phases.length - 1];
141
-
122
+ const closedStatuses = new Set(["completed", "cancelled"]);
123
+
124
+ const readyTasks = tasks
125
+ .filter((task) => {
126
+ if (task.status !== "pending") return false;
127
+ return (task.dependsOn || []).every((dep) => completedIds.has(dep));
128
+ })
129
+ .sort((a, b) => {
130
+ const focusPhase = control.meta.focusPhase || "";
131
+ const aFocused = a.phase === focusPhase ? 0 : 1;
132
+ const bFocused = b.phase === focusPhase ? 0 : 1;
133
+ if (aFocused !== bFocused) return aFocused - bFocused;
134
+ return compareTasks(a, b, phases);
135
+ });
136
+
137
+ const blockers = tasks.filter((t) => t.status === "blocked");
138
+ const activeTasks = tasks.filter((t) => t.status === "in_progress");
139
+ const reviewTasks = tasks.filter((t) => t.status === "in_review");
140
+ const openTasks = tasks.filter((t) => !["completed", "cancelled"].includes(t.status));
141
+ const requiredOpenTasks = tasks.filter((t) => t.required !== false && !["completed", "cancelled"].includes(t.status));
142
+
143
+ const activePhase =
144
+ phases.find((p) => requiredOpenTasks.some((t) => t.phase === p.id)) ||
145
+ phases[phases.length - 1];
146
+
142
147
  const phaseStats = phases.map((phase) => {
143
148
  const phaseTasks = tasks.filter((t) => t.phase === phase.id && t.required !== false);
144
149
  const completed = phaseTasks.filter((t) => t.status === "completed").length;
145
- return { ...phase, total: phaseTasks.length, completed, remaining: phaseTasks.length - completed };
150
+ const closed = phaseTasks.filter((t) => closedStatuses.has(t.status)).length;
151
+ return { ...phase, total: phaseTasks.length, completed, closed, remaining: phaseTasks.length - closed };
146
152
  });
147
-
148
- const nextTask = activeTasks[0] || readyTasks[0] || blockers[0] || openTasks[0] || null;
149
-
150
- return {
151
- tasks, blockers, activeTasks, reviewTasks, readyTasks, nextTask, activePhase, phaseStats,
152
- openFindings: (control.findings || []).filter((f) => f.status === "open"),
153
- resolvedFindings: (control.findings || []).filter((f) => f.status === "resolved"),
154
- totals: {
155
- all: tasks.length,
156
- completed: tasks.filter((t) => t.status === "completed").length,
157
- pending: tasks.filter((t) => t.status === "pending").length,
158
- inProgress: activeTasks.length,
159
- inReview: reviewTasks.length,
160
- blocked: blockers.length,
161
- cancelled: tasks.filter((t) => t.status === "cancelled").length,
162
- },
163
- };
164
- }
165
-
166
- /* ── render ── */
167
-
168
- function renderTask(task, phases) {
169
- const phase = getPhaseInfo(task.phase, phases);
170
- const detail = task.blocker || task.summary || "";
171
- const detailSuffix = detail ? ` — ${detail}` : "";
172
- return `- ${STATUS_ICONS[task.status]} \`${task.id}\` [${task.priority}] ${task.title} (${phase.id} · ${phase.label} · ${task.stream})${detailSuffix}`;
173
- }
174
-
175
- function renderTaskPlan(control) {
176
- const phases = config.getPhases(control);
177
- const state = derive(control);
178
- const blockersLabel = state.blockers.length
179
- ? state.blockers.map((t) => t.title).join("; ")
180
- : t("doc.label.noBlockers");
181
- const nextStep = state.nextTask ? state.nextTask.title : t("doc.label.noOpenTasks");
182
-
183
- const externalDecisions = (control.decisionsPending || []).length
184
- ? control.decisionsPending.map((d) => `- [${d.owner}] ${d.title} — ${d.impact}`).join("\n")
185
- : `- ${t("doc.label.noDecisions")}`;
186
-
187
- const readyTasks = state.readyTasks.length
188
- ? state.readyTasks.slice(0, 6).map((task) => renderTask(task, phases)).join("\n")
189
- : `- ${t("doc.label.noReadyTasks")}`;
190
-
191
- const phaseBlocks = phases.map((phase) => {
192
- const phaseTasks = state.tasks.filter((task) => task.phase === phase.id);
193
- const stats = state.phaseStats.find((s) => s.id === phase.id);
194
- const lines = phaseTasks.length
195
- ? phaseTasks.map((task) => renderTask(task, phases)).join("\n")
196
- : `- ${t("doc.label.noTasks")}`;
197
-
198
- const phaseStatus = phase.id === state.activePhase.id
199
- ? t("doc.label.phaseActive")
200
- : stats.remaining === 0
201
- ? t("doc.label.phaseClosed")
202
- : t("doc.label.phasePending");
203
-
204
- return [
205
- `## ${t("doc.section.phase", { phaseId: phase.id, phaseLabel: phase.label })}`,
206
- `- ${t("doc.label.progress", { completed: stats.completed, total: stats.total })}`,
207
- `- ${t("doc.label.findingStatus")}: ${phaseStatus}`,
208
- "",
209
- lines,
210
- ].join("\n");
211
- }).join("\n\n---\n\n");
212
-
213
- return [
214
- `# ${t("doc.header.taskPlan", { projectName: control.meta.projectName })}`,
215
- "",
216
- `> ${t("doc.autogenerated")}`,
217
- "",
218
- `## ${t("doc.section.operativeState")}`,
219
- `- ${t("doc.label.activePhase")}: ${state.activePhase.id} — ${state.activePhase.label} (${state.activePhase.index}/${phases.length})`,
220
- `- ${t("doc.label.currentFocus")}: ${control.meta.currentFocus}`,
221
- `- ${t("doc.label.deliveryTarget")}: ${control.meta.deliveryTarget}`,
222
- `- ${t("doc.label.blockers")}: ${blockersLabel}`,
223
- `- ${t("doc.label.nextStep")}: ${nextStep}`,
224
- "",
225
- `### ${t("doc.section.externalDecisions")}`,
226
- externalDecisions,
227
- "",
228
- `### ${t("doc.section.readyTasks")}`,
229
- readyTasks,
230
- "",
231
- "---",
232
- "",
233
- phaseBlocks,
234
- ].join("\n");
235
- }
236
-
237
- function renderProgress(control) {
238
- const phases = config.getPhases(control);
239
- const state = derive(control);
240
- const blockersLabel = state.blockers.length
241
- ? state.blockers.map((t) => t.title).join("; ")
242
- : t("doc.label.noBlockers");
243
- const nextStep = state.nextTask ? state.nextTask.title : t("doc.label.noOpenTasks");
244
- const lastTest = (control.checks || {}).lastTest || { status: "pending" };
245
- const latestHistory = state.tasks
246
- .flatMap((task) => (task.history || []).map((entry) => ({ ...entry, taskId: task.id, taskTitle: task.title })))
247
- .sort((a, b) => (a.at < b.at ? 1 : -1))
248
- .slice(0, 8);
249
-
250
- const activeLines = state.activeTasks.length
251
- ? state.activeTasks.map((task) => renderTask(task, phases)).join("\n")
252
- : `- ${t("doc.label.noActiveTasks")}`;
253
-
254
- const reviewLines = state.reviewTasks.length
255
- ? state.reviewTasks.map((task) => renderTask(task, phases)).join("\n")
256
- : `- ${t("doc.label.noReviewTasks")}`;
257
-
258
- const blockerLines = state.blockers.length
259
- ? state.blockers.map((task) => renderTask(task, phases)).join("\n")
260
- : `- ${t("doc.label.noActiveBlockers")}`;
261
-
262
- const historyLines = latestHistory.length
263
- ? latestHistory.map((e) => `- [${e.at.slice(0, 10)}] \`${e.taskId}\` ${e.action}${e.note ? ` — ${e.note}` : ""}`).join("\n")
264
- : `- ${t("doc.label.noHistory")}`;
265
-
266
- const milestoneLines = (control.milestones || [])
267
- .map((m) => {
268
- const items = m.items.map((item) => `- ${item}`).join("\n");
269
- return [`### [${m.date}] — ${m.title}`, items].join("\n");
270
- })
271
- .join("\n\n");
272
-
273
- return [
274
- `# ${t("doc.header.progress", { projectName: control.meta.projectName })}`,
275
- "",
276
- `> ${t("doc.autogenerated")}`,
277
- "",
278
- `## ${t("doc.section.currentState")}`,
279
- `- ${t("doc.label.phase")}: ${state.activePhase.label} (${state.activePhase.index}/${phases.length})`,
280
- `- ${t("doc.label.blockers")}: ${blockersLabel}`,
281
- `- ${t("doc.label.lastTest")}: ${CHECK_ICONS[lastTest.status]} ${lastTest.date || t("status.pending")}${lastTest.note ? ` — ${lastTest.note}` : ""}`,
282
- `- ${t("doc.label.nextStepShort")}: ${nextStep}`,
283
- `- ${t("doc.label.lastUpdate")}: ${(control.meta.updatedAt || "").slice(0, 10)}`,
284
- "",
285
- "---",
286
- "",
287
- `## ${t("doc.section.executionSummary")}`,
288
- `- ${t("doc.label.totalTasks")}: ${state.totals.all}`,
289
- `- ${t("doc.label.completedTasks")}: ${state.totals.completed}`,
290
- `- ${t("doc.label.inProgressTasks")}: ${state.totals.inProgress}`,
291
- `- ${t("doc.label.inReviewTasks")}: ${state.totals.inReview}`,
292
- `- ${t("doc.label.pendingTasks")}: ${state.totals.pending}`,
293
- `- ${t("doc.label.blockedTasks")}: ${state.totals.blocked}`,
294
- "",
295
- `### ${t("doc.section.activeTasks")}`,
296
- activeLines,
297
- "",
298
- `### ${t("doc.section.reviewTasks")}`,
299
- reviewLines,
300
- "",
301
- `### ${t("doc.section.activeBlockers")}`,
302
- blockerLines,
303
- "",
304
- `### ${t("doc.section.recentActivity")}`,
305
- historyLines,
306
- "",
307
- "---",
308
- "",
309
- `## ${t("doc.section.milestones")}`,
310
- "",
311
- milestoneLines,
312
- ].join("\n");
313
- }
314
-
315
- function renderFindings(control) {
316
- const state = derive(control);
317
- const openLines = state.openFindings.length
318
- ? state.openFindings
319
- .map((f) =>
320
- `### [${f.severity.toUpperCase()}] ${f.title}\n- ${t("doc.label.findingStatus")}: ${t("doc.label.findingOpen")}\n- ${t("doc.label.findingDetail")}: ${f.detail}\n- ${t("doc.label.findingImpact")}: ${f.impact}`
321
- )
322
- .join("\n\n")
323
- : t("doc.label.noFindings");
324
-
325
- const resolvedLines = state.resolvedFindings.length
326
- ? state.resolvedFindings
327
- .map((f) =>
328
- `### [${f.severity.toUpperCase()}] ${f.title}\n- ${t("doc.label.findingStatus")}: ${t("doc.label.findingResolved")}\n- ${t("doc.label.findingDetail")}: ${f.detail}\n- ${t("doc.label.findingImpact")}: ${f.impact}`
329
- )
330
- .join("\n\n")
331
- : t("doc.label.noResolvedFindings");
332
-
333
- return [
334
- `# ${t("doc.header.findings", { projectName: control.meta.projectName })}`,
335
- "",
336
- `> ${t("doc.autogenerated")}`,
337
- "",
338
- `## ${t("doc.section.openFindings")}`,
339
- "",
340
- openLines,
341
- "",
342
- "---",
343
- "",
344
- `## ${t("doc.section.resolvedFindings")}`,
345
- "",
346
- resolvedLines,
347
- ].join("\n");
348
- }
349
-
350
- /* ── doc sync ── */
351
-
352
- function buildDocMap(control) {
353
- return { taskPlan: renderTaskPlan(control), progress: renderProgress(control), findings: renderFindings(control) };
354
- }
355
-
153
+
154
+ const nextTask = activeTasks[0] || readyTasks[0] || blockers[0] || openTasks[0] || null;
155
+
156
+ return {
157
+ tasks, blockers, activeTasks, reviewTasks, readyTasks, nextTask, activePhase, phaseStats,
158
+ openFindings: (control.findings || []).filter((f) => f.status === "open"),
159
+ resolvedFindings: (control.findings || []).filter((f) => f.status === "resolved"),
160
+ totals: {
161
+ all: tasks.length,
162
+ completed: tasks.filter((t) => t.status === "completed").length,
163
+ pending: tasks.filter((t) => t.status === "pending").length,
164
+ inProgress: activeTasks.length,
165
+ inReview: reviewTasks.length,
166
+ blocked: blockers.length,
167
+ cancelled: tasks.filter((t) => t.status === "cancelled").length,
168
+ },
169
+ };
170
+ }
171
+
172
+ /* ── render ── */
173
+
174
+ function renderTask(task, phases) {
175
+ const phase = getPhaseInfo(task.phase, phases);
176
+ const detail = task.blocker || task.summary || "";
177
+ const detailSuffix = detail ? ` — ${detail}` : "";
178
+ return `- ${STATUS_ICONS[task.status]} \`${task.id}\` [${task.priority}] ${task.title} (${phase.id} · ${phase.label} · ${task.stream})${detailSuffix}`;
179
+ }
180
+
181
+ function renderTaskPlan(control) {
182
+ const phases = config.getPhases(control);
183
+ const state = derive(control);
184
+ const blockersLabel = state.blockers.length
185
+ ? state.blockers.map((t) => t.title).join("; ")
186
+ : t("doc.label.noBlockers");
187
+ const nextStep = state.nextTask ? state.nextTask.title : t("doc.label.noOpenTasks");
188
+
189
+ const externalDecisions = (control.decisionsPending || []).length
190
+ ? control.decisionsPending.map((d) => `- [${d.owner}] ${d.title} — ${d.impact}`).join("\n")
191
+ : `- ${t("doc.label.noDecisions")}`;
192
+
193
+ const readyTasks = state.readyTasks.length
194
+ ? state.readyTasks.slice(0, 6).map((task) => renderTask(task, phases)).join("\n")
195
+ : `- ${t("doc.label.noReadyTasks")}`;
196
+
197
+ const phaseBlocks = phases.map((phase) => {
198
+ const phaseTasks = state.tasks.filter((task) => task.phase === phase.id);
199
+ const stats = state.phaseStats.find((s) => s.id === phase.id);
200
+ const lines = phaseTasks.length
201
+ ? phaseTasks.map((task) => renderTask(task, phases)).join("\n")
202
+ : `- ${t("doc.label.noTasks")}`;
203
+
204
+ const phaseStatus = phase.id === state.activePhase.id
205
+ ? t("doc.label.phaseActive")
206
+ : stats.remaining === 0
207
+ ? t("doc.label.phaseClosed")
208
+ : t("doc.label.phasePending");
209
+
210
+ return [
211
+ `## ${t("doc.section.phase", { phaseId: phase.id, phaseLabel: phase.label })}`,
212
+ `- ${t("doc.label.progress", { completed: stats.completed, total: stats.total })}`,
213
+ `- ${t("doc.label.findingStatus")}: ${phaseStatus}`,
214
+ "",
215
+ lines,
216
+ ].join("\n");
217
+ }).join("\n\n---\n\n");
218
+
219
+ return [
220
+ `# ${t("doc.header.taskPlan", { projectName: control.meta.projectName })}`,
221
+ "",
222
+ `> ${t("doc.autogenerated")}`,
223
+ "",
224
+ `## ${t("doc.section.operativeState")}`,
225
+ `- ${t("doc.label.activePhase")}: ${state.activePhase.id} — ${state.activePhase.label} (${state.activePhase.index}/${phases.length})`,
226
+ `- ${t("doc.label.currentFocus")}: ${control.meta.currentFocus}`,
227
+ `- ${t("doc.label.deliveryTarget")}: ${control.meta.deliveryTarget}`,
228
+ `- ${t("doc.label.blockers")}: ${blockersLabel}`,
229
+ `- ${t("doc.label.nextStep")}: ${nextStep}`,
230
+ "",
231
+ `### ${t("doc.section.externalDecisions")}`,
232
+ externalDecisions,
233
+ "",
234
+ `### ${t("doc.section.readyTasks")}`,
235
+ readyTasks,
236
+ "",
237
+ "---",
238
+ "",
239
+ phaseBlocks,
240
+ ].join("\n");
241
+ }
242
+
243
+ function renderProgress(control) {
244
+ const phases = config.getPhases(control);
245
+ const state = derive(control);
246
+ const blockersLabel = state.blockers.length
247
+ ? state.blockers.map((t) => t.title).join("; ")
248
+ : t("doc.label.noBlockers");
249
+ const nextStep = state.nextTask ? state.nextTask.title : t("doc.label.noOpenTasks");
250
+ const lastTest = (control.checks || {}).lastTest || { status: "pending" };
251
+ const latestHistory = state.tasks
252
+ .flatMap((task) => (task.history || []).map((entry) => ({ ...entry, taskId: task.id, taskTitle: task.title })))
253
+ .sort((a, b) => (a.at < b.at ? 1 : -1))
254
+ .slice(0, 8);
255
+
256
+ const activeLines = state.activeTasks.length
257
+ ? state.activeTasks.map((task) => renderTask(task, phases)).join("\n")
258
+ : `- ${t("doc.label.noActiveTasks")}`;
259
+
260
+ const reviewLines = state.reviewTasks.length
261
+ ? state.reviewTasks.map((task) => renderTask(task, phases)).join("\n")
262
+ : `- ${t("doc.label.noReviewTasks")}`;
263
+
264
+ const blockerLines = state.blockers.length
265
+ ? state.blockers.map((task) => renderTask(task, phases)).join("\n")
266
+ : `- ${t("doc.label.noActiveBlockers")}`;
267
+
268
+ const historyLines = latestHistory.length
269
+ ? latestHistory.map((e) => `- [${e.at.slice(0, 10)}] \`${e.taskId}\` ${e.action}${e.note ? ` — ${e.note}` : ""}`).join("\n")
270
+ : `- ${t("doc.label.noHistory")}`;
271
+
272
+ const milestoneLines = (control.milestones || [])
273
+ .map((m) => {
274
+ const items = m.items.map((item) => `- ${item}`).join("\n");
275
+ return [`### [${m.date}] — ${m.title}`, items].join("\n");
276
+ })
277
+ .join("\n\n");
278
+
279
+ return [
280
+ `# ${t("doc.header.progress", { projectName: control.meta.projectName })}`,
281
+ "",
282
+ `> ${t("doc.autogenerated")}`,
283
+ "",
284
+ `## ${t("doc.section.currentState")}`,
285
+ `- ${t("doc.label.phase")}: ${state.activePhase.label} (${state.activePhase.index}/${phases.length})`,
286
+ `- ${t("doc.label.blockers")}: ${blockersLabel}`,
287
+ `- ${t("doc.label.lastTest")}: ${CHECK_ICONS[lastTest.status]} ${lastTest.date || t("status.pending")}${lastTest.note ? ` — ${lastTest.note}` : ""}`,
288
+ `- ${t("doc.label.nextStepShort")}: ${nextStep}`,
289
+ `- ${t("doc.label.lastUpdate")}: ${(control.meta.updatedAt || "").slice(0, 10)}`,
290
+ "",
291
+ "---",
292
+ "",
293
+ `## ${t("doc.section.executionSummary")}`,
294
+ `- ${t("doc.label.totalTasks")}: ${state.totals.all}`,
295
+ `- ${t("doc.label.completedTasks")}: ${state.totals.completed}`,
296
+ `- ${t("doc.label.inProgressTasks")}: ${state.totals.inProgress}`,
297
+ `- ${t("doc.label.inReviewTasks")}: ${state.totals.inReview}`,
298
+ `- ${t("doc.label.pendingTasks")}: ${state.totals.pending}`,
299
+ `- ${t("doc.label.blockedTasks")}: ${state.totals.blocked}`,
300
+ "",
301
+ `### ${t("doc.section.activeTasks")}`,
302
+ activeLines,
303
+ "",
304
+ `### ${t("doc.section.reviewTasks")}`,
305
+ reviewLines,
306
+ "",
307
+ `### ${t("doc.section.activeBlockers")}`,
308
+ blockerLines,
309
+ "",
310
+ `### ${t("doc.section.recentActivity")}`,
311
+ historyLines,
312
+ "",
313
+ "---",
314
+ "",
315
+ `## ${t("doc.section.milestones")}`,
316
+ "",
317
+ milestoneLines,
318
+ ].join("\n");
319
+ }
320
+
321
+ function renderFindings(control) {
322
+ const state = derive(control);
323
+ const openLines = state.openFindings.length
324
+ ? state.openFindings
325
+ .map((f) =>
326
+ `### [${f.severity.toUpperCase()}] ${f.title}\n- ${t("doc.label.findingStatus")}: ${t("doc.label.findingOpen")}\n- ${t("doc.label.findingDetail")}: ${f.detail}\n- ${t("doc.label.findingImpact")}: ${f.impact}`
327
+ )
328
+ .join("\n\n")
329
+ : t("doc.label.noFindings");
330
+
331
+ const resolvedLines = state.resolvedFindings.length
332
+ ? state.resolvedFindings
333
+ .map((f) =>
334
+ `### [${f.severity.toUpperCase()}] ${f.title}\n- ${t("doc.label.findingStatus")}: ${t("doc.label.findingResolved")}\n- ${t("doc.label.findingDetail")}: ${f.detail}\n- ${t("doc.label.findingImpact")}: ${f.impact}`
335
+ )
336
+ .join("\n\n")
337
+ : t("doc.label.noResolvedFindings");
338
+
339
+ return [
340
+ `# ${t("doc.header.findings", { projectName: control.meta.projectName })}`,
341
+ "",
342
+ `> ${t("doc.autogenerated")}`,
343
+ "",
344
+ `## ${t("doc.section.openFindings")}`,
345
+ "",
346
+ openLines,
347
+ "",
348
+ "---",
349
+ "",
350
+ `## ${t("doc.section.resolvedFindings")}`,
351
+ "",
352
+ resolvedLines,
353
+ ].join("\n");
354
+ }
355
+
356
+ /* ── doc sync ── */
357
+
358
+ function buildDocMap(control) {
359
+ return { taskPlan: renderTaskPlan(control), progress: renderProgress(control), findings: renderFindings(control) };
360
+ }
361
+
356
362
  function getDocDrift(root, control) {
363
+ const context = config.ensureContext(root);
357
364
  const docs = buildDocMap(control);
358
- const docFiles = config.docFilePaths(root);
365
+ const docFiles = config.docFilePaths(context);
359
366
  return Object.entries({ task_plan: [docFiles.taskPlan, docs.taskPlan], progress: [docFiles.progress, docs.progress], findings: [docFiles.findings, docs.findings] })
360
- .filter(([, [filePath, expected]]) => {
361
- if (!fs.existsSync(filePath)) return true;
362
- return fs.readFileSync(filePath, "utf8").replace(/\r\n/g, "\n") !== `${expected}\n`;
363
- })
364
- .map(([name]) => name);
365
- }
366
-
367
+ .filter(([, [filePath, expected]]) => {
368
+ if (!fs.existsSync(filePath)) return true;
369
+ return fs.readFileSync(filePath, "utf8").replace(/\r\n/g, "\n") !== `${expected}\n`;
370
+ })
371
+ .map(([name]) => name);
372
+ }
373
+
367
374
  function syncDocs(root, control) {
375
+ const context = config.ensureContext(root);
368
376
  const docs = buildDocMap(control);
369
- const docFiles = config.docFilePaths(root);
377
+ const docFiles = config.docFilePaths(context);
370
378
  writeText(docFiles.taskPlan, `${docs.taskPlan}\n`);
371
379
  writeText(docFiles.progress, `${docs.progress}\n`);
372
380
  writeText(docFiles.findings, `${docs.findings}\n`);
373
381
  }
374
-
375
- /* ── task management ── */
376
-
382
+
383
+ /* ── task management ── */
384
+
377
385
  function updateTask(root, control, action, taskId, note) {
386
+ const context = config.ensureContext(root);
378
387
  const task = control.tasks.find((item) => item.id === taskId);
379
388
  if (!task) throw new Error(t("cli.taskNotFound", { taskId }));
380
-
381
- const actionMap = { start: "in_progress", review: "in_review", complete: "completed", done: "completed", block: "blocked", pending: "pending", cancel: "cancelled" };
382
-
383
- if (action === "note") {
384
- task.history = task.history || [];
385
- task.history.push({ at: nowIso(), action: "note", note: note || t("cli.emptyNote") });
386
- } else {
387
- const nextStatus = actionMap[action];
388
- if (!nextStatus) throw new Error(t("cli.actionNotSupported", { action }));
389
-
390
- task.status = nextStatus;
391
- if (nextStatus === "blocked") {
392
- task.blocker = note || task.blocker || t("cli.undocumentedBlocker");
393
- } else {
394
- delete task.blocker;
395
- }
396
- task.history = task.history || [];
397
- task.history.push({ at: nowIso(), action, note: note || "" });
398
- }
399
-
400
- config.saveControl(root, control);
401
- syncDocs(root, control);
402
- refreshRepoRuntime(root, { quiet: true });
389
+
390
+ const actionMap = { start: "in_progress", review: "in_review", complete: "completed", done: "completed", block: "blocked", pending: "pending", cancel: "cancelled" };
391
+
392
+ if (action === "note") {
393
+ task.history = task.history || [];
394
+ task.history.push({ at: nowIso(), action: "note", note: note || t("cli.emptyNote") });
395
+ } else {
396
+ const nextStatus = actionMap[action];
397
+ if (!nextStatus) throw new Error(t("cli.actionNotSupported", { action }));
398
+
399
+ task.status = nextStatus;
400
+ if (nextStatus === "blocked") {
401
+ task.blocker = note || task.blocker || t("cli.undocumentedBlocker");
402
+ } else {
403
+ delete task.blocker;
404
+ }
405
+ task.history = task.history || [];
406
+ task.history.push({ at: nowIso(), action, note: note || "" });
407
+ }
408
+
409
+ config.saveControl(context, control);
410
+ syncDocs(context, control);
411
+ refreshRepoRuntime(context, { quiet: true });
403
412
  }
404
-
405
- /* ── CLI commands ── */
406
-
413
+
414
+ /* ── CLI commands ── */
415
+
407
416
  function initLocale(root) {
408
417
  try {
409
- const control = config.loadControl(root);
418
+ const control = config.loadControl(config.ensureContext(root));
410
419
  setLocale(config.getLocale(control));
411
420
  } catch (_err) {
412
421
  setLocale("es");
413
- }
414
- }
415
-
422
+ }
423
+ }
424
+
416
425
  function cmdStatus(root) {
417
- initLocale(root);
418
- const control = config.loadControl(root);
426
+ const context = config.ensureContext(root);
427
+ initLocale(context);
428
+ const control = config.loadControl(context);
419
429
  const state = derive(control);
420
430
  const phases = config.getPhases(control);
421
- const repo = refreshRepoRuntime(root, { quiet: true });
422
- const drift = getDocDrift(root, control);
431
+ const repo = refreshRepoRuntime(context, { quiet: true });
432
+ const drift = getDocDrift(context, control);
433
+ const envAudit = env.auditEnvironment(context, control);
423
434
 
424
435
  console.log(t("cli.status.title", { projectName: control.meta.projectName }));
425
436
  console.log(t("cli.status.focus", { focus: control.meta.currentFocus }));
426
437
  console.log(t("cli.status.activePhase", { phaseId: state.activePhase.id, phaseLabel: state.activePhase.label }));
427
- console.log(t("cli.status.tasks", {
428
- completed: state.totals.completed, inProgress: state.totals.inProgress,
429
- inReview: state.totals.inReview, pending: state.totals.pending, blocked: state.totals.blocked,
430
- }));
431
- console.log("");
432
- console.log(t("cli.status.readyTasks"));
433
- if (state.readyTasks.length) {
434
- state.readyTasks.slice(0, 5).forEach((task) => console.log(renderTask(task, phases)));
435
- } else {
436
- console.log(t("cli.status.noReadyTasks"));
437
- }
438
- console.log("");
439
- console.log(t("cli.status.blockers"));
440
- if (state.blockers.length) {
441
- state.blockers.forEach((task) => console.log(renderTask(task, phases)));
442
- } else {
443
- console.log(t("cli.status.noBlockers"));
444
- }
445
- console.log("");
446
- console.log(t("cli.status.decisions"));
447
- if ((control.decisionsPending || []).length) {
448
- control.decisionsPending.forEach((d) => console.log(`- ${d.title} (${d.owner}) — ${d.impact}`));
449
- } else {
450
- console.log(`- ${t("cli.status.noDecisions")}`);
438
+ console.log(`Layout: ${context.layout} | Workspace: ${context.workspaceRoot}`);
439
+ if (context.layout === "split") {
440
+ console.log(`App: ${context.appRoot}`);
441
+ console.log(`Ops: ${context.opsRoot}`);
451
442
  }
452
- console.log("");
453
- console.log(t("cli.status.repo"));
454
- const treeStatus = repo.clean
455
- ? t("cli.status.treeClean")
456
- : t("cli.status.treeDirty", { staged: repo.staged, unstaged: repo.unstaged, untracked: repo.untracked });
457
- console.log(`- ${t("cli.status.branch", { branch: repo.branch, treeStatus })}`);
458
- if (repo.lastCommit) {
459
- console.log(`- ${t("cli.status.lastCommit", { hash: repo.lastCommit.shortHash, subject: repo.lastCommit.subject })}`);
443
+ if (control.meta?.opera?.bootstrap?.status) {
444
+ console.log(t("cli.status.bootstrap", { status: control.meta.opera.bootstrap.status, locale: config.getLocale(control) }));
445
+ if (control.meta.opera.bootstrap.mode) {
446
+ console.log(`Bootstrap mode: ${control.meta.opera.bootstrap.mode}`);
447
+ }
448
+ if (control.meta.opera.bootstrap.routeReason) {
449
+ console.log(`Bootstrap route: ${control.meta.opera.bootstrap.routeReason}`);
450
+ }
460
451
  }
461
- if (repo.ahead || repo.behind) {
462
- console.log(`- ${t("cli.status.divergence", { ahead: repo.ahead, behind: repo.behind })}`);
452
+ console.log(t("cli.status.tasks", {
453
+ completed: state.totals.completed, inProgress: state.totals.inProgress,
454
+ inReview: state.totals.inReview, pending: state.totals.pending, blocked: state.totals.blocked,
455
+ }));
456
+ console.log("");
457
+ console.log(t("cli.status.readyTasks"));
458
+ if (state.readyTasks.length) {
459
+ state.readyTasks.slice(0, 5).forEach((task) => console.log(renderTask(task, phases)));
460
+ } else {
461
+ console.log(t("cli.status.noReadyTasks"));
462
+ }
463
+ console.log("");
464
+ console.log(t("cli.status.blockers"));
465
+ if (state.blockers.length) {
466
+ state.blockers.forEach((task) => console.log(renderTask(task, phases)));
467
+ } else {
468
+ console.log(t("cli.status.noBlockers"));
469
+ }
470
+ console.log("");
471
+ console.log(t("cli.status.decisions"));
472
+ if ((control.decisionsPending || []).length) {
473
+ control.decisionsPending.forEach((d) => console.log(`- ${d.title} (${d.owner}) — ${d.impact}`));
474
+ } else {
475
+ console.log(`- ${t("cli.status.noDecisions")}`);
476
+ }
477
+ console.log("");
478
+ console.log(t("cli.status.repo"));
479
+ const treeStatus = repo.clean
480
+ ? t("cli.status.treeClean")
481
+ : t("cli.status.treeDirty", { staged: repo.staged, unstaged: repo.unstaged, untracked: repo.untracked });
482
+ console.log(`- ${t("cli.status.branch", { branch: repo.branch, treeStatus })}`);
483
+ if (repo.lastCommit) {
484
+ console.log(`- ${t("cli.status.lastCommit", { hash: repo.lastCommit.shortHash, subject: repo.lastCommit.subject })}`);
485
+ }
486
+ if (repo.ahead || repo.behind) {
487
+ console.log(`- ${t("cli.status.divergence", { ahead: repo.ahead, behind: repo.behind })}`);
463
488
  }
464
- console.log(`- ${t("cli.status.runtime", { path: path.relative(root, config.runtimeFilePath(root)) })}`);
489
+ console.log(`- ${t("cli.status.runtime", { path: path.relative(context.workspaceRoot, config.runtimeFilePath(context)) })}`);
490
+ console.log(`- Env present: ${envAudit.presentKeys.length ? envAudit.presentKeys.join(", ") : "none"}`);
491
+ console.log(`- Env missing: ${envAudit.missingKeys.length ? envAudit.missingKeys.join(", ") : "none"}`);
465
492
  console.log("");
466
493
  const syncStatus = drift.length
467
494
  ? t("cli.status.docsSyncedNo", { files: drift.join(", ") })
468
- : t("cli.status.docsSyncedYes");
469
- console.log(t("cli.status.docsSynced", { status: syncStatus }));
470
- }
471
-
495
+ : t("cli.status.docsSyncedYes");
496
+ console.log(t("cli.status.docsSynced", { status: syncStatus }));
497
+ }
498
+
472
499
  function cmdNext(root) {
473
- initLocale(root);
474
- const control = config.loadControl(root);
500
+ const context = config.ensureContext(root);
501
+ initLocale(context);
502
+ const control = config.loadControl(context);
475
503
  const ready = derive(control).readyTasks.slice(0, 10);
476
- if (!ready.length) {
477
- console.log(t("cli.noReadyTasks"));
478
- return;
479
- }
480
- ready.forEach((task, i) => {
481
- console.log(`${i + 1}. ${task.title}`);
482
- console.log(` id: ${task.id}`);
483
- console.log(` ${t("cli.next.phase")}: ${task.phase} · ${t("cli.next.priority")}: ${task.priority} · ${t("cli.next.stream")}: ${task.stream}`);
484
- if (task.summary) console.log(` ${t("cli.next.summary")}: ${task.summary}`);
485
- });
486
- }
487
-
504
+ if (!ready.length) {
505
+ console.log(t("cli.noReadyTasks"));
506
+ return;
507
+ }
508
+ ready.forEach((task, i) => {
509
+ console.log(`${i + 1}. ${task.title}`);
510
+ console.log(` id: ${task.id}`);
511
+ console.log(` ${t("cli.next.phase")}: ${task.phase} · ${t("cli.next.priority")}: ${task.priority} · ${t("cli.next.stream")}: ${task.stream}`);
512
+ if (task.summary) console.log(` ${t("cli.next.summary")}: ${task.summary}`);
513
+ });
514
+ }
515
+
488
516
  function cmdSync(root) {
489
- initLocale(root);
490
- const control = config.loadControl(root);
491
- syncDocs(root, control);
492
- refreshRepoRuntime(root, { quiet: true });
517
+ const context = config.ensureContext(root);
518
+ initLocale(context);
519
+ const control = config.loadControl(context);
520
+ env.syncEnvironment(context, control);
521
+ syncDocs(context, control);
522
+ refreshRepoRuntime(context, { quiet: true });
493
523
  console.log(t("cli.docsSynced"));
494
524
  }
495
525
 
496
526
  function cmdRefreshRepo(root, args) {
497
- refreshRepoRuntime(root, { quiet: (args || []).includes("--quiet") });
527
+ refreshRepoRuntime(config.ensureContext(root), { quiet: (args || []).includes("--quiet") });
498
528
  }
499
529
 
500
530
  function cmdTask(root, args) {
501
- initLocale(root);
531
+ const context = config.ensureContext(root);
532
+ initLocale(context);
502
533
  const [action, taskId, ...noteParts] = args || [];
503
534
  if (!action || !taskId) throw new Error(t("cli.mustProvideActionAndId"));
504
- const control = config.loadControl(root);
505
- updateTask(root, control, action, taskId, noteParts.join(" ").trim());
535
+ const control = config.loadControl(context);
536
+ updateTask(context, control, action, taskId, noteParts.join(" ").trim());
506
537
  console.log(t("cli.taskUpdated", { taskId, action }));
507
538
  }
508
539
 
509
540
  function cmdInstallHooks(root) {
510
- initLocale(root);
511
- const result = spawnSync("git", ["config", "core.hooksPath", ".githooks"], { cwd: root, encoding: "utf8" });
541
+ const context = config.ensureContext(root);
542
+ initLocale(context);
543
+ const hooksPath = context.layout === "split" ? "ops/.githooks" : ".githooks";
544
+ const result = spawnSync("git", ["config", "core.hooksPath", hooksPath], { cwd: context.workspaceRoot, encoding: "utf8" });
512
545
  if (result.error || result.status !== 0) throw new Error(t("cli.hooksError"));
513
546
  console.log(t("cli.hooksInstalled"));
514
547
  }
515
548
 
516
549
  function cmdHelp() {
517
- console.log("trackops — Operational project control");
550
+ console.log(`trackops — ${t("cli.help.title")}`);
518
551
  console.log("");
519
- console.log("Usage: trackops <command> [args]");
552
+ console.log(`${t("cli.help.usage")} trackops <command> [args]`);
520
553
  console.log("");
521
- console.log("Commands:");
522
- console.log(" init [--with-opera] [--locale es|en] [--name \"...\"]");
523
- console.log(" Legacy alias available: --with-etapa");
524
- console.log(" Initialize trackops in the current directory.");
554
+ console.log(t("cli.help.commands"));
555
+ console.log(" init [--with-opera] [--legacy-layout] [--locale es|en] [--name \"...\"] [--no-bootstrap]");
556
+ console.log(" [--bootstrap-mode auto|direct|handoff] [--technical-level low|medium|high|senior]");
557
+ console.log(" [--project-state idea|draft|existing_repo|advanced] [--docs-state none|notes|sos|spec_dossier|repo_docs]");
558
+ console.log(" [--decision-ownership user|shared|agent]");
559
+ console.log(` ${t("cli.help.init.desc")}`);
560
+ console.log(" workspace status|migrate");
561
+ console.log(` ${t("cli.help.workspace.desc")}`);
562
+ console.log(" env status|sync");
563
+ console.log(` ${t("cli.help.env.desc")}`);
564
+ console.log(" release [--push]");
565
+ console.log(` ${t("cli.help.release.desc")}`);
566
+ console.log(" version");
567
+ console.log(` ${t("cli.help.version.desc")}`);
525
568
  console.log(" status");
526
- console.log(" Show project state: focus, active phase, ready tasks, blockers, repo.");
569
+ console.log(` ${t("cli.help.status.desc")}`);
527
570
  console.log(" next");
528
- console.log(" Prioritized queue of next executable tasks.");
571
+ console.log(` ${t("cli.help.next.desc")}`);
529
572
  console.log(" sync");
530
- console.log(" Regenerate task_plan.md, progress.md, findings.md from project_control.json.");
531
- console.log(" dashboard");
532
- console.log(" Launch local web dashboard.");
573
+ console.log(` ${t("cli.help.sync.desc")}`);
574
+ console.log(" dashboard [--port N] [--host HOST] [--public] [--strict-port]");
575
+ console.log(` ${t("cli.help.dashboard.desc")}`);
533
576
  console.log(" refresh-repo [--quiet]");
534
- console.log(" Update .tmp/project-control-runtime.json with repo state.");
577
+ console.log(` ${t("cli.help.refreshRepo.desc")}`);
535
578
  console.log(" install-hooks");
536
- console.log(" Configure git core.hooksPath to use .githooks.");
579
+ console.log(` ${t("cli.help.installHooks.desc")}`);
537
580
  console.log(" register");
538
- console.log(" Register current project in the multi-project portfolio.");
581
+ console.log(` ${t("cli.help.register.desc")}`);
539
582
  console.log(" projects");
540
- console.log(" List registered projects.");
583
+ console.log(` ${t("cli.help.projects.desc")}`);
541
584
  console.log(" task <action> <id> [note]");
542
- console.log(" Actions: start, review, complete, block, pending, cancel, note.");
543
- console.log(" opera install|status|configure|upgrade");
544
- console.log(" Manage OPERA methodology.");
585
+ console.log(` ${t("cli.help.task.desc")}`);
586
+ console.log(" opera install|bootstrap|handoff|status|configure|upgrade");
587
+ console.log(` ${t("cli.help.opera.desc")}`);
588
+ console.log(` ${t("cli.help.opera.upgradeHint")}`);
589
+ console.log(" locale get|set [es|en]");
590
+ console.log(` ${t("cli.help.locale.desc")}`);
591
+ console.log(" doctor locale");
592
+ console.log(` ${t("cli.help.doctor.desc")}`);
545
593
  console.log(" skill install|list|remove|catalog <name>");
546
- console.log(" Manage skills.");
594
+ console.log(` ${t("cli.help.skill.desc")}`);
547
595
  console.log(" help");
548
- console.log(" Show this help.");
596
+ console.log(` ${t("cli.help.help.desc")}`);
597
+ console.log("");
598
+ console.log(t("cli.help.globalWorkflow"));
599
+ console.log(` ${t("cli.help.globalWorkflow.line1")}`);
600
+ console.log(` ${t("cli.help.globalWorkflow.line2")}`);
549
601
  }
550
-
551
- /* ── project-scoped API (used by server) ── */
552
-
602
+
603
+ /* ── project-scoped API (used by server) ── */
604
+
553
605
  function forProject(root) {
554
- initLocale(root);
606
+ const context = config.ensureContext(root);
607
+ initLocale(context);
555
608
  return {
556
- loadControl: () => config.loadControl(root),
557
- saveControl: (ctrl) => config.saveControl(root, ctrl),
609
+ loadControl: () => config.loadControl(context),
610
+ saveControl: (ctrl) => config.saveControl(context, ctrl),
558
611
  derive,
559
612
  buildDocMap,
560
- getDocDrift: (ctrl) => getDocDrift(root, ctrl),
561
- syncDocs: (ctrl) => syncDocs(root, ctrl),
562
- updateTask: (ctrl, action, id, note) => updateTask(root, ctrl, action, id, note),
563
- getRepoSnapshot: () => getRepoSnapshot(root),
564
- refreshRepoRuntime: (opts) => refreshRepoRuntime(root, opts),
613
+ getDocDrift: (ctrl) => getDocDrift(context, ctrl),
614
+ syncDocs: (ctrl) => syncDocs(context, ctrl),
615
+ updateTask: (ctrl, action, id, note) => updateTask(context, ctrl, action, id, note),
616
+ getRepoSnapshot: () => getRepoSnapshot(context),
617
+ refreshRepoRuntime: (opts) => refreshRepoRuntime(context, opts),
565
618
  getPhases: (ctrl) => config.getPhases(ctrl),
566
619
  getLocale: (ctrl) => config.getLocale(ctrl),
567
620
  statusLabel,
621
+ context,
568
622
  };
569
623
  }
570
-
571
- module.exports = {
572
- buildDocMap, derive, getDocDrift, getRepoSnapshot, refreshRepoRuntime, syncDocs, updateTask,
573
- forProject, statusLabel, renderTask, getPhaseInfo,
574
- cmdStatus, cmdNext, cmdSync, cmdRefreshRepo, cmdTask, cmdInstallHooks, cmdHelp,
575
- PRIORITY_ORDER, STATUS_ORDER, STATUS_ICONS, CHECK_ICONS,
576
- };
624
+
625
+ module.exports = {
626
+ buildDocMap, derive, getDocDrift, getRepoSnapshot, refreshRepoRuntime, syncDocs, updateTask,
627
+ forProject, statusLabel, renderTask, getPhaseInfo,
628
+ cmdStatus, cmdNext, cmdSync, cmdRefreshRepo, cmdTask, cmdInstallHooks, cmdHelp,
629
+ PRIORITY_ORDER, STATUS_ORDER, STATUS_ICONS, CHECK_ICONS,
630
+ };