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/bin/trackops.js
CHANGED
|
@@ -1,13 +1,33 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
const nodeVersion = parseInt(process.versions.node, 10);
|
|
4
|
+
if (nodeVersion < 18) {
|
|
5
|
+
console.error(`TrackOps requiere Node.js 18 o superior. Version actual: ${process.versions.node}`);
|
|
6
|
+
process.exit(1);
|
|
7
|
+
}
|
|
8
8
|
|
|
9
|
-
const
|
|
10
|
-
const
|
|
9
|
+
const path = require("path");
|
|
10
|
+
const config = require("../lib/config");
|
|
11
|
+
const runtimeState = require("../lib/runtime-state");
|
|
12
|
+
const { setLocale, t } = require("../lib/i18n");
|
|
13
|
+
const fmt = require("../lib/cli-format");
|
|
14
|
+
const pkg = require("../package.json");
|
|
15
|
+
|
|
16
|
+
function parseCliTokens(argv) {
|
|
17
|
+
const plain = argv.includes("--plain") || argv.includes("--a11y");
|
|
18
|
+
const tokens = argv.filter((token) => !["--plain", "--a11y"].includes(token));
|
|
19
|
+
return {
|
|
20
|
+
plain,
|
|
21
|
+
command: tokens[0],
|
|
22
|
+
args: tokens.slice(1),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const parsed = parseCliTokens(process.argv.slice(2));
|
|
27
|
+
fmt.configure({ plain: parsed.plain });
|
|
28
|
+
|
|
29
|
+
const command = parsed.command;
|
|
30
|
+
const args = parsed.args;
|
|
11
31
|
|
|
12
32
|
function initCliLocale() {
|
|
13
33
|
let projectLocale = null;
|
|
@@ -27,6 +47,7 @@ function resolveRoot() {
|
|
|
27
47
|
const context = config.resolveWorkspaceContext();
|
|
28
48
|
if (!context) {
|
|
29
49
|
console.error(t("cli.error.noWorkspace"));
|
|
50
|
+
console.error(t("cli.help.initFirst"));
|
|
30
51
|
process.exit(1);
|
|
31
52
|
}
|
|
32
53
|
return context.workspaceRoot;
|
|
@@ -122,22 +143,30 @@ async function run() {
|
|
|
122
143
|
break;
|
|
123
144
|
}
|
|
124
145
|
|
|
125
|
-
case "skill": {
|
|
126
|
-
const skills = require("../lib/skills");
|
|
127
|
-
const sub = args[0];
|
|
128
|
-
const root = config.resolveProjectRoot() || process.cwd();
|
|
129
|
-
if (sub === "install") skills.cmdInstall(root, args[1]);
|
|
130
|
-
else if (sub === "list") skills.cmdList(root);
|
|
131
|
-
else if (sub === "remove") skills.cmdRemove(root, args[1]);
|
|
132
|
-
else if (sub === "catalog") skills.cmdCatalog();
|
|
133
|
-
else { console.log(t("cli.usage.skill")); }
|
|
134
|
-
break;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
case "
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
146
|
+
case "skill": {
|
|
147
|
+
const skills = require("../lib/skills");
|
|
148
|
+
const sub = args[0];
|
|
149
|
+
const root = config.resolveProjectRoot() || process.cwd();
|
|
150
|
+
if (sub === "install") skills.cmdInstall(root, args[1]);
|
|
151
|
+
else if (sub === "list") skills.cmdList(root);
|
|
152
|
+
else if (sub === "remove") skills.cmdRemove(root, args[1]);
|
|
153
|
+
else if (sub === "catalog") skills.cmdCatalog();
|
|
154
|
+
else { console.log(t("cli.usage.skill")); }
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
case "plan":
|
|
159
|
+
require("../lib/plans").cmdPlan(config.resolveProjectRoot() || process.cwd(), args);
|
|
160
|
+
break;
|
|
161
|
+
|
|
162
|
+
case "quality":
|
|
163
|
+
require("../lib/quality").cmdQuality(config.resolveProjectRoot() || process.cwd(), args);
|
|
164
|
+
break;
|
|
165
|
+
|
|
166
|
+
case "version":
|
|
167
|
+
case "--version":
|
|
168
|
+
case "-v":
|
|
169
|
+
console.log(pkg.version);
|
|
141
170
|
break;
|
|
142
171
|
|
|
143
172
|
case "help":
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CLI output formatting helpers for accessible, scannable terminal output.
|
|
5
|
+
* Supports a plain mode for screen readers, logs, and limited terminals.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const state = {
|
|
9
|
+
plain: false,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const UNICODE_SEP = "\u2500".repeat(48);
|
|
13
|
+
const PLAIN_SEP = "-".repeat(48);
|
|
14
|
+
|
|
15
|
+
const STATUS_TOKENS = {
|
|
16
|
+
pending: ["\u23F3", "PENDING"],
|
|
17
|
+
in_progress: ["\uD83D\uDEA7", "IN PROGRESS"],
|
|
18
|
+
in_review: ["\uD83D\uDC40", "IN REVIEW"],
|
|
19
|
+
blocked: ["\u26D4", "BLOCKED"],
|
|
20
|
+
completed: ["\u2705", "DONE"],
|
|
21
|
+
cancelled: ["\uD83D\uDDD1\uFE0F", "CANCELLED"],
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const CHECK_TOKENS = {
|
|
25
|
+
pass: ["\u2705", "PASS"],
|
|
26
|
+
warn: ["\u26A0\uFE0F", "WARN"],
|
|
27
|
+
fail: ["\u274C", "FAIL"],
|
|
28
|
+
pending: ["\u23F3", "PENDING"],
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function configure(options = {}) {
|
|
32
|
+
if (Object.prototype.hasOwnProperty.call(options, "plain")) {
|
|
33
|
+
state.plain = Boolean(options.plain);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isPlain() {
|
|
38
|
+
return state.plain;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function pick(unicodeValue, plainValue) {
|
|
42
|
+
return state.plain ? plainValue : unicodeValue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function sep() {
|
|
46
|
+
console.log(pick(UNICODE_SEP, PLAIN_SEP));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function blank() {
|
|
50
|
+
console.log("");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function header(text) {
|
|
54
|
+
console.log("");
|
|
55
|
+
console.log(pick(UNICODE_SEP, PLAIN_SEP));
|
|
56
|
+
console.log(` ${text}`);
|
|
57
|
+
console.log(pick(UNICODE_SEP, PLAIN_SEP));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function success(text) {
|
|
61
|
+
console.log(` ${pick("\u2713", "OK")} ${text}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function info(text) {
|
|
65
|
+
console.log(` ${text}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function step(label, value) {
|
|
69
|
+
if (value !== undefined) {
|
|
70
|
+
console.log(` ${pick("\u2192", "->")} ${label}: ${value}`);
|
|
71
|
+
} else {
|
|
72
|
+
console.log(` ${pick("\u2192", "->")} ${label}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function hint(text) {
|
|
77
|
+
console.log(` ${text}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function warn(text) {
|
|
81
|
+
console.log(` ${pick("\u26A0", "WARN")} ${text}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function bullet(text) {
|
|
85
|
+
console.log(` - ${text}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function statusToken(status) {
|
|
89
|
+
const pair = STATUS_TOKENS[status] || [status, String(status || "").toUpperCase()];
|
|
90
|
+
return pick(pair[0], `[${pair[1]}]`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function checkToken(status) {
|
|
94
|
+
const pair = CHECK_TOKENS[status] || [status, String(status || "").toUpperCase()];
|
|
95
|
+
return pick(pair[0], `[${pair[1]}]`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function boolToken(value) {
|
|
99
|
+
return value ? pick("\u2705", "[YES]") : pick("\u274C", "[NO]");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
module.exports = {
|
|
103
|
+
configure,
|
|
104
|
+
isPlain,
|
|
105
|
+
pick,
|
|
106
|
+
sep,
|
|
107
|
+
blank,
|
|
108
|
+
header,
|
|
109
|
+
success,
|
|
110
|
+
info,
|
|
111
|
+
step,
|
|
112
|
+
hint,
|
|
113
|
+
warn,
|
|
114
|
+
bullet,
|
|
115
|
+
statusToken,
|
|
116
|
+
checkToken,
|
|
117
|
+
boolToken,
|
|
118
|
+
};
|
package/lib/config.js
CHANGED
|
@@ -22,12 +22,27 @@ const DEFAULT_PHASE_LABELS = {
|
|
|
22
22
|
},
|
|
23
23
|
};
|
|
24
24
|
|
|
25
|
-
const DEFAULT_LOCALE = "es";
|
|
26
|
-
const WORKSPACE_MANIFEST = ".trackops-workspace.json";
|
|
27
|
-
const DEFAULT_APP_DIR = "app";
|
|
28
|
-
const DEFAULT_OPS_DIR = "ops";
|
|
29
|
-
const DEFAULT_DEV_BRANCH = "develop";
|
|
30
|
-
const DEFAULT_PUBLISH_BRANCH = "master";
|
|
25
|
+
const DEFAULT_LOCALE = "es";
|
|
26
|
+
const WORKSPACE_MANIFEST = ".trackops-workspace.json";
|
|
27
|
+
const DEFAULT_APP_DIR = "app";
|
|
28
|
+
const DEFAULT_OPS_DIR = "ops";
|
|
29
|
+
const DEFAULT_DEV_BRANCH = "develop";
|
|
30
|
+
const DEFAULT_PUBLISH_BRANCH = "master";
|
|
31
|
+
const DEFAULT_CONTROL_VERSION = 3;
|
|
32
|
+
const DEFAULT_MANAGED_PLAN_FIELDS = [
|
|
33
|
+
"title",
|
|
34
|
+
"summary",
|
|
35
|
+
"acceptance",
|
|
36
|
+
"dependsOn",
|
|
37
|
+
"phase",
|
|
38
|
+
"stream",
|
|
39
|
+
"priority",
|
|
40
|
+
"required",
|
|
41
|
+
"parentId",
|
|
42
|
+
"sequence",
|
|
43
|
+
];
|
|
44
|
+
const DEFAULT_EXECUTION_OWNER = "shared";
|
|
45
|
+
const EXECUTION_OWNERS = ["agent", "user", "shared"];
|
|
31
46
|
|
|
32
47
|
function buildDefaultPhases(locale) {
|
|
33
48
|
const normalized = normalizeLocale(locale) || DEFAULT_LOCALE;
|
|
@@ -82,11 +97,11 @@ function createSplitContext(workspaceRoot, manifest = {}) {
|
|
|
82
97
|
progress: path.join(opsRoot, "progress.md"),
|
|
83
98
|
findings: path.join(opsRoot, "findings.md"),
|
|
84
99
|
},
|
|
85
|
-
paths: {
|
|
86
|
-
taskPlan: path.join(opsRoot, "task_plan.md"),
|
|
87
|
-
progress: path.join(opsRoot, "progress.md"),
|
|
88
|
-
findings: path.join(opsRoot, "findings.md"),
|
|
89
|
-
architectureDir: path.join(opsRoot, "architecture"),
|
|
100
|
+
paths: {
|
|
101
|
+
taskPlan: path.join(opsRoot, "task_plan.md"),
|
|
102
|
+
progress: path.join(opsRoot, "progress.md"),
|
|
103
|
+
findings: path.join(opsRoot, "findings.md"),
|
|
104
|
+
architectureDir: path.join(opsRoot, "architecture"),
|
|
90
105
|
hooksDir: path.join(opsRoot, ".githooks"),
|
|
91
106
|
tmpDir: path.join(opsRoot, ".tmp"),
|
|
92
107
|
bootstrapDir: path.join(opsRoot, "bootstrap"),
|
|
@@ -96,10 +111,16 @@ function createSplitContext(workspaceRoot, manifest = {}) {
|
|
|
96
111
|
autonomyPolicyFile: path.join(opsRoot, "policy", "autonomy.json"),
|
|
97
112
|
reviewsDir: path.join(opsRoot, "reviews"),
|
|
98
113
|
skillsDir: path.join(opsRoot, ".agents", "skills"),
|
|
99
|
-
registryPath: path.join(opsRoot, ".agents", "skills", "_registry.md"),
|
|
100
|
-
agentHubDir: path.join(opsRoot, ".agent", "hub"),
|
|
101
|
-
genesisFile: path.join(opsRoot, "genesis.md"),
|
|
102
|
-
|
|
114
|
+
registryPath: path.join(opsRoot, ".agents", "skills", "_registry.md"),
|
|
115
|
+
agentHubDir: path.join(opsRoot, ".agent", "hub"),
|
|
116
|
+
genesisFile: path.join(opsRoot, "genesis.md"),
|
|
117
|
+
plansDir: path.join(opsRoot, "plans"),
|
|
118
|
+
plansRegistryFile: path.join(opsRoot, "plans", "_registry.json"),
|
|
119
|
+
qualityDir: path.join(opsRoot, "quality"),
|
|
120
|
+
qualityLatestFile: path.join(opsRoot, "quality", "latest.json"),
|
|
121
|
+
qualityRunsDir: path.join(opsRoot, "quality", "runs"),
|
|
122
|
+
qualityWaiversFile: path.join(opsRoot, "quality", "waivers.json"),
|
|
123
|
+
},
|
|
103
124
|
env: {
|
|
104
125
|
rootFile: path.join(workspace, env.rootFile || ".env"),
|
|
105
126
|
exampleFile: path.join(workspace, env.exampleFile || ".env.example"),
|
|
@@ -140,11 +161,11 @@ function createLegacyContext(rootDir) {
|
|
|
140
161
|
progress: path.join(root, "progress.md"),
|
|
141
162
|
findings: path.join(root, "findings.md"),
|
|
142
163
|
},
|
|
143
|
-
paths: {
|
|
144
|
-
taskPlan: path.join(root, "task_plan.md"),
|
|
145
|
-
progress: path.join(root, "progress.md"),
|
|
146
|
-
findings: path.join(root, "findings.md"),
|
|
147
|
-
architectureDir: path.join(root, "architecture"),
|
|
164
|
+
paths: {
|
|
165
|
+
taskPlan: path.join(root, "task_plan.md"),
|
|
166
|
+
progress: path.join(root, "progress.md"),
|
|
167
|
+
findings: path.join(root, "findings.md"),
|
|
168
|
+
architectureDir: path.join(root, "architecture"),
|
|
148
169
|
hooksDir: path.join(root, ".githooks"),
|
|
149
170
|
tmpDir: path.join(root, ".tmp"),
|
|
150
171
|
bootstrapDir: path.join(root, "bootstrap"),
|
|
@@ -154,10 +175,16 @@ function createLegacyContext(rootDir) {
|
|
|
154
175
|
autonomyPolicyFile: path.join(root, "policy", "autonomy.json"),
|
|
155
176
|
reviewsDir: path.join(root, "reviews"),
|
|
156
177
|
skillsDir: path.join(root, ".agents", "skills"),
|
|
157
|
-
registryPath: path.join(root, ".agents", "skills", "_registry.md"),
|
|
158
|
-
agentHubDir: path.join(root, ".agent", "hub"),
|
|
159
|
-
genesisFile: path.join(root, "genesis.md"),
|
|
160
|
-
|
|
178
|
+
registryPath: path.join(root, ".agents", "skills", "_registry.md"),
|
|
179
|
+
agentHubDir: path.join(root, ".agent", "hub"),
|
|
180
|
+
genesisFile: path.join(root, "genesis.md"),
|
|
181
|
+
plansDir: path.join(root, "plans"),
|
|
182
|
+
plansRegistryFile: path.join(root, "plans", "_registry.json"),
|
|
183
|
+
qualityDir: path.join(root, "quality"),
|
|
184
|
+
qualityLatestFile: path.join(root, "quality", "latest.json"),
|
|
185
|
+
qualityRunsDir: path.join(root, "quality", "runs"),
|
|
186
|
+
qualityWaiversFile: path.join(root, "quality", "waivers.json"),
|
|
187
|
+
},
|
|
161
188
|
env: {
|
|
162
189
|
rootFile: path.join(root, ".env"),
|
|
163
190
|
exampleFile: path.join(root, ".env.example"),
|
|
@@ -265,22 +292,218 @@ function isOperaInstalled(control) {
|
|
|
265
292
|
return control.meta?.opera?.installed === true;
|
|
266
293
|
}
|
|
267
294
|
|
|
268
|
-
function getOperaVersion(control) {
|
|
269
|
-
return control.meta?.opera?.version || null;
|
|
295
|
+
function getOperaVersion(control) {
|
|
296
|
+
return control.meta?.opera?.version || null;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function normalizeStringList(values) {
|
|
300
|
+
return Array.isArray(values)
|
|
301
|
+
? values.map((value) => String(value || "").trim()).filter(Boolean)
|
|
302
|
+
: [];
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function normalizeExecutionOwner(value) {
|
|
306
|
+
const normalized = String(value || "").trim().toLowerCase();
|
|
307
|
+
return EXECUTION_OWNERS.includes(normalized) ? normalized : DEFAULT_EXECUTION_OWNER;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function normalizeTaskExecution(execution) {
|
|
311
|
+
const normalized = execution && typeof execution === "object" ? { ...execution } : {};
|
|
312
|
+
normalized.owner = normalizeExecutionOwner(normalized.owner);
|
|
313
|
+
normalized.lastActor = normalized.lastActor ? String(normalized.lastActor).trim() : null;
|
|
314
|
+
normalized.lastSource = normalized.lastSource ? String(normalized.lastSource).trim() : null;
|
|
315
|
+
normalized.currentSessionId = normalized.currentSessionId ? String(normalized.currentSessionId).trim() : null;
|
|
316
|
+
normalized.lastSessionId = normalized.lastSessionId ? String(normalized.lastSessionId).trim() : null;
|
|
317
|
+
normalized.lastSessionStatus = normalized.lastSessionStatus ? String(normalized.lastSessionStatus).trim() : null;
|
|
318
|
+
normalized.awaitingUserConfirmation = normalized.awaitingUserConfirmation === true;
|
|
319
|
+
normalized.verificationPending = normalized.verificationPending === true;
|
|
320
|
+
normalized.updatedAt = normalized.updatedAt ? String(normalized.updatedAt).trim() : null;
|
|
321
|
+
return normalized;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function normalizeAgentInboxItem(item) {
|
|
325
|
+
const normalized = item && typeof item === "object" ? { ...item } : {};
|
|
326
|
+
normalized.id = String(normalized.id || "").trim();
|
|
327
|
+
normalized.taskId = normalized.taskId ? String(normalized.taskId).trim() : null;
|
|
328
|
+
normalized.kind = String(normalized.kind || "verify_status").trim();
|
|
329
|
+
normalized.message = String(normalized.message || "").trim();
|
|
330
|
+
normalized.status = String(normalized.status || "pending").trim();
|
|
331
|
+
normalized.createdAt = normalized.createdAt ? String(normalized.createdAt).trim() : null;
|
|
332
|
+
normalized.updatedAt = normalized.updatedAt ? String(normalized.updatedAt).trim() : null;
|
|
333
|
+
normalized.createdBy = normalized.createdBy ? String(normalized.createdBy).trim() : null;
|
|
334
|
+
normalized.source = normalized.source ? String(normalized.source).trim() : null;
|
|
335
|
+
normalized.expectedStatus = normalized.expectedStatus ? String(normalized.expectedStatus).trim() : null;
|
|
336
|
+
normalized.sessionId = normalized.sessionId ? String(normalized.sessionId).trim() : null;
|
|
337
|
+
normalized.resolvedAt = normalized.resolvedAt ? String(normalized.resolvedAt).trim() : null;
|
|
338
|
+
normalized.resolutionNote = normalized.resolutionNote ? String(normalized.resolutionNote).trim() : null;
|
|
339
|
+
return normalized;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function normalizeAgentInbox(metaInbox) {
|
|
343
|
+
const normalized = metaInbox && typeof metaInbox === "object" ? { ...metaInbox } : {};
|
|
344
|
+
normalized.pending = Array.isArray(normalized.pending)
|
|
345
|
+
? normalized.pending.map((item) => normalizeAgentInboxItem(item)).filter((item) => item.id)
|
|
346
|
+
: [];
|
|
347
|
+
normalized.history = Array.isArray(normalized.history)
|
|
348
|
+
? normalized.history.map((item) => normalizeAgentInboxItem(item)).filter((item) => item.id)
|
|
349
|
+
: [];
|
|
350
|
+
normalized.lastIssuedAt = normalized.lastIssuedAt ? String(normalized.lastIssuedAt).trim() : null;
|
|
351
|
+
return normalized;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function normalizeTaskShape(task) {
|
|
355
|
+
const normalized = { ...(task || {}) };
|
|
356
|
+
normalized.id = String(normalized.id || "").trim();
|
|
357
|
+
normalized.title = String(normalized.title || normalized.id || "").trim();
|
|
358
|
+
normalized.phase = String(normalized.phase || DEFAULT_PHASE_IDS[0]).trim() || DEFAULT_PHASE_IDS[0];
|
|
359
|
+
normalized.stream = String(normalized.stream || "Operations").trim() || "Operations";
|
|
360
|
+
normalized.priority = String(normalized.priority || "P1").trim() || "P1";
|
|
361
|
+
normalized.status = String(normalized.status || "pending").trim() || "pending";
|
|
362
|
+
normalized.required = normalized.required !== false;
|
|
363
|
+
normalized.dependsOn = normalizeStringList(normalized.dependsOn);
|
|
364
|
+
normalized.summary = String(normalized.summary || "").trim();
|
|
365
|
+
normalized.acceptance = normalizeStringList(normalized.acceptance);
|
|
366
|
+
normalized.history = Array.isArray(normalized.history) ? normalized.history.map((entry) => ({ ...entry })) : [];
|
|
367
|
+
normalized.parentId = normalized.parentId == null ? null : String(normalized.parentId).trim() || null;
|
|
368
|
+
const numericSequence = Number(normalized.sequence);
|
|
369
|
+
normalized.sequence = Number.isFinite(numericSequence) ? numericSequence : null;
|
|
370
|
+
|
|
371
|
+
const origin = normalized.origin && typeof normalized.origin === "object"
|
|
372
|
+
? { ...normalized.origin }
|
|
373
|
+
: {};
|
|
374
|
+
const kind = origin.kind === "plan_import" ? "plan_import" : "manual";
|
|
375
|
+
origin.kind = kind;
|
|
376
|
+
origin.sourceId = origin.sourceId ? String(origin.sourceId).trim() : null;
|
|
377
|
+
origin.adapter = origin.adapter ? String(origin.adapter).trim() : null;
|
|
378
|
+
origin.externalNodeId = origin.externalNodeId ? String(origin.externalNodeId).trim() : null;
|
|
379
|
+
origin.importId = origin.importId ? String(origin.importId).trim() : null;
|
|
380
|
+
origin.fingerprint = origin.fingerprint ? String(origin.fingerprint).trim() : null;
|
|
381
|
+
origin.lastImportedAt = origin.lastImportedAt ? String(origin.lastImportedAt).trim() : null;
|
|
382
|
+
origin.detached = origin.detached === true;
|
|
383
|
+
origin.managedFields = Array.isArray(origin.managedFields) && origin.managedFields.length
|
|
384
|
+
? origin.managedFields.map((field) => String(field || "").trim()).filter(Boolean)
|
|
385
|
+
: kind === "plan_import"
|
|
386
|
+
? [...DEFAULT_MANAGED_PLAN_FIELDS]
|
|
387
|
+
: [];
|
|
388
|
+
normalized.origin = origin;
|
|
389
|
+
normalized.execution = normalizeTaskExecution(normalized.execution);
|
|
390
|
+
return normalized;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function normalizePlansMeta(meta) {
|
|
394
|
+
const normalized = meta && typeof meta === "object" ? { ...meta } : {};
|
|
395
|
+
normalized.activeSourceId = normalized.activeSourceId ? String(normalized.activeSourceId).trim() : null;
|
|
396
|
+
normalized.lastImportAt = normalized.lastImportAt ? String(normalized.lastImportAt).trim() : null;
|
|
397
|
+
normalized.unresolvedConflicts = Number.isFinite(Number(normalized.unresolvedConflicts))
|
|
398
|
+
? Number(normalized.unresolvedConflicts)
|
|
399
|
+
: 0;
|
|
400
|
+
normalized.sources = Array.isArray(normalized.sources)
|
|
401
|
+
? normalized.sources.map((source) => ({
|
|
402
|
+
id: String(source?.id || "").trim(),
|
|
403
|
+
title: String(source?.title || source?.id || "").trim(),
|
|
404
|
+
adapter: String(source?.adapter || "unknown").trim(),
|
|
405
|
+
status: String(source?.status || "previewed").trim(),
|
|
406
|
+
lastPreviewAt: source?.lastPreviewAt ? String(source.lastPreviewAt).trim() : null,
|
|
407
|
+
lastApplyAt: source?.lastApplyAt ? String(source.lastApplyAt).trim() : null,
|
|
408
|
+
warnings: Number.isFinite(Number(source?.warnings)) ? Number(source.warnings) : 0,
|
|
409
|
+
conflicts: Number.isFinite(Number(source?.conflicts)) ? Number(source.conflicts) : 0,
|
|
410
|
+
managedTaskCount: Number.isFinite(Number(source?.managedTaskCount)) ? Number(source.managedTaskCount) : 0,
|
|
411
|
+
})).filter((source) => source.id)
|
|
412
|
+
: [];
|
|
413
|
+
return normalized;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function normalizeCommandList(value) {
|
|
417
|
+
if (Array.isArray(value)) {
|
|
418
|
+
return value.map((item) => String(item || "").trim()).filter(Boolean);
|
|
419
|
+
}
|
|
420
|
+
if (typeof value === "string" && value.trim()) {
|
|
421
|
+
return [value.trim()];
|
|
422
|
+
}
|
|
423
|
+
return [];
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function normalizeQualityMeta(meta) {
|
|
427
|
+
const normalized = meta && typeof meta === "object" ? { ...meta } : {};
|
|
428
|
+
normalized.baselineProfile = String(normalized.baselineProfile || "baseline").trim() || "baseline";
|
|
429
|
+
normalized.activeProfiles = normalizeStringList(normalized.activeProfiles);
|
|
430
|
+
normalized.verification = normalized.verification && typeof normalized.verification === "object"
|
|
431
|
+
? { ...normalized.verification }
|
|
432
|
+
: {};
|
|
433
|
+
normalized.verification.testCommands = normalizeCommandList(normalized.verification.testCommands);
|
|
434
|
+
normalized.verification.buildCommands = normalizeCommandList(normalized.verification.buildCommands);
|
|
435
|
+
normalized.verification.smokeCommands = normalizeCommandList(normalized.verification.smokeCommands);
|
|
436
|
+
normalized.verification.reviewRequired = normalized.verification.reviewRequired === true;
|
|
437
|
+
normalized.lastReportAt = normalized.lastReportAt ? String(normalized.lastReportAt).trim() : null;
|
|
438
|
+
normalized.lastVerificationAt = normalized.lastVerificationAt ? String(normalized.lastVerificationAt).trim() : null;
|
|
439
|
+
normalized.lastReleaseReadiness = normalized.lastReleaseReadiness ? String(normalized.lastReleaseReadiness).trim() : null;
|
|
440
|
+
normalized.lastPromotionReadiness = normalized.lastPromotionReadiness ? String(normalized.lastPromotionReadiness).trim() : null;
|
|
441
|
+
return normalized;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function normalizeControlShape(control) {
|
|
445
|
+
const normalized = { ...(control || {}) };
|
|
446
|
+
normalized.meta = normalized.meta && typeof normalized.meta === "object" ? { ...normalized.meta } : {};
|
|
447
|
+
normalized.meta.controlVersion = DEFAULT_CONTROL_VERSION;
|
|
448
|
+
normalized.meta.plans = normalizePlansMeta(normalized.meta.plans);
|
|
449
|
+
normalized.meta.quality = normalizeQualityMeta(normalized.meta.quality);
|
|
450
|
+
normalized.meta.agentInbox = normalizeAgentInbox(normalized.meta.agentInbox);
|
|
451
|
+
normalized.tasks = Array.isArray(normalized.tasks)
|
|
452
|
+
? normalized.tasks.map((task) => normalizeTaskShape(task))
|
|
453
|
+
: [];
|
|
454
|
+
const changed = JSON.stringify(control || {}) !== JSON.stringify(normalized);
|
|
455
|
+
Object.defineProperty(normalized, "__trackopsMigrated", {
|
|
456
|
+
value: changed,
|
|
457
|
+
enumerable: false,
|
|
458
|
+
configurable: true,
|
|
459
|
+
});
|
|
460
|
+
return normalized;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function validateControl(control) {
|
|
464
|
+
if (!control || typeof control !== "object") throw new Error("project_control.json is not a valid object.");
|
|
465
|
+
if (!control.meta || typeof control.meta !== "object") throw new Error("project_control.json is missing required field: meta");
|
|
466
|
+
if (!Array.isArray(control.tasks)) throw new Error("project_control.json is missing required field: tasks");
|
|
467
|
+
for (let i = 0; i < control.tasks.length; i++) {
|
|
468
|
+
const task = control.tasks[i];
|
|
469
|
+
if (!task.id) throw new Error(`Task at index ${i} is missing required field: id`);
|
|
470
|
+
if (!task.status) throw new Error(`Task at index ${i} is missing required field: status`);
|
|
471
|
+
}
|
|
270
472
|
}
|
|
271
473
|
|
|
272
474
|
function loadControl(contextOrRoot) {
|
|
273
475
|
const filePath = controlFilePath(contextOrRoot);
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
}
|
|
476
|
+
let raw;
|
|
477
|
+
try {
|
|
478
|
+
raw = fs.readFileSync(filePath, "utf8");
|
|
479
|
+
} catch (err) {
|
|
480
|
+
throw new Error(`No se puede leer project_control.json.\n Ruta: ${filePath}\n Detalle: ${err.code === "ENOENT" ? "El archivo no existe. Ejecuta 'trackops init' primero." : err.message}`);
|
|
481
|
+
}
|
|
482
|
+
let control;
|
|
483
|
+
try {
|
|
484
|
+
control = JSON.parse(raw);
|
|
485
|
+
} catch (err) {
|
|
486
|
+
throw new Error(`project_control.json esta corrupto o no es JSON valido.\n Ruta: ${filePath}\n Detalle: ${err.message}`);
|
|
487
|
+
}
|
|
488
|
+
control = normalizeControlShape(control);
|
|
489
|
+
validateControl(control);
|
|
490
|
+
return control;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function saveControl(contextOrRoot, control, options) {
|
|
494
|
+
const normalized = normalizeControlShape(control);
|
|
495
|
+
if (process.env.TRACKOPS_ASSERT === "1") {
|
|
496
|
+
require("./quality-assert").assertControlInvariants(normalized, { context: ensureContext(contextOrRoot) });
|
|
497
|
+
}
|
|
498
|
+
normalized.meta = normalized.meta || {};
|
|
499
|
+
if (!(options && options.skipTimestamp)) {
|
|
500
|
+
normalized.meta.updatedAt = new Date().toISOString();
|
|
501
|
+
}
|
|
502
|
+
const filePath = controlFilePath(contextOrRoot);
|
|
503
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
504
|
+
fs.writeFileSync(filePath, JSON.stringify(normalized, null, 2) + "\n", "utf8");
|
|
505
|
+
return normalized;
|
|
506
|
+
}
|
|
284
507
|
|
|
285
508
|
function loadWorkspaceManifest(contextOrRoot) {
|
|
286
509
|
const context = ensureContext(contextOrRoot);
|
|
@@ -300,9 +523,13 @@ module.exports = {
|
|
|
300
523
|
DEFAULT_LOCALE,
|
|
301
524
|
DEFAULT_APP_DIR,
|
|
302
525
|
DEFAULT_OPS_DIR,
|
|
303
|
-
DEFAULT_DEV_BRANCH,
|
|
304
|
-
DEFAULT_PUBLISH_BRANCH,
|
|
305
|
-
|
|
526
|
+
DEFAULT_DEV_BRANCH,
|
|
527
|
+
DEFAULT_PUBLISH_BRANCH,
|
|
528
|
+
DEFAULT_CONTROL_VERSION,
|
|
529
|
+
DEFAULT_MANAGED_PLAN_FIELDS,
|
|
530
|
+
DEFAULT_EXECUTION_OWNER,
|
|
531
|
+
EXECUTION_OWNERS,
|
|
532
|
+
WORKSPACE_MANIFEST,
|
|
306
533
|
buildDefaultPhases,
|
|
307
534
|
createSplitContext,
|
|
308
535
|
createLegacyContext,
|
|
@@ -319,8 +546,14 @@ module.exports = {
|
|
|
319
546
|
saveWorkspaceManifest,
|
|
320
547
|
getPhases,
|
|
321
548
|
getLocale,
|
|
322
|
-
isOperaInstalled,
|
|
323
|
-
getOperaVersion,
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
549
|
+
isOperaInstalled,
|
|
550
|
+
getOperaVersion,
|
|
551
|
+
normalizeExecutionOwner,
|
|
552
|
+
normalizeTaskExecution,
|
|
553
|
+
normalizeAgentInbox,
|
|
554
|
+
normalizeTaskShape,
|
|
555
|
+
normalizeQualityMeta,
|
|
556
|
+
normalizeControlShape,
|
|
557
|
+
loadControl,
|
|
558
|
+
saveControl,
|
|
559
|
+
};
|