oreshnik-cli 0.1.3 → 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 +248 -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 = [];
|
|
@@ -4079,6 +4305,28 @@ async function preflightCommand(options) {
|
|
|
4079
4305
|
if (poolReadyTasks.length > 5) console.log(` ... and ${poolReadyTasks.length - 5} more`);
|
|
4080
4306
|
console.log("");
|
|
4081
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
|
+
}
|
|
4082
4330
|
const otherActiveTasks = board.tasks.filter(
|
|
4083
4331
|
(t) => t.owner.toLowerCase() !== operator.toLowerCase() && !(t.owner.toLowerCase() === "architect" && operator.toLowerCase() === "manuel") && (t.status === "active" || t.status === "ready")
|
|
4084
4332
|
);
|