trackops 1.0.1 → 1.1.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.
- package/README.md +326 -270
- package/bin/trackops.js +102 -70
- package/lib/config.js +260 -35
- package/lib/control.js +517 -475
- package/lib/env.js +227 -0
- package/lib/i18n.js +61 -53
- package/lib/init.js +135 -46
- package/lib/locale.js +63 -0
- package/lib/opera-bootstrap.js +523 -0
- package/lib/opera.js +319 -170
- package/lib/registry.js +27 -13
- package/lib/release.js +56 -0
- package/lib/resources.js +42 -0
- package/lib/server.js +907 -554
- package/lib/skills.js +148 -124
- package/lib/workspace.js +260 -0
- package/locales/en.json +331 -139
- package/locales/es.json +331 -139
- package/package.json +7 -9
- package/scripts/skills-marketplace-smoke.js +124 -0
- package/scripts/smoke-tests.js +445 -0
- package/scripts/sync-skill-version.js +21 -0
- package/scripts/validate-skill.js +88 -0
- package/skills/trackops/SKILL.md +64 -0
- package/skills/trackops/agents/openai.yaml +3 -0
- package/skills/trackops/references/activation.md +39 -0
- package/skills/trackops/references/troubleshooting.md +34 -0
- package/skills/trackops/references/workflow.md +20 -0
- package/skills/trackops/scripts/bootstrap-trackops.js +201 -0
- package/skills/trackops/skill.json +29 -0
- package/templates/opera/en/agent.md +26 -0
- package/templates/opera/en/genesis.md +79 -0
- package/templates/opera/en/references/autonomy-and-recovery.md +23 -0
- package/templates/opera/en/references/opera-cycle.md +62 -0
- package/templates/opera/en/registry.md +28 -0
- package/templates/opera/en/router.md +39 -0
- package/templates/opera/genesis.md +79 -94
- package/templates/skills/changelog-updater/locales/en/SKILL.md +11 -0
- package/templates/skills/commiter/locales/en/SKILL.md +11 -0
- package/templates/skills/project-starter-skill/locales/en/SKILL.md +24 -0
- package/ui/css/panels.css +956 -953
- package/ui/index.html +1 -1
- package/ui/js/api.js +211 -194
- package/ui/js/app.js +200 -199
- package/ui/js/i18n.js +14 -0
- package/ui/js/onboarding.js +439 -437
- package/ui/js/state.js +130 -129
- package/ui/js/utils.js +175 -172
- package/ui/js/views/board.js +255 -254
- package/ui/js/views/execution.js +256 -256
- package/ui/js/views/insights.js +340 -339
- package/ui/js/views/overview.js +365 -364
- package/ui/js/views/settings.js +340 -202
- package/ui/js/views/sidebar.js +131 -132
- package/ui/js/views/skills.js +163 -162
- package/ui/js/views/tasks.js +406 -405
- package/ui/js/views/topbar.js +239 -183
package/lib/control.js
CHANGED
|
@@ -1,514 +1,541 @@
|
|
|
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(
|
|
54
|
-
const
|
|
55
|
-
const
|
|
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"],
|
|
58
|
-
const divergenceRaw = git(["rev-list", "--left-right", "--count", "@{upstream}...HEAD"],
|
|
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
|
|
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(
|
|
94
|
+
const snapshot = getRepoSnapshot(context);
|
|
91
95
|
writeJson(runtimeFile, snapshot);
|
|
92
96
|
if (!options.quiet) {
|
|
93
|
-
console.log(t("cli.runtimeUpdated", { path: path.relative(
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const
|
|
134
|
-
const
|
|
135
|
-
const
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
phases
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
401
|
-
syncDocs(
|
|
402
|
-
refreshRepoRuntime(
|
|
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
|
-
|
|
418
|
-
|
|
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(
|
|
422
|
-
const drift = getDocDrift(
|
|
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(
|
|
428
|
-
|
|
429
|
-
|
|
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"));
|
|
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}`);
|
|
437
442
|
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
if (state.blockers.length) {
|
|
441
|
-
state.blockers.forEach((task) => console.log(renderTask(task, phases)));
|
|
442
|
-
} else {
|
|
443
|
-
console.log(t("cli.status.noBlockers"));
|
|
443
|
+
if (control.meta?.opera?.bootstrap?.status) {
|
|
444
|
+
console.log(t("cli.status.bootstrap", { status: control.meta.opera.bootstrap.status, locale: config.getLocale(control) }));
|
|
444
445
|
}
|
|
445
|
-
console.log(""
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
446
|
+
console.log(t("cli.status.tasks", {
|
|
447
|
+
completed: state.totals.completed, inProgress: state.totals.inProgress,
|
|
448
|
+
inReview: state.totals.inReview, pending: state.totals.pending, blocked: state.totals.blocked,
|
|
449
|
+
}));
|
|
450
|
+
console.log("");
|
|
451
|
+
console.log(t("cli.status.readyTasks"));
|
|
452
|
+
if (state.readyTasks.length) {
|
|
453
|
+
state.readyTasks.slice(0, 5).forEach((task) => console.log(renderTask(task, phases)));
|
|
454
|
+
} else {
|
|
455
|
+
console.log(t("cli.status.noReadyTasks"));
|
|
456
|
+
}
|
|
457
|
+
console.log("");
|
|
458
|
+
console.log(t("cli.status.blockers"));
|
|
459
|
+
if (state.blockers.length) {
|
|
460
|
+
state.blockers.forEach((task) => console.log(renderTask(task, phases)));
|
|
461
|
+
} else {
|
|
462
|
+
console.log(t("cli.status.noBlockers"));
|
|
463
|
+
}
|
|
464
|
+
console.log("");
|
|
465
|
+
console.log(t("cli.status.decisions"));
|
|
466
|
+
if ((control.decisionsPending || []).length) {
|
|
467
|
+
control.decisionsPending.forEach((d) => console.log(`- ${d.title} (${d.owner}) — ${d.impact}`));
|
|
468
|
+
} else {
|
|
469
|
+
console.log(`- ${t("cli.status.noDecisions")}`);
|
|
470
|
+
}
|
|
471
|
+
console.log("");
|
|
472
|
+
console.log(t("cli.status.repo"));
|
|
473
|
+
const treeStatus = repo.clean
|
|
474
|
+
? t("cli.status.treeClean")
|
|
475
|
+
: t("cli.status.treeDirty", { staged: repo.staged, unstaged: repo.unstaged, untracked: repo.untracked });
|
|
476
|
+
console.log(`- ${t("cli.status.branch", { branch: repo.branch, treeStatus })}`);
|
|
477
|
+
if (repo.lastCommit) {
|
|
478
|
+
console.log(`- ${t("cli.status.lastCommit", { hash: repo.lastCommit.shortHash, subject: repo.lastCommit.subject })}`);
|
|
479
|
+
}
|
|
480
|
+
if (repo.ahead || repo.behind) {
|
|
481
|
+
console.log(`- ${t("cli.status.divergence", { ahead: repo.ahead, behind: repo.behind })}`);
|
|
451
482
|
}
|
|
452
|
-
console.log("");
|
|
453
|
-
console.log(
|
|
454
|
-
|
|
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 })}`);
|
|
460
|
-
}
|
|
461
|
-
if (repo.ahead || repo.behind) {
|
|
462
|
-
console.log(`- ${t("cli.status.divergence", { ahead: repo.ahead, behind: repo.behind })}`);
|
|
463
|
-
}
|
|
464
|
-
console.log(`- ${t("cli.status.runtime", { path: path.relative(root, config.runtimeFilePath(root)) })}`);
|
|
483
|
+
console.log(`- ${t("cli.status.runtime", { path: path.relative(context.workspaceRoot, config.runtimeFilePath(context)) })}`);
|
|
484
|
+
console.log(`- Env present: ${envAudit.presentKeys.length ? envAudit.presentKeys.join(", ") : "none"}`);
|
|
485
|
+
console.log(`- Env missing: ${envAudit.missingKeys.length ? envAudit.missingKeys.join(", ") : "none"}`);
|
|
465
486
|
console.log("");
|
|
466
487
|
const syncStatus = drift.length
|
|
467
488
|
? t("cli.status.docsSyncedNo", { files: drift.join(", ") })
|
|
468
|
-
: t("cli.status.docsSyncedYes");
|
|
469
|
-
console.log(t("cli.status.docsSynced", { status: syncStatus }));
|
|
470
|
-
}
|
|
471
|
-
|
|
489
|
+
: t("cli.status.docsSyncedYes");
|
|
490
|
+
console.log(t("cli.status.docsSynced", { status: syncStatus }));
|
|
491
|
+
}
|
|
492
|
+
|
|
472
493
|
function cmdNext(root) {
|
|
473
|
-
|
|
474
|
-
|
|
494
|
+
const context = config.ensureContext(root);
|
|
495
|
+
initLocale(context);
|
|
496
|
+
const control = config.loadControl(context);
|
|
475
497
|
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
|
-
|
|
498
|
+
if (!ready.length) {
|
|
499
|
+
console.log(t("cli.noReadyTasks"));
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
ready.forEach((task, i) => {
|
|
503
|
+
console.log(`${i + 1}. ${task.title}`);
|
|
504
|
+
console.log(` id: ${task.id}`);
|
|
505
|
+
console.log(` ${t("cli.next.phase")}: ${task.phase} · ${t("cli.next.priority")}: ${task.priority} · ${t("cli.next.stream")}: ${task.stream}`);
|
|
506
|
+
if (task.summary) console.log(` ${t("cli.next.summary")}: ${task.summary}`);
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
|
|
488
510
|
function cmdSync(root) {
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
511
|
+
const context = config.ensureContext(root);
|
|
512
|
+
initLocale(context);
|
|
513
|
+
const control = config.loadControl(context);
|
|
514
|
+
env.syncEnvironment(context, control);
|
|
515
|
+
syncDocs(context, control);
|
|
516
|
+
refreshRepoRuntime(context, { quiet: true });
|
|
493
517
|
console.log(t("cli.docsSynced"));
|
|
494
518
|
}
|
|
495
519
|
|
|
496
520
|
function cmdRefreshRepo(root, args) {
|
|
497
|
-
refreshRepoRuntime(root, { quiet: (args || []).includes("--quiet") });
|
|
521
|
+
refreshRepoRuntime(config.ensureContext(root), { quiet: (args || []).includes("--quiet") });
|
|
498
522
|
}
|
|
499
523
|
|
|
500
524
|
function cmdTask(root, args) {
|
|
501
|
-
|
|
525
|
+
const context = config.ensureContext(root);
|
|
526
|
+
initLocale(context);
|
|
502
527
|
const [action, taskId, ...noteParts] = args || [];
|
|
503
528
|
if (!action || !taskId) throw new Error(t("cli.mustProvideActionAndId"));
|
|
504
|
-
const control = config.loadControl(
|
|
505
|
-
updateTask(
|
|
529
|
+
const control = config.loadControl(context);
|
|
530
|
+
updateTask(context, control, action, taskId, noteParts.join(" ").trim());
|
|
506
531
|
console.log(t("cli.taskUpdated", { taskId, action }));
|
|
507
532
|
}
|
|
508
533
|
|
|
509
534
|
function cmdInstallHooks(root) {
|
|
510
|
-
|
|
511
|
-
|
|
535
|
+
const context = config.ensureContext(root);
|
|
536
|
+
initLocale(context);
|
|
537
|
+
const hooksPath = context.layout === "split" ? "ops/.githooks" : ".githooks";
|
|
538
|
+
const result = spawnSync("git", ["config", "core.hooksPath", hooksPath], { cwd: context.workspaceRoot, encoding: "utf8" });
|
|
512
539
|
if (result.error || result.status !== 0) throw new Error(t("cli.hooksError"));
|
|
513
540
|
console.log(t("cli.hooksInstalled"));
|
|
514
541
|
}
|
|
@@ -519,58 +546,73 @@ function cmdHelp() {
|
|
|
519
546
|
console.log("Usage: trackops <command> [args]");
|
|
520
547
|
console.log("");
|
|
521
548
|
console.log("Commands:");
|
|
522
|
-
console.log(" init [--with-opera] [--locale es|en] [--name \"...\"]");
|
|
549
|
+
console.log(" init [--with-opera] [--legacy-layout] [--locale es|en] [--name \"...\"] [--no-bootstrap]");
|
|
523
550
|
console.log(" Legacy alias available: --with-etapa");
|
|
524
551
|
console.log(" Initialize trackops in the current directory.");
|
|
552
|
+
console.log(" workspace status|migrate");
|
|
553
|
+
console.log(" Show or migrate the current workspace layout.");
|
|
554
|
+
console.log(" env status|sync");
|
|
555
|
+
console.log(" Audit or sync the workspace .env contract.");
|
|
556
|
+
console.log(" release [--push]");
|
|
557
|
+
console.log(" Publish the configured app/ branch snapshot.");
|
|
558
|
+
console.log(" version");
|
|
559
|
+
console.log(" Print the installed trackops version.");
|
|
525
560
|
console.log(" status");
|
|
526
561
|
console.log(" Show project state: focus, active phase, ready tasks, blockers, repo.");
|
|
527
|
-
console.log(" next");
|
|
528
|
-
console.log(" Prioritized queue of next executable tasks.");
|
|
529
|
-
console.log(" sync");
|
|
562
|
+
console.log(" next");
|
|
563
|
+
console.log(" Prioritized queue of next executable tasks.");
|
|
564
|
+
console.log(" sync");
|
|
530
565
|
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.");
|
|
566
|
+
console.log(" dashboard [--port N] [--host HOST] [--public] [--strict-port]");
|
|
567
|
+
console.log(" Launch local web dashboard on a free port and print local/network URLs.");
|
|
533
568
|
console.log(" refresh-repo [--quiet]");
|
|
534
|
-
console.log(" Update
|
|
569
|
+
console.log(" Update the repo runtime snapshot with git state.");
|
|
535
570
|
console.log(" install-hooks");
|
|
536
|
-
console.log(" Configure git core.hooksPath to use .
|
|
537
|
-
console.log(" register");
|
|
538
|
-
console.log(" Register current project in the multi-project portfolio.");
|
|
539
|
-
console.log(" projects");
|
|
540
|
-
console.log(" List registered projects.");
|
|
541
|
-
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");
|
|
571
|
+
console.log(" Configure git core.hooksPath to use the TrackOps hooks dir.");
|
|
572
|
+
console.log(" register");
|
|
573
|
+
console.log(" Register current project in the multi-project portfolio.");
|
|
574
|
+
console.log(" projects");
|
|
575
|
+
console.log(" List registered projects.");
|
|
576
|
+
console.log(" task <action> <id> [note]");
|
|
577
|
+
console.log(" Actions: start, review, complete, block, pending, cancel, note.");
|
|
578
|
+
console.log(" opera install|bootstrap|status|configure|upgrade");
|
|
544
579
|
console.log(" Manage OPERA methodology.");
|
|
545
580
|
console.log(" skill install|list|remove|catalog <name>");
|
|
546
581
|
console.log(" Manage skills.");
|
|
547
582
|
console.log(" help");
|
|
548
583
|
console.log(" Show this help.");
|
|
584
|
+
console.log("");
|
|
585
|
+
console.log("Global agent workflow:");
|
|
586
|
+
console.log(" Install with 'npx skills add Baxahaun/trackops --skill trackops --full-depth'");
|
|
587
|
+
console.log(" and the agent/global flags you need, then use 'trackops init' and");
|
|
588
|
+
console.log(" 'trackops opera install' explicitly inside each project you want to manage.");
|
|
549
589
|
}
|
|
550
|
-
|
|
551
|
-
/* ── project-scoped API (used by server) ── */
|
|
552
|
-
|
|
590
|
+
|
|
591
|
+
/* ── project-scoped API (used by server) ── */
|
|
592
|
+
|
|
553
593
|
function forProject(root) {
|
|
554
|
-
|
|
594
|
+
const context = config.ensureContext(root);
|
|
595
|
+
initLocale(context);
|
|
555
596
|
return {
|
|
556
|
-
loadControl: () => config.loadControl(
|
|
557
|
-
saveControl: (ctrl) => config.saveControl(
|
|
597
|
+
loadControl: () => config.loadControl(context),
|
|
598
|
+
saveControl: (ctrl) => config.saveControl(context, ctrl),
|
|
558
599
|
derive,
|
|
559
600
|
buildDocMap,
|
|
560
|
-
getDocDrift: (ctrl) => getDocDrift(
|
|
561
|
-
syncDocs: (ctrl) => syncDocs(
|
|
562
|
-
updateTask: (ctrl, action, id, note) => updateTask(
|
|
563
|
-
getRepoSnapshot: () => getRepoSnapshot(
|
|
564
|
-
refreshRepoRuntime: (opts) => refreshRepoRuntime(
|
|
601
|
+
getDocDrift: (ctrl) => getDocDrift(context, ctrl),
|
|
602
|
+
syncDocs: (ctrl) => syncDocs(context, ctrl),
|
|
603
|
+
updateTask: (ctrl, action, id, note) => updateTask(context, ctrl, action, id, note),
|
|
604
|
+
getRepoSnapshot: () => getRepoSnapshot(context),
|
|
605
|
+
refreshRepoRuntime: (opts) => refreshRepoRuntime(context, opts),
|
|
565
606
|
getPhases: (ctrl) => config.getPhases(ctrl),
|
|
566
607
|
getLocale: (ctrl) => config.getLocale(ctrl),
|
|
567
608
|
statusLabel,
|
|
609
|
+
context,
|
|
568
610
|
};
|
|
569
611
|
}
|
|
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
|
-
};
|
|
612
|
+
|
|
613
|
+
module.exports = {
|
|
614
|
+
buildDocMap, derive, getDocDrift, getRepoSnapshot, refreshRepoRuntime, syncDocs, updateTask,
|
|
615
|
+
forProject, statusLabel, renderTask, getPhaseInfo,
|
|
616
|
+
cmdStatus, cmdNext, cmdSync, cmdRefreshRepo, cmdTask, cmdInstallHooks, cmdHelp,
|
|
617
|
+
PRIORITY_ORDER, STATUS_ORDER, STATUS_ICONS, CHECK_ICONS,
|
|
618
|
+
};
|