oreshnik-cli 0.1.2 → 0.1.4
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/dist/cli.js +289 -0
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +28 -28
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2212,6 +2212,208 @@ var init_helpers = __esm({
|
|
|
2212
2212
|
}
|
|
2213
2213
|
});
|
|
2214
2214
|
|
|
2215
|
+
// src/core/task-distributor.service.ts
|
|
2216
|
+
var task_distributor_service_exports = {};
|
|
2217
|
+
__export(task_distributor_service_exports, {
|
|
2218
|
+
TaskDistributor: () => TaskDistributor,
|
|
2219
|
+
createTaskDistributor: () => createTaskDistributor
|
|
2220
|
+
});
|
|
2221
|
+
function createTaskDistributor() {
|
|
2222
|
+
return new TaskDistributor();
|
|
2223
|
+
}
|
|
2224
|
+
var TaskDistributor;
|
|
2225
|
+
var init_task_distributor_service = __esm({
|
|
2226
|
+
"src/core/task-distributor.service.ts"() {
|
|
2227
|
+
"use strict";
|
|
2228
|
+
init_esm_shims();
|
|
2229
|
+
TaskDistributor = class {
|
|
2230
|
+
/**
|
|
2231
|
+
* Distribute tasks equitably among operators, considering:
|
|
2232
|
+
* 1. Explicit assignments ('asignar a Jean', 'para Manuel')
|
|
2233
|
+
* 2. Zone conflict avoidance (same zone → same operator)
|
|
2234
|
+
* 3. Current workload balance
|
|
2235
|
+
* 4. Dependency ordering (dependency owner gets priority)
|
|
2236
|
+
*/
|
|
2237
|
+
distribute(newTasks, existingBoard, zoneMap, portfolio) {
|
|
2238
|
+
const operators = this.getOperators(portfolio, existingBoard);
|
|
2239
|
+
if (operators.length === 0) {
|
|
2240
|
+
return newTasks.map((t) => ({
|
|
2241
|
+
taskId: t.taskId,
|
|
2242
|
+
assignedOwner: "architect",
|
|
2243
|
+
reason: "no operators defined"
|
|
2244
|
+
}));
|
|
2245
|
+
}
|
|
2246
|
+
const workload = /* @__PURE__ */ new Map();
|
|
2247
|
+
for (const op of operators) {
|
|
2248
|
+
workload.set(op, existingBoard.tasks.filter((t) => t.owner === op && t.status !== "done").length);
|
|
2249
|
+
}
|
|
2250
|
+
const zoneOwner = /* @__PURE__ */ new Map();
|
|
2251
|
+
for (const [pattern, entry] of Object.entries(zoneMap)) {
|
|
2252
|
+
if (entry.owner && entry.owner !== "shared" && entry.owner !== "none") {
|
|
2253
|
+
zoneOwner.set(pattern, entry.owner);
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
const operatorZones = /* @__PURE__ */ new Map();
|
|
2257
|
+
for (const task of existingBoard.tasks) {
|
|
2258
|
+
if (task.status === "done") continue;
|
|
2259
|
+
for (const zone of task.zone || []) {
|
|
2260
|
+
if (!operatorZones.has(task.owner)) operatorZones.set(task.owner, /* @__PURE__ */ new Set());
|
|
2261
|
+
operatorZones.get(task.owner).add(zone);
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
const results = [];
|
|
2265
|
+
const newlyAssigned = /* @__PURE__ */ new Map();
|
|
2266
|
+
for (const task of newTasks) {
|
|
2267
|
+
const explicit = this.detectExplicitOwner(task.taskTitle, operators);
|
|
2268
|
+
if (explicit) {
|
|
2269
|
+
workload.set(explicit, (workload.get(explicit) || 0) + 1);
|
|
2270
|
+
results.push({ taskId: task.taskId, assignedOwner: explicit, reason: `explicit assignment in title` });
|
|
2271
|
+
continue;
|
|
2272
|
+
}
|
|
2273
|
+
let bestOperator = operators[0];
|
|
2274
|
+
let bestScore = Infinity;
|
|
2275
|
+
for (const op of operators) {
|
|
2276
|
+
let score = (workload.get(op) || 0) + (newlyAssigned.get(op) || 0);
|
|
2277
|
+
const opZones = operatorZones.get(op);
|
|
2278
|
+
if (opZones && task.zones.length > 0) {
|
|
2279
|
+
const sharedZoneCount = task.zones.filter((z2) => opZones.has(z2)).length;
|
|
2280
|
+
if (sharedZoneCount === 0) score += 2;
|
|
2281
|
+
else score -= sharedZoneCount;
|
|
2282
|
+
}
|
|
2283
|
+
for (const depId of task.dependsOn) {
|
|
2284
|
+
const depTask = existingBoard.tasks.find((t) => t.id === depId);
|
|
2285
|
+
if (depTask && depTask.owner === op) {
|
|
2286
|
+
score -= 1;
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
if (score < bestScore) {
|
|
2290
|
+
bestScore = score;
|
|
2291
|
+
bestOperator = op;
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
newlyAssigned.set(bestOperator, (newlyAssigned.get(bestOperator) || 0) + 1);
|
|
2295
|
+
let blockingBy;
|
|
2296
|
+
let blockingReason;
|
|
2297
|
+
for (const depId of task.dependsOn) {
|
|
2298
|
+
const depTask = existingBoard.tasks.find((t) => t.id === depId);
|
|
2299
|
+
if (depTask && depTask.status !== "done" && depTask.owner !== bestOperator) {
|
|
2300
|
+
blockingBy = depTask.owner;
|
|
2301
|
+
blockingReason = `depends on ${depId} (${depTask.status}, owned by ${depTask.owner})`;
|
|
2302
|
+
break;
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
2305
|
+
results.push({
|
|
2306
|
+
taskId: task.taskId,
|
|
2307
|
+
assignedOwner: bestOperator,
|
|
2308
|
+
reason: `workload=${bestScore}, zone affinity, equitable distribution`,
|
|
2309
|
+
blockingBy,
|
|
2310
|
+
blockingReason
|
|
2311
|
+
});
|
|
2312
|
+
}
|
|
2313
|
+
return results;
|
|
2314
|
+
}
|
|
2315
|
+
/**
|
|
2316
|
+
* Detect explicit operator assignment in task title
|
|
2317
|
+
* Examples: "asignar a Jean", "esto es para Manuel", "Jean debe hacer"
|
|
2318
|
+
*/
|
|
2319
|
+
detectExplicitOwner(title, operators) {
|
|
2320
|
+
const lower = title.toLowerCase();
|
|
2321
|
+
const patterns = [
|
|
2322
|
+
/asignar\s+a\s+(\w+)/i,
|
|
2323
|
+
/para\s+(\w+)\b/i,
|
|
2324
|
+
/(\w+)\s+debe\s+hacer/i,
|
|
2325
|
+
/(\w+)\s+se\s+encarga/i,
|
|
2326
|
+
/owner[:\s]+(\w+)/i,
|
|
2327
|
+
/@(\w+)/i
|
|
2328
|
+
];
|
|
2329
|
+
for (const pattern of patterns) {
|
|
2330
|
+
const match = lower.match(pattern);
|
|
2331
|
+
if (match) {
|
|
2332
|
+
const name = match[1].toLowerCase();
|
|
2333
|
+
const found = operators.find((op) => op.toLowerCase() === name);
|
|
2334
|
+
if (found) return found;
|
|
2335
|
+
const partial = operators.find((op) => op.toLowerCase().includes(name) || name.includes(op.toLowerCase()));
|
|
2336
|
+
if (partial) return partial;
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
return null;
|
|
2340
|
+
}
|
|
2341
|
+
/**
|
|
2342
|
+
* Get operators from portfolio and existing task-board
|
|
2343
|
+
*/
|
|
2344
|
+
getOperators(portfolio, board) {
|
|
2345
|
+
const operators = /* @__PURE__ */ new Set();
|
|
2346
|
+
for (const project of portfolio.projects) {
|
|
2347
|
+
for (const op of project.operators) {
|
|
2348
|
+
operators.add(op);
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
for (const task of board.tasks) {
|
|
2352
|
+
if (task.owner && !task.owner.includes("+")) {
|
|
2353
|
+
operators.add(task.owner);
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2357
|
+
const result = [];
|
|
2358
|
+
for (const op of operators) {
|
|
2359
|
+
const key = op.toLowerCase();
|
|
2360
|
+
if (!seen.has(key)) {
|
|
2361
|
+
seen.add(key);
|
|
2362
|
+
result.push(op);
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
return result;
|
|
2366
|
+
}
|
|
2367
|
+
/**
|
|
2368
|
+
* Rebalance: if one operator finishes faster, move pending tasks from overloaded to underloaded.
|
|
2369
|
+
* Respects explicit assignments (stays with original owner) and zone conflicts.
|
|
2370
|
+
* Returns a list of suggested reassignments. Does NOT mutate the board.
|
|
2371
|
+
*/
|
|
2372
|
+
rebalance(board, zoneMap, maxImbalance = 3) {
|
|
2373
|
+
const operators = this.getOperators({ projects: [] }, board);
|
|
2374
|
+
if (operators.length <= 1) return [];
|
|
2375
|
+
const workload = /* @__PURE__ */ new Map();
|
|
2376
|
+
const pendingTasks = /* @__PURE__ */ new Map();
|
|
2377
|
+
for (const op of operators) {
|
|
2378
|
+
const pts = board.tasks.filter((t) => t.owner === op && t.status !== "done");
|
|
2379
|
+
workload.set(op, pts.length);
|
|
2380
|
+
pendingTasks.set(op, pts);
|
|
2381
|
+
}
|
|
2382
|
+
const sorted = [...operators].sort((a, b) => (workload.get(b) || 0) - (workload.get(a) || 0));
|
|
2383
|
+
const mostLoaded = sorted[0];
|
|
2384
|
+
const leastLoaded = sorted[sorted.length - 1];
|
|
2385
|
+
const maxLoad = workload.get(mostLoaded) || 0;
|
|
2386
|
+
const minLoad = workload.get(leastLoaded) || 0;
|
|
2387
|
+
if (maxLoad - minLoad <= maxImbalance) return [];
|
|
2388
|
+
const reassignments = [];
|
|
2389
|
+
const excess = maxLoad - Math.ceil((maxLoad + minLoad) / 2);
|
|
2390
|
+
const candidates = pendingTasks.get(mostLoaded) || [];
|
|
2391
|
+
let moved = 0;
|
|
2392
|
+
for (const task of candidates) {
|
|
2393
|
+
if (moved >= excess) break;
|
|
2394
|
+
if (this.isExplicitAssignment(task.title, operators)) continue;
|
|
2395
|
+
const taskZones = task.zone || [];
|
|
2396
|
+
const otherTasks = pendingTasks.get(leastLoaded) || [];
|
|
2397
|
+
const zoneConflict = taskZones.some((z2) => otherTasks.some((ot) => (ot.zone || []).includes(z2)));
|
|
2398
|
+
if (zoneConflict) continue;
|
|
2399
|
+
reassignments.push({
|
|
2400
|
+
taskId: task.id,
|
|
2401
|
+
from: mostLoaded,
|
|
2402
|
+
to: leastLoaded,
|
|
2403
|
+
reason: `rebalance: ${mostLoaded}=${maxLoad} \u2192 ${leastLoaded}=${minLoad} (diff=${maxLoad - minLoad} > ${maxImbalance})`
|
|
2404
|
+
});
|
|
2405
|
+
moved++;
|
|
2406
|
+
}
|
|
2407
|
+
return reassignments;
|
|
2408
|
+
}
|
|
2409
|
+
isExplicitAssignment(title, operators) {
|
|
2410
|
+
const detected = this.detectExplicitOwner(title, operators);
|
|
2411
|
+
return detected !== null && detected !== void 0;
|
|
2412
|
+
}
|
|
2413
|
+
};
|
|
2414
|
+
}
|
|
2415
|
+
});
|
|
2416
|
+
|
|
2215
2417
|
// src/core/injection.service.ts
|
|
2216
2418
|
import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
|
|
2217
2419
|
import { isAbsolute as isAbsolute3, join as join6, resolve as resolve3 } from "path";
|
|
@@ -2228,6 +2430,7 @@ var init_injection_service = __esm({
|
|
|
2228
2430
|
init_helpers();
|
|
2229
2431
|
init_git_service();
|
|
2230
2432
|
init_state_manager();
|
|
2433
|
+
init_task_distributor_service();
|
|
2231
2434
|
InjectionService = class {
|
|
2232
2435
|
constructor(root) {
|
|
2233
2436
|
this.root = root;
|
|
@@ -3804,6 +4007,29 @@ async function preflightCommand(options) {
|
|
|
3804
4007
|
operator = OperatorIdSchema.parse(operator);
|
|
3805
4008
|
}
|
|
3806
4009
|
header("ORESHNIK PREFLIGHT", (/* @__PURE__ */ new Date()).toISOString());
|
|
4010
|
+
try {
|
|
4011
|
+
const { execSync } = await import("child_process");
|
|
4012
|
+
const installed = execSync("npm list -g oreshnik-cli --depth=0 --json 2>nul", { encoding: "utf8", timeout: 1e4 });
|
|
4013
|
+
const latest = execSync("npm view oreshnik-cli version 2>nul", { encoding: "utf8", timeout: 1e4 });
|
|
4014
|
+
if (installed && latest) {
|
|
4015
|
+
const installedMatch = installed.match(/"oreshnik-cli":\s*\{[^}]*"version":\s*"([^"]+)"/);
|
|
4016
|
+
const installedVer = installedMatch?.[1];
|
|
4017
|
+
const latestVer = latest.trim();
|
|
4018
|
+
if (installedVer && latestVer && installedVer !== latestVer) {
|
|
4019
|
+
console.log(` [UPDATE] oreshnik-cli ${installedVer} \u2192 ${latestVer}`);
|
|
4020
|
+
try {
|
|
4021
|
+
execSync("npm install -g oreshnik-cli@alpha", { encoding: "utf8", timeout: 6e4, stdio: "inherit" });
|
|
4022
|
+
console.log(` [OK] Updated to ${latestVer}. Restarting...`);
|
|
4023
|
+
const { spawnSync: spawnSync4 } = await import("child_process");
|
|
4024
|
+
spawnSync4(process.argv[0], process.argv.slice(1), { stdio: "inherit", shell: true });
|
|
4025
|
+
process.exit(0);
|
|
4026
|
+
} catch {
|
|
4027
|
+
console.log(" [WARN] Update failed. Continuing with current version.");
|
|
4028
|
+
}
|
|
4029
|
+
}
|
|
4030
|
+
}
|
|
4031
|
+
} catch {
|
|
4032
|
+
}
|
|
3807
4033
|
const blockers = [];
|
|
3808
4034
|
const warnings = [];
|
|
3809
4035
|
const okChecks = [];
|
|
@@ -3958,6 +4184,31 @@ async function preflightCommand(options) {
|
|
|
3958
4184
|
console.log("");
|
|
3959
4185
|
}
|
|
3960
4186
|
}
|
|
4187
|
+
const zoneMapPathEarly = join11(ROOT2, "docs", "07_handoffs", "zone-map.json");
|
|
4188
|
+
if (sprint && existsSync10(zoneMapPathEarly)) {
|
|
4189
|
+
try {
|
|
4190
|
+
const zoneRaw = JSON.parse(readFileSync10(zoneMapPathEarly, "utf8").replace(/^\uFEFF/, ""));
|
|
4191
|
+
const zones = zoneRaw.zones || {};
|
|
4192
|
+
let acquiredCount = 0;
|
|
4193
|
+
for (const [pattern, zone] of Object.entries(zones)) {
|
|
4194
|
+
const lockStr = zone.lock || "";
|
|
4195
|
+
const ownerStr = zone.owner || "";
|
|
4196
|
+
const sprints = zone.sprints || [];
|
|
4197
|
+
if (ownerStr !== "none" && lockStr !== "forbidden") {
|
|
4198
|
+
const myLock = lockResult.ok ? lockResult.value.active.find((l) => l.operator === operator && l.zone === pattern) : null;
|
|
4199
|
+
if (!myLock) {
|
|
4200
|
+
const acquireResult = lockService.acquire(pattern, operator, sprint, 7200);
|
|
4201
|
+
if (acquireResult.ok && acquireResult.value.acquired) acquiredCount++;
|
|
4202
|
+
}
|
|
4203
|
+
}
|
|
4204
|
+
}
|
|
4205
|
+
if (acquiredCount > 0) {
|
|
4206
|
+
console.log(` [LOCK] Auto-acquired ${acquiredCount} zone lock(s) for sprint ${sprint}`);
|
|
4207
|
+
console.log("");
|
|
4208
|
+
}
|
|
4209
|
+
} catch {
|
|
4210
|
+
}
|
|
4211
|
+
}
|
|
3961
4212
|
log("INFO", "5/11 Canonical alignment");
|
|
3962
4213
|
const canonical2 = createCanonicalService(state, ROOT2);
|
|
3963
4214
|
const taskBoardPath = join11(ROOT2, "var", "oreshnik", "task-board.json");
|
|
@@ -4054,6 +4305,28 @@ async function preflightCommand(options) {
|
|
|
4054
4305
|
if (poolReadyTasks.length > 5) console.log(` ... and ${poolReadyTasks.length - 5} more`);
|
|
4055
4306
|
console.log("");
|
|
4056
4307
|
}
|
|
4308
|
+
if (poolTasks.length > 3 && operatorTasks.length < 3) {
|
|
4309
|
+
try {
|
|
4310
|
+
const { createTaskDistributor: createTaskDistributor3 } = await Promise.resolve().then(() => (init_task_distributor_service(), task_distributor_service_exports));
|
|
4311
|
+
const distributor = createTaskDistributor3();
|
|
4312
|
+
const zoneMapForRebalance = {};
|
|
4313
|
+
const zonePath = join11(ROOT2, "docs", "07_handoffs", "zone-map.json");
|
|
4314
|
+
if (existsSync10(zonePath)) {
|
|
4315
|
+
const raw = JSON.parse(readFileSync10(zonePath, "utf8").replace(/^\uFEFF/, ""));
|
|
4316
|
+
for (const [k, v] of Object.entries(raw.zones || {})) zoneMapForRebalance[k] = v;
|
|
4317
|
+
}
|
|
4318
|
+
const rebalances = distributor.rebalance(board, zoneMapForRebalance);
|
|
4319
|
+
if (rebalances.length > 0) {
|
|
4320
|
+
console.log(` [REBALANCE] Load imbalance detected:`);
|
|
4321
|
+
for (const r of rebalances) {
|
|
4322
|
+
console.log(` ${r.from} \u2192 ${r.to}: ${r.taskId.slice(0, 50)}`);
|
|
4323
|
+
}
|
|
4324
|
+
console.log(` Run: oreshnik inject --rebalance to apply`);
|
|
4325
|
+
console.log("");
|
|
4326
|
+
}
|
|
4327
|
+
} catch {
|
|
4328
|
+
}
|
|
4329
|
+
}
|
|
4057
4330
|
const otherActiveTasks = board.tasks.filter(
|
|
4058
4331
|
(t) => t.owner.toLowerCase() !== operator.toLowerCase() && !(t.owner.toLowerCase() === "architect" && operator.toLowerCase() === "manuel") && (t.status === "active" || t.status === "ready")
|
|
4059
4332
|
);
|
|
@@ -5250,6 +5523,22 @@ async function closeCommand(options) {
|
|
|
5250
5523
|
mustGit(git4.push("origin", newMother), `Failed to push mother branch ${newMother}`);
|
|
5251
5524
|
mustGit(git4.pushTag("origin", tagName), `Failed to push checkpoint tag ${tagName}`);
|
|
5252
5525
|
log("OK", "Pushed child branch, mother branch, and checkpoint tag.");
|
|
5526
|
+
try {
|
|
5527
|
+
const { createLockService: createLockService2 } = await Promise.resolve().then(() => (init_lock_service(), lock_service_exports));
|
|
5528
|
+
const lockService = createLockService2(ROOT13);
|
|
5529
|
+
const zoneMapPath = join14(ROOT13, "docs", "07_handoffs", "zone-map.json");
|
|
5530
|
+
if (existsSync14(zoneMapPath)) {
|
|
5531
|
+
const zoneRaw = JSON.parse(readFileSync13(zoneMapPath, "utf8").replace(/^\uFEFF/, ""));
|
|
5532
|
+
const zones = zoneRaw.zones || {};
|
|
5533
|
+
let released = 0;
|
|
5534
|
+
for (const [pattern] of Object.entries(zones)) {
|
|
5535
|
+
lockService.release(pattern);
|
|
5536
|
+
released++;
|
|
5537
|
+
}
|
|
5538
|
+
if (released > 0) log("INFO", `Released ${released} zone lock(s).`);
|
|
5539
|
+
}
|
|
5540
|
+
} catch {
|
|
5541
|
+
}
|
|
5253
5542
|
mustGit(git4.checkout(branch), `Failed to checkout ${branch}`);
|
|
5254
5543
|
console.log("");
|
|
5255
5544
|
console.log(`SPRINT CLOSED: ${sprint}`);
|