trackops 2.0.5 → 2.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 +319 -695
- package/bin/trackops.js +52 -23
- package/lib/cli-format.js +118 -0
- package/lib/config.js +277 -44
- package/lib/control.js +1052 -352
- package/lib/env.js +40 -28
- package/lib/i18n.js +5 -4
- package/lib/init.js +194 -56
- package/lib/opera-bootstrap.js +326 -106
- package/lib/opera-phase-dod.js +485 -0
- package/lib/opera.js +243 -78
- package/lib/plans.js +1329 -0
- package/lib/quality-assert.js +49 -0
- package/lib/quality.js +1759 -0
- package/lib/release.js +18 -11
- package/lib/server.js +504 -192
- package/lib/skills.js +43 -35
- package/lib/workspace.js +32 -21
- package/locales/en.json +431 -75
- package/locales/es.json +432 -76
- package/package.json +6 -5
- package/scripts/quality-unit-tests.js +130 -0
- package/scripts/smoke-tests.js +438 -96
- package/skills/trackops/skill.json +29 -29
- package/templates/skills/opera-quality-guard/SKILL.md +26 -0
- package/templates/skills/opera-quality-guard/locales/en/SKILL.md +26 -0
- package/templates/skills/opera-skill/SKILL.md +8 -0
- package/templates/skills/opera-skill/locales/en/SKILL.md +8 -0
- package/ui/js/api.js +93 -26
- package/ui/js/app.js +13 -7
- package/ui/js/filters.js +49 -29
- package/ui/js/time-tracker.js +41 -28
- package/ui/js/views/board.js +22 -14
- package/ui/js/views/dashboard.js +206 -49
- package/ui/js/views/execution.js +7 -3
- package/ui/js/views/plans.js +284 -0
- package/ui/js/views/scrum.js +25 -13
- package/ui/js/views/sidebar.js +9 -8
- package/ui/js/views/tasks.js +238 -134
package/lib/control.js
CHANGED
|
@@ -4,26 +4,28 @@ const fs = require("fs");
|
|
|
4
4
|
const path = require("path");
|
|
5
5
|
const { spawnSync } = require("child_process");
|
|
6
6
|
|
|
7
|
-
const config = require("./config");
|
|
8
|
-
const env = require("./env");
|
|
9
|
-
const { t, setLocale, getLocale } = require("./i18n");
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
7
|
+
const config = require("./config");
|
|
8
|
+
const env = require("./env");
|
|
9
|
+
const { t, setLocale, getLocale } = require("./i18n");
|
|
10
|
+
const fmt = require("./cli-format");
|
|
11
|
+
|
|
12
|
+
const PRIORITY_ORDER = ["P0", "P1", "P2", "P3"];
|
|
13
|
+
const STATUS_ORDER = ["in_progress", "in_review", "pending", "blocked", "completed", "cancelled"];
|
|
14
|
+
const STATUS_ICONS = {
|
|
15
|
+
pending: "\u23F3",
|
|
16
|
+
in_progress: "\uD83D\uDEA7",
|
|
17
|
+
in_review: "\uD83D\uDC40",
|
|
18
|
+
blocked: "\u26D4",
|
|
19
|
+
completed: "\u2705",
|
|
20
|
+
cancelled: "\uD83D\uDDD1\uFE0F",
|
|
21
|
+
};
|
|
22
|
+
const CHECK_ICONS = {
|
|
23
|
+
pass: "\u2705",
|
|
24
|
+
warn: "\u26A0\uFE0F",
|
|
25
|
+
fail: "\u274C",
|
|
26
|
+
pending: "\u23F3",
|
|
27
|
+
};
|
|
28
|
+
const AUTO_GENERATED_MARKER = "<!-- trackops:auto-generated -->";
|
|
27
29
|
|
|
28
30
|
/* ── helpers ── */
|
|
29
31
|
|
|
@@ -39,29 +41,273 @@ function nowIso() {
|
|
|
39
41
|
return new Date().toISOString();
|
|
40
42
|
}
|
|
41
43
|
|
|
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
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
44
|
+
function git(args, root) {
|
|
45
|
+
const result = spawnSync("git", args, { cwd: root, encoding: "utf8" });
|
|
46
|
+
if (result.error || result.status !== 0) return null;
|
|
47
|
+
return result.stdout.replace(/\s+$/, "");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function gitResult(args, root) {
|
|
51
|
+
const result = spawnSync("git", args, { cwd: root, encoding: "utf8" });
|
|
52
|
+
return {
|
|
53
|
+
ok: !result.error && result.status === 0,
|
|
54
|
+
status: result.status,
|
|
55
|
+
stdout: String(result.stdout || "").replace(/\s+$/, ""),
|
|
56
|
+
stderr: String(result.stderr || "").replace(/\s+$/, ""),
|
|
57
|
+
error: result.error || null,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function statusLabel(status) {
|
|
62
|
+
return t(`status.${status}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function formatBootstrapStatus(status) {
|
|
66
|
+
return t(`bootstrap.status.${String(status || "").trim()}`) || status || t("locale.none");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function formatBootstrapMode(mode) {
|
|
70
|
+
return t(`bootstrap.mode.${String(mode || "").trim()}`) || mode || t("locale.none");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function formatBootstrapReason(reason) {
|
|
74
|
+
return t(`bootstrap.reason.${String(reason || "").trim()}`) || reason || t("locale.none");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function listOrNone(items) {
|
|
78
|
+
const values = (items || []).filter(Boolean);
|
|
79
|
+
return values.length ? values.join(", ") : t("locale.none");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function ensureAgentInbox(control) {
|
|
83
|
+
control.meta = control.meta || {};
|
|
84
|
+
control.meta.agentInbox = config.normalizeAgentInbox(control.meta.agentInbox);
|
|
85
|
+
return control.meta.agentInbox;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function makeInboxId(taskId, kind) {
|
|
89
|
+
return `ai-${String(taskId || "general").trim() || "general"}-${String(kind || "info").trim() || "info"}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function upsertAgentInstruction(control, payload = {}) {
|
|
93
|
+
const inbox = ensureAgentInbox(control);
|
|
94
|
+
const now = nowIso();
|
|
95
|
+
const instruction = config.normalizeAgentInbox({
|
|
96
|
+
pending: [{
|
|
97
|
+
id: payload.id || makeInboxId(payload.taskId, payload.kind),
|
|
98
|
+
taskId: payload.taskId || null,
|
|
99
|
+
kind: payload.kind || "verify_status",
|
|
100
|
+
message: payload.message || "",
|
|
101
|
+
status: "pending",
|
|
102
|
+
createdAt: payload.createdAt || now,
|
|
103
|
+
updatedAt: now,
|
|
104
|
+
createdBy: payload.createdBy || "system",
|
|
105
|
+
source: payload.source || null,
|
|
106
|
+
expectedStatus: payload.expectedStatus || null,
|
|
107
|
+
sessionId: payload.sessionId || null,
|
|
108
|
+
}],
|
|
109
|
+
}).pending[0];
|
|
110
|
+
|
|
111
|
+
const existingIndex = inbox.pending.findIndex((item) => item.id === instruction.id);
|
|
112
|
+
if (existingIndex >= 0) {
|
|
113
|
+
const existing = inbox.pending[existingIndex];
|
|
114
|
+
inbox.pending[existingIndex] = {
|
|
115
|
+
...existing,
|
|
116
|
+
...instruction,
|
|
117
|
+
createdAt: existing.createdAt || instruction.createdAt,
|
|
118
|
+
};
|
|
119
|
+
} else {
|
|
120
|
+
inbox.pending.unshift(instruction);
|
|
121
|
+
}
|
|
122
|
+
inbox.lastIssuedAt = now;
|
|
123
|
+
return instruction;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function resolveAgentInstructions(control, taskId, options = {}) {
|
|
127
|
+
const inbox = ensureAgentInbox(control);
|
|
128
|
+
const now = nowIso();
|
|
129
|
+
const kinds = Array.isArray(options.kinds) && options.kinds.length
|
|
130
|
+
? new Set(options.kinds.map((kind) => String(kind || "").trim()))
|
|
131
|
+
: null;
|
|
132
|
+
const resolved = [];
|
|
133
|
+
const pending = [];
|
|
134
|
+
|
|
135
|
+
inbox.pending.forEach((item) => {
|
|
136
|
+
const matchesTask = !taskId || item.taskId === taskId;
|
|
137
|
+
const matchesKind = !kinds || kinds.has(item.kind);
|
|
138
|
+
if (matchesTask && matchesKind) {
|
|
139
|
+
const nextItem = {
|
|
140
|
+
...item,
|
|
141
|
+
status: "resolved",
|
|
142
|
+
resolvedAt: now,
|
|
143
|
+
resolutionNote: String(options.note || "").trim() || null,
|
|
144
|
+
updatedAt: now,
|
|
145
|
+
};
|
|
146
|
+
inbox.history.unshift(nextItem);
|
|
147
|
+
resolved.push(nextItem);
|
|
148
|
+
} else {
|
|
149
|
+
pending.push(item);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
inbox.pending = pending;
|
|
154
|
+
return resolved;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function ensureTaskExecution(task) {
|
|
158
|
+
task.execution = config.normalizeTaskExecution(task.execution);
|
|
159
|
+
return task.execution;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function setTaskExecutionContext(task, options = {}) {
|
|
163
|
+
const execution = ensureTaskExecution(task);
|
|
164
|
+
if (options.owner) execution.owner = config.normalizeExecutionOwner(options.owner);
|
|
165
|
+
if (options.actor) execution.lastActor = String(options.actor || "").trim() || execution.lastActor;
|
|
166
|
+
if (options.source) execution.lastSource = String(options.source || "").trim() || execution.lastSource;
|
|
167
|
+
if (Object.prototype.hasOwnProperty.call(options, "sessionId")) {
|
|
168
|
+
execution.currentSessionId = options.sessionId ? String(options.sessionId).trim() : null;
|
|
169
|
+
if (execution.currentSessionId) execution.lastSessionId = execution.currentSessionId;
|
|
170
|
+
}
|
|
171
|
+
if (Object.prototype.hasOwnProperty.call(options, "sessionStatus")) {
|
|
172
|
+
execution.lastSessionStatus = options.sessionStatus ? String(options.sessionStatus).trim() : null;
|
|
173
|
+
}
|
|
174
|
+
if (Object.prototype.hasOwnProperty.call(options, "awaitingUserConfirmation")) {
|
|
175
|
+
execution.awaitingUserConfirmation = options.awaitingUserConfirmation === true;
|
|
176
|
+
}
|
|
177
|
+
if (Object.prototype.hasOwnProperty.call(options, "verificationPending")) {
|
|
178
|
+
execution.verificationPending = options.verificationPending === true;
|
|
179
|
+
}
|
|
180
|
+
execution.updatedAt = nowIso();
|
|
181
|
+
return execution;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function buildUserExecutionInstruction(task, status, source) {
|
|
185
|
+
const sourceLabel = String(source || t("locale.none")).replace(/_/g, " ");
|
|
186
|
+
return t("agentInbox.awaitUser.message", {
|
|
187
|
+
taskId: task.id,
|
|
188
|
+
title: task.title,
|
|
189
|
+
status: statusLabel(status),
|
|
190
|
+
source: sourceLabel,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function buildVerificationInstruction(task, status, source) {
|
|
195
|
+
const sourceLabel = String(source || t("locale.none")).replace(/_/g, " ");
|
|
196
|
+
return t("agentInbox.verify.message", {
|
|
197
|
+
taskId: task.id,
|
|
198
|
+
title: task.title,
|
|
199
|
+
status: statusLabel(status),
|
|
200
|
+
source: sourceLabel,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function applyTaskStateTransition(control, task, transition, options = {}) {
|
|
205
|
+
const nextStatus = String(transition.status || "").trim();
|
|
206
|
+
const action = String(transition.action || "").trim();
|
|
207
|
+
const note = String(transition.note || "").trim();
|
|
208
|
+
const actor = String(options.actor || "system").trim() || "system";
|
|
209
|
+
const source = String(options.source || "system").trim() || "system";
|
|
210
|
+
const execution = setTaskExecutionContext(task, {
|
|
211
|
+
owner: options.owner || task.execution?.owner,
|
|
212
|
+
actor,
|
|
213
|
+
source,
|
|
214
|
+
sessionId: Object.prototype.hasOwnProperty.call(options, "sessionId") ? options.sessionId : undefined,
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
if (nextStatus && task.status !== nextStatus) {
|
|
218
|
+
task.status = nextStatus;
|
|
219
|
+
}
|
|
220
|
+
if (nextStatus === "blocked") {
|
|
221
|
+
task.blocker = note || task.blocker || t("cli.undocumentedBlocker");
|
|
222
|
+
} else if (nextStatus && nextStatus !== "blocked") {
|
|
223
|
+
delete task.blocker;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (execution.owner === "user" && nextStatus === "in_progress") {
|
|
227
|
+
execution.awaitingUserConfirmation = true;
|
|
228
|
+
upsertAgentInstruction(control, {
|
|
229
|
+
taskId: task.id,
|
|
230
|
+
kind: "await_user_report",
|
|
231
|
+
message: buildUserExecutionInstruction(task, nextStatus, source),
|
|
232
|
+
createdBy: actor,
|
|
233
|
+
source,
|
|
234
|
+
expectedStatus: nextStatus,
|
|
235
|
+
sessionId: options.sessionId || null,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (actor === "user" && nextStatus) {
|
|
240
|
+
execution.verificationPending = true;
|
|
241
|
+
upsertAgentInstruction(control, {
|
|
242
|
+
taskId: task.id,
|
|
243
|
+
kind: "verify_status",
|
|
244
|
+
message: buildVerificationInstruction(task, nextStatus, source),
|
|
245
|
+
createdBy: actor,
|
|
246
|
+
source,
|
|
247
|
+
expectedStatus: nextStatus,
|
|
248
|
+
sessionId: options.sessionId || null,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (actor === "agent" || actor === "system") {
|
|
253
|
+
execution.verificationPending = false;
|
|
254
|
+
resolveAgentInstructions(control, task.id, {
|
|
255
|
+
kinds: ["verify_status"],
|
|
256
|
+
note: note || t("agentInbox.verify.resolved"),
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (nextStatus === "completed" || nextStatus === "blocked" || nextStatus === "cancelled") {
|
|
261
|
+
execution.awaitingUserConfirmation = false;
|
|
262
|
+
resolveAgentInstructions(control, task.id, {
|
|
263
|
+
kinds: ["await_user_report"],
|
|
264
|
+
note: note || t("agentInbox.awaitUser.resolved"),
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (action) {
|
|
269
|
+
task.history = task.history || [];
|
|
270
|
+
task.history.push({ at: nowIso(), action, note: note || "" });
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return task;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/* ── repo snapshot ── */
|
|
277
|
+
|
|
278
|
+
function getRepoSnapshot(contextOrRoot) {
|
|
279
|
+
const context = config.ensureContext(contextOrRoot);
|
|
280
|
+
const repoRoot = context.workspaceRoot;
|
|
281
|
+
const insideWorkTree = gitResult(["rev-parse", "--is-inside-work-tree"], repoRoot);
|
|
282
|
+
if (!insideWorkTree.ok || insideWorkTree.stdout.trim() !== "true") {
|
|
283
|
+
return {
|
|
284
|
+
generatedAt: nowIso(),
|
|
285
|
+
available: false,
|
|
286
|
+
state: "not_initialized",
|
|
287
|
+
branch: null,
|
|
288
|
+
clean: null,
|
|
289
|
+
staged: 0,
|
|
290
|
+
unstaged: 0,
|
|
291
|
+
untracked: 0,
|
|
292
|
+
ahead: 0,
|
|
293
|
+
behind: 0,
|
|
294
|
+
hasUpstream: false,
|
|
295
|
+
lastCommit: null,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const branchResult = gitResult(["branch", "--show-current"], repoRoot);
|
|
300
|
+
const branch = branchResult.ok ? branchResult.stdout.trim() : "";
|
|
301
|
+
const status = git(["status", "--short"], repoRoot) || "";
|
|
302
|
+
const lines = status.split(/\r?\n/).filter(Boolean);
|
|
303
|
+
const lastCommitRaw = git(["log", "-1", "--pretty=format:%H%n%cs%n%s"], repoRoot);
|
|
304
|
+
const upstream = gitResult(["rev-parse", "--abbrev-ref", "@{upstream}"], repoRoot);
|
|
305
|
+
const divergenceRaw = upstream.ok
|
|
306
|
+
? git(["rev-list", "--left-right", "--count", "@{upstream}...HEAD"], repoRoot)
|
|
307
|
+
: null;
|
|
308
|
+
|
|
309
|
+
let staged = 0;
|
|
310
|
+
let unstaged = 0;
|
|
65
311
|
let untracked = 0;
|
|
66
312
|
|
|
67
313
|
lines.forEach((line) => {
|
|
@@ -78,14 +324,27 @@ function getRepoSnapshot(contextOrRoot) {
|
|
|
78
324
|
|
|
79
325
|
let ahead = 0;
|
|
80
326
|
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 {
|
|
88
|
-
|
|
327
|
+
if (divergenceRaw) {
|
|
328
|
+
const [left, right] = divergenceRaw.split(/\s+/).map(Number);
|
|
329
|
+
behind = Number.isFinite(left) ? left : 0;
|
|
330
|
+
ahead = Number.isFinite(right) ? right : 0;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return {
|
|
334
|
+
generatedAt: nowIso(),
|
|
335
|
+
available: true,
|
|
336
|
+
state: branch ? "ready" : "detached",
|
|
337
|
+
branch: branch || null,
|
|
338
|
+
clean: lines.length === 0,
|
|
339
|
+
staged,
|
|
340
|
+
unstaged,
|
|
341
|
+
untracked,
|
|
342
|
+
ahead,
|
|
343
|
+
behind,
|
|
344
|
+
hasUpstream: upstream.ok,
|
|
345
|
+
lastCommit,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
89
348
|
|
|
90
349
|
function refreshRepoRuntime(root, options = {}) {
|
|
91
350
|
const context = config.ensureContext(root);
|
|
@@ -101,86 +360,299 @@ function refreshRepoRuntime(root, options = {}) {
|
|
|
101
360
|
|
|
102
361
|
/* ── derive ── */
|
|
103
362
|
|
|
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
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
.
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
const
|
|
176
|
-
const
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
363
|
+
function getPhaseInfo(phaseId, phases) {
|
|
364
|
+
return phases.find((p) => p.id === phaseId) || { id: phaseId, label: phaseId, index: 99 };
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function compareTasks(a, b, phases) {
|
|
368
|
+
const aSequence = Number.isFinite(Number(a.sequence)) ? Number(a.sequence) : Number.POSITIVE_INFINITY;
|
|
369
|
+
const bSequence = Number.isFinite(Number(b.sequence)) ? Number(b.sequence) : Number.POSITIVE_INFINITY;
|
|
370
|
+
if (aSequence !== bSequence) return aSequence - bSequence;
|
|
371
|
+
const phaseDelta = getPhaseInfo(a.phase, phases).index - getPhaseInfo(b.phase, phases).index;
|
|
372
|
+
if (phaseDelta !== 0) return phaseDelta;
|
|
373
|
+
const priorityDelta = PRIORITY_ORDER.indexOf(a.priority) - PRIORITY_ORDER.indexOf(b.priority);
|
|
374
|
+
if (priorityDelta !== 0) return priorityDelta;
|
|
375
|
+
const statusDelta = STATUS_ORDER.indexOf(a.status) - STATUS_ORDER.indexOf(b.status);
|
|
376
|
+
if (statusDelta !== 0) return statusDelta;
|
|
377
|
+
const titleDelta = a.title.localeCompare(b.title, getLocale());
|
|
378
|
+
if (titleDelta !== 0) return titleDelta;
|
|
379
|
+
return a.id.localeCompare(b.id, getLocale());
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function detectCircularDeps(tasks) {
|
|
383
|
+
const taskMap = new Map(tasks.map((task) => [task.id, task]));
|
|
384
|
+
const visited = new Set();
|
|
385
|
+
const inStack = new Set();
|
|
386
|
+
const cycles = new Set();
|
|
387
|
+
function dfs(id) {
|
|
388
|
+
if (inStack.has(id)) { cycles.add(id); return; }
|
|
389
|
+
if (visited.has(id)) return;
|
|
390
|
+
visited.add(id);
|
|
391
|
+
inStack.add(id);
|
|
392
|
+
const task = taskMap.get(id);
|
|
393
|
+
if (task) (task.dependsOn || []).forEach((dep) => dfs(dep));
|
|
394
|
+
inStack.delete(id);
|
|
395
|
+
}
|
|
396
|
+
tasks.forEach((t) => dfs(t.id));
|
|
397
|
+
return [...cycles];
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function detectHierarchyIssues(tasks) {
|
|
401
|
+
const taskMap = new Map(tasks.map((task) => [task.id, task]));
|
|
402
|
+
const hierarchyCycles = new Set();
|
|
403
|
+
const phantomParents = [];
|
|
404
|
+
const visiting = new Set();
|
|
405
|
+
const visited = new Set();
|
|
406
|
+
|
|
407
|
+
function visit(task) {
|
|
408
|
+
if (!task || visited.has(task.id)) return;
|
|
409
|
+
if (visiting.has(task.id)) {
|
|
410
|
+
hierarchyCycles.add(task.id);
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
visiting.add(task.id);
|
|
414
|
+
if (task.parentId) {
|
|
415
|
+
if (task.parentId === task.id) {
|
|
416
|
+
hierarchyCycles.add(task.id);
|
|
417
|
+
} else if (!taskMap.has(task.parentId)) {
|
|
418
|
+
phantomParents.push({ taskId: task.id, missingParentId: task.parentId });
|
|
419
|
+
} else {
|
|
420
|
+
visit(taskMap.get(task.parentId));
|
|
421
|
+
if (hierarchyCycles.has(task.parentId)) hierarchyCycles.add(task.id);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
visiting.delete(task.id);
|
|
425
|
+
visited.add(task.id);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
tasks.forEach((task) => visit(task));
|
|
429
|
+
return { hierarchyCycles: [...hierarchyCycles], phantomParents };
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function derive(control) {
|
|
433
|
+
const phases = config.getPhases(control);
|
|
434
|
+
const baseTasks = (control.tasks || []).map((task) => config.normalizeTaskShape(task));
|
|
435
|
+
const taskMap = new Map(baseTasks.map((task) => [task.id, { ...task, rawStatus: task.status }]));
|
|
436
|
+
const { hierarchyCycles, phantomParents } = detectHierarchyIssues(baseTasks);
|
|
437
|
+
const cycleSet = new Set(hierarchyCycles);
|
|
438
|
+
const childrenByParent = new Map();
|
|
439
|
+
|
|
440
|
+
taskMap.forEach((task) => {
|
|
441
|
+
const declaredParentId = task.parentId;
|
|
442
|
+
task.declaredParentId = declaredParentId;
|
|
443
|
+
if (!declaredParentId || declaredParentId === task.id || cycleSet.has(task.id) || !taskMap.has(declaredParentId)) {
|
|
444
|
+
task.parentId = null;
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
if (!childrenByParent.has(declaredParentId)) childrenByParent.set(declaredParentId, []);
|
|
448
|
+
childrenByParent.get(declaredParentId).push(task);
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
childrenByParent.forEach((children) => children.sort((a, b) => compareTasks(a, b, phases)));
|
|
452
|
+
|
|
453
|
+
const leafCache = new Map();
|
|
454
|
+
const statusCache = new Map();
|
|
455
|
+
const visitedTree = new Set();
|
|
456
|
+
const closedStatuses = new Set(["completed", "cancelled"]);
|
|
457
|
+
|
|
458
|
+
function getLeafDescendants(taskId) {
|
|
459
|
+
if (leafCache.has(taskId)) return leafCache.get(taskId);
|
|
460
|
+
const children = childrenByParent.get(taskId) || [];
|
|
461
|
+
if (!children.length) {
|
|
462
|
+
const leaves = [taskMap.get(taskId)].filter(Boolean);
|
|
463
|
+
leafCache.set(taskId, leaves);
|
|
464
|
+
return leaves;
|
|
465
|
+
}
|
|
466
|
+
const leaves = children.flatMap((child) => getLeafDescendants(child.id));
|
|
467
|
+
leafCache.set(taskId, leaves);
|
|
468
|
+
return leaves;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function computeStatus(taskId) {
|
|
472
|
+
if (statusCache.has(taskId)) return statusCache.get(taskId);
|
|
473
|
+
const task = taskMap.get(taskId);
|
|
474
|
+
if (!task) return "pending";
|
|
475
|
+
const children = childrenByParent.get(taskId) || [];
|
|
476
|
+
if (!children.length) {
|
|
477
|
+
statusCache.set(taskId, task.rawStatus);
|
|
478
|
+
return task.rawStatus;
|
|
479
|
+
}
|
|
480
|
+
const requiredLeaves = getLeafDescendants(taskId).filter((leaf) => leaf.required !== false);
|
|
481
|
+
let nextStatus = "pending";
|
|
482
|
+
if (requiredLeaves.some((leaf) => leaf.rawStatus === "blocked")) nextStatus = "blocked";
|
|
483
|
+
else if (requiredLeaves.some((leaf) => leaf.rawStatus === "in_review")) nextStatus = "in_review";
|
|
484
|
+
else if (requiredLeaves.some((leaf) => leaf.rawStatus === "in_progress")) nextStatus = "in_progress";
|
|
485
|
+
else if (requiredLeaves.length && requiredLeaves.every((leaf) => closedStatuses.has(leaf.rawStatus)) && requiredLeaves.some((leaf) => leaf.rawStatus === "completed")) nextStatus = "completed";
|
|
486
|
+
statusCache.set(taskId, nextStatus);
|
|
487
|
+
return nextStatus;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const roots = [...taskMap.values()]
|
|
491
|
+
.filter((task) => !task.parentId)
|
|
492
|
+
.sort((a, b) => compareTasks(a, b, phases));
|
|
493
|
+
const treeTasks = [];
|
|
494
|
+
|
|
495
|
+
function flatten(task, depth = 0, rootId = task.id, rootTitle = task.title) {
|
|
496
|
+
if (!task || visitedTree.has(task.id)) return;
|
|
497
|
+
visitedTree.add(task.id);
|
|
498
|
+
const children = childrenByParent.get(task.id) || [];
|
|
499
|
+
const effectiveStatus = computeStatus(task.id);
|
|
500
|
+
const execution = config.normalizeTaskExecution(task.execution);
|
|
501
|
+
const derivedTask = {
|
|
502
|
+
...task,
|
|
503
|
+
status: effectiveStatus,
|
|
504
|
+
effectiveStatus,
|
|
505
|
+
isParent: children.length > 0,
|
|
506
|
+
isLeaf: children.length === 0,
|
|
507
|
+
childrenIds: children.map((child) => child.id),
|
|
508
|
+
childrenCount: children.length,
|
|
509
|
+
depth,
|
|
510
|
+
rootId,
|
|
511
|
+
rootTitle,
|
|
512
|
+
sourceId: task.origin?.sourceId || null,
|
|
513
|
+
detached: task.origin?.detached === true,
|
|
514
|
+
execution,
|
|
515
|
+
executionOwner: execution.owner,
|
|
516
|
+
awaitingUserConfirmation: execution.awaitingUserConfirmation === true,
|
|
517
|
+
verificationPending: execution.verificationPending === true,
|
|
518
|
+
currentSessionId: execution.currentSessionId || null,
|
|
519
|
+
};
|
|
520
|
+
treeTasks.push(derivedTask);
|
|
521
|
+
children.forEach((child) => flatten(child, depth + 1, rootId, rootTitle));
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
roots.forEach((task) => flatten(task));
|
|
525
|
+
[...taskMap.values()].filter((task) => !visitedTree.has(task.id)).sort((a, b) => compareTasks(a, b, phases)).forEach((task) => flatten(task));
|
|
526
|
+
|
|
527
|
+
const allIds = new Set(treeTasks.map((task) => task.id));
|
|
528
|
+
const completedIds = new Set(treeTasks.filter((task) => task.status === "completed").map((task) => task.id));
|
|
529
|
+
const phantomDeps = [];
|
|
530
|
+
const actionableTasks = treeTasks.filter((task) => task.isLeaf);
|
|
531
|
+
const focusPhase = control.meta.focusPhase || "";
|
|
532
|
+
|
|
533
|
+
const readyTasks = actionableTasks
|
|
534
|
+
.filter((task) => {
|
|
535
|
+
if (task.status !== "pending") return false;
|
|
536
|
+
const validDeps = (task.dependsOn || []).filter((dep) => {
|
|
537
|
+
if (!allIds.has(dep)) {
|
|
538
|
+
phantomDeps.push({ taskId: task.id, missingDep: dep });
|
|
539
|
+
return false;
|
|
540
|
+
}
|
|
541
|
+
return true;
|
|
542
|
+
});
|
|
543
|
+
return validDeps.every((dep) => completedIds.has(dep));
|
|
544
|
+
})
|
|
545
|
+
.sort((a, b) => {
|
|
546
|
+
const aFocused = a.phase === focusPhase ? 0 : 1;
|
|
547
|
+
const bFocused = b.phase === focusPhase ? 0 : 1;
|
|
548
|
+
if (aFocused !== bFocused) return aFocused - bFocused;
|
|
549
|
+
return compareTasks(a, b, phases);
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
const blockers = actionableTasks.filter((task) => task.status === "blocked");
|
|
553
|
+
const activeTasks = actionableTasks.filter((task) => task.status === "in_progress");
|
|
554
|
+
const reviewTasks = actionableTasks.filter((task) => task.status === "in_review");
|
|
555
|
+
const awaitingUserTasks = actionableTasks.filter((task) => task.awaitingUserConfirmation);
|
|
556
|
+
const verificationPendingTasks = actionableTasks.filter((task) => task.verificationPending);
|
|
557
|
+
const openTasks = actionableTasks.filter((task) => !closedStatuses.has(task.status));
|
|
558
|
+
const requiredOpenTasks = actionableTasks.filter((task) => task.required !== false && !closedStatuses.has(task.status));
|
|
559
|
+
const projectCompleted = requiredOpenTasks.length === 0 && actionableTasks.length > 0;
|
|
560
|
+
const agentInbox = config.normalizeAgentInbox(control.meta?.agentInbox);
|
|
561
|
+
|
|
562
|
+
const activePhase =
|
|
563
|
+
phases.find((phase) => requiredOpenTasks.some((task) => task.phase === phase.id)) ||
|
|
564
|
+
(projectCompleted ? phases[phases.length - 1] : phases[0]);
|
|
565
|
+
|
|
566
|
+
const phaseStats = phases.map((phase) => {
|
|
567
|
+
const phaseTasks = actionableTasks.filter((task) => task.phase === phase.id && task.required !== false);
|
|
568
|
+
const completed = phaseTasks.filter((task) => task.status === "completed").length;
|
|
569
|
+
const closed = phaseTasks.filter((task) => closedStatuses.has(task.status)).length;
|
|
570
|
+
return { ...phase, total: phaseTasks.length, completed, closed, remaining: phaseTasks.length - closed };
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
const nextTask = activeTasks[0] || readyTasks[0] || blockers[0] || openTasks[0] || null;
|
|
574
|
+
const circularDeps = detectCircularDeps(treeTasks);
|
|
575
|
+
|
|
576
|
+
return {
|
|
577
|
+
tasks: treeTasks,
|
|
578
|
+
roots: roots.map((task) => task.id),
|
|
579
|
+
taskMap: Object.fromEntries(treeTasks.map((task) => [task.id, task])),
|
|
580
|
+
blockers,
|
|
581
|
+
activeTasks,
|
|
582
|
+
reviewTasks,
|
|
583
|
+
awaitingUserTasks,
|
|
584
|
+
verificationPendingTasks,
|
|
585
|
+
agentInbox,
|
|
586
|
+
readyTasks,
|
|
587
|
+
actionableTasks,
|
|
588
|
+
nextTask,
|
|
589
|
+
activePhase,
|
|
590
|
+
phaseStats,
|
|
591
|
+
projectCompleted,
|
|
592
|
+
circularDeps,
|
|
593
|
+
hierarchyCycles,
|
|
594
|
+
phantomDeps,
|
|
595
|
+
phantomParents,
|
|
596
|
+
openFindings: (control.findings || []).filter((finding) => finding.status === "open"),
|
|
597
|
+
resolvedFindings: (control.findings || []).filter((finding) => finding.status === "resolved"),
|
|
598
|
+
totals: {
|
|
599
|
+
all: actionableTasks.length,
|
|
600
|
+
completed: actionableTasks.filter((task) => task.status === "completed").length,
|
|
601
|
+
pending: actionableTasks.filter((task) => task.status === "pending").length,
|
|
602
|
+
inProgress: activeTasks.length,
|
|
603
|
+
inReview: reviewTasks.length,
|
|
604
|
+
blocked: blockers.length,
|
|
605
|
+
cancelled: actionableTasks.filter((task) => task.status === "cancelled").length,
|
|
606
|
+
parents: treeTasks.filter((task) => task.isParent).length,
|
|
607
|
+
awaitingUser: awaitingUserTasks.length,
|
|
608
|
+
verificationPending: verificationPendingTasks.length,
|
|
609
|
+
agentInboxPending: agentInbox.pending.length,
|
|
610
|
+
},
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/* ── render ── */
|
|
615
|
+
|
|
616
|
+
function renderTask(task, phases, options = {}) {
|
|
617
|
+
const phase = getPhaseInfo(task.phase, phases);
|
|
618
|
+
const detail = task.blocker || task.summary || "";
|
|
619
|
+
const detailSuffix = detail ? ` — ${detail}` : "";
|
|
620
|
+
const token = options.cli ? fmt.statusToken(task.status) : STATUS_ICONS[task.status];
|
|
621
|
+
const indent = options.hierarchy ? " ".repeat(task.depth || 0) : "";
|
|
622
|
+
const sourceSuffix = task.sourceId ? ` [plan:${task.sourceId}]` : "";
|
|
623
|
+
const detachedSuffix = task.detached ? " [detached]" : "";
|
|
624
|
+
const parentSuffix = task.isParent ? ` [${task.childrenCount} child${task.childrenCount === 1 ? "" : "ren"}]` : "";
|
|
625
|
+
const executionSuffix = task.executionOwner ? ` [exec:${task.executionOwner}]` : "";
|
|
626
|
+
const awaitingSuffix = task.awaitingUserConfirmation ? " [await-user]" : "";
|
|
627
|
+
const verifySuffix = task.verificationPending ? " [verify]" : "";
|
|
628
|
+
return `${indent}- ${token} \`${task.id}\` [${task.priority}] ${task.title}${sourceSuffix}${detachedSuffix}${parentSuffix}${executionSuffix}${awaitingSuffix}${verifySuffix} (${phase.id} · ${phase.label} · ${task.stream})${detailSuffix}`;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function renderPlanSummary(control) {
|
|
632
|
+
const sources = control.meta?.plans?.sources || [];
|
|
633
|
+
if (!sources.length) return `- No imported plans.`;
|
|
634
|
+
return sources
|
|
635
|
+
.map((source) => `- [plan:${source.id}] ${source.title} (${source.adapter}) · status=${source.status} · warnings=${source.warnings} · conflicts=${source.conflicts}`)
|
|
636
|
+
.join("\n");
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function renderAgentInboxSummary(state) {
|
|
640
|
+
const pending = state.agentInbox?.pending || [];
|
|
641
|
+
if (!pending.length) return `- ${t("doc.label.noAgentInbox")}`;
|
|
642
|
+
return pending
|
|
643
|
+
.slice(0, 8)
|
|
644
|
+
.map((item) => `- [${item.kind}] \`${item.taskId || "general"}\` ${item.message}`)
|
|
645
|
+
.join("\n");
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function renderHierarchyOverview(state, phases) {
|
|
649
|
+
if (!state.tasks.length) return `- ${t("doc.label.noTasks")}`;
|
|
650
|
+
return state.tasks.map((task) => renderTask(task, phases, { hierarchy: true })).join("\n");
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function renderTaskPlan(control) {
|
|
654
|
+
const phases = config.getPhases(control);
|
|
655
|
+
const state = derive(control);
|
|
184
656
|
const blockersLabel = state.blockers.length
|
|
185
657
|
? state.blockers.map((t) => t.title).join("; ")
|
|
186
658
|
: t("doc.label.noBlockers");
|
|
@@ -189,17 +661,21 @@ function renderTaskPlan(control) {
|
|
|
189
661
|
const externalDecisions = (control.decisionsPending || []).length
|
|
190
662
|
? control.decisionsPending.map((d) => `- [${d.owner}] ${d.title} — ${d.impact}`).join("\n")
|
|
191
663
|
: `- ${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
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
664
|
+
|
|
665
|
+
const readyTasks = state.readyTasks.length
|
|
666
|
+
? state.readyTasks.slice(0, 6).map((task) => renderTask(task, phases)).join("\n")
|
|
667
|
+
: `- ${t("doc.label.noReadyTasks")}`;
|
|
668
|
+
|
|
669
|
+
const hierarchyOverview = renderHierarchyOverview(state, phases);
|
|
670
|
+
const planSummary = renderPlanSummary(control);
|
|
671
|
+
const agentInboxSummary = renderAgentInboxSummary(state);
|
|
672
|
+
|
|
673
|
+
const phaseBlocks = phases.map((phase) => {
|
|
674
|
+
const phaseTasks = state.actionableTasks.filter((task) => task.phase === phase.id);
|
|
675
|
+
const stats = state.phaseStats.find((s) => s.id === phase.id);
|
|
676
|
+
const lines = phaseTasks.length
|
|
677
|
+
? phaseTasks.map((task) => renderTask(task, phases)).join("\n")
|
|
678
|
+
: `- ${t("doc.label.noTasks")}`;
|
|
203
679
|
|
|
204
680
|
const phaseStatus = phase.id === state.activePhase.id
|
|
205
681
|
? t("doc.label.phaseActive")
|
|
@@ -216,29 +692,44 @@ function renderTaskPlan(control) {
|
|
|
216
692
|
].join("\n");
|
|
217
693
|
}).join("\n\n---\n\n");
|
|
218
694
|
|
|
219
|
-
return [
|
|
220
|
-
`# ${t("doc.header.taskPlan", { projectName: control.meta.projectName })}`,
|
|
221
|
-
"",
|
|
222
|
-
|
|
223
|
-
"",
|
|
695
|
+
return [
|
|
696
|
+
`# ${t("doc.header.taskPlan", { projectName: control.meta.projectName })}`,
|
|
697
|
+
"",
|
|
698
|
+
AUTO_GENERATED_MARKER,
|
|
699
|
+
"",
|
|
700
|
+
`> ${t("doc.autogenerated")}`,
|
|
701
|
+
"",
|
|
224
702
|
`## ${t("doc.section.operativeState")}`,
|
|
225
703
|
`- ${t("doc.label.activePhase")}: ${state.activePhase.id} — ${state.activePhase.label} (${state.activePhase.index}/${phases.length})`,
|
|
226
704
|
`- ${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
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
"
|
|
238
|
-
"",
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
}
|
|
705
|
+
`- ${t("doc.label.deliveryTarget")}: ${control.meta.deliveryTarget}`,
|
|
706
|
+
`- ${t("doc.label.blockers")}: ${blockersLabel}`,
|
|
707
|
+
`- ${t("doc.label.nextStep")}: ${nextStep}`,
|
|
708
|
+
`- Imported plans: ${(control.meta?.plans?.sources || []).length}`,
|
|
709
|
+
`- Unresolved plan conflicts: ${control.meta?.plans?.unresolvedConflicts || 0}`,
|
|
710
|
+
`- Agent inbox pending: ${state.totals.agentInboxPending}`,
|
|
711
|
+
`- Awaiting user confirmation: ${state.totals.awaitingUser}`,
|
|
712
|
+
"",
|
|
713
|
+
"### Plan Sources",
|
|
714
|
+
planSummary,
|
|
715
|
+
"",
|
|
716
|
+
"### Agent Coordination",
|
|
717
|
+
agentInboxSummary,
|
|
718
|
+
"",
|
|
719
|
+
`### ${t("doc.section.externalDecisions")}`,
|
|
720
|
+
externalDecisions,
|
|
721
|
+
"",
|
|
722
|
+
`### ${t("doc.section.readyTasks")}`,
|
|
723
|
+
readyTasks,
|
|
724
|
+
"",
|
|
725
|
+
"### Hierarchy",
|
|
726
|
+
hierarchyOverview,
|
|
727
|
+
"",
|
|
728
|
+
"---",
|
|
729
|
+
"",
|
|
730
|
+
phaseBlocks,
|
|
731
|
+
].join("\n");
|
|
732
|
+
}
|
|
242
733
|
|
|
243
734
|
function renderProgress(control) {
|
|
244
735
|
const phases = config.getPhases(control);
|
|
@@ -248,14 +739,17 @@ function renderProgress(control) {
|
|
|
248
739
|
: t("doc.label.noBlockers");
|
|
249
740
|
const nextStep = state.nextTask ? state.nextTask.title : t("doc.label.noOpenTasks");
|
|
250
741
|
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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
742
|
+
const latestHistory = state.tasks
|
|
743
|
+
.flatMap((task) => (task.history || []).map((entry) => ({ ...entry, taskId: task.id, taskTitle: task.title })))
|
|
744
|
+
.sort((a, b) => (a.at < b.at ? 1 : -1))
|
|
745
|
+
.slice(0, 8);
|
|
746
|
+
const planActivity = latestHistory
|
|
747
|
+
.filter((entry) => String(entry.action || "").startsWith("plan_") || entry.action === "detach_from_plan")
|
|
748
|
+
.slice(0, 5);
|
|
749
|
+
|
|
750
|
+
const activeLines = state.activeTasks.length
|
|
751
|
+
? state.activeTasks.map((task) => renderTask(task, phases)).join("\n")
|
|
752
|
+
: `- ${t("doc.label.noActiveTasks")}`;
|
|
259
753
|
|
|
260
754
|
const reviewLines = state.reviewTasks.length
|
|
261
755
|
? state.reviewTasks.map((task) => renderTask(task, phases)).join("\n")
|
|
@@ -264,27 +758,33 @@ function renderProgress(control) {
|
|
|
264
758
|
const blockerLines = state.blockers.length
|
|
265
759
|
? state.blockers.map((task) => renderTask(task, phases)).join("\n")
|
|
266
760
|
: `- ${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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
761
|
+
|
|
762
|
+
const historyLines = latestHistory.length
|
|
763
|
+
? latestHistory.map((e) => `- [${e.at.slice(0, 10)}] \`${e.taskId}\` ${e.action}${e.note ? ` — ${e.note}` : ""}`).join("\n")
|
|
764
|
+
: `- ${t("doc.label.noHistory")}`;
|
|
765
|
+
const planActivityLines = planActivity.length
|
|
766
|
+
? planActivity.map((e) => `- [${e.at.slice(0, 10)}] \`${e.taskId}\` ${e.action}${e.note ? ` — ${e.note}` : ""}`).join("\n")
|
|
767
|
+
: "- No plan imports yet.";
|
|
768
|
+
const agentInboxLines = renderAgentInboxSummary(state);
|
|
769
|
+
|
|
770
|
+
const milestoneLines = (control.milestones || [])
|
|
771
|
+
.map((m) => {
|
|
772
|
+
const items = m.items.map((item) => `- ${item}`).join("\n");
|
|
773
|
+
return [`### [${m.date}] — ${m.title}`, items].join("\n");
|
|
276
774
|
})
|
|
277
775
|
.join("\n\n");
|
|
278
776
|
|
|
279
|
-
return [
|
|
280
|
-
`# ${t("doc.header.progress", { projectName: control.meta.projectName })}`,
|
|
281
|
-
"",
|
|
282
|
-
|
|
283
|
-
"",
|
|
777
|
+
return [
|
|
778
|
+
`# ${t("doc.header.progress", { projectName: control.meta.projectName })}`,
|
|
779
|
+
"",
|
|
780
|
+
AUTO_GENERATED_MARKER,
|
|
781
|
+
"",
|
|
782
|
+
`> ${t("doc.autogenerated")}`,
|
|
783
|
+
"",
|
|
284
784
|
`## ${t("doc.section.currentState")}`,
|
|
285
785
|
`- ${t("doc.label.phase")}: ${state.activePhase.label} (${state.activePhase.index}/${phases.length})`,
|
|
286
786
|
`- ${t("doc.label.blockers")}: ${blockersLabel}`,
|
|
287
|
-
`- ${t("doc.label.lastTest")}: ${CHECK_ICONS[lastTest.status]} ${lastTest.date || t("status.pending")}${lastTest.note ? ` — ${lastTest.note}` : ""}`,
|
|
787
|
+
`- ${t("doc.label.lastTest")}: ${CHECK_ICONS[lastTest.status]} ${lastTest.date || t("status.pending")}${lastTest.note ? ` — ${lastTest.note}` : ""}`,
|
|
288
788
|
`- ${t("doc.label.nextStepShort")}: ${nextStep}`,
|
|
289
789
|
`- ${t("doc.label.lastUpdate")}: ${(control.meta.updatedAt || "").slice(0, 10)}`,
|
|
290
790
|
"",
|
|
@@ -295,37 +795,72 @@ function renderProgress(control) {
|
|
|
295
795
|
`- ${t("doc.label.completedTasks")}: ${state.totals.completed}`,
|
|
296
796
|
`- ${t("doc.label.inProgressTasks")}: ${state.totals.inProgress}`,
|
|
297
797
|
`- ${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
|
-
|
|
302
|
-
|
|
798
|
+
`- ${t("doc.label.pendingTasks")}: ${state.totals.pending}`,
|
|
799
|
+
`- ${t("doc.label.blockedTasks")}: ${state.totals.blocked}`,
|
|
800
|
+
`- ${t("doc.label.awaitingUserTasks")}: ${state.totals.awaitingUser}`,
|
|
801
|
+
`- ${t("doc.label.agentInboxPending")}: ${state.totals.agentInboxPending}`,
|
|
802
|
+
"",
|
|
803
|
+
`### ${t("doc.section.activeTasks")}`,
|
|
804
|
+
activeLines,
|
|
303
805
|
"",
|
|
304
806
|
`### ${t("doc.section.reviewTasks")}`,
|
|
305
807
|
reviewLines,
|
|
306
808
|
"",
|
|
307
809
|
`### ${t("doc.section.activeBlockers")}`,
|
|
308
810
|
blockerLines,
|
|
309
|
-
"",
|
|
310
|
-
`### ${t("doc.section.recentActivity")}`,
|
|
311
|
-
historyLines,
|
|
312
|
-
"",
|
|
313
|
-
"
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
"",
|
|
317
|
-
|
|
811
|
+
"",
|
|
812
|
+
`### ${t("doc.section.recentActivity")}`,
|
|
813
|
+
historyLines,
|
|
814
|
+
"",
|
|
815
|
+
"### Agent Coordination",
|
|
816
|
+
agentInboxLines,
|
|
817
|
+
"",
|
|
818
|
+
"### Plan Activity",
|
|
819
|
+
planActivityLines,
|
|
820
|
+
"",
|
|
821
|
+
"---",
|
|
822
|
+
"",
|
|
823
|
+
`## ${t("doc.section.milestones")}`,
|
|
824
|
+
"",
|
|
825
|
+
milestoneLines,
|
|
318
826
|
].join("\n");
|
|
319
827
|
}
|
|
320
828
|
|
|
321
|
-
function renderFindings(control) {
|
|
322
|
-
const state = derive(control);
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
829
|
+
function renderFindings(control) {
|
|
830
|
+
const state = derive(control);
|
|
831
|
+
const planFindings = [];
|
|
832
|
+
if (state.hierarchyCycles.length) {
|
|
833
|
+
planFindings.push({
|
|
834
|
+
severity: "high",
|
|
835
|
+
title: "Hierarchy cycle detected",
|
|
836
|
+
detail: `Tasks with parent cycles: ${state.hierarchyCycles.join(", ")}`,
|
|
837
|
+
impact: "Parent rollups and tree rendering are degraded until hierarchy is corrected.",
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
if (state.phantomParents.length) {
|
|
841
|
+
planFindings.push({
|
|
842
|
+
severity: "medium",
|
|
843
|
+
title: "Tasks with missing parents",
|
|
844
|
+
detail: state.phantomParents.map((entry) => `${entry.taskId} -> ${entry.missingParentId}`).join("; "),
|
|
845
|
+
impact: "Affected tasks are promoted to roots in the derived tree.",
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
if (control.meta?.plans?.unresolvedConflicts) {
|
|
849
|
+
planFindings.push({
|
|
850
|
+
severity: "medium",
|
|
851
|
+
title: "Imported plans have unresolved conflicts",
|
|
852
|
+
detail: `${control.meta.plans.unresolvedConflicts} linked plan conflict(s) remain unresolved.`,
|
|
853
|
+
impact: "Applying previews with conflict policy 'abort' will be blocked.",
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
const openFindings = [...planFindings, ...state.openFindings];
|
|
858
|
+
const openLines = openFindings.length
|
|
859
|
+
? openFindings
|
|
860
|
+
.map((f) =>
|
|
861
|
+
`### [${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}`
|
|
862
|
+
)
|
|
863
|
+
.join("\n\n")
|
|
329
864
|
: t("doc.label.noFindings");
|
|
330
865
|
|
|
331
866
|
const resolvedLines = state.resolvedFindings.length
|
|
@@ -336,11 +871,13 @@ function renderFindings(control) {
|
|
|
336
871
|
.join("\n\n")
|
|
337
872
|
: t("doc.label.noResolvedFindings");
|
|
338
873
|
|
|
339
|
-
return [
|
|
340
|
-
`# ${t("doc.header.findings", { projectName: control.meta.projectName })}`,
|
|
341
|
-
"",
|
|
342
|
-
|
|
343
|
-
"",
|
|
874
|
+
return [
|
|
875
|
+
`# ${t("doc.header.findings", { projectName: control.meta.projectName })}`,
|
|
876
|
+
"",
|
|
877
|
+
AUTO_GENERATED_MARKER,
|
|
878
|
+
"",
|
|
879
|
+
`> ${t("doc.autogenerated")}`,
|
|
880
|
+
"",
|
|
344
881
|
`## ${t("doc.section.openFindings")}`,
|
|
345
882
|
"",
|
|
346
883
|
openLines,
|
|
@@ -371,44 +908,111 @@ function getDocDrift(root, control) {
|
|
|
371
908
|
.map(([name]) => name);
|
|
372
909
|
}
|
|
373
910
|
|
|
374
|
-
function syncDocs(root, control) {
|
|
911
|
+
function syncDocs(root, control, options = {}) {
|
|
375
912
|
const context = config.ensureContext(root);
|
|
376
913
|
const docs = buildDocMap(control);
|
|
377
914
|
const docFiles = config.docFilePaths(context);
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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 || "" });
|
|
915
|
+
const pairs = [
|
|
916
|
+
[docFiles.taskPlan, docs.taskPlan],
|
|
917
|
+
[docFiles.progress, docs.progress],
|
|
918
|
+
[docFiles.findings, docs.findings],
|
|
919
|
+
];
|
|
920
|
+
for (const [filePath, content] of pairs) {
|
|
921
|
+
if (!options.force && fs.existsSync(filePath)) {
|
|
922
|
+
const existing = fs.readFileSync(filePath, "utf8");
|
|
923
|
+
const isAutoGenerated = existing.includes(AUTO_GENERATED_MARKER) || existing.includes(t("doc.autogenerated"));
|
|
924
|
+
if (!isAutoGenerated && existing.trim()) {
|
|
925
|
+
console.error(t("control.docsOverwriteWarning", { file: path.basename(filePath) }));
|
|
926
|
+
continue;
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
writeText(filePath, `${content}\n`);
|
|
407
930
|
}
|
|
931
|
+
}
|
|
408
932
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
933
|
+
/* ── task management ── */
|
|
934
|
+
|
|
935
|
+
function taskHasChildren(controlState, taskId) {
|
|
936
|
+
return (controlState.tasks || []).some((task) => String(task.parentId || "").trim() === taskId);
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
function setTaskStatus(root, control, taskId, status, note, options = {}) {
|
|
940
|
+
const context = config.ensureContext(root);
|
|
941
|
+
const task = control.tasks.find((item) => item.id === taskId);
|
|
942
|
+
if (!task) throw new Error(t("cli.taskNotFound", { taskId }));
|
|
943
|
+
if (taskHasChildren(control, taskId)) {
|
|
944
|
+
throw new Error(`Task '${taskId}' is a parent task. Update its leaf tasks instead; parent status is rolled up automatically.`);
|
|
945
|
+
}
|
|
946
|
+
const nextStatus = String(status || "").trim();
|
|
947
|
+
if (!nextStatus) throw new Error(t("cli.actionNotSupported", { action: status }));
|
|
948
|
+
if (task.status === nextStatus) {
|
|
949
|
+
setTaskExecutionContext(task, {
|
|
950
|
+
owner: options.owner,
|
|
951
|
+
actor: options.actor || "system",
|
|
952
|
+
source: options.source || "system",
|
|
953
|
+
sessionId: Object.prototype.hasOwnProperty.call(options, "sessionId") ? options.sessionId : undefined,
|
|
954
|
+
verificationPending: task.execution?.verificationPending,
|
|
955
|
+
awaitingUserConfirmation: task.execution?.awaitingUserConfirmation,
|
|
956
|
+
});
|
|
957
|
+
config.saveControl(context, control);
|
|
958
|
+
syncDocs(context, control);
|
|
959
|
+
refreshRepoRuntime(context, { quiet: true });
|
|
960
|
+
return task;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
applyTaskStateTransition(control, task, {
|
|
964
|
+
status: nextStatus,
|
|
965
|
+
action: options.historyAction || "edit",
|
|
966
|
+
note,
|
|
967
|
+
}, options);
|
|
968
|
+
|
|
969
|
+
config.saveControl(context, control);
|
|
970
|
+
syncDocs(context, control);
|
|
971
|
+
refreshRepoRuntime(context, { quiet: true });
|
|
972
|
+
return task;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
function updateTask(root, control, action, taskId, note, options = {}) {
|
|
976
|
+
const context = config.ensureContext(root);
|
|
977
|
+
const task = control.tasks.find((item) => item.id === taskId);
|
|
978
|
+
if (!task) throw new Error(t("cli.taskNotFound", { taskId }));
|
|
979
|
+
|
|
980
|
+
const actionMap = { start: "in_progress", review: "in_review", complete: "completed", done: "completed", block: "blocked", pending: "pending", cancel: "cancelled" };
|
|
981
|
+
|
|
982
|
+
if (action === "note") {
|
|
983
|
+
task.history = task.history || [];
|
|
984
|
+
task.history.push({ at: nowIso(), action: "note", note: note || t("cli.emptyNote") });
|
|
985
|
+
setTaskExecutionContext(task, {
|
|
986
|
+
owner: options.owner,
|
|
987
|
+
actor: options.actor || "agent",
|
|
988
|
+
source: options.source || "cli",
|
|
989
|
+
sessionId: Object.prototype.hasOwnProperty.call(options, "sessionId") ? options.sessionId : undefined,
|
|
990
|
+
});
|
|
991
|
+
} else {
|
|
992
|
+
const nextStatus = actionMap[action];
|
|
993
|
+
if (!nextStatus) throw new Error(t("cli.actionNotSupported", { action }));
|
|
994
|
+
if (taskHasChildren(control, taskId)) {
|
|
995
|
+
throw new Error(`Task '${taskId}' is a parent task. Update its leaf tasks instead; parent status is rolled up automatically.`);
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
if (task.status === nextStatus) {
|
|
999
|
+
console.log(t("control.taskAlreadyStatus", { taskId, status: nextStatus }));
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
applyTaskStateTransition(control, task, {
|
|
1003
|
+
status: nextStatus,
|
|
1004
|
+
action,
|
|
1005
|
+
note,
|
|
1006
|
+
}, {
|
|
1007
|
+
...options,
|
|
1008
|
+
actor: options.actor || "agent",
|
|
1009
|
+
source: options.source || "cli",
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
config.saveControl(context, control);
|
|
1014
|
+
syncDocs(context, control);
|
|
1015
|
+
refreshRepoRuntime(context, { quiet: true });
|
|
412
1016
|
}
|
|
413
1017
|
|
|
414
1018
|
/* ── CLI commands ── */
|
|
@@ -422,103 +1026,190 @@ function initLocale(root) {
|
|
|
422
1026
|
}
|
|
423
1027
|
}
|
|
424
1028
|
|
|
425
|
-
function cmdStatus(root) {
|
|
426
|
-
const context = config.ensureContext(root);
|
|
427
|
-
initLocale(context);
|
|
428
|
-
const control = config.loadControl(context);
|
|
429
|
-
const state = derive(control);
|
|
1029
|
+
function cmdStatus(root) {
|
|
1030
|
+
const context = config.ensureContext(root);
|
|
1031
|
+
initLocale(context);
|
|
1032
|
+
const control = config.loadControl(context);
|
|
1033
|
+
const state = derive(control);
|
|
430
1034
|
const phases = config.getPhases(control);
|
|
431
1035
|
const repo = refreshRepoRuntime(context, { quiet: true });
|
|
432
1036
|
const drift = getDocDrift(context, control);
|
|
433
1037
|
const envAudit = env.auditEnvironment(context, control);
|
|
434
1038
|
|
|
435
|
-
console.log(t("cli.status.title", { projectName: control.meta.projectName }));
|
|
436
|
-
console.log(t("cli.status.focus", { focus: control.meta.currentFocus }));
|
|
437
|
-
console.log(t("cli.status.activePhase", { phaseId: state.activePhase.id, phaseLabel: state.activePhase.label }));
|
|
438
|
-
console.log(
|
|
439
|
-
if (context.layout === "split") {
|
|
440
|
-
console.log(
|
|
441
|
-
console.log(
|
|
442
|
-
}
|
|
443
|
-
if (control.meta?.opera?.bootstrap?.status) {
|
|
444
|
-
console.log(t("cli.status.bootstrap", {
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
}
|
|
448
|
-
if (control.meta.opera.bootstrap.
|
|
449
|
-
console.log(
|
|
450
|
-
}
|
|
451
|
-
|
|
1039
|
+
console.log(t("cli.status.title", { projectName: control.meta.projectName }));
|
|
1040
|
+
console.log(t("cli.status.focus", { focus: control.meta.currentFocus }));
|
|
1041
|
+
console.log(t("cli.status.activePhase", { phaseId: state.activePhase.id, phaseLabel: state.activePhase.label }));
|
|
1042
|
+
console.log(t("cli.status.layout", { layout: context.layout, workspace: context.workspaceRoot }));
|
|
1043
|
+
if (context.layout === "split") {
|
|
1044
|
+
console.log(t("cli.status.appRoot", { path: context.appRoot }));
|
|
1045
|
+
console.log(t("cli.status.opsRoot", { path: context.opsRoot }));
|
|
1046
|
+
}
|
|
1047
|
+
if (control.meta?.opera?.bootstrap?.status) {
|
|
1048
|
+
console.log(t("cli.status.bootstrap", {
|
|
1049
|
+
status: formatBootstrapStatus(control.meta.opera.bootstrap.status),
|
|
1050
|
+
locale: config.getLocale(control),
|
|
1051
|
+
}));
|
|
1052
|
+
if (control.meta.opera.bootstrap.mode) {
|
|
1053
|
+
console.log(t("cli.status.bootstrapMode", { value: formatBootstrapMode(control.meta.opera.bootstrap.mode) }));
|
|
1054
|
+
}
|
|
1055
|
+
if (control.meta.opera.bootstrap.routeReason) {
|
|
1056
|
+
console.log(t("cli.status.bootstrapReason", { value: formatBootstrapReason(control.meta.opera.bootstrap.routeReason) }));
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
452
1059
|
console.log(t("cli.status.tasks", {
|
|
453
1060
|
completed: state.totals.completed, inProgress: state.totals.inProgress,
|
|
454
1061
|
inReview: state.totals.inReview, pending: state.totals.pending, blocked: state.totals.blocked,
|
|
455
1062
|
}));
|
|
456
1063
|
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 })}`);
|
|
488
|
-
}
|
|
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"}`);
|
|
1064
|
+
console.log(t("cli.status.readyTasks"));
|
|
1065
|
+
if (state.readyTasks.length) {
|
|
1066
|
+
state.readyTasks.slice(0, 5).forEach((task) => console.log(renderTask(task, phases, { cli: true })));
|
|
1067
|
+
} else {
|
|
1068
|
+
console.log(t("cli.status.noReadyTasks"));
|
|
1069
|
+
}
|
|
492
1070
|
console.log("");
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
1071
|
+
console.log(t("cli.status.blockers"));
|
|
1072
|
+
if (state.blockers.length) {
|
|
1073
|
+
state.blockers.forEach((task) => console.log(renderTask(task, phases, { cli: true })));
|
|
1074
|
+
} else {
|
|
1075
|
+
console.log(t("cli.status.noBlockers"));
|
|
1076
|
+
}
|
|
1077
|
+
console.log("");
|
|
1078
|
+
console.log(t("cli.status.decisions"));
|
|
1079
|
+
if ((control.decisionsPending || []).length) {
|
|
1080
|
+
control.decisionsPending.forEach((d) => console.log(`- ${d.title} (${d.owner}) — ${d.impact}`));
|
|
1081
|
+
} else {
|
|
1082
|
+
console.log(`- ${t("cli.status.noDecisions")}`);
|
|
1083
|
+
}
|
|
1084
|
+
console.log("");
|
|
1085
|
+
console.log(t("cli.status.agentInbox"));
|
|
1086
|
+
if (state.agentInbox.pending.length) {
|
|
1087
|
+
state.agentInbox.pending.slice(0, 5).forEach((item) => console.log(`- [${item.kind}] ${item.taskId || "general"} — ${item.message}`));
|
|
1088
|
+
} else {
|
|
1089
|
+
console.log(`- ${t("cli.status.noAgentInbox")}`);
|
|
1090
|
+
}
|
|
1091
|
+
console.log("");
|
|
1092
|
+
console.log(t("cli.status.repo"));
|
|
1093
|
+
if (!repo.available) {
|
|
1094
|
+
console.log(`- ${t("cli.status.gitState", { state: t("cli.status.gitNotInitialized") })}`);
|
|
1095
|
+
console.log(`- ${t("cli.status.gitAction")}`);
|
|
1096
|
+
} else {
|
|
1097
|
+
const branchLabel = repo.state === "detached" ? t("cli.status.branchDetached") : repo.branch;
|
|
1098
|
+
const treeStatus = repo.clean
|
|
1099
|
+
? t("cli.status.treeClean")
|
|
1100
|
+
: t("cli.status.treeDirty", { staged: repo.staged, unstaged: repo.unstaged, untracked: repo.untracked });
|
|
1101
|
+
console.log(`- ${t("cli.status.branch", { branch: branchLabel, treeStatus })}`);
|
|
1102
|
+
if (repo.lastCommit) {
|
|
1103
|
+
console.log(`- ${t("cli.status.lastCommit", { hash: repo.lastCommit.shortHash, subject: repo.lastCommit.subject })}`);
|
|
1104
|
+
}
|
|
1105
|
+
if (!repo.hasUpstream) {
|
|
1106
|
+
console.log(`- ${t("cli.status.noUpstream")}`);
|
|
1107
|
+
} else if (repo.ahead || repo.behind) {
|
|
1108
|
+
console.log(`- ${t("cli.status.divergence", { ahead: repo.ahead, behind: repo.behind })}`);
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
console.log(`- ${t("cli.status.runtime", { path: path.relative(context.workspaceRoot, config.runtimeFilePath(context)) })}`);
|
|
1112
|
+
console.log(`- ${t("cli.status.envPresent", { value: listOrNone(envAudit.presentKeys) })}`);
|
|
1113
|
+
console.log(`- ${t("cli.status.envMissing", { value: listOrNone(envAudit.missingKeys) })}`);
|
|
1114
|
+
console.log("");
|
|
1115
|
+
const syncStatus = drift.length
|
|
1116
|
+
? t("cli.status.docsSyncedNo", { files: drift.join(", ") })
|
|
1117
|
+
: t("cli.status.docsSyncedYes");
|
|
1118
|
+
console.log(t("cli.status.docsSynced", { status: syncStatus }));
|
|
1119
|
+
try {
|
|
1120
|
+
const quality = require("./quality");
|
|
1121
|
+
const snapshot = quality.buildQualitySnapshot(context, control);
|
|
1122
|
+
console.log("");
|
|
1123
|
+
console.log(t("quality.statusBlock.title"));
|
|
1124
|
+
console.log(`- ${t("quality.statusBlock.status")}: ${quality.formatQualityStatus(snapshot.report.summary.overallStatus)}`);
|
|
1125
|
+
console.log(`- ${t("quality.statusBlock.phaseReadiness")}: ${quality.formatQualityStatus(snapshot.phaseReadiness.status)}`);
|
|
1126
|
+
console.log(`- ${t("quality.statusBlock.releaseReadiness")}: ${quality.formatQualityStatus(snapshot.releaseReadiness.status)}`);
|
|
1127
|
+
if (snapshot.releaseReadiness.blockers.length) {
|
|
1128
|
+
snapshot.releaseReadiness.blockers.slice(0, 3).forEach((item) => {
|
|
1129
|
+
console.log(`- ${t("quality.statusBlock.blocker")}: ${item.id} — ${item.message}`);
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
} catch (_error) {
|
|
1133
|
+
// Quality is complementary; status should still work if it cannot be computed.
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
function cmdNext(root) {
|
|
1138
|
+
const context = config.ensureContext(root);
|
|
1139
|
+
initLocale(context);
|
|
1140
|
+
const control = config.loadControl(context);
|
|
1141
|
+
const state = derive(control);
|
|
1142
|
+
const printTasks = (tasks) => {
|
|
1143
|
+
tasks.forEach((task, i) => {
|
|
1144
|
+
console.log(`${i + 1}. ${task.title}`);
|
|
1145
|
+
console.log(` id: ${task.id}`);
|
|
1146
|
+
console.log(` ${t("cli.next.phase")}: ${task.phase} · ${t("cli.next.priority")}: ${task.priority} · ${t("cli.next.stream")}: ${task.stream}`);
|
|
1147
|
+
if (task.summary) console.log(` ${t("cli.next.summary")}: ${task.summary}`);
|
|
1148
|
+
});
|
|
1149
|
+
};
|
|
1150
|
+
|
|
1151
|
+
if (state.circularDeps.length) {
|
|
1152
|
+
console.log(t("control.circularDependency", { taskIds: state.circularDeps.join(", ") }));
|
|
1153
|
+
}
|
|
1154
|
+
const active = state.activeTasks.slice(0, 5);
|
|
1155
|
+
const ready = state.readyTasks.slice(0, 10);
|
|
1156
|
+
if (active.length) {
|
|
1157
|
+
console.log(t("cli.next.activeTasks"));
|
|
1158
|
+
printTasks(active);
|
|
1159
|
+
if (ready.length) {
|
|
1160
|
+
console.log("");
|
|
1161
|
+
console.log(t("cli.next.readyQueue"));
|
|
1162
|
+
printTasks(ready.slice(0, 5));
|
|
1163
|
+
}
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
if (!ready.length) {
|
|
1167
|
+
if (state.projectCompleted) {
|
|
1168
|
+
console.log(t("cli.noReadyTasks.allDone"));
|
|
1169
|
+
} else if (state.blockers.length) {
|
|
1170
|
+
console.log(t("cli.noReadyTasks.blocked", { count: state.blockers.length }));
|
|
1171
|
+
for (const task of state.blockers.slice(0, 5)) {
|
|
1172
|
+
console.log(` - ${task.id}: ${task.blocker || task.title}`);
|
|
1173
|
+
}
|
|
1174
|
+
} else {
|
|
1175
|
+
console.log(t("cli.noReadyTasks"));
|
|
1176
|
+
}
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1179
|
+
printTasks(ready);
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
function cmdSync(root, args) {
|
|
1183
|
+
const context = config.ensureContext(root);
|
|
1184
|
+
initLocale(context);
|
|
1185
|
+
const control = config.loadControl(context);
|
|
1186
|
+
if (control.__trackopsMigrated) {
|
|
1187
|
+
config.saveControl(context, control);
|
|
1188
|
+
}
|
|
1189
|
+
if ((args || []).includes("--dry-run")) {
|
|
1190
|
+
const drift = getDocDrift(context, control);
|
|
1191
|
+
if (drift.length) {
|
|
1192
|
+
console.log(t("cli.sync.dryRunWouldUpdate", { files: drift.join(", ") }));
|
|
1193
|
+
} else {
|
|
1194
|
+
console.log(t("cli.sync.dryRunInSync"));
|
|
1195
|
+
}
|
|
1196
|
+
return;
|
|
1197
|
+
}
|
|
1198
|
+
const state = derive(control);
|
|
1199
|
+
if (state.activePhase && control.meta.focusPhase !== state.activePhase.id) {
|
|
1200
|
+
control.meta.focusPhase = state.activePhase.id;
|
|
1201
|
+
config.saveControl(context, control);
|
|
507
1202
|
}
|
|
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
|
-
|
|
516
|
-
function cmdSync(root) {
|
|
517
|
-
const context = config.ensureContext(root);
|
|
518
|
-
initLocale(context);
|
|
519
|
-
const control = config.loadControl(context);
|
|
520
1203
|
env.syncEnvironment(context, control);
|
|
521
|
-
|
|
1204
|
+
if (config.isOperaInstalled(control)) {
|
|
1205
|
+
const bootstrap = require("./opera-bootstrap");
|
|
1206
|
+
const result = bootstrap.revalidateContract(context, control);
|
|
1207
|
+
if (result.changed) {
|
|
1208
|
+
console.log(t("control.contractStale"));
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
const force = (args || []).includes("--force");
|
|
1212
|
+
syncDocs(context, control, { force });
|
|
522
1213
|
refreshRepoRuntime(context, { quiet: true });
|
|
523
1214
|
console.log(t("cli.docsSynced"));
|
|
524
1215
|
}
|
|
@@ -530,12 +1221,12 @@ function cmdRefreshRepo(root, args) {
|
|
|
530
1221
|
function cmdTask(root, args) {
|
|
531
1222
|
const context = config.ensureContext(root);
|
|
532
1223
|
initLocale(context);
|
|
533
|
-
const [action, taskId, ...noteParts] = args || [];
|
|
534
|
-
if (!action || !taskId) throw new Error(t("cli.mustProvideActionAndId"));
|
|
535
|
-
const control = config.loadControl(context);
|
|
536
|
-
updateTask(context, control, action, taskId, noteParts.join(" ").trim());
|
|
537
|
-
console.log(t("cli.taskUpdated", { taskId, action }));
|
|
538
|
-
}
|
|
1224
|
+
const [action, taskId, ...noteParts] = args || [];
|
|
1225
|
+
if (!action || !taskId) throw new Error(t("cli.mustProvideActionAndId"));
|
|
1226
|
+
const control = config.loadControl(context);
|
|
1227
|
+
updateTask(context, control, action, taskId, noteParts.join(" ").trim(), { actor: "agent", source: "cli" });
|
|
1228
|
+
console.log(t("cli.taskUpdated", { taskId, action }));
|
|
1229
|
+
}
|
|
539
1230
|
|
|
540
1231
|
function cmdInstallHooks(root) {
|
|
541
1232
|
const context = config.ensureContext(root);
|
|
@@ -581,10 +1272,14 @@ function cmdHelp() {
|
|
|
581
1272
|
console.log(` ${t("cli.help.register.desc")}`);
|
|
582
1273
|
console.log(" projects");
|
|
583
1274
|
console.log(` ${t("cli.help.projects.desc")}`);
|
|
584
|
-
console.log(" task <action> <id> [note]");
|
|
585
|
-
console.log(` ${t("cli.help.task.desc")}`);
|
|
586
|
-
console.log("
|
|
587
|
-
console.log(` ${t("cli.help.
|
|
1275
|
+
console.log(" task <action> <id> [note]");
|
|
1276
|
+
console.log(` ${t("cli.help.task.desc")}`);
|
|
1277
|
+
console.log(" plan scan|import|show|apply|list|unlink");
|
|
1278
|
+
console.log(` ${t("cli.help.plan.desc")}`);
|
|
1279
|
+
console.log(" quality status|verify|phase-readiness|release-readiness|promote-readiness|waiver");
|
|
1280
|
+
console.log(` ${t("cli.help.quality.desc")}`);
|
|
1281
|
+
console.log(" opera install|bootstrap|handoff|status|configure|upgrade");
|
|
1282
|
+
console.log(` ${t("cli.help.opera.desc")}`);
|
|
588
1283
|
console.log(` ${t("cli.help.opera.upgradeHint")}`);
|
|
589
1284
|
console.log(" locale get|set [es|en]");
|
|
590
1285
|
console.log(` ${t("cli.help.locale.desc")}`);
|
|
@@ -593,11 +1288,14 @@ function cmdHelp() {
|
|
|
593
1288
|
console.log(" skill install|list|remove|catalog <name>");
|
|
594
1289
|
console.log(` ${t("cli.help.skill.desc")}`);
|
|
595
1290
|
console.log(" help");
|
|
596
|
-
console.log(` ${t("cli.help.help.desc")}`);
|
|
597
|
-
console.log("");
|
|
598
|
-
console.log(t("cli.help.
|
|
599
|
-
console.log(` ${t("cli.help.
|
|
600
|
-
console.log(
|
|
1291
|
+
console.log(` ${t("cli.help.help.desc")}`);
|
|
1292
|
+
console.log("");
|
|
1293
|
+
console.log(t("cli.help.globalFlags"));
|
|
1294
|
+
console.log(` ${t("cli.help.globalFlags.line1")}`);
|
|
1295
|
+
console.log("");
|
|
1296
|
+
console.log(t("cli.help.globalWorkflow"));
|
|
1297
|
+
console.log(` ${t("cli.help.globalWorkflow.line1")}`);
|
|
1298
|
+
console.log(` ${t("cli.help.globalWorkflow.line2")}`);
|
|
601
1299
|
}
|
|
602
1300
|
|
|
603
1301
|
/* ── project-scoped API (used by server) ── */
|
|
@@ -611,20 +1309,22 @@ function forProject(root) {
|
|
|
611
1309
|
derive,
|
|
612
1310
|
buildDocMap,
|
|
613
1311
|
getDocDrift: (ctrl) => getDocDrift(context, ctrl),
|
|
614
|
-
syncDocs: (ctrl) => syncDocs(context, ctrl),
|
|
615
|
-
updateTask: (ctrl, action, id, note) => updateTask(context, ctrl, action, id, note),
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
1312
|
+
syncDocs: (ctrl) => syncDocs(context, ctrl),
|
|
1313
|
+
updateTask: (ctrl, action, id, note, options) => updateTask(context, ctrl, action, id, note, options),
|
|
1314
|
+
setTaskStatus: (ctrl, id, status, note, options) => setTaskStatus(context, ctrl, id, status, note, options),
|
|
1315
|
+
getRepoSnapshot: () => getRepoSnapshot(context),
|
|
1316
|
+
refreshRepoRuntime: (opts) => refreshRepoRuntime(context, opts),
|
|
1317
|
+
getPhases: (ctrl) => config.getPhases(ctrl),
|
|
619
1318
|
getLocale: (ctrl) => config.getLocale(ctrl),
|
|
620
1319
|
statusLabel,
|
|
621
1320
|
context,
|
|
622
1321
|
};
|
|
623
1322
|
}
|
|
624
1323
|
|
|
625
|
-
module.exports = {
|
|
626
|
-
buildDocMap, derive, getDocDrift, getRepoSnapshot, refreshRepoRuntime, syncDocs, updateTask,
|
|
627
|
-
|
|
1324
|
+
module.exports = {
|
|
1325
|
+
buildDocMap, derive, getDocDrift, getRepoSnapshot, refreshRepoRuntime, syncDocs, updateTask,
|
|
1326
|
+
setTaskStatus, ensureAgentInbox, upsertAgentInstruction, resolveAgentInstructions,
|
|
1327
|
+
forProject, statusLabel, renderTask, getPhaseInfo,
|
|
628
1328
|
cmdStatus, cmdNext, cmdSync, cmdRefreshRepo, cmdTask, cmdInstallHooks, cmdHelp,
|
|
629
1329
|
PRIORITY_ORDER, STATUS_ORDER, STATUS_ICONS, CHECK_ICONS,
|
|
630
1330
|
};
|