trackops 2.0.6 → 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 +295 -701
- package/bin/trackops.js +24 -16
- package/lib/config.js +265 -58
- package/lib/control.js +830 -292
- package/lib/init.js +46 -16
- package/lib/opera-bootstrap.js +85 -45
- package/lib/opera-phase-dod.js +485 -0
- package/lib/opera.js +8 -5
- 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/locales/en.json +249 -15
- package/locales/es.json +249 -15
- package/package.json +6 -5
- package/scripts/quality-unit-tests.js +130 -0
- package/scripts/smoke-tests.js +357 -57
- 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
|
@@ -20,11 +20,12 @@ const STATUS_ICONS = {
|
|
|
20
20
|
cancelled: "\uD83D\uDDD1\uFE0F",
|
|
21
21
|
};
|
|
22
22
|
const CHECK_ICONS = {
|
|
23
|
-
pass: "\u2705",
|
|
24
|
-
warn: "\u26A0\uFE0F",
|
|
25
|
-
fail: "\u274C",
|
|
26
|
-
pending: "\u23F3",
|
|
27
|
-
};
|
|
23
|
+
pass: "\u2705",
|
|
24
|
+
warn: "\u26A0\uFE0F",
|
|
25
|
+
fail: "\u274C",
|
|
26
|
+
pending: "\u23F3",
|
|
27
|
+
};
|
|
28
|
+
const AUTO_GENERATED_MARKER = "<!-- trackops:auto-generated -->";
|
|
28
29
|
|
|
29
30
|
/* ── helpers ── */
|
|
30
31
|
|
|
@@ -78,6 +79,200 @@ function listOrNone(items) {
|
|
|
78
79
|
return values.length ? values.join(", ") : t("locale.none");
|
|
79
80
|
}
|
|
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
|
+
|
|
81
276
|
/* ── repo snapshot ── */
|
|
82
277
|
|
|
83
278
|
function getRepoSnapshot(contextOrRoot) {
|
|
@@ -165,118 +360,299 @@ function refreshRepoRuntime(root, options = {}) {
|
|
|
165
360
|
|
|
166
361
|
/* ── derive ── */
|
|
167
362
|
|
|
168
|
-
function getPhaseInfo(phaseId, phases) {
|
|
169
|
-
return phases.find((p) => p.id === phaseId) || { id: phaseId, label: phaseId, index: 99 };
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function compareTasks(a, b, phases) {
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
inStack.
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
.
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
+
}
|
|
268
613
|
|
|
614
|
+
/* ── render ── */
|
|
615
|
+
|
|
269
616
|
function renderTask(task, phases, options = {}) {
|
|
270
617
|
const phase = getPhaseInfo(task.phase, phases);
|
|
271
618
|
const detail = task.blocker || task.summary || "";
|
|
272
619
|
const detailSuffix = detail ? ` — ${detail}` : "";
|
|
273
620
|
const token = options.cli ? fmt.statusToken(task.status) : STATUS_ICONS[task.status];
|
|
274
|
-
|
|
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}`;
|
|
275
629
|
}
|
|
276
|
-
|
|
277
|
-
function
|
|
278
|
-
const
|
|
279
|
-
|
|
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);
|
|
280
656
|
const blockersLabel = state.blockers.length
|
|
281
657
|
? state.blockers.map((t) => t.title).join("; ")
|
|
282
658
|
: t("doc.label.noBlockers");
|
|
@@ -285,17 +661,21 @@ function renderTaskPlan(control) {
|
|
|
285
661
|
const externalDecisions = (control.decisionsPending || []).length
|
|
286
662
|
? control.decisionsPending.map((d) => `- [${d.owner}] ${d.title} — ${d.impact}`).join("\n")
|
|
287
663
|
: `- ${t("doc.label.noDecisions")}`;
|
|
288
|
-
|
|
289
|
-
const readyTasks = state.readyTasks.length
|
|
290
|
-
? state.readyTasks.slice(0, 6).map((task) => renderTask(task, phases)).join("\n")
|
|
291
|
-
: `- ${t("doc.label.noReadyTasks")}`;
|
|
292
|
-
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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")}`;
|
|
299
679
|
|
|
300
680
|
const phaseStatus = phase.id === state.activePhase.id
|
|
301
681
|
? t("doc.label.phaseActive")
|
|
@@ -312,29 +692,44 @@ function renderTaskPlan(control) {
|
|
|
312
692
|
].join("\n");
|
|
313
693
|
}).join("\n\n---\n\n");
|
|
314
694
|
|
|
315
|
-
return [
|
|
316
|
-
`# ${t("doc.header.taskPlan", { projectName: control.meta.projectName })}`,
|
|
317
|
-
"",
|
|
318
|
-
|
|
319
|
-
"",
|
|
695
|
+
return [
|
|
696
|
+
`# ${t("doc.header.taskPlan", { projectName: control.meta.projectName })}`,
|
|
697
|
+
"",
|
|
698
|
+
AUTO_GENERATED_MARKER,
|
|
699
|
+
"",
|
|
700
|
+
`> ${t("doc.autogenerated")}`,
|
|
701
|
+
"",
|
|
320
702
|
`## ${t("doc.section.operativeState")}`,
|
|
321
703
|
`- ${t("doc.label.activePhase")}: ${state.activePhase.id} — ${state.activePhase.label} (${state.activePhase.index}/${phases.length})`,
|
|
322
704
|
`- ${t("doc.label.currentFocus")}: ${control.meta.currentFocus}`,
|
|
323
|
-
`- ${t("doc.label.deliveryTarget")}: ${control.meta.deliveryTarget}`,
|
|
324
|
-
`- ${t("doc.label.blockers")}: ${blockersLabel}`,
|
|
325
|
-
`- ${t("doc.label.nextStep")}: ${nextStep}`,
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
"
|
|
334
|
-
"",
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
}
|
|
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
|
+
}
|
|
338
733
|
|
|
339
734
|
function renderProgress(control) {
|
|
340
735
|
const phases = config.getPhases(control);
|
|
@@ -344,14 +739,17 @@ function renderProgress(control) {
|
|
|
344
739
|
: t("doc.label.noBlockers");
|
|
345
740
|
const nextStep = state.nextTask ? state.nextTask.title : t("doc.label.noOpenTasks");
|
|
346
741
|
const lastTest = (control.checks || {}).lastTest || { status: "pending" };
|
|
347
|
-
const latestHistory = state.tasks
|
|
348
|
-
.flatMap((task) => (task.history || []).map((entry) => ({ ...entry, taskId: task.id, taskTitle: task.title })))
|
|
349
|
-
.sort((a, b) => (a.at < b.at ? 1 : -1))
|
|
350
|
-
.slice(0, 8);
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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")}`;
|
|
355
753
|
|
|
356
754
|
const reviewLines = state.reviewTasks.length
|
|
357
755
|
? state.reviewTasks.map((task) => renderTask(task, phases)).join("\n")
|
|
@@ -360,23 +758,29 @@ function renderProgress(control) {
|
|
|
360
758
|
const blockerLines = state.blockers.length
|
|
361
759
|
? state.blockers.map((task) => renderTask(task, phases)).join("\n")
|
|
362
760
|
: `- ${t("doc.label.noActiveBlockers")}`;
|
|
363
|
-
|
|
364
|
-
const historyLines = latestHistory.length
|
|
365
|
-
? latestHistory.map((e) => `- [${e.at.slice(0, 10)}] \`${e.taskId}\` ${e.action}${e.note ? ` — ${e.note}` : ""}`).join("\n")
|
|
366
|
-
: `- ${t("doc.label.noHistory")}`;
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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");
|
|
372
774
|
})
|
|
373
775
|
.join("\n\n");
|
|
374
776
|
|
|
375
|
-
return [
|
|
376
|
-
`# ${t("doc.header.progress", { projectName: control.meta.projectName })}`,
|
|
377
|
-
"",
|
|
378
|
-
|
|
379
|
-
"",
|
|
777
|
+
return [
|
|
778
|
+
`# ${t("doc.header.progress", { projectName: control.meta.projectName })}`,
|
|
779
|
+
"",
|
|
780
|
+
AUTO_GENERATED_MARKER,
|
|
781
|
+
"",
|
|
782
|
+
`> ${t("doc.autogenerated")}`,
|
|
783
|
+
"",
|
|
380
784
|
`## ${t("doc.section.currentState")}`,
|
|
381
785
|
`- ${t("doc.label.phase")}: ${state.activePhase.label} (${state.activePhase.index}/${phases.length})`,
|
|
382
786
|
`- ${t("doc.label.blockers")}: ${blockersLabel}`,
|
|
@@ -391,37 +795,72 @@ function renderProgress(control) {
|
|
|
391
795
|
`- ${t("doc.label.completedTasks")}: ${state.totals.completed}`,
|
|
392
796
|
`- ${t("doc.label.inProgressTasks")}: ${state.totals.inProgress}`,
|
|
393
797
|
`- ${t("doc.label.inReviewTasks")}: ${state.totals.inReview}`,
|
|
394
|
-
`- ${t("doc.label.pendingTasks")}: ${state.totals.pending}`,
|
|
395
|
-
`- ${t("doc.label.blockedTasks")}: ${state.totals.blocked}`,
|
|
396
|
-
""
|
|
397
|
-
|
|
398
|
-
|
|
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,
|
|
399
805
|
"",
|
|
400
806
|
`### ${t("doc.section.reviewTasks")}`,
|
|
401
807
|
reviewLines,
|
|
402
808
|
"",
|
|
403
809
|
`### ${t("doc.section.activeBlockers")}`,
|
|
404
810
|
blockerLines,
|
|
405
|
-
"",
|
|
406
|
-
`### ${t("doc.section.recentActivity")}`,
|
|
407
|
-
historyLines,
|
|
408
|
-
"",
|
|
409
|
-
"
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
"",
|
|
413
|
-
|
|
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,
|
|
414
826
|
].join("\n");
|
|
415
827
|
}
|
|
416
828
|
|
|
417
|
-
function renderFindings(control) {
|
|
418
|
-
const state = derive(control);
|
|
419
|
-
const
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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")
|
|
425
864
|
: t("doc.label.noFindings");
|
|
426
865
|
|
|
427
866
|
const resolvedLines = state.resolvedFindings.length
|
|
@@ -432,11 +871,13 @@ function renderFindings(control) {
|
|
|
432
871
|
.join("\n\n")
|
|
433
872
|
: t("doc.label.noResolvedFindings");
|
|
434
873
|
|
|
435
|
-
return [
|
|
436
|
-
`# ${t("doc.header.findings", { projectName: control.meta.projectName })}`,
|
|
437
|
-
"",
|
|
438
|
-
|
|
439
|
-
"",
|
|
874
|
+
return [
|
|
875
|
+
`# ${t("doc.header.findings", { projectName: control.meta.projectName })}`,
|
|
876
|
+
"",
|
|
877
|
+
AUTO_GENERATED_MARKER,
|
|
878
|
+
"",
|
|
879
|
+
`> ${t("doc.autogenerated")}`,
|
|
880
|
+
"",
|
|
440
881
|
`## ${t("doc.section.openFindings")}`,
|
|
441
882
|
"",
|
|
442
883
|
openLines,
|
|
@@ -477,51 +918,101 @@ function syncDocs(root, control, options = {}) {
|
|
|
477
918
|
[docFiles.findings, docs.findings],
|
|
478
919
|
];
|
|
479
920
|
for (const [filePath, content] of pairs) {
|
|
480
|
-
if (!options.force && fs.existsSync(filePath)) {
|
|
481
|
-
const existing = fs.readFileSync(filePath, "utf8");
|
|
482
|
-
const isAutoGenerated = existing.includes(t("doc.autogenerated"));
|
|
483
|
-
if (!isAutoGenerated && existing.trim()) {
|
|
484
|
-
console.
|
|
485
|
-
continue;
|
|
486
|
-
}
|
|
487
|
-
}
|
|
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
|
+
}
|
|
488
929
|
writeText(filePath, `${content}\n`);
|
|
489
930
|
}
|
|
490
931
|
}
|
|
491
932
|
|
|
492
|
-
/* ── task management ── */
|
|
493
|
-
|
|
494
|
-
function
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
const
|
|
500
|
-
|
|
501
|
-
if (
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
task
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
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 });
|
|
525
1016
|
}
|
|
526
1017
|
|
|
527
1018
|
/* ── CLI commands ── */
|
|
@@ -535,11 +1026,11 @@ function initLocale(root) {
|
|
|
535
1026
|
}
|
|
536
1027
|
}
|
|
537
1028
|
|
|
538
|
-
function cmdStatus(root) {
|
|
539
|
-
const context = config.ensureContext(root);
|
|
540
|
-
initLocale(context);
|
|
541
|
-
const control = config.loadControl(context);
|
|
542
|
-
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);
|
|
543
1034
|
const phases = config.getPhases(control);
|
|
544
1035
|
const repo = refreshRepoRuntime(context, { quiet: true });
|
|
545
1036
|
const drift = getDocDrift(context, control);
|
|
@@ -583,12 +1074,19 @@ function cmdStatus(root) {
|
|
|
583
1074
|
} else {
|
|
584
1075
|
console.log(t("cli.status.noBlockers"));
|
|
585
1076
|
}
|
|
586
|
-
console.log("");
|
|
587
|
-
console.log(t("cli.status.decisions"));
|
|
588
|
-
if ((control.decisionsPending || []).length) {
|
|
589
|
-
control.decisionsPending.forEach((d) => console.log(`- ${d.title} (${d.owner}) — ${d.impact}`));
|
|
590
|
-
} else {
|
|
591
|
-
console.log(`- ${t("cli.status.noDecisions")}`);
|
|
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")}`);
|
|
592
1090
|
}
|
|
593
1091
|
console.log("");
|
|
594
1092
|
console.log(t("cli.status.repo"));
|
|
@@ -616,44 +1114,78 @@ function cmdStatus(root) {
|
|
|
616
1114
|
console.log("");
|
|
617
1115
|
const syncStatus = drift.length
|
|
618
1116
|
? t("cli.status.docsSyncedNo", { files: drift.join(", ") })
|
|
619
|
-
: t("cli.status.docsSyncedYes");
|
|
620
|
-
console.log(t("cli.status.docsSynced", { status: syncStatus }));
|
|
621
|
-
|
|
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
|
+
}
|
|
622
1136
|
|
|
623
|
-
function cmdNext(root) {
|
|
624
|
-
const context = config.ensureContext(root);
|
|
625
|
-
initLocale(context);
|
|
626
|
-
const control = config.loadControl(context);
|
|
627
|
-
const state = derive(control);
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
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)) {
|
|
638
1172
|
console.log(` - ${task.id}: ${task.blocker || task.title}`);
|
|
639
1173
|
}
|
|
640
1174
|
} else {
|
|
641
1175
|
console.log(t("cli.noReadyTasks"));
|
|
642
|
-
}
|
|
643
|
-
return;
|
|
644
|
-
}
|
|
645
|
-
ready
|
|
646
|
-
|
|
647
|
-
console.log(` id: ${task.id}`);
|
|
648
|
-
console.log(` ${t("cli.next.phase")}: ${task.phase} · ${t("cli.next.priority")}: ${task.priority} · ${t("cli.next.stream")}: ${task.stream}`);
|
|
649
|
-
if (task.summary) console.log(` ${t("cli.next.summary")}: ${task.summary}`);
|
|
650
|
-
});
|
|
651
|
-
}
|
|
1176
|
+
}
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1179
|
+
printTasks(ready);
|
|
1180
|
+
}
|
|
652
1181
|
|
|
653
|
-
function cmdSync(root, args) {
|
|
654
|
-
const context = config.ensureContext(root);
|
|
655
|
-
initLocale(context);
|
|
656
|
-
const control = config.loadControl(context);
|
|
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
|
+
}
|
|
657
1189
|
if ((args || []).includes("--dry-run")) {
|
|
658
1190
|
const drift = getDocDrift(context, control);
|
|
659
1191
|
if (drift.length) {
|
|
@@ -689,12 +1221,12 @@ function cmdRefreshRepo(root, args) {
|
|
|
689
1221
|
function cmdTask(root, args) {
|
|
690
1222
|
const context = config.ensureContext(root);
|
|
691
1223
|
initLocale(context);
|
|
692
|
-
const [action, taskId, ...noteParts] = args || [];
|
|
693
|
-
if (!action || !taskId) throw new Error(t("cli.mustProvideActionAndId"));
|
|
694
|
-
const control = config.loadControl(context);
|
|
695
|
-
updateTask(context, control, action, taskId, noteParts.join(" ").trim());
|
|
696
|
-
console.log(t("cli.taskUpdated", { taskId, action }));
|
|
697
|
-
}
|
|
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
|
+
}
|
|
698
1230
|
|
|
699
1231
|
function cmdInstallHooks(root) {
|
|
700
1232
|
const context = config.ensureContext(root);
|
|
@@ -740,10 +1272,14 @@ function cmdHelp() {
|
|
|
740
1272
|
console.log(` ${t("cli.help.register.desc")}`);
|
|
741
1273
|
console.log(" projects");
|
|
742
1274
|
console.log(` ${t("cli.help.projects.desc")}`);
|
|
743
|
-
console.log(" task <action> <id> [note]");
|
|
744
|
-
console.log(` ${t("cli.help.task.desc")}`);
|
|
745
|
-
console.log("
|
|
746
|
-
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")}`);
|
|
747
1283
|
console.log(` ${t("cli.help.opera.upgradeHint")}`);
|
|
748
1284
|
console.log(" locale get|set [es|en]");
|
|
749
1285
|
console.log(` ${t("cli.help.locale.desc")}`);
|
|
@@ -773,20 +1309,22 @@ function forProject(root) {
|
|
|
773
1309
|
derive,
|
|
774
1310
|
buildDocMap,
|
|
775
1311
|
getDocDrift: (ctrl) => getDocDrift(context, ctrl),
|
|
776
|
-
syncDocs: (ctrl) => syncDocs(context, ctrl),
|
|
777
|
-
updateTask: (ctrl, action, id, note) => updateTask(context, ctrl, action, id, note),
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
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),
|
|
781
1318
|
getLocale: (ctrl) => config.getLocale(ctrl),
|
|
782
1319
|
statusLabel,
|
|
783
1320
|
context,
|
|
784
1321
|
};
|
|
785
1322
|
}
|
|
786
1323
|
|
|
787
|
-
module.exports = {
|
|
788
|
-
buildDocMap, derive, getDocDrift, getRepoSnapshot, refreshRepoRuntime, syncDocs, updateTask,
|
|
789
|
-
|
|
1324
|
+
module.exports = {
|
|
1325
|
+
buildDocMap, derive, getDocDrift, getRepoSnapshot, refreshRepoRuntime, syncDocs, updateTask,
|
|
1326
|
+
setTaskStatus, ensureAgentInbox, upsertAgentInstruction, resolveAgentInstructions,
|
|
1327
|
+
forProject, statusLabel, renderTask, getPhaseInfo,
|
|
790
1328
|
cmdStatus, cmdNext, cmdSync, cmdRefreshRepo, cmdTask, cmdInstallHooks, cmdHelp,
|
|
791
1329
|
PRIORITY_ORDER, STATUS_ORDER, STATUS_ICONS, CHECK_ICONS,
|
|
792
1330
|
};
|