oreshnik-cli 0.1.3 → 0.1.5

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 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,33 @@ 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 treeDirty = git2.statusPorcelain();
4013
+ const isClean = treeDirty.ok && treeDirty.value.length === 0;
4014
+ if (isClean) {
4015
+ const installed = execSync("npm list -g oreshnik-cli --depth=0 --json 2>nul", { encoding: "utf8", timeout: 1e4 });
4016
+ const latest = execSync("npm view oreshnik-cli version 2>nul", { encoding: "utf8", timeout: 1e4 });
4017
+ if (installed && latest) {
4018
+ const installedMatch = installed.match(/"oreshnik-cli":\s*\{[^}]*"version":\s*"([^"]+)"/);
4019
+ const installedVer = installedMatch?.[1];
4020
+ const latestVer = latest.trim();
4021
+ if (installedVer && latestVer && installedVer !== latestVer) {
4022
+ console.log(` [UPDATE] oreshnik-cli ${installedVer} \u2192 ${latestVer} (tree clean, safe to update)`);
4023
+ try {
4024
+ execSync("npm install -g oreshnik-cli@alpha", { encoding: "utf8", timeout: 6e4, stdio: "inherit" });
4025
+ console.log(` [OK] Updated to ${latestVer}. Restarting preflight...`);
4026
+ const { spawnSync: spawnSync4 } = await import("child_process");
4027
+ spawnSync4(process.argv[0], process.argv.slice(1), { stdio: "inherit", shell: true });
4028
+ process.exit(0);
4029
+ } catch {
4030
+ console.log(" [WARN] Update failed. Continuing with current version.");
4031
+ }
4032
+ }
4033
+ }
4034
+ }
4035
+ } catch {
4036
+ }
3807
4037
  const blockers = [];
3808
4038
  const warnings = [];
3809
4039
  const okChecks = [];
@@ -4079,6 +4309,28 @@ async function preflightCommand(options) {
4079
4309
  if (poolReadyTasks.length > 5) console.log(` ... and ${poolReadyTasks.length - 5} more`);
4080
4310
  console.log("");
4081
4311
  }
4312
+ if (poolTasks.length > 3 && operatorTasks.length < 3) {
4313
+ try {
4314
+ const { createTaskDistributor: createTaskDistributor3 } = await Promise.resolve().then(() => (init_task_distributor_service(), task_distributor_service_exports));
4315
+ const distributor = createTaskDistributor3();
4316
+ const zoneMapForRebalance = {};
4317
+ const zonePath = join11(ROOT2, "docs", "07_handoffs", "zone-map.json");
4318
+ if (existsSync10(zonePath)) {
4319
+ const raw = JSON.parse(readFileSync10(zonePath, "utf8").replace(/^\uFEFF/, ""));
4320
+ for (const [k, v] of Object.entries(raw.zones || {})) zoneMapForRebalance[k] = v;
4321
+ }
4322
+ const rebalances = distributor.rebalance(board, zoneMapForRebalance);
4323
+ if (rebalances.length > 0) {
4324
+ console.log(` [REBALANCE] Load imbalance detected:`);
4325
+ for (const r of rebalances) {
4326
+ console.log(` ${r.from} \u2192 ${r.to}: ${r.taskId.slice(0, 50)}`);
4327
+ }
4328
+ console.log(` Run: oreshnik inject --rebalance to apply`);
4329
+ console.log("");
4330
+ }
4331
+ } catch {
4332
+ }
4333
+ }
4082
4334
  const otherActiveTasks = board.tasks.filter(
4083
4335
  (t) => t.owner.toLowerCase() !== operator.toLowerCase() && !(t.owner.toLowerCase() === "architect" && operator.toLowerCase() === "manuel") && (t.status === "active" || t.status === "ready")
4084
4336
  );