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/bin/trackops.js
CHANGED
|
@@ -143,22 +143,30 @@ async function run() {
|
|
|
143
143
|
break;
|
|
144
144
|
}
|
|
145
145
|
|
|
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 "
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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);
|
|
162
170
|
break;
|
|
163
171
|
|
|
164
172
|
case "help":
|
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,14 +292,178 @@ 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;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
function
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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");
|
|
276
467
|
for (let i = 0; i < control.tasks.length; i++) {
|
|
277
468
|
const task = control.tasks[i];
|
|
278
469
|
if (!task.id) throw new Error(`Task at index ${i} is missing required field: id`);
|
|
@@ -289,24 +480,30 @@ function loadControl(contextOrRoot) {
|
|
|
289
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}`);
|
|
290
481
|
}
|
|
291
482
|
let control;
|
|
292
|
-
try {
|
|
293
|
-
control = JSON.parse(raw);
|
|
294
|
-
} catch (err) {
|
|
295
|
-
throw new Error(`project_control.json esta corrupto o no es JSON valido.\n Ruta: ${filePath}\n Detalle: ${err.message}`);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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
|
+
}
|
|
310
507
|
|
|
311
508
|
function loadWorkspaceManifest(contextOrRoot) {
|
|
312
509
|
const context = ensureContext(contextOrRoot);
|
|
@@ -326,9 +523,13 @@ module.exports = {
|
|
|
326
523
|
DEFAULT_LOCALE,
|
|
327
524
|
DEFAULT_APP_DIR,
|
|
328
525
|
DEFAULT_OPS_DIR,
|
|
329
|
-
DEFAULT_DEV_BRANCH,
|
|
330
|
-
DEFAULT_PUBLISH_BRANCH,
|
|
331
|
-
|
|
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,
|
|
332
533
|
buildDefaultPhases,
|
|
333
534
|
createSplitContext,
|
|
334
535
|
createLegacyContext,
|
|
@@ -345,8 +546,14 @@ module.exports = {
|
|
|
345
546
|
saveWorkspaceManifest,
|
|
346
547
|
getPhases,
|
|
347
548
|
getLocale,
|
|
348
|
-
isOperaInstalled,
|
|
349
|
-
getOperaVersion,
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
549
|
+
isOperaInstalled,
|
|
550
|
+
getOperaVersion,
|
|
551
|
+
normalizeExecutionOwner,
|
|
552
|
+
normalizeTaskExecution,
|
|
553
|
+
normalizeAgentInbox,
|
|
554
|
+
normalizeTaskShape,
|
|
555
|
+
normalizeQualityMeta,
|
|
556
|
+
normalizeControlShape,
|
|
557
|
+
loadControl,
|
|
558
|
+
saveControl,
|
|
559
|
+
};
|