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 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}`);