@kitsy/coop-core 0.0.1 → 2.0.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/dist/index.js CHANGED
@@ -1,3 +1,116 @@
1
+ import {
2
+ allocate,
3
+ allocate_ai,
4
+ allocate_ai_tokens,
5
+ build_capacity_ledger,
6
+ check_wip,
7
+ create_seeded_rng,
8
+ effective_weekly_hours,
9
+ effort_or_default,
10
+ get_remaining_tokens,
11
+ monte_carlo_forecast,
12
+ pert_hours,
13
+ pert_stddev,
14
+ run_monte_carlo_chunk,
15
+ sample_pert_beta,
16
+ sample_task_hours,
17
+ simulate_schedule,
18
+ task_effort_hours
19
+ } from "./chunk-UK4JN4TZ.js";
20
+
21
+ // src/graph/external.ts
22
+ var EXTERNAL_PREFIX = "external:";
23
+ function is_external_dependency(value) {
24
+ return value.trim().toLowerCase().startsWith(EXTERNAL_PREFIX);
25
+ }
26
+ function parse_external_dependency(value) {
27
+ const normalized = value.trim();
28
+ if (!is_external_dependency(normalized)) {
29
+ return null;
30
+ }
31
+ const rest = normalized.slice(EXTERNAL_PREFIX.length);
32
+ const separator = rest.indexOf("/");
33
+ if (separator <= 0 || separator === rest.length - 1) {
34
+ return null;
35
+ }
36
+ const repo = rest.slice(0, separator).trim();
37
+ const taskId = rest.slice(separator + 1).trim();
38
+ if (!repo || !taskId) {
39
+ return null;
40
+ }
41
+ return {
42
+ raw: normalized,
43
+ repo,
44
+ task_id: taskId
45
+ };
46
+ }
47
+ function external_dependencies_for_task(graph, taskId) {
48
+ return [...graph.external_dependencies?.get(taskId) ?? []];
49
+ }
50
+ function joinUrl(baseUrl, pathName) {
51
+ return `${baseUrl.replace(/\/+$/g, "")}${pathName.startsWith("/") ? pathName : `/${pathName}`}`;
52
+ }
53
+ function resolvedStatus(taskStatus) {
54
+ if (!taskStatus) return "unknown";
55
+ if (taskStatus === "done" || taskStatus === "canceled") return "resolved";
56
+ return "blocked";
57
+ }
58
+ async function resolve_external_dependencies(graph, options) {
59
+ const fetchImpl = options.fetch_impl ?? fetch;
60
+ const resolutions = /* @__PURE__ */ new Map();
61
+ const externalMap = graph.external_dependencies ?? /* @__PURE__ */ new Map();
62
+ for (const [taskId, deps] of externalMap.entries()) {
63
+ const results = [];
64
+ for (const dep of deps) {
65
+ const remote = options.remotes[dep.repo];
66
+ if (!remote?.base_url) {
67
+ results.push({
68
+ dependency: dep,
69
+ status: "unknown",
70
+ warning: `No API base URL configured for external repo '${dep.repo}'.`
71
+ });
72
+ continue;
73
+ }
74
+ try {
75
+ const response = await fetchImpl(joinUrl(remote.base_url, `/api/tasks/${encodeURIComponent(dep.task_id)}`));
76
+ if (response.status === 404) {
77
+ results.push({
78
+ dependency: dep,
79
+ status: "missing",
80
+ warning: `External task '${dep.raw}' was not found at ${remote.base_url}.`
81
+ });
82
+ continue;
83
+ }
84
+ if (!response.ok) {
85
+ results.push({
86
+ dependency: dep,
87
+ status: "unreachable",
88
+ warning: `External task '${dep.raw}' returned HTTP ${response.status}.`
89
+ });
90
+ continue;
91
+ }
92
+ const payload = await response.json();
93
+ const taskRecord = payload && typeof payload === "object" && payload.task && typeof payload.task === "object" ? payload.task : payload;
94
+ const taskStatus = typeof taskRecord.status === "string" ? taskRecord.status : null;
95
+ results.push({
96
+ dependency: dep,
97
+ status: resolvedStatus(taskStatus),
98
+ task_status: taskStatus,
99
+ warning: resolvedStatus(taskStatus) === "resolved" ? void 0 : `External task '${dep.raw}' is ${taskStatus ?? "unknown"}.`
100
+ });
101
+ } catch {
102
+ results.push({
103
+ dependency: dep,
104
+ status: "unreachable",
105
+ warning: `External task '${dep.raw}' could not be reached.`
106
+ });
107
+ }
108
+ }
109
+ resolutions.set(taskId, results);
110
+ }
111
+ return resolutions;
112
+ }
113
+
1
114
  // src/graph/dag.ts
2
115
  function existingDeps(graph, nodeId) {
3
116
  const deps = graph.forward.get(nodeId) ?? /* @__PURE__ */ new Set();
@@ -118,13 +231,25 @@ function build_graph(tasks, context = {}) {
118
231
  }
119
232
  const forward = /* @__PURE__ */ new Map();
120
233
  const reverse = /* @__PURE__ */ new Map();
234
+ const externalDependencies = /* @__PURE__ */ new Map();
121
235
  for (const nodeId of nodes.keys()) {
122
236
  forward.set(nodeId, /* @__PURE__ */ new Set());
123
237
  reverse.set(nodeId, /* @__PURE__ */ new Set());
238
+ externalDependencies.set(nodeId, []);
124
239
  }
125
240
  for (const task of tasks) {
126
- const deps = new Set(task.depends_on ?? []);
241
+ const deps = /* @__PURE__ */ new Set();
242
+ const externalDeps = [];
243
+ for (const depId of task.depends_on ?? []) {
244
+ const external = parse_external_dependency(depId);
245
+ if (external) {
246
+ externalDeps.push(external);
247
+ continue;
248
+ }
249
+ deps.add(depId);
250
+ }
127
251
  forward.set(task.id, deps);
252
+ externalDependencies.set(task.id, externalDeps);
128
253
  for (const depId of deps) {
129
254
  if (!nodes.has(depId)) continue;
130
255
  reverse.get(depId)?.add(task.id);
@@ -137,16 +262,266 @@ function build_graph(tasks, context = {}) {
137
262
  topological_order: [],
138
263
  tracks: context.tracks ? new Map(context.tracks) : /* @__PURE__ */ new Map(),
139
264
  resources: context.resources ? new Map(context.resources) : /* @__PURE__ */ new Map(),
140
- deliveries: context.deliveries ? new Map(context.deliveries) : /* @__PURE__ */ new Map()
265
+ deliveries: context.deliveries ? new Map(context.deliveries) : /* @__PURE__ */ new Map(),
266
+ external_dependencies: externalDependencies
141
267
  };
142
268
  graph.topological_order = topological_sort(graph);
143
269
  return graph;
144
270
  }
145
271
 
146
- // src/graph/loader.ts
272
+ // src/graph/subgraph.ts
273
+ function extract_subgraph(graph, taskIds) {
274
+ const selected = new Set(taskIds.filter((taskId) => graph.nodes.has(taskId)));
275
+ const nodes = /* @__PURE__ */ new Map();
276
+ for (const taskId of selected) {
277
+ const task = graph.nodes.get(taskId);
278
+ if (task) nodes.set(taskId, task);
279
+ }
280
+ const forward = /* @__PURE__ */ new Map();
281
+ const reverse = /* @__PURE__ */ new Map();
282
+ for (const taskId of selected) {
283
+ forward.set(taskId, /* @__PURE__ */ new Set());
284
+ reverse.set(taskId, /* @__PURE__ */ new Set());
285
+ }
286
+ for (const taskId of selected) {
287
+ const deps = graph.forward.get(taskId) ?? /* @__PURE__ */ new Set();
288
+ const internalDeps = new Set(Array.from(deps).filter((depId) => selected.has(depId)));
289
+ forward.set(taskId, internalDeps);
290
+ for (const depId of internalDeps) {
291
+ reverse.get(depId)?.add(taskId);
292
+ }
293
+ }
294
+ const subgraph = {
295
+ nodes,
296
+ forward,
297
+ reverse,
298
+ topological_order: [],
299
+ tracks: new Map(graph.tracks),
300
+ resources: new Map(graph.resources),
301
+ deliveries: new Map(graph.deliveries),
302
+ external_dependencies: new Map(
303
+ Array.from(selected).map((taskId) => [taskId, external_dependencies_for_task(graph, taskId)])
304
+ )
305
+ };
306
+ subgraph.topological_order = topological_sort(subgraph);
307
+ return subgraph;
308
+ }
309
+ function find_external_dependencies(subgraph, fullGraph) {
310
+ const external = /* @__PURE__ */ new Set();
311
+ for (const taskId of subgraph.nodes.keys()) {
312
+ const deps = fullGraph.forward.get(taskId) ?? /* @__PURE__ */ new Set();
313
+ for (const depId of deps) {
314
+ if (!subgraph.nodes.has(depId) && fullGraph.nodes.has(depId)) {
315
+ external.add(depId);
316
+ }
317
+ }
318
+ for (const dep of external_dependencies_for_task(fullGraph, taskId)) {
319
+ external.add(dep.raw);
320
+ }
321
+ }
322
+ return Array.from(external).sort((a, b) => a.localeCompare(b));
323
+ }
324
+
325
+ // src/graph/critical-path.ts
326
+ var DEFAULT_CONFIG = {
327
+ version: 2,
328
+ project: {
329
+ name: "COOP",
330
+ id: "coop"
331
+ },
332
+ defaults: {
333
+ task: {
334
+ complexity: "medium"
335
+ }
336
+ }
337
+ };
338
+ var EPSILON = 1e-9;
339
+ function durationHours(task) {
340
+ return effort_or_default(task, DEFAULT_CONFIG);
341
+ }
342
+ function rootsOf(graph) {
343
+ const roots = [];
344
+ for (const id of graph.nodes.keys()) {
345
+ if ((graph.forward.get(id)?.size ?? 0) === 0) {
346
+ roots.push(id);
347
+ }
348
+ }
349
+ return roots;
350
+ }
351
+ function leavesOf(graph) {
352
+ const leaves = [];
353
+ for (const id of graph.nodes.keys()) {
354
+ if ((graph.reverse.get(id)?.size ?? 0) === 0) {
355
+ leaves.push(id);
356
+ }
357
+ }
358
+ return leaves;
359
+ }
360
+ function compute_critical_path(delivery, graph) {
361
+ const include = new Set(delivery.scope.include ?? []);
362
+ for (const excluded of delivery.scope.exclude ?? []) {
363
+ include.delete(excluded);
364
+ }
365
+ const subgraph = extract_subgraph(graph, [...include]);
366
+ if (subgraph.nodes.size === 0) {
367
+ return {
368
+ critical_path: [],
369
+ project_duration_hours: 0,
370
+ per_task: []
371
+ };
372
+ }
373
+ const roots = new Set(rootsOf(subgraph));
374
+ const leaves = new Set(leavesOf(subgraph));
375
+ const es = /* @__PURE__ */ new Map();
376
+ const ef = /* @__PURE__ */ new Map();
377
+ const ls = /* @__PURE__ */ new Map();
378
+ const lf = /* @__PURE__ */ new Map();
379
+ const durations = /* @__PURE__ */ new Map();
380
+ for (const taskId of subgraph.topological_order) {
381
+ const task = subgraph.nodes.get(taskId);
382
+ if (!task) continue;
383
+ const duration = durationHours(task);
384
+ durations.set(taskId, duration);
385
+ let earliestStart = 0;
386
+ if (!roots.has(taskId)) {
387
+ const deps = [...subgraph.forward.get(taskId) ?? /* @__PURE__ */ new Set()];
388
+ earliestStart = deps.length === 0 ? 0 : Math.max(...deps.map((depId) => ef.get(depId) ?? 0));
389
+ }
390
+ es.set(taskId, earliestStart);
391
+ ef.set(taskId, earliestStart + duration);
392
+ }
393
+ const project_duration_hours = Math.max(...[...leaves].map((id) => ef.get(id) ?? 0));
394
+ const reverseTopo = [...subgraph.topological_order].reverse();
395
+ for (const taskId of reverseTopo) {
396
+ const duration = durations.get(taskId) ?? 0;
397
+ let latestFinish = project_duration_hours;
398
+ if (!leaves.has(taskId)) {
399
+ const successors = [...subgraph.reverse.get(taskId) ?? /* @__PURE__ */ new Set()];
400
+ latestFinish = successors.length === 0 ? project_duration_hours : Math.min(...successors.map((id) => ls.get(id) ?? project_duration_hours));
401
+ }
402
+ lf.set(taskId, latestFinish);
403
+ ls.set(taskId, latestFinish - duration);
404
+ }
405
+ const per_task = [];
406
+ for (const id of subgraph.nodes.keys()) {
407
+ const ES = es.get(id) ?? 0;
408
+ const EF = ef.get(id) ?? 0;
409
+ const LS = ls.get(id) ?? 0;
410
+ const LF = lf.get(id) ?? 0;
411
+ const slack = LS - ES;
412
+ per_task.push({
413
+ id,
414
+ ES,
415
+ EF,
416
+ LS,
417
+ LF,
418
+ slack,
419
+ on_critical_path: Math.abs(slack) <= EPSILON
420
+ });
421
+ }
422
+ per_task.sort((a, b) => {
423
+ if (a.ES !== b.ES) return a.ES - b.ES;
424
+ return a.id.localeCompare(b.id);
425
+ });
426
+ const critical_path = per_task.filter((task) => task.on_critical_path).map((task) => task.id);
427
+ return {
428
+ critical_path,
429
+ project_duration_hours,
430
+ per_task
431
+ };
432
+ }
433
+
434
+ // src/index/index-manager.ts
147
435
  import fs5 from "fs";
148
436
  import path from "path";
149
437
 
438
+ // src/planning/readiness.ts
439
+ function isResolvedDependencyStatus(status) {
440
+ return status === "done" || status === "canceled";
441
+ }
442
+ function compute_readiness(task, graph) {
443
+ if (task.status === "done" || task.status === "canceled") {
444
+ return "done";
445
+ }
446
+ if (task.status === "in_review") {
447
+ return "waiting_review";
448
+ }
449
+ if (task.status === "in_progress") {
450
+ return "in_progress";
451
+ }
452
+ for (const depId of task.depends_on ?? []) {
453
+ const dep = graph.nodes.get(depId);
454
+ if (!dep || !isResolvedDependencyStatus(dep.status)) {
455
+ return "blocked";
456
+ }
457
+ }
458
+ return "ready";
459
+ }
460
+ function compute_all_readiness(graph) {
461
+ const readiness = /* @__PURE__ */ new Map();
462
+ for (const [taskId, task] of graph.nodes.entries()) {
463
+ readiness.set(taskId, compute_readiness(task, graph));
464
+ }
465
+ return readiness;
466
+ }
467
+ function partition_by_readiness(graph) {
468
+ const partitions = {
469
+ ready: [],
470
+ blocked: [],
471
+ in_progress: [],
472
+ waiting_review: [],
473
+ done: []
474
+ };
475
+ for (const task of graph.nodes.values()) {
476
+ const state = compute_readiness(task, graph);
477
+ partitions[state].push(task);
478
+ }
479
+ return partitions;
480
+ }
481
+ function compute_readiness_with_corrections(graph) {
482
+ const readiness = /* @__PURE__ */ new Map();
483
+ const corrections = [];
484
+ const warnings = [];
485
+ const partitions = {
486
+ ready: [],
487
+ blocked: [],
488
+ in_progress: [],
489
+ waiting_review: [],
490
+ done: []
491
+ };
492
+ for (const [taskId, task] of graph.nodes.entries()) {
493
+ const state = compute_readiness(task, graph);
494
+ readiness.set(taskId, state);
495
+ partitions[state].push(task);
496
+ if (task.status === "blocked" && state === "ready") {
497
+ corrections.push({
498
+ type: "task.transitioned",
499
+ task_id: taskId,
500
+ from: "blocked",
501
+ to: "todo",
502
+ reason: "All dependencies are resolved."
503
+ });
504
+ }
505
+ if (task.status === "todo" && state === "blocked") {
506
+ const unresolved = (task.depends_on ?? []).filter((depId) => {
507
+ const depTask = graph.nodes.get(depId);
508
+ return !depTask || !isResolvedDependencyStatus(depTask.status);
509
+ });
510
+ warnings.push({
511
+ code: "todo_with_unresolved_dependencies",
512
+ task_id: taskId,
513
+ message: `Task is todo but has unresolved dependencies: ${unresolved.join(", ")}.`
514
+ });
515
+ }
516
+ }
517
+ return {
518
+ readiness,
519
+ partitions,
520
+ corrections,
521
+ warnings
522
+ };
523
+ }
524
+
150
525
  // src/parser/delivery-parser.ts
151
526
  import fs3 from "fs";
152
527
 
@@ -314,6 +689,12 @@ function parseYamlFile(filePath) {
314
689
  const content = fs2.readFileSync(filePath, "utf8");
315
690
  return parseYamlContent(content, filePath);
316
691
  }
692
+ function stringifyYamlContent(content) {
693
+ return YAML2.stringify(content);
694
+ }
695
+ function writeYamlFile(filePath, content) {
696
+ fs2.writeFileSync(filePath, stringifyYamlContent(content), "utf8");
697
+ }
317
698
 
318
699
  // src/parser/delivery-parser.ts
319
700
  function asStringArray(value) {
@@ -410,7 +791,7 @@ function parseTaskFile(filePath) {
410
791
  return parseTaskContent(content, filePath);
411
792
  }
412
793
 
413
- // src/graph/loader.ts
794
+ // src/index/index-manager.ts
414
795
  function walkFiles(dirPath, extensions) {
415
796
  if (!fs5.existsSync(dirPath)) return [];
416
797
  const out = [];
@@ -429,114 +810,455 @@ function walkFiles(dirPath, extensions) {
429
810
  }
430
811
  return out.sort((a, b) => a.localeCompare(b));
431
812
  }
432
- function loadTasks(tasksDir) {
433
- const files = walkFiles(tasksDir, /* @__PURE__ */ new Set([".md"]));
434
- const tasks = [];
435
- const seen = /* @__PURE__ */ new Set();
813
+ function toPosixPath(input) {
814
+ return input.replace(/\\/g, "/");
815
+ }
816
+ function fromRecordMap(value) {
817
+ return new Map(Object.entries(value));
818
+ }
819
+ function toRecordMap(value) {
820
+ return Object.fromEntries(value.entries());
821
+ }
822
+ function round2(value) {
823
+ return Number(value.toFixed(2));
824
+ }
825
+ function parseTaskCollection(files, baseDir) {
826
+ const tasks = /* @__PURE__ */ new Map();
827
+ const fileToId = {};
436
828
  for (const filePath of files) {
437
- const parsed = parseTaskFile(filePath);
438
- if (seen.has(parsed.task.id)) {
439
- throw new Error(`Duplicate task id '${parsed.task.id}' found at ${filePath}.`);
829
+ const parsed = parseTaskFile(filePath).task;
830
+ if (tasks.has(parsed.id)) {
831
+ throw new Error(`Duplicate task id '${parsed.id}' found at ${filePath}.`);
440
832
  }
441
- seen.add(parsed.task.id);
442
- tasks.push(parsed.task);
833
+ tasks.set(parsed.id, parsed);
834
+ fileToId[toPosixPath(path.relative(baseDir, filePath))] = parsed.id;
443
835
  }
444
- return tasks;
836
+ return { tasks, fileToId };
445
837
  }
446
- function loadTracks(tracksDir) {
447
- const files = walkFiles(tracksDir, /* @__PURE__ */ new Set([".yml", ".yaml"]));
838
+ function parseTrackCollection(files, baseDir) {
448
839
  const tracks = /* @__PURE__ */ new Map();
840
+ const fileToId = {};
449
841
  for (const filePath of files) {
450
- const data = parseYamlFile(filePath);
451
- if (!data.id) continue;
452
- tracks.set(data.id, data);
842
+ const parsed = parseYamlFile(filePath);
843
+ if (!parsed.id) continue;
844
+ tracks.set(parsed.id, parsed);
845
+ fileToId[toPosixPath(path.relative(baseDir, filePath))] = parsed.id;
453
846
  }
454
- return tracks;
847
+ return { tracks, fileToId };
455
848
  }
456
- function loadResources(resourcesDir) {
457
- const files = walkFiles(resourcesDir, /* @__PURE__ */ new Set([".yml", ".yaml"]));
849
+ function parseResourceCollection(files, baseDir) {
458
850
  const resources = /* @__PURE__ */ new Map();
851
+ const fileToId = {};
459
852
  for (const filePath of files) {
460
- const data = parseYamlFile(filePath);
461
- if (!data.id) continue;
462
- resources.set(data.id, data);
853
+ const parsed = parseYamlFile(filePath);
854
+ if (!parsed.id) continue;
855
+ resources.set(parsed.id, parsed);
856
+ fileToId[toPosixPath(path.relative(baseDir, filePath))] = parsed.id;
463
857
  }
464
- return resources;
858
+ return { resources, fileToId };
465
859
  }
466
- function loadDeliveries(deliveriesDir) {
467
- const files = walkFiles(deliveriesDir, /* @__PURE__ */ new Set([".yml", ".yaml", ".md"]));
860
+ function parseDeliveryCollection(files, baseDir) {
468
861
  const deliveries = /* @__PURE__ */ new Map();
862
+ const fileToId = {};
469
863
  for (const filePath of files) {
470
- const parsed = parseDeliveryFile(filePath);
471
- deliveries.set(parsed.delivery.id, parsed.delivery);
864
+ const parsed = parseDeliveryFile(filePath).delivery;
865
+ deliveries.set(parsed.id, parsed);
866
+ fileToId[toPosixPath(path.relative(baseDir, filePath))] = parsed.id;
472
867
  }
473
- return deliveries;
474
- }
475
- function load_graph(coopDir) {
476
- const tasks = loadTasks(path.join(coopDir, "tasks"));
477
- const tracks = loadTracks(path.join(coopDir, "tracks"));
478
- const resources = loadResources(path.join(coopDir, "resources"));
479
- const deliveries = loadDeliveries(path.join(coopDir, "deliveries"));
480
- return build_graph(tasks, {
481
- tracks,
482
- resources,
483
- deliveries
484
- });
868
+ return { deliveries, fileToId };
485
869
  }
486
-
487
- // src/graph/subgraph.ts
488
- function extract_subgraph(graph, taskIds) {
489
- const selected = new Set(taskIds.filter((taskId) => graph.nodes.has(taskId)));
490
- const nodes = /* @__PURE__ */ new Map();
491
- for (const taskId of selected) {
492
- const task = graph.nodes.get(taskId);
493
- if (task) nodes.set(taskId, task);
494
- }
495
- const forward = /* @__PURE__ */ new Map();
496
- const reverse = /* @__PURE__ */ new Map();
497
- for (const taskId of selected) {
498
- forward.set(taskId, /* @__PURE__ */ new Set());
499
- reverse.set(taskId, /* @__PURE__ */ new Set());
870
+ var IndexManager = class {
871
+ coopDir;
872
+ indexDir;
873
+ graphPath;
874
+ tasksPath;
875
+ capacityPath;
876
+ constructor(coopDir) {
877
+ this.coopDir = path.resolve(coopDir);
878
+ this.indexDir = path.join(this.coopDir, ".index");
879
+ this.graphPath = path.join(this.indexDir, "graph.json");
880
+ this.tasksPath = path.join(this.indexDir, "tasks.json");
881
+ this.capacityPath = path.join(this.indexDir, "capacity.json");
882
+ }
883
+ is_stale() {
884
+ return this.status().stale;
885
+ }
886
+ status() {
887
+ const scan = this.scanSources();
888
+ if (!fs5.existsSync(this.graphPath)) {
889
+ return {
890
+ exists: false,
891
+ stale: true,
892
+ graph_path: this.graphPath,
893
+ source_max_mtime_ms: scan.sourceMaxMtimeMs,
894
+ changed_files: Object.keys(scan.sourceFiles).sort((a, b) => a.localeCompare(b)),
895
+ reason: "graph index missing"
896
+ };
897
+ }
898
+ const indexed = this.readGraphIndexFile();
899
+ if (!indexed) {
900
+ return {
901
+ exists: true,
902
+ stale: true,
903
+ graph_path: this.graphPath,
904
+ source_max_mtime_ms: scan.sourceMaxMtimeMs,
905
+ changed_files: Object.keys(scan.sourceFiles).sort((a, b) => a.localeCompare(b)),
906
+ reason: "graph index unreadable"
907
+ };
908
+ }
909
+ const changed = /* @__PURE__ */ new Set();
910
+ for (const [relativePath, mtime] of Object.entries(scan.sourceFiles)) {
911
+ const indexedMtime = indexed.source_files[relativePath];
912
+ if (indexedMtime === void 0 || indexedMtime !== mtime) {
913
+ changed.add(relativePath);
914
+ }
915
+ }
916
+ for (const relativePath of Object.keys(indexed.source_files)) {
917
+ if (scan.sourceFiles[relativePath] === void 0) {
918
+ changed.add(relativePath);
919
+ }
920
+ }
921
+ return {
922
+ exists: true,
923
+ stale: changed.size > 0 || indexed.version !== 1,
924
+ graph_path: this.graphPath,
925
+ source_max_mtime_ms: scan.sourceMaxMtimeMs,
926
+ changed_files: Array.from(changed).sort((a, b) => a.localeCompare(b)),
927
+ generated_at: indexed.generated_at,
928
+ reason: indexed.version === 1 ? void 0 : "index version mismatch"
929
+ };
500
930
  }
501
- for (const taskId of selected) {
502
- const deps = graph.forward.get(taskId) ?? /* @__PURE__ */ new Set();
503
- const internalDeps = new Set(Array.from(deps).filter((depId) => selected.has(depId)));
504
- forward.set(taskId, internalDeps);
505
- for (const depId of internalDeps) {
506
- reverse.get(depId)?.add(taskId);
931
+ load_indexed_graph() {
932
+ const indexed = this.readGraphIndexFile();
933
+ if (!indexed) return null;
934
+ try {
935
+ return this.deserializeGraph(indexed.graph);
936
+ } catch {
937
+ return null;
507
938
  }
508
939
  }
509
- const subgraph = {
510
- nodes,
511
- forward,
512
- reverse,
513
- topological_order: [],
514
- tracks: new Map(graph.tracks),
515
- resources: new Map(graph.resources),
516
- deliveries: new Map(graph.deliveries)
517
- };
518
- subgraph.topological_order = topological_sort(subgraph);
519
- return subgraph;
520
- }
521
- function find_external_dependencies(subgraph, fullGraph) {
522
- const external = /* @__PURE__ */ new Set();
523
- for (const taskId of subgraph.nodes.keys()) {
524
- const deps = fullGraph.forward.get(taskId) ?? /* @__PURE__ */ new Set();
525
- for (const depId of deps) {
526
- if (!subgraph.nodes.has(depId) && fullGraph.nodes.has(depId)) {
527
- external.add(depId);
940
+ build_full_index() {
941
+ const scan = this.scanSources();
942
+ const taskData = parseTaskCollection(scan.taskFiles, this.coopDir);
943
+ const trackData = parseTrackCollection(scan.trackFiles, this.coopDir);
944
+ const resourceData = parseResourceCollection(scan.resourceFiles, this.coopDir);
945
+ const deliveryData = parseDeliveryCollection(scan.deliveryFiles, this.coopDir);
946
+ const graph = build_graph(Array.from(taskData.tasks.values()), {
947
+ tracks: trackData.tracks,
948
+ resources: resourceData.resources,
949
+ deliveries: deliveryData.deliveries
950
+ });
951
+ this.writeIndexFiles(
952
+ graph,
953
+ scan,
954
+ {
955
+ tasks: taskData.fileToId,
956
+ tracks: trackData.fileToId,
957
+ resources: resourceData.fileToId,
958
+ deliveries: deliveryData.fileToId
528
959
  }
529
- }
960
+ );
961
+ return graph;
530
962
  }
531
- return Array.from(external).sort((a, b) => a.localeCompare(b));
532
- }
533
-
534
- // src/graph/validator.ts
535
- function error(invariant, message, task_ids) {
536
- return {
537
- level: "error",
538
- invariant,
539
- message,
963
+ update_incremental(changedFiles) {
964
+ const indexed = this.readGraphIndexFile();
965
+ if (!indexed) {
966
+ return this.build_full_index();
967
+ }
968
+ const scan = this.scanSources();
969
+ const changed = changedFiles.length > 0 ? changedFiles : this.status().changed_files;
970
+ const normalized = new Set(
971
+ changed.map((entry) => this.normalizeChangedFile(entry)).filter((entry) => Boolean(entry))
972
+ );
973
+ if (normalized.size === 0) {
974
+ const fromIndex = this.load_indexed_graph();
975
+ return fromIndex ?? this.build_full_index();
976
+ }
977
+ const taskMap = fromRecordMap(indexed.graph.nodes);
978
+ const trackMap = fromRecordMap(indexed.graph.tracks);
979
+ const resourceMap = fromRecordMap(indexed.graph.resources);
980
+ const deliveryMap = fromRecordMap(indexed.graph.deliveries);
981
+ const fileMaps = {
982
+ tasks: { ...indexed.files.tasks },
983
+ tracks: { ...indexed.files.tracks },
984
+ resources: { ...indexed.files.resources },
985
+ deliveries: { ...indexed.files.deliveries }
986
+ };
987
+ for (const relativePath of normalized) {
988
+ const kind = this.fileKind(relativePath);
989
+ if (!kind) {
990
+ return this.build_full_index();
991
+ }
992
+ const absolutePath = path.join(this.coopDir, relativePath);
993
+ const exists = fs5.existsSync(absolutePath);
994
+ if (!exists) {
995
+ const oldId = fileMaps[kind][relativePath];
996
+ if (!oldId) {
997
+ return this.build_full_index();
998
+ }
999
+ delete fileMaps[kind][relativePath];
1000
+ if (kind === "tasks") taskMap.delete(oldId);
1001
+ if (kind === "tracks") trackMap.delete(oldId);
1002
+ if (kind === "resources") resourceMap.delete(oldId);
1003
+ if (kind === "deliveries") deliveryMap.delete(oldId);
1004
+ continue;
1005
+ }
1006
+ try {
1007
+ if (kind === "tasks") {
1008
+ const oldId2 = fileMaps.tasks[relativePath];
1009
+ if (oldId2) taskMap.delete(oldId2);
1010
+ const parsed2 = parseTaskFile(absolutePath).task;
1011
+ taskMap.set(parsed2.id, parsed2);
1012
+ fileMaps.tasks[relativePath] = parsed2.id;
1013
+ continue;
1014
+ }
1015
+ if (kind === "tracks") {
1016
+ const oldId2 = fileMaps.tracks[relativePath];
1017
+ if (oldId2) trackMap.delete(oldId2);
1018
+ const parsed2 = parseYamlFile(absolutePath);
1019
+ if (parsed2.id) {
1020
+ trackMap.set(parsed2.id, parsed2);
1021
+ fileMaps.tracks[relativePath] = parsed2.id;
1022
+ }
1023
+ continue;
1024
+ }
1025
+ if (kind === "resources") {
1026
+ const oldId2 = fileMaps.resources[relativePath];
1027
+ if (oldId2) resourceMap.delete(oldId2);
1028
+ const parsed2 = parseYamlFile(absolutePath);
1029
+ if (parsed2.id) {
1030
+ resourceMap.set(parsed2.id, parsed2);
1031
+ fileMaps.resources[relativePath] = parsed2.id;
1032
+ }
1033
+ continue;
1034
+ }
1035
+ const oldId = fileMaps.deliveries[relativePath];
1036
+ if (oldId) deliveryMap.delete(oldId);
1037
+ const parsed = parseDeliveryFile(absolutePath).delivery;
1038
+ deliveryMap.set(parsed.id, parsed);
1039
+ fileMaps.deliveries[relativePath] = parsed.id;
1040
+ } catch {
1041
+ return this.build_full_index();
1042
+ }
1043
+ }
1044
+ const graph = build_graph(Array.from(taskMap.values()), {
1045
+ tracks: trackMap,
1046
+ resources: resourceMap,
1047
+ deliveries: deliveryMap
1048
+ });
1049
+ this.writeIndexFiles(graph, scan, fileMaps);
1050
+ return graph;
1051
+ }
1052
+ ensureIndexDir() {
1053
+ fs5.mkdirSync(this.indexDir, { recursive: true });
1054
+ }
1055
+ writeIndexFiles(graph, scan, fileMaps) {
1056
+ this.ensureIndexDir();
1057
+ const graphIndex = {
1058
+ version: 1,
1059
+ generated_at: (/* @__PURE__ */ new Date()).toISOString(),
1060
+ source_max_mtime_ms: scan.sourceMaxMtimeMs,
1061
+ source_files: scan.sourceFiles,
1062
+ files: fileMaps,
1063
+ graph: this.serializeGraph(graph)
1064
+ };
1065
+ fs5.writeFileSync(this.graphPath, `${JSON.stringify(graphIndex, null, 2)}
1066
+ `, "utf8");
1067
+ fs5.writeFileSync(this.tasksPath, `${JSON.stringify(this.computeTaskIndexRows(graph), null, 2)}
1068
+ `, "utf8");
1069
+ fs5.writeFileSync(
1070
+ this.capacityPath,
1071
+ `${JSON.stringify(this.computeCapacitySnapshot(graph.resources), null, 2)}
1072
+ `,
1073
+ "utf8"
1074
+ );
1075
+ }
1076
+ readGraphIndexFile() {
1077
+ if (!fs5.existsSync(this.graphPath)) return null;
1078
+ try {
1079
+ const parsed = JSON.parse(fs5.readFileSync(this.graphPath, "utf8"));
1080
+ if (!parsed || typeof parsed !== "object") return null;
1081
+ if (!parsed.graph || typeof parsed.graph !== "object") return null;
1082
+ if (!parsed.source_files || typeof parsed.source_files !== "object") return null;
1083
+ if (!parsed.files || typeof parsed.files !== "object") return null;
1084
+ return parsed;
1085
+ } catch {
1086
+ return null;
1087
+ }
1088
+ }
1089
+ serializeGraph(graph) {
1090
+ return {
1091
+ nodes: toRecordMap(graph.nodes),
1092
+ forward: Object.fromEntries(
1093
+ Array.from(graph.forward.entries()).map(([id, deps]) => [id, Array.from(deps).sort((a, b) => a.localeCompare(b))])
1094
+ ),
1095
+ reverse: Object.fromEntries(
1096
+ Array.from(graph.reverse.entries()).map(([id, deps]) => [id, Array.from(deps).sort((a, b) => a.localeCompare(b))])
1097
+ ),
1098
+ topological_order: [...graph.topological_order],
1099
+ tracks: toRecordMap(graph.tracks),
1100
+ resources: toRecordMap(graph.resources),
1101
+ deliveries: toRecordMap(graph.deliveries),
1102
+ external_dependencies: Object.fromEntries(
1103
+ Array.from(graph.external_dependencies?.entries() ?? []).map(([id, deps]) => [id, deps])
1104
+ )
1105
+ };
1106
+ }
1107
+ deserializeGraph(data) {
1108
+ return {
1109
+ nodes: fromRecordMap(data.nodes ?? {}),
1110
+ forward: new Map(
1111
+ Object.entries(data.forward ?? {}).map(([id, deps]) => [id, new Set(deps ?? [])])
1112
+ ),
1113
+ reverse: new Map(
1114
+ Object.entries(data.reverse ?? {}).map(([id, deps]) => [id, new Set(deps ?? [])])
1115
+ ),
1116
+ topological_order: Array.isArray(data.topological_order) ? [...data.topological_order] : [],
1117
+ tracks: fromRecordMap(data.tracks ?? {}),
1118
+ resources: fromRecordMap(data.resources ?? {}),
1119
+ deliveries: fromRecordMap(data.deliveries ?? {}),
1120
+ external_dependencies: new Map(Object.entries(data.external_dependencies ?? {}))
1121
+ };
1122
+ }
1123
+ scanSources() {
1124
+ const taskFiles = walkFiles(path.join(this.coopDir, "tasks"), /* @__PURE__ */ new Set([".md"]));
1125
+ const trackFiles = walkFiles(path.join(this.coopDir, "tracks"), /* @__PURE__ */ new Set([".yml", ".yaml"]));
1126
+ const resourceFiles = walkFiles(path.join(this.coopDir, "resources"), /* @__PURE__ */ new Set([".yml", ".yaml"]));
1127
+ const deliveryFiles = walkFiles(path.join(this.coopDir, "deliveries"), /* @__PURE__ */ new Set([".yml", ".yaml", ".md"]));
1128
+ const sourceFiles = {};
1129
+ let sourceMaxMtimeMs = 0;
1130
+ const allFiles = [...taskFiles, ...trackFiles, ...resourceFiles, ...deliveryFiles];
1131
+ for (const filePath of allFiles) {
1132
+ const stat = fs5.statSync(filePath);
1133
+ const relative = toPosixPath(path.relative(this.coopDir, filePath));
1134
+ sourceFiles[relative] = stat.mtimeMs;
1135
+ if (stat.mtimeMs > sourceMaxMtimeMs) {
1136
+ sourceMaxMtimeMs = stat.mtimeMs;
1137
+ }
1138
+ }
1139
+ return {
1140
+ taskFiles,
1141
+ trackFiles,
1142
+ resourceFiles,
1143
+ deliveryFiles,
1144
+ sourceFiles,
1145
+ sourceMaxMtimeMs
1146
+ };
1147
+ }
1148
+ normalizeChangedFile(value) {
1149
+ if (!value || !value.trim()) return null;
1150
+ const normalized = path.normalize(value.trim());
1151
+ const absolute = path.isAbsolute(normalized) ? normalized : path.resolve(this.coopDir, normalized);
1152
+ const relative = path.relative(this.coopDir, absolute);
1153
+ if (relative.startsWith("..")) return null;
1154
+ return toPosixPath(relative);
1155
+ }
1156
+ fileKind(relativePath) {
1157
+ if (relativePath.startsWith("tasks/")) return "tasks";
1158
+ if (relativePath.startsWith("tracks/")) return "tracks";
1159
+ if (relativePath.startsWith("resources/")) return "resources";
1160
+ if (relativePath.startsWith("deliveries/")) return "deliveries";
1161
+ return null;
1162
+ }
1163
+ computeTaskIndexRows(graph) {
1164
+ const depth = /* @__PURE__ */ new Map();
1165
+ for (const id of graph.topological_order) {
1166
+ const deps = Array.from(graph.forward.get(id) ?? /* @__PURE__ */ new Set()).filter((dep) => graph.nodes.has(dep));
1167
+ if (deps.length === 0) {
1168
+ depth.set(id, 0);
1169
+ continue;
1170
+ }
1171
+ const maxDepDepth = Math.max(...deps.map((dep) => depth.get(dep) ?? 0));
1172
+ depth.set(id, maxDepDepth + 1);
1173
+ }
1174
+ const rows = {};
1175
+ for (const [id, task] of graph.nodes.entries()) {
1176
+ const estimate = task.estimate;
1177
+ let pertHours;
1178
+ let pertStddev;
1179
+ if (estimate && typeof estimate.optimistic_hours === "number" && typeof estimate.expected_hours === "number" && typeof estimate.pessimistic_hours === "number") {
1180
+ pertHours = round2((estimate.optimistic_hours + 4 * estimate.expected_hours + estimate.pessimistic_hours) / 6);
1181
+ pertStddev = round2((estimate.pessimistic_hours - estimate.optimistic_hours) / 6);
1182
+ }
1183
+ const blocks = Array.from(graph.reverse.get(id) ?? /* @__PURE__ */ new Set()).sort((a, b) => a.localeCompare(b));
1184
+ const row = {
1185
+ id,
1186
+ status: task.status,
1187
+ priority: task.priority ?? null,
1188
+ track: task.track ?? null,
1189
+ blocks,
1190
+ readiness: compute_readiness(task, graph),
1191
+ depth: depth.get(id) ?? 0
1192
+ };
1193
+ if (pertHours !== void 0) row.pert_hours = pertHours;
1194
+ if (pertStddev !== void 0) row.pert_stddev = pertStddev;
1195
+ rows[id] = row;
1196
+ }
1197
+ return rows;
1198
+ }
1199
+ computeCapacitySnapshot(resources) {
1200
+ const profiles = {};
1201
+ for (const [id, profile] of resources.entries()) {
1202
+ if (profile.type === "human") {
1203
+ const members = profile.members ?? [];
1204
+ const baseHours = members.reduce((sum, member) => sum + (member.hours_per_week ?? 0), 0);
1205
+ const meetings = profile.overhead?.meetings_percent ?? 0;
1206
+ const context = profile.overhead?.context_switch_percent ?? 0;
1207
+ const overheadPercent = meetings + context;
1208
+ const effectiveHours = baseHours * Math.max(0, 1 - overheadPercent / 100);
1209
+ profiles[id] = {
1210
+ id,
1211
+ type: profile.type,
1212
+ members: members.length,
1213
+ total_weekly_hours: round2(baseHours),
1214
+ effective_weekly_hours: round2(effectiveHours),
1215
+ overhead_percent: round2(overheadPercent)
1216
+ };
1217
+ continue;
1218
+ }
1219
+ if (profile.type === "ai") {
1220
+ profiles[id] = {
1221
+ id,
1222
+ type: profile.type,
1223
+ agents: profile.agents.length,
1224
+ effective_capacity: profile.effective_capacity ?? null
1225
+ };
1226
+ continue;
1227
+ }
1228
+ profiles[id] = {
1229
+ id,
1230
+ type: profile.type,
1231
+ nodes: profile.nodes.length,
1232
+ effective_capacity: profile.effective_capacity ?? null
1233
+ };
1234
+ }
1235
+ return {
1236
+ generated_at: (/* @__PURE__ */ new Date()).toISOString(),
1237
+ profiles
1238
+ };
1239
+ }
1240
+ };
1241
+
1242
+ // src/graph/loader.ts
1243
+ function load_graph(coopDir) {
1244
+ const index = new IndexManager(coopDir);
1245
+ const status = index.status();
1246
+ if (status.exists && !status.stale) {
1247
+ const cached = index.load_indexed_graph();
1248
+ if (cached) return cached;
1249
+ }
1250
+ if (!status.exists) {
1251
+ return index.build_full_index();
1252
+ }
1253
+ return index.update_incremental(status.changed_files);
1254
+ }
1255
+
1256
+ // src/graph/validator.ts
1257
+ function error(invariant, message, task_ids) {
1258
+ return {
1259
+ level: "error",
1260
+ invariant,
1261
+ message,
540
1262
  task_ids
541
1263
  };
542
1264
  }
@@ -594,10 +1316,17 @@ function checkTerminalConvergence(graph) {
594
1316
  }
595
1317
  return issues;
596
1318
  }
597
- function validate_graph(graph) {
1319
+ function validate_graph(graph, context = {}) {
598
1320
  const issues = [];
599
1321
  const cycle = detect_cycle(graph);
600
1322
  if (cycle) {
1323
+ context.eventEmitter?.emit({
1324
+ type: "graph.cycle_detected",
1325
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1326
+ payload: {
1327
+ cycle
1328
+ }
1329
+ });
601
1330
  issues.push(error("acyclicity", `Dependency cycle detected: ${cycle.join(" -> ")}.`, cycle));
602
1331
  }
603
1332
  for (const [taskId, deps] of graph.forward.entries()) {
@@ -635,8 +1364,540 @@ function validate_graph(graph) {
635
1364
  return issues;
636
1365
  }
637
1366
 
638
- // src/parser/idea-parser.ts
1367
+ // src/events/emitter.ts
1368
+ var CoopEventEmitter = class {
1369
+ handlers = /* @__PURE__ */ new Map();
1370
+ on(eventType, handler) {
1371
+ const existing = this.handlers.get(eventType) ?? /* @__PURE__ */ new Set();
1372
+ existing.add(handler);
1373
+ this.handlers.set(eventType, existing);
1374
+ return () => {
1375
+ const bucket = this.handlers.get(eventType);
1376
+ if (!bucket) return;
1377
+ bucket.delete(handler);
1378
+ if (bucket.size === 0) {
1379
+ this.handlers.delete(eventType);
1380
+ }
1381
+ };
1382
+ }
1383
+ emit(event) {
1384
+ const listeners = this.handlers.get(event.type);
1385
+ if (!listeners || listeners.size === 0) return;
1386
+ for (const listener of listeners) {
1387
+ listener(event);
1388
+ }
1389
+ }
1390
+ };
1391
+
1392
+ // src/events/events.ts
1393
+ var COOP_EVENT_TYPES = [
1394
+ "task.created",
1395
+ "task.transitioned",
1396
+ "task.assigned",
1397
+ "delivery.committed",
1398
+ "delivery.at_risk",
1399
+ "run.started",
1400
+ "run.completed",
1401
+ "run.failed",
1402
+ "graph.cycle_detected"
1403
+ ];
1404
+
1405
+ // src/events/hook-runner.ts
1406
+ import { spawn } from "child_process";
1407
+ import path2 from "path";
1408
+ function commandFor(hookPath) {
1409
+ const ext = path2.extname(hookPath).toLowerCase();
1410
+ if (ext === ".js" || ext === ".mjs" || ext === ".cjs") {
1411
+ return {
1412
+ command: process.execPath,
1413
+ args: [hookPath]
1414
+ };
1415
+ }
1416
+ return {
1417
+ command: hookPath,
1418
+ args: []
1419
+ };
1420
+ }
1421
+ function run_hook(hookPath, event, timeoutMs = 1e4) {
1422
+ return new Promise((resolve) => {
1423
+ const payload = JSON.stringify(event);
1424
+ const runner = commandFor(hookPath);
1425
+ const child = spawn(runner.command, runner.args, {
1426
+ stdio: ["pipe", "pipe", "pipe"],
1427
+ shell: runner.command === hookPath,
1428
+ windowsHide: true
1429
+ });
1430
+ let stdout = "";
1431
+ let stderr = "";
1432
+ let settled = false;
1433
+ let timedOut = false;
1434
+ const settle = (result) => {
1435
+ if (settled) return;
1436
+ settled = true;
1437
+ resolve(result);
1438
+ };
1439
+ const timeout = setTimeout(() => {
1440
+ timedOut = true;
1441
+ child.kill("SIGTERM");
1442
+ settle({
1443
+ exitCode: null,
1444
+ stdout,
1445
+ stderr: `${stderr}${stderr ? "\n" : ""}Hook timed out after ${timeoutMs}ms.`,
1446
+ timedOut: true
1447
+ });
1448
+ }, timeoutMs);
1449
+ child.stdout.on("data", (chunk) => {
1450
+ stdout += chunk.toString();
1451
+ });
1452
+ child.stderr.on("data", (chunk) => {
1453
+ stderr += chunk.toString();
1454
+ });
1455
+ child.on("error", (error6) => {
1456
+ clearTimeout(timeout);
1457
+ settle({
1458
+ exitCode: -1,
1459
+ stdout,
1460
+ stderr: `${stderr}${stderr ? "\n" : ""}${error6.message}`,
1461
+ timedOut: false
1462
+ });
1463
+ });
1464
+ child.on("close", (code) => {
1465
+ clearTimeout(timeout);
1466
+ settle({
1467
+ exitCode: timedOut ? null : code,
1468
+ stdout,
1469
+ stderr,
1470
+ timedOut
1471
+ });
1472
+ });
1473
+ child.stdin.write(payload, "utf8");
1474
+ child.stdin.end();
1475
+ });
1476
+ }
1477
+
1478
+ // src/auth/auth.ts
1479
+ var DEFAULT_AUTH_CONFIG = {
1480
+ enabled: false,
1481
+ roles: {
1482
+ admin: { permissions: ["all"] },
1483
+ lead: {
1484
+ permissions: ["create_task", "transition_task", "create_delivery", "plan_delivery", "assign_task"]
1485
+ },
1486
+ contributor: { permissions: ["transition_own_task", "comment"] },
1487
+ viewer: { permissions: ["read"] }
1488
+ },
1489
+ assignments: {},
1490
+ policies: [
1491
+ { action: "delete_task", requires: ["admin"] },
1492
+ { action: "commit_delivery", requires: ["admin", "lead"] },
1493
+ { action: "override_blocked", requires: ["admin", "lead"] }
1494
+ ]
1495
+ };
1496
+ function normalizeRole(value) {
1497
+ if (typeof value !== "string") return null;
1498
+ if (value === "admin" || value === "lead" || value === "contributor" || value === "viewer") return value;
1499
+ return null;
1500
+ }
1501
+ function asPermissions(value) {
1502
+ if (!Array.isArray(value)) return [];
1503
+ return value.map((entry) => String(entry)).filter((entry) => {
1504
+ return entry === "all" || entry === "read" || entry === "comment" || entry === "create_task" || entry === "transition_task" || entry === "transition_own_task" || entry === "create_delivery" || entry === "plan_delivery" || entry === "assign_task" || entry === "delete_task" || entry === "commit_delivery" || entry === "override_blocked";
1505
+ });
1506
+ }
1507
+ function normalizeUser(value) {
1508
+ return value.trim().toLowerCase();
1509
+ }
1510
+ function loadPolicies(raw) {
1511
+ if (!Array.isArray(raw)) return [...DEFAULT_AUTH_CONFIG.policies];
1512
+ const out = [];
1513
+ for (const entry of raw) {
1514
+ if (!entry || typeof entry !== "object") continue;
1515
+ const record = entry;
1516
+ const action = typeof record.action === "string" ? record.action : "";
1517
+ if (action !== "delete_task" && action !== "commit_delivery" && action !== "override_blocked" && action !== "transition_task" && action !== "create_delivery") {
1518
+ continue;
1519
+ }
1520
+ const requiresRaw = record.requires;
1521
+ const requiresList = Array.isArray(requiresRaw) ? requiresRaw : [requiresRaw];
1522
+ const requires = requiresList.map((role) => normalizeRole(role)).filter((role) => Boolean(role));
1523
+ if (requires.length === 0) continue;
1524
+ out.push({ action, requires });
1525
+ }
1526
+ return out.length > 0 ? out : [...DEFAULT_AUTH_CONFIG.policies];
1527
+ }
1528
+ function load_auth_config(config) {
1529
+ const source = config && typeof config === "object" ? config : {};
1530
+ const rawAuth = source.authorization && typeof source.authorization === "object" ? source.authorization : {};
1531
+ const enabled = Boolean(rawAuth.enabled ?? false);
1532
+ const rawRoles = rawAuth.roles && typeof rawAuth.roles === "object" ? rawAuth.roles : {};
1533
+ const roles = {
1534
+ admin: { permissions: [...DEFAULT_AUTH_CONFIG.roles.admin.permissions] },
1535
+ lead: { permissions: [...DEFAULT_AUTH_CONFIG.roles.lead.permissions] },
1536
+ contributor: { permissions: [...DEFAULT_AUTH_CONFIG.roles.contributor.permissions] },
1537
+ viewer: { permissions: [...DEFAULT_AUTH_CONFIG.roles.viewer.permissions] }
1538
+ };
1539
+ for (const role of Object.keys(roles)) {
1540
+ const roleConfig = rawRoles[role];
1541
+ if (!roleConfig || typeof roleConfig !== "object") continue;
1542
+ const permissions = asPermissions(roleConfig.permissions);
1543
+ if (permissions.length > 0) {
1544
+ roles[role].permissions = permissions;
1545
+ }
1546
+ }
1547
+ const rawAssignments = rawAuth.assignments && typeof rawAuth.assignments === "object" ? rawAuth.assignments : {};
1548
+ const assignments = {};
1549
+ for (const [user, roleRaw] of Object.entries(rawAssignments)) {
1550
+ const role = normalizeRole(roleRaw);
1551
+ if (!role) continue;
1552
+ assignments[normalizeUser(user)] = role;
1553
+ }
1554
+ const policies = loadPolicies(rawAuth.policies);
1555
+ return {
1556
+ enabled,
1557
+ roles,
1558
+ assignments,
1559
+ policies
1560
+ };
1561
+ }
1562
+ function get_user_role(user, config) {
1563
+ const key = normalizeUser(user);
1564
+ return config.assignments[key] ?? "viewer";
1565
+ }
1566
+ function hasRolePolicyOverride(role, action, config) {
1567
+ const policy = config.policies.find((entry) => entry.action === action);
1568
+ if (!policy) return true;
1569
+ return policy.requires.includes(role);
1570
+ }
1571
+ function check_permission(user, action, context) {
1572
+ const { config } = context;
1573
+ if (!config.enabled) return true;
1574
+ const role = get_user_role(user, config);
1575
+ const rolePermissions = new Set(config.roles[role]?.permissions ?? []);
1576
+ let allowed = rolePermissions.has("all") || rolePermissions.has(action);
1577
+ if (!allowed && action === "transition_task" && rolePermissions.has("transition_own_task")) {
1578
+ const owner = context.taskOwner?.trim() ?? "";
1579
+ allowed = owner.length > 0 && normalizeUser(owner) === normalizeUser(user);
1580
+ }
1581
+ if (!allowed) return false;
1582
+ return hasRolePolicyOverride(role, action, config);
1583
+ }
1584
+
1585
+ // src/plugins/plugin-loader.ts
639
1586
  import fs6 from "fs";
1587
+ import path3 from "path";
1588
+ var EVENT_TYPES = new Set(COOP_EVENT_TYPES);
1589
+ var PLUGIN_ID_PATTERN = /^[a-z0-9]+(?:[-_][a-z0-9]+)*$/i;
1590
+ function pluginDir(coopDir) {
1591
+ return path3.join(coopDir, "plugins");
1592
+ }
1593
+ function listPluginFiles(coopDir) {
1594
+ const dir = pluginDir(coopDir);
1595
+ if (!fs6.existsSync(dir)) return [];
1596
+ const entries = fs6.readdirSync(dir, { withFileTypes: true });
1597
+ return entries.filter((entry) => entry.isFile() && /\.(yml|yaml)$/i.test(entry.name)).map((entry) => path3.join(dir, entry.name)).sort((a, b) => a.localeCompare(b));
1598
+ }
1599
+ function asString(value, field, source) {
1600
+ if (typeof value !== "string" || value.trim().length === 0) {
1601
+ throw new Error(`${source}: ${field} must be a non-empty string.`);
1602
+ }
1603
+ return value.trim();
1604
+ }
1605
+ function asStringMap(value, field, source) {
1606
+ if (value === void 0) return {};
1607
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1608
+ throw new Error(`${source}: ${field} must be a mapping of string values.`);
1609
+ }
1610
+ const out = {};
1611
+ for (const [key, entry] of Object.entries(value)) {
1612
+ if (typeof entry !== "string") {
1613
+ throw new Error(`${source}: ${field}.${key} must be a string.`);
1614
+ }
1615
+ out[key] = entry;
1616
+ }
1617
+ return out;
1618
+ }
1619
+ function parseAction(raw, source) {
1620
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
1621
+ throw new Error(`${source}: trigger action must be a mapping object.`);
1622
+ }
1623
+ const record = raw;
1624
+ const actionType = asString(record.type, "action.type", source).toLowerCase();
1625
+ if (actionType === "webhook") {
1626
+ const url = asString(record.url, "action.url", source);
1627
+ const template = record.template === void 0 ? void 0 : asString(record.template, "action.template", source);
1628
+ const headers = asStringMap(record.headers, "action.headers", source);
1629
+ return { type: "webhook", url, template, headers };
1630
+ }
1631
+ if (actionType === "console") {
1632
+ const template = asString(record.template, "action.template", source);
1633
+ return { type: "console", template };
1634
+ }
1635
+ if (actionType === "github_pr") {
1636
+ const operation = asString(record.operation, "action.operation", source).toLowerCase();
1637
+ if (operation !== "create_or_update" && operation !== "merge") {
1638
+ throw new Error(`${source}: unsupported action.operation '${operation}' for github_pr.`);
1639
+ }
1640
+ const branch = record.branch === void 0 ? void 0 : asString(record.branch, "action.branch", source);
1641
+ const baseBranch = record.base_branch === void 0 ? void 0 : asString(record.base_branch, "action.base_branch", source);
1642
+ const draft = record.draft === void 0 ? void 0 : Boolean(record.draft);
1643
+ return {
1644
+ type: "github_pr",
1645
+ operation,
1646
+ branch,
1647
+ base_branch: baseBranch,
1648
+ draft
1649
+ };
1650
+ }
1651
+ throw new Error(`${source}: unsupported action.type '${actionType}'.`);
1652
+ }
1653
+ function parseTrigger(raw, source, index) {
1654
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
1655
+ throw new Error(`${source}: triggers[${index}] must be a mapping object.`);
1656
+ }
1657
+ const record = raw;
1658
+ const event = asString(record.event, `triggers[${index}].event`, source);
1659
+ if (!EVENT_TYPES.has(event)) {
1660
+ throw new Error(`${source}: unsupported event '${event}' in triggers[${index}].`);
1661
+ }
1662
+ const filterRaw = record.filter;
1663
+ if (filterRaw !== void 0 && (!filterRaw || typeof filterRaw !== "object" || Array.isArray(filterRaw))) {
1664
+ throw new Error(`${source}: triggers[${index}].filter must be a mapping object.`);
1665
+ }
1666
+ const action = parseAction(record.action, `${source} triggers[${index}]`);
1667
+ return {
1668
+ event,
1669
+ filter: filterRaw,
1670
+ action
1671
+ };
1672
+ }
1673
+ function parseSecrets(value, source) {
1674
+ if (value === void 0) return [];
1675
+ if (!Array.isArray(value)) {
1676
+ throw new Error(`${source}: secrets must be a list of environment variable names.`);
1677
+ }
1678
+ return value.map((entry, index) => asString(entry, `secrets[${index}]`, source));
1679
+ }
1680
+ function parsePlugin(filePath) {
1681
+ const source = path3.relative(path3.dirname(path3.dirname(filePath)), filePath);
1682
+ const raw = parseYamlFile(filePath);
1683
+ const id = asString(raw.id, "id", source);
1684
+ if (!PLUGIN_ID_PATTERN.test(id)) {
1685
+ throw new Error(`${source}: id '${id}' contains invalid characters.`);
1686
+ }
1687
+ const name = asString(raw.name, "name", source);
1688
+ const version = asString(raw.version, "version", source);
1689
+ if (!Array.isArray(raw.triggers) || raw.triggers.length === 0) {
1690
+ throw new Error(`${source}: triggers must be a non-empty list.`);
1691
+ }
1692
+ const triggers = raw.triggers.map((entry, index) => parseTrigger(entry, source, index));
1693
+ const secrets = parseSecrets(raw.secrets, source);
1694
+ return {
1695
+ id,
1696
+ name,
1697
+ version,
1698
+ triggers,
1699
+ secrets,
1700
+ file_path: filePath
1701
+ };
1702
+ }
1703
+ function load_plugins(coopDir) {
1704
+ const files = listPluginFiles(coopDir);
1705
+ const plugins = files.map((filePath) => parsePlugin(filePath));
1706
+ const seen = /* @__PURE__ */ new Set();
1707
+ for (const plugin of plugins) {
1708
+ if (seen.has(plugin.id)) {
1709
+ throw new Error(`Duplicate plugin id '${plugin.id}'.`);
1710
+ }
1711
+ seen.add(plugin.id);
1712
+ }
1713
+ return plugins;
1714
+ }
1715
+
1716
+ // src/plugins/plugin-runner.ts
1717
+ import fs7 from "fs";
1718
+ import path4 from "path";
1719
+ function isObject(value) {
1720
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
1721
+ }
1722
+ function resolvePathValue(input, pathExpr) {
1723
+ const tokens = pathExpr.split(".").map((entry) => entry.trim()).filter(Boolean);
1724
+ if (tokens.length === 0) return void 0;
1725
+ let cursor = input;
1726
+ for (const token of tokens) {
1727
+ if (!isObject(cursor)) return void 0;
1728
+ cursor = cursor[token];
1729
+ }
1730
+ return cursor;
1731
+ }
1732
+ function renderTemplate(template, context) {
1733
+ return template.replace(/{{\s*([^}]+?)\s*}}/g, (_, expr) => {
1734
+ const value = resolvePathValue(context, expr);
1735
+ if (value === null || value === void 0) return "";
1736
+ if (typeof value === "object") return JSON.stringify(value);
1737
+ return String(value);
1738
+ });
1739
+ }
1740
+ function resolveSecrets(template, env, plugin) {
1741
+ return template.replace(/\$\{([A-Z0-9_]+)\}/g, (_, secretName) => {
1742
+ if (!plugin.secrets.includes(secretName)) {
1743
+ throw new Error(`Plugin '${plugin.id}' references undeclared secret '${secretName}'.`);
1744
+ }
1745
+ const value = env[secretName];
1746
+ if (!value) {
1747
+ throw new Error(`Plugin '${plugin.id}' missing environment value for '${secretName}'.`);
1748
+ }
1749
+ return value;
1750
+ });
1751
+ }
1752
+ function filterMatches(filter, eventView) {
1753
+ if (!filter) return true;
1754
+ for (const [key, expected] of Object.entries(filter)) {
1755
+ const actual = resolvePathValue(eventView, key);
1756
+ if (Array.isArray(expected)) {
1757
+ const allowed = expected.map((entry) => String(entry));
1758
+ if (!allowed.includes(String(actual))) return false;
1759
+ continue;
1760
+ }
1761
+ if (String(actual) !== String(expected)) return false;
1762
+ }
1763
+ return true;
1764
+ }
1765
+ function eventTemplateView(event) {
1766
+ return {
1767
+ type: event.type,
1768
+ timestamp: event.timestamp,
1769
+ ...event.payload
1770
+ };
1771
+ }
1772
+ function buildContext(event, graph) {
1773
+ const context = {
1774
+ event: eventTemplateView(event)
1775
+ };
1776
+ if (graph) {
1777
+ const payload = event.payload;
1778
+ const taskId = typeof payload.task_id === "string" ? payload.task_id : void 0;
1779
+ const deliveryId = typeof payload.delivery_id === "string" ? payload.delivery_id : void 0;
1780
+ if (taskId) {
1781
+ const task = graph.nodes.get(taskId);
1782
+ if (task) {
1783
+ context.task = task;
1784
+ }
1785
+ }
1786
+ if (deliveryId) {
1787
+ const delivery = graph.deliveries.get(deliveryId);
1788
+ if (delivery) {
1789
+ context.delivery = delivery;
1790
+ }
1791
+ }
1792
+ }
1793
+ return context;
1794
+ }
1795
+ function appendRunLog(coopDir, record) {
1796
+ const runsDir = path4.join(coopDir, "runs");
1797
+ fs7.mkdirSync(runsDir, { recursive: true });
1798
+ const logPath = path4.join(runsDir, "plugin-events.log");
1799
+ fs7.appendFileSync(logPath, `${JSON.stringify(record)}
1800
+ `, "utf8");
1801
+ }
1802
+ async function executeWebhookAction(plugin, trigger, action, context, env, fetchImpl) {
1803
+ const resolvedUrl = resolveSecrets(action.url, env, plugin);
1804
+ const template = action.template ?? "{{ event.type }}";
1805
+ const body = renderTemplate(resolveSecrets(template, env, plugin), context);
1806
+ const headers = {
1807
+ "content-type": "text/plain; charset=utf-8"
1808
+ };
1809
+ for (const [key, value] of Object.entries(action.headers ?? {})) {
1810
+ headers[key] = resolveSecrets(value, env, plugin);
1811
+ }
1812
+ const response = await fetchImpl(resolvedUrl, {
1813
+ method: "POST",
1814
+ headers,
1815
+ body
1816
+ });
1817
+ return {
1818
+ statusCode: response.status,
1819
+ message: `${trigger.event} -> ${response.status}`
1820
+ };
1821
+ }
1822
+ function executeConsoleAction(plugin, action, context, env) {
1823
+ const template = resolveSecrets(action.template, env, plugin);
1824
+ const message = renderTemplate(template, context);
1825
+ console.log(`[COOP][plugin:${plugin.id}] ${message}`);
1826
+ return { message };
1827
+ }
1828
+ async function executeCustomAction(plugin, trigger, action, context, env, actionHandlers) {
1829
+ const handler = actionHandlers[action.type];
1830
+ if (!handler) {
1831
+ throw new Error(`Plugin action '${action.type}' requires a registered action handler.`);
1832
+ }
1833
+ return handler(plugin, trigger, action, context, env);
1834
+ }
1835
+ async function run_plugins_for_event(coopDir, event, options = {}) {
1836
+ const env = options.env ?? process.env;
1837
+ const fetchImpl = options.fetch_impl ?? fetch;
1838
+ const graph = options.graph;
1839
+ const actionHandlers = options.action_handlers ?? {};
1840
+ const plugins = load_plugins(coopDir);
1841
+ const context = buildContext(event, graph);
1842
+ const eventView = context.event;
1843
+ const results = [];
1844
+ for (const plugin of plugins) {
1845
+ for (const trigger of plugin.triggers) {
1846
+ if (trigger.event !== event.type) continue;
1847
+ if (!filterMatches(trigger.filter, eventView)) continue;
1848
+ const base = {
1849
+ plugin_id: plugin.id,
1850
+ plugin_name: plugin.name,
1851
+ trigger_event: trigger.event,
1852
+ action_type: trigger.action.type,
1853
+ success: false,
1854
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1855
+ };
1856
+ try {
1857
+ if (trigger.action.type === "webhook") {
1858
+ const result = await executeWebhookAction(plugin, trigger, trigger.action, context, env, fetchImpl);
1859
+ base.success = true;
1860
+ base.status_code = result.statusCode;
1861
+ base.message = result.message;
1862
+ } else if (trigger.action.type === "console") {
1863
+ const result = executeConsoleAction(plugin, trigger.action, context, env);
1864
+ base.success = true;
1865
+ base.message = result.message;
1866
+ } else {
1867
+ const result = await executeCustomAction(plugin, trigger, trigger.action, context, env, actionHandlers);
1868
+ base.success = true;
1869
+ base.status_code = result.statusCode;
1870
+ base.message = result.message;
1871
+ }
1872
+ } catch (error6) {
1873
+ base.success = false;
1874
+ base.error = error6 instanceof Error ? error6.message : String(error6);
1875
+ }
1876
+ appendRunLog(coopDir, base);
1877
+ results.push(base);
1878
+ }
1879
+ }
1880
+ return results;
1881
+ }
1882
+
1883
+ // src/models/run.ts
1884
+ var RunStatus = {
1885
+ pending: "pending",
1886
+ running: "running",
1887
+ paused: "paused",
1888
+ completed: "completed",
1889
+ failed: "failed",
1890
+ canceled: "canceled"
1891
+ };
1892
+ var RunStepStatus = {
1893
+ completed: "completed",
1894
+ failed: "failed",
1895
+ paused: "paused",
1896
+ skipped: "skipped"
1897
+ };
1898
+
1899
+ // src/parser/idea-parser.ts
1900
+ import fs8 from "fs";
640
1901
  function asStringArray2(value) {
641
1902
  if (!Array.isArray(value)) {
642
1903
  return [];
@@ -668,6 +1929,7 @@ function parseIdeaFromRaw(raw, source) {
668
1929
  id,
669
1930
  title,
670
1931
  created,
1932
+ aliases: asStringArray2(raw.aliases),
671
1933
  author,
672
1934
  status,
673
1935
  tags: asStringArray2(raw.tags),
@@ -684,12 +1946,12 @@ function parseIdeaContent(content, source = "<content>") {
684
1946
  };
685
1947
  }
686
1948
  function parseIdeaFile(filePath) {
687
- const content = fs6.readFileSync(filePath, "utf8");
1949
+ const content = fs8.readFileSync(filePath, "utf8");
688
1950
  return parseIdeaContent(content, filePath);
689
1951
  }
690
1952
 
691
1953
  // src/parser/task-writer.ts
692
- import fs7 from "fs";
1954
+ import fs9 from "fs";
693
1955
  var TASK_FIELD_ORDER = [
694
1956
  "id",
695
1957
  "title",
@@ -697,6 +1959,7 @@ var TASK_FIELD_ORDER = [
697
1959
  "status",
698
1960
  "created",
699
1961
  "updated",
1962
+ "aliases",
700
1963
  "priority",
701
1964
  "track",
702
1965
  "assignee",
@@ -734,123 +1997,1098 @@ function buildOrderedFrontmatter(task, raw) {
734
1997
  if (!raw) {
735
1998
  return ordered;
736
1999
  }
737
- for (const key of Object.keys(raw)) {
738
- if (ORDERED_TASK_FIELDS.has(key)) {
739
- continue;
2000
+ for (const key of Object.keys(raw)) {
2001
+ if (ORDERED_TASK_FIELDS.has(key)) {
2002
+ continue;
2003
+ }
2004
+ ordered[key] = raw[key];
2005
+ }
2006
+ return ordered;
2007
+ }
2008
+ function writeTask(task, options = {}) {
2009
+ const frontmatter = buildOrderedFrontmatter(task, options.raw);
2010
+ const output = stringifyFrontmatter(frontmatter, options.body ?? "");
2011
+ if (options.filePath) {
2012
+ const existedBeforeWrite = fs9.existsSync(options.filePath);
2013
+ fs9.writeFileSync(options.filePath, output, "utf8");
2014
+ if (!existedBeforeWrite) {
2015
+ options.eventEmitter?.emit({
2016
+ type: "task.created",
2017
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2018
+ payload: {
2019
+ task_id: task.id,
2020
+ title: task.title,
2021
+ task_type: task.type,
2022
+ file_path: options.filePath
2023
+ }
2024
+ });
2025
+ }
2026
+ }
2027
+ return output;
2028
+ }
2029
+
2030
+ // src/planning/feasibility.ts
2031
+ var DEFAULT_ESTIMATION_CONFIG = {
2032
+ version: 2,
2033
+ project: {
2034
+ name: "COOP",
2035
+ id: "coop"
2036
+ },
2037
+ defaults: {
2038
+ task: {
2039
+ complexity: "medium"
2040
+ }
2041
+ }
2042
+ };
2043
+ function asDate(value) {
2044
+ if (value instanceof Date) return new Date(value.getTime());
2045
+ const parsed = /* @__PURE__ */ new Date(`${value}T00:00:00.000Z`);
2046
+ if (Number.isNaN(parsed.valueOf())) {
2047
+ throw new Error(`Invalid date '${value}'.`);
2048
+ }
2049
+ return parsed;
2050
+ }
2051
+ function normalize_track(track) {
2052
+ return (track ?? "unassigned").trim().toLowerCase();
2053
+ }
2054
+ function isBusinessDay(date) {
2055
+ const day = date.getUTCDay();
2056
+ return day >= 1 && day <= 5;
2057
+ }
2058
+ function business_days(from, to) {
2059
+ const start = asDate(from);
2060
+ const end = asDate(to);
2061
+ if (start.getTime() === end.getTime()) return 0;
2062
+ const forward = start.getTime() < end.getTime();
2063
+ const low = forward ? start : end;
2064
+ const high = forward ? end : start;
2065
+ let count = 0;
2066
+ const cursor = new Date(low.getTime());
2067
+ while (cursor < high) {
2068
+ if (isBusinessDay(cursor)) count += 1;
2069
+ cursor.setUTCDate(cursor.getUTCDate() + 1);
2070
+ }
2071
+ return forward ? count : -count;
2072
+ }
2073
+ function sumTaskEffort(tasks) {
2074
+ let total = 0;
2075
+ for (const task of tasks) {
2076
+ total += effort_or_default(task, DEFAULT_ESTIMATION_CONFIG);
2077
+ }
2078
+ return total;
2079
+ }
2080
+ function sumPert(tasks) {
2081
+ let mean2 = 0;
2082
+ let variance = 0;
2083
+ for (const task of tasks) {
2084
+ if (task.estimate) {
2085
+ mean2 += pert_hours(task.estimate);
2086
+ const sigma = pert_stddev(task.estimate);
2087
+ variance += sigma * sigma;
2088
+ } else {
2089
+ mean2 += effort_or_default(task, DEFAULT_ESTIMATION_CONFIG);
2090
+ }
2091
+ }
2092
+ return {
2093
+ mean: mean2,
2094
+ stddev: Math.sqrt(variance)
2095
+ };
2096
+ }
2097
+ function find_track_slots_hours(slots, track) {
2098
+ const direct = slots.get(track);
2099
+ if (direct) {
2100
+ let sum2 = 0;
2101
+ for (const value of direct.values()) sum2 += value;
2102
+ return sum2;
2103
+ }
2104
+ const fallback = slots.get("unassigned");
2105
+ if (!fallback) return 0;
2106
+ let sum = 0;
2107
+ for (const value of fallback.values()) sum += value;
2108
+ return sum;
2109
+ }
2110
+ function track_effort(tasks) {
2111
+ const out = /* @__PURE__ */ new Map();
2112
+ for (const task of tasks) {
2113
+ const track = normalize_track(task.track);
2114
+ out.set(track, (out.get(track) ?? 0) + effort_or_default(task, DEFAULT_ESTIMATION_CONFIG));
2115
+ }
2116
+ return out;
2117
+ }
2118
+ function analyze_feasibility(deliveryId, graph, today) {
2119
+ const delivery = graph.deliveries.get(deliveryId);
2120
+ if (!delivery) {
2121
+ throw new Error(`Delivery '${deliveryId}' not found.`);
2122
+ }
2123
+ const include = new Set(delivery.scope.include);
2124
+ for (const excluded of delivery.scope.exclude) {
2125
+ include.delete(excluded);
2126
+ }
2127
+ const scope_tasks = Array.from(include).map((id) => graph.nodes.get(id)).filter((task) => Boolean(task));
2128
+ const subgraph = extract_subgraph(
2129
+ graph,
2130
+ scope_tasks.map((task) => task.id)
2131
+ );
2132
+ const external_deps = find_external_dependencies(subgraph, graph);
2133
+ const done_tasks = scope_tasks.filter((task) => task.status === "done");
2134
+ const remaining_tasks = scope_tasks.filter(
2135
+ (task) => task.status !== "done" && task.status !== "canceled"
2136
+ );
2137
+ const total_effort = sumTaskEffort(remaining_tasks);
2138
+ const pert = sumPert(remaining_tasks);
2139
+ const cpm = compute_critical_path(delivery, graph);
2140
+ const ledger = build_capacity_ledger(delivery, graph.resources, today);
2141
+ const simulation = simulate_schedule(remaining_tasks, graph, delivery, today);
2142
+ const tracks = new Set(remaining_tasks.map((task) => normalize_track(task.track)));
2143
+ const capacity_by_track = {};
2144
+ for (const track of tracks) {
2145
+ capacity_by_track[track] = find_track_slots_hours(ledger.slots, track);
2146
+ }
2147
+ const total_capacity = Object.values(capacity_by_track).reduce((sum, value) => sum + value, 0);
2148
+ const budget_hours = delivery.budget.engineering_hours ?? Number.POSITIVE_INFINITY;
2149
+ const budget_cost = delivery.budget.cost_usd ?? Number.POSITIVE_INFINITY;
2150
+ const total_cost = remaining_tasks.reduce((sum, task) => sum + (task.resources?.cost_usd ?? 0), 0);
2151
+ let feasible = true;
2152
+ const risks = [];
2153
+ if (total_effort > budget_hours) {
2154
+ feasible = false;
2155
+ risks.push({
2156
+ type: "budget_exceeded",
2157
+ message: `Required ${total_effort.toFixed(1)}h exceeds budget ${budget_hours.toFixed(1)}h.`,
2158
+ overage_hours: total_effort - budget_hours
2159
+ });
2160
+ }
2161
+ if (total_cost > budget_cost) {
2162
+ feasible = false;
2163
+ risks.push({
2164
+ type: "cost_budget_exceeded",
2165
+ message: `Required $${total_cost.toFixed(2)} exceeds budget $${budget_cost.toFixed(2)}.`,
2166
+ overage_cost_usd: total_cost - budget_cost
2167
+ });
2168
+ }
2169
+ if (total_effort > total_capacity) {
2170
+ feasible = false;
2171
+ risks.push({
2172
+ type: "capacity_exceeded",
2173
+ message: `Required ${total_effort.toFixed(1)}h exceeds capacity ${total_capacity.toFixed(1)}h.`,
2174
+ overage_hours: total_effort - total_capacity
2175
+ });
2176
+ }
2177
+ if (simulation.error) {
2178
+ feasible = false;
2179
+ risks.push({
2180
+ type: "schedule_overflow",
2181
+ message: `Simulation overflow while scheduling task ${simulation.error.task}.`,
2182
+ tasks: [simulation.error.task]
2183
+ });
2184
+ }
2185
+ if (delivery.target_date && simulation.projected_completion && simulation.projected_completion > delivery.target_date) {
2186
+ feasible = false;
2187
+ risks.push({
2188
+ type: "schedule_exceeded",
2189
+ message: `Projected ${simulation.projected_completion} exceeds target ${delivery.target_date}.`,
2190
+ overage_days: business_days(delivery.target_date, simulation.projected_completion)
2191
+ });
2192
+ }
2193
+ const effort_by_track = track_effort(remaining_tasks);
2194
+ for (const [track, effort] of effort_by_track.entries()) {
2195
+ const capacity = capacity_by_track[track] ?? 0;
2196
+ if (capacity <= 0) continue;
2197
+ const utilization = effort / capacity;
2198
+ if (utilization > 0.85) {
2199
+ risks.push({
2200
+ type: "high_utilization",
2201
+ message: `Track '${track}' at ${Math.round(utilization * 100)}% utilization.`,
2202
+ track,
2203
+ utilization
2204
+ });
2205
+ }
2206
+ }
2207
+ if (external_deps.length > 0) {
2208
+ risks.push({
2209
+ type: "external_dependencies",
2210
+ message: `${external_deps.length} tasks depend on work outside this delivery.`,
2211
+ tasks: external_deps
2212
+ });
2213
+ }
2214
+ const buffer_days = delivery.target_date && simulation.projected_completion ? business_days(simulation.projected_completion, delivery.target_date) : null;
2215
+ return {
2216
+ delivery: deliveryId,
2217
+ status: feasible ? "FEASIBLE" : "NOT_FEASIBLE",
2218
+ risks,
2219
+ summary: {
2220
+ total_tasks: scope_tasks.length,
2221
+ completed: done_tasks.length,
2222
+ remaining: remaining_tasks.length,
2223
+ effort_hours: {
2224
+ required: total_effort,
2225
+ budget: budget_hours
2226
+ },
2227
+ capacity_hours: {
2228
+ available: total_capacity,
2229
+ by_track: capacity_by_track
2230
+ },
2231
+ pert,
2232
+ dates: {
2233
+ target: delivery.target_date,
2234
+ projected: simulation.projected_completion,
2235
+ buffer_days
2236
+ },
2237
+ critical_path: cpm.critical_path,
2238
+ critical_path_hours: cpm.project_duration_hours
2239
+ },
2240
+ simulation
2241
+ };
2242
+ }
2243
+
2244
+ // src/planning/risk-detector.ts
2245
+ function toIsoTimestamp(now) {
2246
+ if (typeof now === "string") {
2247
+ if (now.includes("T")) return now;
2248
+ return `${now}T00:00:00.000Z`;
2249
+ }
2250
+ const date = now instanceof Date ? now : /* @__PURE__ */ new Date();
2251
+ return date.toISOString();
2252
+ }
2253
+ function detect_delivery_risks(delivery, graph, velocity, options = {}) {
2254
+ const today = options.today ?? /* @__PURE__ */ new Date();
2255
+ const feasibility = analyze_feasibility(delivery.id, graph, today);
2256
+ const criticalPath = compute_critical_path(delivery, graph);
2257
+ const risks = [];
2258
+ if (velocity.trend === "decelerating" && (feasibility.status === "NOT_FEASIBLE" || feasibility.summary.dates.target && feasibility.summary.dates.projected && feasibility.summary.dates.projected > feasibility.summary.dates.target || typeof feasibility.summary.dates.buffer_days === "number" && feasibility.summary.dates.buffer_days < 0)) {
2259
+ risks.push({
2260
+ type: "behind_schedule",
2261
+ message: `Delivery '${delivery.id}' is slipping while velocity is decelerating.`
2262
+ });
2263
+ }
2264
+ for (const utilization of feasibility.simulation.utilization_by_track) {
2265
+ if (utilization.utilization >= 0.9) {
2266
+ risks.push({
2267
+ type: "capacity_crunch",
2268
+ message: `Track '${utilization.track}' is at ${Math.round(utilization.utilization * 100)}% utilization.`,
2269
+ track: utilization.track
2270
+ });
2271
+ }
2272
+ }
2273
+ const notStarted = criticalPath.critical_path.filter((taskId) => {
2274
+ const task = graph.nodes.get(taskId);
2275
+ return task?.status === "todo" || task?.status === "blocked";
2276
+ });
2277
+ if (notStarted.length > 0) {
2278
+ risks.push({
2279
+ type: "critical_path_not_started",
2280
+ message: `${notStarted.length} critical path tasks are not started.`,
2281
+ task_ids: notStarted
2282
+ });
2283
+ }
2284
+ const highRiskCritical = criticalPath.critical_path.filter((taskId) => {
2285
+ const task = graph.nodes.get(taskId);
2286
+ return task?.risk?.level === "high" || task?.risk?.level === "critical";
2287
+ });
2288
+ if (highRiskCritical.length > 0) {
2289
+ risks.push({
2290
+ type: "high_risk_critical_path",
2291
+ message: `${highRiskCritical.length} high-risk tasks are on the critical path.`,
2292
+ task_ids: highRiskCritical
2293
+ });
2294
+ }
2295
+ if (risks.length > 0) {
2296
+ options.eventEmitter?.emit({
2297
+ type: "delivery.at_risk",
2298
+ timestamp: toIsoTimestamp(today),
2299
+ payload: {
2300
+ delivery_id: delivery.id,
2301
+ status: feasibility.status,
2302
+ risks: risks.map((risk) => risk.message)
2303
+ }
2304
+ });
2305
+ }
2306
+ return risks;
2307
+ }
2308
+
2309
+ // src/planning/scorer.ts
2310
+ var DEFAULT_SCORE_WEIGHTS = {
2311
+ priority: {
2312
+ p0: 100,
2313
+ p1: 75,
2314
+ p2: 50,
2315
+ p3: 25
2316
+ },
2317
+ urgency: {
2318
+ days_7: 40,
2319
+ days_14: 25,
2320
+ days_28: 10
2321
+ },
2322
+ dependency_unlock: {
2323
+ gte_5: 30,
2324
+ gte_3: 20,
2325
+ gte_1: 10,
2326
+ transitive_cap: 15
2327
+ },
2328
+ critical_path: 35,
2329
+ determinism: {
2330
+ high: 30,
2331
+ medium: 15,
2332
+ low: 0,
2333
+ experimental: -10
2334
+ },
2335
+ executor_fit: {
2336
+ match: 15,
2337
+ mismatch: -15,
2338
+ ci_match: 10
2339
+ },
2340
+ type: {
2341
+ bug: 10,
2342
+ spike: 5,
2343
+ feature: 0,
2344
+ chore: 0,
2345
+ epic: 0
2346
+ },
2347
+ complexity_penalty: {
2348
+ trivial: 0,
2349
+ small: 0,
2350
+ medium: -10,
2351
+ large: -25,
2352
+ unknown: -40
2353
+ },
2354
+ risk_penalty: {
2355
+ low: 0,
2356
+ medium: -10,
2357
+ high: -25,
2358
+ critical: -40
2359
+ }
2360
+ };
2361
+ function asDate2(value) {
2362
+ if (value instanceof Date) {
2363
+ return new Date(value.getTime());
2364
+ }
2365
+ const parsed = /* @__PURE__ */ new Date(`${value}T00:00:00.000Z`);
2366
+ if (Number.isNaN(parsed.valueOf())) {
2367
+ throw new Error(`Invalid date '${value}'.`);
2368
+ }
2369
+ return parsed;
2370
+ }
2371
+ function calendar_days(today, target) {
2372
+ const from = asDate2(today);
2373
+ const to = asDate2(target);
2374
+ const millis = to.getTime() - from.getTime();
2375
+ return Math.floor(millis / (1e3 * 60 * 60 * 24));
2376
+ }
2377
+ function toMap(deliveries) {
2378
+ if (!deliveries) return /* @__PURE__ */ new Map();
2379
+ if (deliveries instanceof Map) return deliveries;
2380
+ return new Map(deliveries.map((delivery) => [delivery.id, delivery]));
2381
+ }
2382
+ function isRecord(value) {
2383
+ return typeof value === "object" && value !== null;
2384
+ }
2385
+ function number_or_default(value, fallback) {
2386
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
2387
+ }
2388
+ function configured_weights(config) {
2389
+ const rawWeights = config?.scheduling?.weights;
2390
+ if (!isRecord(rawWeights)) {
2391
+ return DEFAULT_SCORE_WEIGHTS;
2392
+ }
2393
+ const priority = isRecord(rawWeights.priority) ? rawWeights.priority : {};
2394
+ const urgency = isRecord(rawWeights.urgency) ? rawWeights.urgency : {};
2395
+ const dependency_unlock = isRecord(rawWeights.dependency_unlock) ? rawWeights.dependency_unlock : {};
2396
+ const determinism = isRecord(rawWeights.determinism) ? rawWeights.determinism : {};
2397
+ const executor_fit = isRecord(rawWeights.executor_fit) ? rawWeights.executor_fit : {};
2398
+ const type = isRecord(rawWeights.type) ? rawWeights.type : {};
2399
+ const complexity_penalty2 = isRecord(rawWeights.complexity_penalty) ? rawWeights.complexity_penalty : {};
2400
+ const risk_penalty2 = isRecord(rawWeights.risk_penalty) ? rawWeights.risk_penalty : {};
2401
+ return {
2402
+ priority: {
2403
+ p0: number_or_default(priority.p0, DEFAULT_SCORE_WEIGHTS.priority.p0),
2404
+ p1: number_or_default(priority.p1, DEFAULT_SCORE_WEIGHTS.priority.p1),
2405
+ p2: number_or_default(priority.p2, DEFAULT_SCORE_WEIGHTS.priority.p2),
2406
+ p3: number_or_default(priority.p3, DEFAULT_SCORE_WEIGHTS.priority.p3)
2407
+ },
2408
+ urgency: {
2409
+ days_7: number_or_default(urgency.days_7, DEFAULT_SCORE_WEIGHTS.urgency.days_7),
2410
+ days_14: number_or_default(urgency.days_14, DEFAULT_SCORE_WEIGHTS.urgency.days_14),
2411
+ days_28: number_or_default(urgency.days_28, DEFAULT_SCORE_WEIGHTS.urgency.days_28)
2412
+ },
2413
+ dependency_unlock: {
2414
+ gte_5: number_or_default(dependency_unlock.gte_5, DEFAULT_SCORE_WEIGHTS.dependency_unlock.gte_5),
2415
+ gte_3: number_or_default(dependency_unlock.gte_3, DEFAULT_SCORE_WEIGHTS.dependency_unlock.gte_3),
2416
+ gte_1: number_or_default(dependency_unlock.gte_1, DEFAULT_SCORE_WEIGHTS.dependency_unlock.gte_1),
2417
+ transitive_cap: number_or_default(
2418
+ dependency_unlock.transitive_cap,
2419
+ DEFAULT_SCORE_WEIGHTS.dependency_unlock.transitive_cap
2420
+ )
2421
+ },
2422
+ critical_path: number_or_default(rawWeights.critical_path, DEFAULT_SCORE_WEIGHTS.critical_path),
2423
+ determinism: {
2424
+ high: number_or_default(determinism.high, DEFAULT_SCORE_WEIGHTS.determinism.high),
2425
+ medium: number_or_default(determinism.medium, DEFAULT_SCORE_WEIGHTS.determinism.medium),
2426
+ low: number_or_default(determinism.low, DEFAULT_SCORE_WEIGHTS.determinism.low),
2427
+ experimental: number_or_default(determinism.experimental, DEFAULT_SCORE_WEIGHTS.determinism.experimental)
2428
+ },
2429
+ executor_fit: {
2430
+ match: number_or_default(executor_fit.match, DEFAULT_SCORE_WEIGHTS.executor_fit.match),
2431
+ mismatch: number_or_default(executor_fit.mismatch, DEFAULT_SCORE_WEIGHTS.executor_fit.mismatch),
2432
+ ci_match: number_or_default(executor_fit.ci_match, DEFAULT_SCORE_WEIGHTS.executor_fit.ci_match)
2433
+ },
2434
+ type: {
2435
+ bug: number_or_default(type.bug, DEFAULT_SCORE_WEIGHTS.type.bug),
2436
+ spike: number_or_default(type.spike, DEFAULT_SCORE_WEIGHTS.type.spike),
2437
+ feature: number_or_default(type.feature, DEFAULT_SCORE_WEIGHTS.type.feature),
2438
+ chore: number_or_default(type.chore, DEFAULT_SCORE_WEIGHTS.type.chore),
2439
+ epic: number_or_default(type.epic, DEFAULT_SCORE_WEIGHTS.type.epic)
2440
+ },
2441
+ complexity_penalty: {
2442
+ trivial: number_or_default(complexity_penalty2.trivial, DEFAULT_SCORE_WEIGHTS.complexity_penalty.trivial),
2443
+ small: number_or_default(complexity_penalty2.small, DEFAULT_SCORE_WEIGHTS.complexity_penalty.small),
2444
+ medium: number_or_default(complexity_penalty2.medium, DEFAULT_SCORE_WEIGHTS.complexity_penalty.medium),
2445
+ large: number_or_default(complexity_penalty2.large, DEFAULT_SCORE_WEIGHTS.complexity_penalty.large),
2446
+ unknown: number_or_default(complexity_penalty2.unknown, DEFAULT_SCORE_WEIGHTS.complexity_penalty.unknown)
2447
+ },
2448
+ risk_penalty: {
2449
+ low: number_or_default(risk_penalty2.low, DEFAULT_SCORE_WEIGHTS.risk_penalty.low),
2450
+ medium: number_or_default(risk_penalty2.medium, DEFAULT_SCORE_WEIGHTS.risk_penalty.medium),
2451
+ high: number_or_default(risk_penalty2.high, DEFAULT_SCORE_WEIGHTS.risk_penalty.high),
2452
+ critical: number_or_default(risk_penalty2.critical, DEFAULT_SCORE_WEIGHTS.risk_penalty.critical)
2453
+ }
2454
+ };
2455
+ }
2456
+ function unresolved_dependencies(task, graph) {
2457
+ return (task.depends_on ?? []).filter((depId) => {
2458
+ const dep = graph.nodes.get(depId);
2459
+ return !dep || dep.status !== "done" && dep.status !== "canceled";
2460
+ });
2461
+ }
2462
+ function priority_weight(task, config) {
2463
+ const weights = configured_weights(config);
2464
+ const priority = task.priority ?? "p2";
2465
+ return weights.priority[priority];
2466
+ }
2467
+ function urgency_weight(task, deliveries, today, config) {
2468
+ if (!task.delivery) return 0;
2469
+ const delivery = toMap(deliveries).get(task.delivery);
2470
+ if (!delivery?.target_date) return 0;
2471
+ const weights = configured_weights(config);
2472
+ const daysRemaining = calendar_days(today, delivery.target_date);
2473
+ if (daysRemaining < 7) return weights.urgency.days_7;
2474
+ if (daysRemaining < 14) return weights.urgency.days_14;
2475
+ if (daysRemaining < 28) return weights.urgency.days_28;
2476
+ return 0;
2477
+ }
2478
+ function dependency_unlock_weight(taskId, graph, config) {
2479
+ const weights = configured_weights(config);
2480
+ const reverse = graph.reverse.get(taskId) ?? /* @__PURE__ */ new Set();
2481
+ const directBlocked = [];
2482
+ for (const dependentId of reverse) {
2483
+ const dependent = graph.nodes.get(dependentId);
2484
+ if (!dependent) continue;
2485
+ if (compute_readiness(dependent, graph) !== "blocked") continue;
2486
+ const unresolved = unresolved_dependencies(dependent, graph);
2487
+ if (unresolved.length === 1 && unresolved[0] === taskId) {
2488
+ directBlocked.push(dependentId);
2489
+ }
2490
+ }
2491
+ const directCount = directBlocked.length;
2492
+ let directWeight = 0;
2493
+ if (directCount >= 5) {
2494
+ directWeight = weights.dependency_unlock.gte_5;
2495
+ } else if (directCount >= 3) {
2496
+ directWeight = weights.dependency_unlock.gte_3;
2497
+ } else if (directCount >= 1) {
2498
+ directWeight = weights.dependency_unlock.gte_1;
2499
+ }
2500
+ const transitive = /* @__PURE__ */ new Set();
2501
+ for (const directId of directBlocked) {
2502
+ const reverseDepth2 = graph.reverse.get(directId) ?? /* @__PURE__ */ new Set();
2503
+ for (const transitiveId of reverseDepth2) {
2504
+ const task = graph.nodes.get(transitiveId);
2505
+ if (!task) continue;
2506
+ if (compute_readiness(task, graph) === "blocked") {
2507
+ transitive.add(transitiveId);
2508
+ }
2509
+ }
2510
+ }
2511
+ const transitiveWeight = Math.min(transitive.size * 2, weights.dependency_unlock.transitive_cap);
2512
+ return directWeight + transitiveWeight;
2513
+ }
2514
+ function critical_path_weight(taskId, cpm, config) {
2515
+ if (!cpm) return 0;
2516
+ const weights = configured_weights(config);
2517
+ const isCritical = cpm.critical_path.includes(taskId);
2518
+ return isCritical ? weights.critical_path : 0;
2519
+ }
2520
+ function determinism_weight(task, config) {
2521
+ const weights = configured_weights(config);
2522
+ const defaultDeterminism = config?.defaults?.task?.determinism ?? "medium";
2523
+ const determinism = task.determinism ?? defaultDeterminism;
2524
+ return weights.determinism[determinism];
2525
+ }
2526
+ function executor_fit_weight(task, targetExecutor, config) {
2527
+ if (!targetExecutor) return 0;
2528
+ const weights = configured_weights(config);
2529
+ const defaultDeterminism = config?.defaults?.task?.determinism ?? "medium";
2530
+ const determinism = task.determinism ?? defaultDeterminism;
2531
+ if (targetExecutor === "ai") {
2532
+ return determinism === "high" || determinism === "medium" ? weights.executor_fit.match : weights.executor_fit.mismatch;
2533
+ }
2534
+ if (targetExecutor === "human") {
2535
+ return determinism === "low" || determinism === "experimental" ? weights.executor_fit.match : weights.executor_fit.mismatch;
2536
+ }
2537
+ if (targetExecutor === "ci") {
2538
+ const runbook = task.execution?.runbook ?? [];
2539
+ const ciReady = task.type === "chore" && runbook.length > 0 && runbook.every((step) => step.action === "run");
2540
+ return ciReady ? weights.executor_fit.ci_match : weights.executor_fit.mismatch;
2541
+ }
2542
+ return 0;
2543
+ }
2544
+ function type_weight(task, config) {
2545
+ const weights = configured_weights(config);
2546
+ return weights.type[task.type];
2547
+ }
2548
+ function complexity_penalty(task, config) {
2549
+ const weights = configured_weights(config);
2550
+ const complexity = task.complexity ?? config?.defaults?.task?.complexity ?? "medium";
2551
+ return weights.complexity_penalty[complexity];
2552
+ }
2553
+ function risk_penalty(task, config) {
2554
+ const weights = configured_weights(config);
2555
+ const level = task.risk?.level ?? "low";
2556
+ return weights.risk_penalty[level];
2557
+ }
2558
+ function compute_score(task, graph, context = {}) {
2559
+ const deliveries = context.deliveries ?? graph.deliveries;
2560
+ const today = context.today ?? /* @__PURE__ */ new Date();
2561
+ const config = context.config;
2562
+ return priority_weight(task, config) + urgency_weight(task, deliveries, today, config) + dependency_unlock_weight(task.id, graph, config) + critical_path_weight(task.id, context.cpm, config) + determinism_weight(task, config) + executor_fit_weight(task, context.target_executor, config) + type_weight(task, config) + complexity_penalty(task, config) + risk_penalty(task, config);
2563
+ }
2564
+
2565
+ // src/planning/scheduler.ts
2566
+ function asDate3(value) {
2567
+ if (value instanceof Date) return new Date(value.getTime());
2568
+ const parsed = /* @__PURE__ */ new Date(`${value}T00:00:00.000Z`);
2569
+ if (Number.isNaN(parsed.valueOf())) {
2570
+ throw new Error(`Invalid date '${value}'.`);
2571
+ }
2572
+ return parsed;
2573
+ }
2574
+ function isoDate(value) {
2575
+ return value.toISOString().slice(0, 10);
2576
+ }
2577
+ function addDays(value, days) {
2578
+ const out = new Date(value.getTime());
2579
+ out.setUTCDate(out.getUTCDate() + days);
2580
+ return out;
2581
+ }
2582
+ function clone_ledger(ledger) {
2583
+ return {
2584
+ slots: new Map(
2585
+ Array.from(ledger.slots.entries(), ([track, byWeek]) => [track, new Map(byWeek)])
2586
+ ),
2587
+ weeks: new Map(ledger.weeks),
2588
+ ai_tokens: new Map(ledger.ai_tokens),
2589
+ ai_tokens_by_agent: new Map(
2590
+ Array.from(ledger.ai_tokens_by_agent.entries(), ([agent, byDay]) => [agent, new Map(byDay)])
2591
+ ),
2592
+ ai_tokens_consumed_by_agent: new Map(
2593
+ Array.from(ledger.ai_tokens_consumed_by_agent.entries(), ([agent, byDay]) => [
2594
+ agent,
2595
+ new Map(byDay)
2596
+ ])
2597
+ )
2598
+ };
2599
+ }
2600
+ function normalize_track2(track) {
2601
+ return (track ?? "unassigned").trim().toLowerCase();
2602
+ }
2603
+ function find_track_slots(ledger, track) {
2604
+ const normalized = normalize_track2(track);
2605
+ return ledger.slots.get(normalized) ?? ledger.slots.get("unassigned") ?? null;
2606
+ }
2607
+ function check_capacity(task, ledger) {
2608
+ try {
2609
+ const slots = find_track_slots(ledger, task.track);
2610
+ if (!slots) return false;
2611
+ const week0 = slots.get(0) ?? 0;
2612
+ if (week0 <= 0) return false;
2613
+ const attempt = clone_ledger(ledger);
2614
+ return allocate(attempt, task, 0).success;
2615
+ } catch {
2616
+ return false;
2617
+ }
2618
+ }
2619
+ function advisory_delivery(graph, tasks, options) {
2620
+ if (options.delivery) {
2621
+ const existing = graph.deliveries.get(options.delivery);
2622
+ if (existing) {
2623
+ return {
2624
+ id: existing.id,
2625
+ name: existing.name,
2626
+ status: "planning",
2627
+ target_date: existing.target_date ?? isoDate(addDays(asDate3(options.today ?? /* @__PURE__ */ new Date()), 56)),
2628
+ started_date: existing.started_date,
2629
+ delivered_date: existing.delivered_date,
2630
+ budget: existing.budget,
2631
+ capacity_profiles: existing.capacity_profiles,
2632
+ scope: existing.scope
2633
+ };
740
2634
  }
741
- ordered[key] = raw[key];
742
2635
  }
743
- return ordered;
2636
+ const targetDate = isoDate(addDays(asDate3(options.today ?? /* @__PURE__ */ new Date()), 56));
2637
+ return {
2638
+ id: "__SCHEDULE__",
2639
+ name: "Advisory Schedule",
2640
+ status: "planning",
2641
+ target_date: targetDate,
2642
+ started_date: null,
2643
+ delivered_date: null,
2644
+ budget: {},
2645
+ capacity_profiles: [],
2646
+ scope: {
2647
+ include: tasks.map((task) => task.id),
2648
+ exclude: []
2649
+ }
2650
+ };
744
2651
  }
745
- function writeTask(task, options = {}) {
746
- const frontmatter = buildOrderedFrontmatter(task, options.raw);
747
- const output = stringifyFrontmatter(frontmatter, options.body ?? "");
748
- if (options.filePath) {
749
- fs7.writeFileSync(options.filePath, output, "utf8");
2652
+ function resolve_cpm(graph, options) {
2653
+ if (options.delivery) {
2654
+ const delivery = graph.deliveries.get(options.delivery);
2655
+ if (!delivery) return void 0;
2656
+ return compute_critical_path(delivery, graph);
2657
+ }
2658
+ const preferred = Array.from(graph.deliveries.values()).find(
2659
+ (delivery) => delivery.status === "committed" || delivery.status === "in_progress"
2660
+ );
2661
+ if (!preferred) return void 0;
2662
+ return compute_critical_path(preferred, graph);
2663
+ }
2664
+ function schedule_next(graph, options = {}) {
2665
+ const ready_tasks = Array.from(graph.nodes.values()).filter(
2666
+ (task) => compute_readiness(task, graph) === "ready"
2667
+ );
2668
+ let filtered = ready_tasks;
2669
+ if (options.track) {
2670
+ filtered = filtered.filter((task) => (task.track ?? "unassigned") === options.track);
2671
+ }
2672
+ const deliveryFilter = options.delivery;
2673
+ if (deliveryFilter) {
2674
+ const scope = graph.deliveries.get(deliveryFilter)?.scope.include ?? [];
2675
+ const scopeSet = new Set(scope);
2676
+ filtered = filtered.filter(
2677
+ (task) => task.delivery === deliveryFilter || scopeSet.has(task.id)
2678
+ );
2679
+ }
2680
+ if (options.executor) {
2681
+ filtered = filtered.filter(
2682
+ (task) => task.execution?.executor === options.executor || task.execution?.executor == null
2683
+ );
2684
+ }
2685
+ const cpm = resolve_cpm(graph, options);
2686
+ const scored = filtered.map((task) => ({
2687
+ task,
2688
+ score: compute_score(task, graph, {
2689
+ deliveries: graph.deliveries,
2690
+ cpm,
2691
+ today: options.today ?? /* @__PURE__ */ new Date(),
2692
+ target_executor: options.executor,
2693
+ config: options.config
2694
+ }),
2695
+ readiness: compute_readiness(task, graph),
2696
+ fits_capacity: true,
2697
+ fits_wip: true
2698
+ }));
2699
+ scored.sort((a, b) => {
2700
+ if (a.score !== b.score) return b.score - a.score;
2701
+ return a.task.id.localeCompare(b.task.id);
2702
+ });
2703
+ const limit = typeof options.limit === "number" && Number.isInteger(options.limit) && options.limit > 0 ? options.limit : void 0;
2704
+ const output = typeof limit === "number" ? scored.slice(0, limit) : scored;
2705
+ if (output.length === 0) return output;
2706
+ const delivery = advisory_delivery(graph, output.map((entry) => entry.task), options);
2707
+ const ledger = build_capacity_ledger(delivery, graph.resources, options.today ?? /* @__PURE__ */ new Date());
2708
+ for (const entry of output) {
2709
+ entry.fits_capacity = check_capacity(entry.task, ledger);
2710
+ entry.fits_wip = check_wip(
2711
+ graph.tracks.get(entry.task.track ?? "unassigned"),
2712
+ graph
2713
+ );
750
2714
  }
751
2715
  return output;
752
2716
  }
753
2717
 
754
- // src/planning/readiness.ts
755
- function isResolvedDependencyStatus(status) {
756
- return status === "done" || status === "canceled";
2718
+ // src/planning/velocity.ts
2719
+ import fs10 from "fs";
2720
+ import path5 from "path";
2721
+ var DEFAULT_ESTIMATION_CONFIG2 = {
2722
+ version: 2,
2723
+ project: {
2724
+ name: "COOP",
2725
+ id: "coop"
2726
+ },
2727
+ defaults: {
2728
+ task: {
2729
+ complexity: "medium"
2730
+ }
2731
+ }
2732
+ };
2733
+ function asDate4(value) {
2734
+ if (value instanceof Date) return new Date(value.getTime());
2735
+ const parsed = new Date(value);
2736
+ if (Number.isNaN(parsed.valueOf())) {
2737
+ throw new Error(`Invalid date '${value}'.`);
2738
+ }
2739
+ return parsed;
757
2740
  }
758
- function compute_readiness(task, graph) {
759
- if (task.status === "done" || task.status === "canceled") {
760
- return "done";
2741
+ function isoDate2(value) {
2742
+ return value.toISOString().slice(0, 10);
2743
+ }
2744
+ function startOfWeek(date) {
2745
+ const copy = new Date(date.getTime());
2746
+ const day = copy.getUTCDay();
2747
+ const offset = day === 0 ? -6 : 1 - day;
2748
+ copy.setUTCDate(copy.getUTCDate() + offset);
2749
+ copy.setUTCHours(0, 0, 0, 0);
2750
+ return copy;
2751
+ }
2752
+ function addDays2(date, days) {
2753
+ const out = new Date(date.getTime());
2754
+ out.setUTCDate(out.getUTCDate() + days);
2755
+ return out;
2756
+ }
2757
+ function durationHours2(run) {
2758
+ const fromSteps = run.steps.reduce((sum, step) => sum + (step.duration_seconds ?? 0), 0) / 3600;
2759
+ if (fromSteps > 0) {
2760
+ return fromSteps;
761
2761
  }
762
- if (task.status === "in_review") {
763
- return "waiting_review";
2762
+ if (run.completed) {
2763
+ const started = asDate4(run.started);
2764
+ const completed = asDate4(run.completed);
2765
+ return Math.max(0, completed.getTime() - started.getTime()) / (60 * 60 * 1e3);
764
2766
  }
765
- if (task.status === "in_progress") {
766
- return "in_progress";
2767
+ return 0;
2768
+ }
2769
+ function mean(values) {
2770
+ if (values.length === 0) return 0;
2771
+ return values.reduce((sum, value) => sum + value, 0) / values.length;
2772
+ }
2773
+ function detectTrend(points) {
2774
+ if (points.length < 4) {
2775
+ return "stable";
767
2776
  }
768
- for (const depId of task.depends_on ?? []) {
769
- const dep = graph.nodes.get(depId);
770
- if (!dep || !isResolvedDependencyStatus(dep.status)) {
771
- return "blocked";
772
- }
2777
+ const midpoint = Math.floor(points.length / 2);
2778
+ const firstHalf = points.slice(0, midpoint).map((point) => point.completed_tasks);
2779
+ const secondHalf = points.slice(midpoint).map((point) => point.completed_tasks);
2780
+ const firstAverage = mean(firstHalf);
2781
+ const secondAverage = mean(secondHalf);
2782
+ if (firstAverage === 0 && secondAverage === 0) {
2783
+ return "stable";
773
2784
  }
774
- return "ready";
2785
+ if (firstAverage === 0 && secondAverage > 0) {
2786
+ return "accelerating";
2787
+ }
2788
+ const ratio = secondAverage / Math.max(firstAverage, 1e-4);
2789
+ if (ratio >= 1.15) {
2790
+ return "accelerating";
2791
+ }
2792
+ if (ratio <= 0.85) {
2793
+ return "decelerating";
2794
+ }
2795
+ return "stable";
775
2796
  }
776
- function compute_all_readiness(graph) {
777
- const readiness = /* @__PURE__ */ new Map();
778
- for (const [taskId, task] of graph.nodes.entries()) {
779
- readiness.set(taskId, compute_readiness(task, graph));
2797
+ function load_completed_runs(coopDir) {
2798
+ const runsDir = path5.join(coopDir, "runs");
2799
+ if (!fs10.existsSync(runsDir)) {
2800
+ return [];
780
2801
  }
781
- return readiness;
2802
+ return fs10.readdirSync(runsDir).filter((entry) => entry.toLowerCase().endsWith(".yml") || entry.toLowerCase().endsWith(".yaml")).sort((a, b) => a.localeCompare(b)).map((entry) => parseYamlFile(path5.join(runsDir, entry))).filter((run) => run.status === "completed" && typeof run.completed === "string");
782
2803
  }
783
- function partition_by_readiness(graph) {
784
- const partitions = {
785
- ready: [],
786
- blocked: [],
787
- in_progress: [],
788
- waiting_review: [],
789
- done: []
2804
+ function compute_velocity(runs, windowWeeks, options = {}) {
2805
+ const normalizedWindow = Number.isInteger(windowWeeks) && windowWeeks > 0 ? windowWeeks : 4;
2806
+ const today = startOfWeek(asDate4(options.today ?? /* @__PURE__ */ new Date()));
2807
+ const windowStart = addDays2(today, -7 * (normalizedWindow - 1));
2808
+ const pointMap = /* @__PURE__ */ new Map();
2809
+ for (let weekIndex = 0; weekIndex < normalizedWindow; weekIndex += 1) {
2810
+ const weekStart = addDays2(windowStart, weekIndex * 7);
2811
+ const weekEnd = addDays2(weekStart, 6);
2812
+ pointMap.set(isoDate2(weekStart), {
2813
+ tasks: /* @__PURE__ */ new Set(),
2814
+ hours: 0,
2815
+ index: weekIndex,
2816
+ end: isoDate2(weekEnd)
2817
+ });
2818
+ }
2819
+ let completedRuns = 0;
2820
+ let estimatedHours = 0;
2821
+ let actualHours = 0;
2822
+ for (const run of runs) {
2823
+ if (!run.completed) continue;
2824
+ const completedAt = asDate4(run.completed);
2825
+ const weekStart = startOfWeek(completedAt);
2826
+ const weekKey = isoDate2(weekStart);
2827
+ const bucket = pointMap.get(weekKey);
2828
+ if (!bucket) continue;
2829
+ completedRuns += 1;
2830
+ const hours = durationHours2(run);
2831
+ actualHours += hours;
2832
+ bucket.tasks.add(run.task);
2833
+ bucket.hours += hours;
2834
+ const task = options.graph?.nodes.get(run.task);
2835
+ if (task) {
2836
+ estimatedHours += effort_or_default(task, DEFAULT_ESTIMATION_CONFIG2);
2837
+ }
2838
+ }
2839
+ const points = Array.from(pointMap.entries()).sort((a, b) => a[1].index - b[1].index).map(([weekStart, bucket]) => ({
2840
+ week_index: bucket.index,
2841
+ week_start: weekStart,
2842
+ week_end: bucket.end,
2843
+ completed_tasks: bucket.tasks.size,
2844
+ delivered_hours: Number(bucket.hours.toFixed(2))
2845
+ }));
2846
+ const tasksCompletedTotal = points.reduce((sum, point) => sum + point.completed_tasks, 0);
2847
+ const deliveredHoursTotal = points.reduce((sum, point) => sum + point.delivered_hours, 0);
2848
+ return {
2849
+ window_weeks: normalizedWindow,
2850
+ completed_runs: completedRuns,
2851
+ tasks_completed_total: tasksCompletedTotal,
2852
+ delivered_hours_total: Number(deliveredHoursTotal.toFixed(2)),
2853
+ tasks_completed_per_week: Number((tasksCompletedTotal / normalizedWindow).toFixed(2)),
2854
+ hours_delivered_per_week: Number((deliveredHoursTotal / normalizedWindow).toFixed(2)),
2855
+ accuracy_ratio: estimatedHours > 0 ? Number((actualHours / estimatedHours).toFixed(3)) : null,
2856
+ trend: detectTrend(points),
2857
+ points
790
2858
  };
791
- for (const task of graph.nodes.values()) {
792
- const state = compute_readiness(task, graph);
793
- partitions[state].push(task);
2859
+ }
2860
+
2861
+ // src/planning/what-if.ts
2862
+ function asDate5(value) {
2863
+ if (value instanceof Date) return new Date(value.getTime());
2864
+ const parsed = /* @__PURE__ */ new Date(`${value}T00:00:00.000Z`);
2865
+ if (Number.isNaN(parsed.valueOf())) {
2866
+ throw new Error(`Invalid date '${value}'.`);
794
2867
  }
795
- return partitions;
2868
+ return parsed;
796
2869
  }
797
- function compute_readiness_with_corrections(graph) {
798
- const readiness = /* @__PURE__ */ new Map();
799
- const corrections = [];
800
- const warnings = [];
801
- const partitions = {
802
- ready: [],
803
- blocked: [],
804
- in_progress: [],
805
- waiting_review: [],
806
- done: []
807
- };
808
- for (const [taskId, task] of graph.nodes.entries()) {
809
- const state = compute_readiness(task, graph);
810
- readiness.set(taskId, state);
811
- partitions[state].push(task);
812
- if (task.status === "blocked" && state === "ready") {
813
- corrections.push({
814
- type: "task.transitioned",
815
- task_id: taskId,
816
- from: "blocked",
817
- to: "todo",
818
- reason: "All dependencies are resolved."
819
- });
2870
+ function calendar_days2(from, to) {
2871
+ const start = asDate5(from);
2872
+ const end = asDate5(to);
2873
+ return Math.round((end.getTime() - start.getTime()) / (24 * 60 * 60 * 1e3));
2874
+ }
2875
+ function signed_number(value, suffix = "") {
2876
+ if (!Number.isFinite(value) || value === 0) return "-";
2877
+ return `${value > 0 ? "+" : ""}${value}${suffix}`;
2878
+ }
2879
+ function signed_hours(value) {
2880
+ if (!Number.isFinite(value) || value === 0) return "-";
2881
+ return `${value > 0 ? "+" : ""}${value.toFixed(1)}h`;
2882
+ }
2883
+ function format_hours(value) {
2884
+ if (!Number.isFinite(value)) return "unbounded";
2885
+ return `${value.toFixed(1)}h`;
2886
+ }
2887
+ function format_headroom(result) {
2888
+ const budget = result.summary.effort_hours.budget;
2889
+ if (!Number.isFinite(budget)) return "unbounded";
2890
+ return `${(budget - result.summary.effort_hours.required).toFixed(1)}h`;
2891
+ }
2892
+ function modification_label(modification) {
2893
+ switch (modification.kind) {
2894
+ case "without":
2895
+ return `--without ${modification.task_id}`;
2896
+ case "add_member":
2897
+ return `--add-member ${modification.target}`;
2898
+ case "target":
2899
+ return `--target ${modification.target_date}`;
2900
+ case "set_priority":
2901
+ return `--set ${modification.task_id}:priority=${modification.priority}`;
2902
+ }
2903
+ }
2904
+ function build_rows(baseline, scenario) {
2905
+ const baselineTarget = baseline.summary.dates.target ?? "-";
2906
+ const scenarioTarget = scenario.summary.dates.target ?? "-";
2907
+ const baselineProjected = baseline.summary.dates.projected ?? "-";
2908
+ const scenarioProjected = scenario.summary.dates.projected ?? "-";
2909
+ const baselineHeadroom = Number.isFinite(baseline.summary.effort_hours.budget) ? baseline.summary.effort_hours.budget - baseline.summary.effort_hours.required : Number.NaN;
2910
+ const scenarioHeadroom = Number.isFinite(scenario.summary.effort_hours.budget) ? scenario.summary.effort_hours.budget - scenario.summary.effort_hours.required : Number.NaN;
2911
+ return [
2912
+ {
2913
+ metric: "Target date",
2914
+ original: baselineTarget,
2915
+ scenario: scenarioTarget,
2916
+ delta: baseline.summary.dates.target && scenario.summary.dates.target ? signed_number(calendar_days2(baseline.summary.dates.target, scenario.summary.dates.target), " days") : "-"
2917
+ },
2918
+ {
2919
+ metric: "Projected date",
2920
+ original: baselineProjected,
2921
+ scenario: scenarioProjected,
2922
+ delta: baseline.summary.dates.projected && scenario.summary.dates.projected ? signed_number(
2923
+ calendar_days2(baseline.summary.dates.projected, scenario.summary.dates.projected),
2924
+ " days"
2925
+ ) : "-"
2926
+ },
2927
+ {
2928
+ metric: "Effort (hours)",
2929
+ original: format_hours(baseline.summary.effort_hours.required),
2930
+ scenario: format_hours(scenario.summary.effort_hours.required),
2931
+ delta: signed_hours(scenario.summary.effort_hours.required - baseline.summary.effort_hours.required)
2932
+ },
2933
+ {
2934
+ metric: "Headroom",
2935
+ original: format_headroom(baseline),
2936
+ scenario: format_headroom(scenario),
2937
+ delta: signed_hours(scenarioHeadroom - baselineHeadroom)
2938
+ },
2939
+ {
2940
+ metric: "Critical path tasks",
2941
+ original: String(baseline.summary.critical_path.length),
2942
+ scenario: String(scenario.summary.critical_path.length),
2943
+ delta: signed_number(scenario.summary.critical_path.length - baseline.summary.critical_path.length)
2944
+ },
2945
+ {
2946
+ metric: "Status",
2947
+ original: baseline.status,
2948
+ scenario: scenario.status,
2949
+ delta: baseline.status === scenario.status ? "-" : `${baseline.status} -> ${scenario.status}`
820
2950
  }
821
- if (task.status === "todo" && state === "blocked") {
822
- const unresolved = (task.depends_on ?? []).filter((depId) => {
823
- const depTask = graph.nodes.get(depId);
824
- return !depTask || !isResolvedDependencyStatus(depTask.status);
825
- });
826
- warnings.push({
827
- code: "todo_with_unresolved_dependencies",
828
- task_id: taskId,
829
- message: `Task is todo but has unresolved dependencies: ${unresolved.join(", ")}.`
830
- });
2951
+ ];
2952
+ }
2953
+ function clone_graph(graph) {
2954
+ return structuredClone(graph);
2955
+ }
2956
+ function resolve_delivery(graph, deliveryId) {
2957
+ const delivery = graph.deliveries.get(deliveryId);
2958
+ if (!delivery) {
2959
+ throw new Error(`Delivery '${deliveryId}' not found.`);
2960
+ }
2961
+ return delivery;
2962
+ }
2963
+ function remove_task_and_exclusive_dependencies(graph, delivery, taskId) {
2964
+ const scoped = new Set(delivery.scope.include);
2965
+ for (const excluded of delivery.scope.exclude) {
2966
+ scoped.delete(excluded);
2967
+ }
2968
+ if (!scoped.has(taskId)) {
2969
+ throw new Error(`Task '${taskId}' is not in delivery '${delivery.id}' scope.`);
2970
+ }
2971
+ const toRemove = /* @__PURE__ */ new Set([taskId]);
2972
+ const queue = [taskId];
2973
+ while (queue.length > 0) {
2974
+ const current = queue.shift();
2975
+ if (!current) continue;
2976
+ for (const dependencyId of graph.forward.get(current) ?? /* @__PURE__ */ new Set()) {
2977
+ if (!scoped.has(dependencyId) || toRemove.has(dependencyId)) continue;
2978
+ const dependents = graph.reverse.get(dependencyId) ?? /* @__PURE__ */ new Set();
2979
+ const hasRemainingDependent = Array.from(dependents).some(
2980
+ (dependentId) => scoped.has(dependentId) && !toRemove.has(dependentId)
2981
+ );
2982
+ if (hasRemainingDependent) continue;
2983
+ toRemove.add(dependencyId);
2984
+ queue.push(dependencyId);
2985
+ }
2986
+ }
2987
+ delivery.scope.include = delivery.scope.include.filter((id) => !toRemove.has(id));
2988
+ delivery.scope.exclude = delivery.scope.exclude.filter((id) => !toRemove.has(id));
2989
+ }
2990
+ function resolve_human_profile(graph, delivery, target) {
2991
+ const direct = graph.resources.get(target);
2992
+ if (direct?.type === "human") {
2993
+ return direct;
2994
+ }
2995
+ const track = graph.tracks.get(target);
2996
+ if (track) {
2997
+ for (const profileId of track.capacity_profiles) {
2998
+ const profile = graph.resources.get(profileId);
2999
+ if (profile?.type === "human") {
3000
+ return profile;
3001
+ }
3002
+ }
3003
+ }
3004
+ for (const profileId of delivery.capacity_profiles) {
3005
+ const profile = graph.resources.get(profileId);
3006
+ if (profile?.type === "human") {
3007
+ return profile;
831
3008
  }
832
3009
  }
3010
+ throw new Error(`No human capacity profile found for '${target}'.`);
3011
+ }
3012
+ function add_member(graph, delivery, target, hoursPerWeek) {
3013
+ const profile = resolve_human_profile(graph, delivery, target);
3014
+ const defaultHours = hoursPerWeek ?? profile.defaults?.hours_per_week ?? profile.members[0]?.hours_per_week ?? 40;
3015
+ const nextMemberIndex = profile.members.length + 1;
3016
+ profile.members = [
3017
+ ...profile.members,
3018
+ {
3019
+ id: `what-if-${profile.id}-${nextMemberIndex}`,
3020
+ hours_per_week: defaultHours
3021
+ }
3022
+ ];
3023
+ graph.resources.set(profile.id, profile);
3024
+ }
3025
+ function set_target(delivery, targetDate) {
3026
+ asDate5(targetDate);
3027
+ delivery.target_date = targetDate;
3028
+ }
3029
+ function set_priority(graph, taskId, priority) {
3030
+ const task = graph.nodes.get(taskId);
3031
+ if (!task) {
3032
+ throw new Error(`Task '${taskId}' not found.`);
3033
+ }
3034
+ task.priority = priority;
3035
+ graph.nodes.set(taskId, task);
3036
+ }
3037
+ function apply_modification(graph, deliveryId, modification) {
3038
+ const delivery = resolve_delivery(graph, deliveryId);
3039
+ switch (modification.kind) {
3040
+ case "without":
3041
+ remove_task_and_exclusive_dependencies(graph, delivery, modification.task_id);
3042
+ return;
3043
+ case "add_member":
3044
+ add_member(graph, delivery, modification.target, modification.hours_per_week);
3045
+ return;
3046
+ case "target":
3047
+ set_target(delivery, modification.target_date);
3048
+ graph.deliveries.set(delivery.id, delivery);
3049
+ return;
3050
+ case "set_priority":
3051
+ set_priority(graph, modification.task_id, modification.priority);
3052
+ return;
3053
+ }
3054
+ }
3055
+ function analyze_what_if(baseline, modification) {
3056
+ const modifications = Array.isArray(modification) ? modification : [modification];
3057
+ if (modifications.length === 0) {
3058
+ throw new Error("At least one what-if modification is required.");
3059
+ }
3060
+ const baselineResult = analyze_feasibility(baseline.delivery_id, baseline.graph, baseline.today);
3061
+ const scenarioGraph = clone_graph(baseline.graph);
3062
+ for (const next of modifications) {
3063
+ apply_modification(scenarioGraph, baseline.delivery_id, next);
3064
+ }
3065
+ const scenarioResult = analyze_feasibility(baseline.delivery_id, scenarioGraph, baseline.today);
833
3066
  return {
834
- readiness,
835
- partitions,
836
- corrections,
837
- warnings
3067
+ delivery: baseline.delivery_id,
3068
+ label: modifications.map(modification_label).join(" "),
3069
+ baseline: baselineResult,
3070
+ scenario: scenarioResult,
3071
+ rows: build_rows(baselineResult, scenarioResult)
838
3072
  };
839
3073
  }
840
3074
 
3075
+ // src/schema/migration.ts
3076
+ import fs12 from "fs";
3077
+ import path7 from "path";
3078
+
841
3079
  // src/schema/version.ts
842
- import fs8 from "fs";
843
- import path2 from "path";
3080
+ import fs11 from "fs";
3081
+ import path6 from "path";
844
3082
  var CURRENT_SCHEMA_VERSION = 2;
845
3083
  function schemaVersionFile(coopDir) {
846
- return path2.join(coopDir, "schema-version");
3084
+ return path6.join(coopDir, "schema-version");
847
3085
  }
848
3086
  function read_schema_version(coopDir) {
849
3087
  const filePath = schemaVersionFile(coopDir);
850
- if (!fs8.existsSync(filePath)) {
3088
+ if (!fs11.existsSync(filePath)) {
851
3089
  throw new Error(`Missing schema-version file at ${filePath}.`);
852
3090
  }
853
- const raw = fs8.readFileSync(filePath, "utf8").trim();
3091
+ const raw = fs11.readFileSync(filePath, "utf8").trim();
854
3092
  if (!raw) {
855
3093
  throw new Error(`Schema version file is empty at ${filePath}.`);
856
3094
  }
@@ -865,10 +3103,168 @@ function write_schema_version(coopDir, version) {
865
3103
  throw new Error(`Schema version must be a positive integer. Received: ${String(version)}.`);
866
3104
  }
867
3105
  const filePath = schemaVersionFile(coopDir);
868
- fs8.writeFileSync(filePath, `${version}
3106
+ fs11.writeFileSync(filePath, `${version}
869
3107
  `, "utf8");
870
3108
  }
871
3109
 
3110
+ // src/schema/migration.ts
3111
+ function asString2(value) {
3112
+ return typeof value === "string" && value.trim().length > 0 ? value : null;
3113
+ }
3114
+ function asRecord(value) {
3115
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
3116
+ }
3117
+ function cloneRaw(raw) {
3118
+ return JSON.parse(JSON.stringify(raw));
3119
+ }
3120
+ function estimate_to_three_point(value) {
3121
+ return {
3122
+ optimistic_hours: Number((value * 0.7).toFixed(2)),
3123
+ expected_hours: Number(value.toFixed(2)),
3124
+ pessimistic_hours: Number((value * 1.8).toFixed(2))
3125
+ };
3126
+ }
3127
+ function walk_files(dirPath, extensions) {
3128
+ if (!fs12.existsSync(dirPath)) return [];
3129
+ const out = [];
3130
+ const entries = fs12.readdirSync(dirPath, { withFileTypes: true });
3131
+ for (const entry of entries) {
3132
+ const fullPath = path7.join(dirPath, entry.name);
3133
+ if (entry.isDirectory()) {
3134
+ out.push(...walk_files(fullPath, extensions));
3135
+ continue;
3136
+ }
3137
+ if (!entry.isFile()) continue;
3138
+ const ext = path7.extname(entry.name).toLowerCase();
3139
+ if (extensions.has(ext)) {
3140
+ out.push(fullPath);
3141
+ }
3142
+ }
3143
+ return out.sort((a, b) => a.localeCompare(b));
3144
+ }
3145
+ function file_mtime_iso(filePath) {
3146
+ const stats = fs12.statSync(filePath);
3147
+ return stats.mtime.toISOString().slice(0, 10);
3148
+ }
3149
+ function migration_v1_to_v2(rawTask, context = {}) {
3150
+ const migrated = { ...rawTask };
3151
+ if (migrated.status === "dropped") {
3152
+ migrated.status = "canceled";
3153
+ }
3154
+ if (migrated.type === "task") {
3155
+ migrated.type = "chore";
3156
+ } else if (migrated.type === "research") {
3157
+ migrated.type = "spike";
3158
+ }
3159
+ if (!Object.prototype.hasOwnProperty.call(migrated, "updated")) {
3160
+ migrated.updated = context.file_mtime_iso ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
3161
+ }
3162
+ if (!Object.prototype.hasOwnProperty.call(migrated, "assignee")) {
3163
+ migrated.assignee = null;
3164
+ }
3165
+ const deliveryRecord = asRecord(migrated.delivery);
3166
+ if (deliveryRecord) {
3167
+ const include = deliveryRecord.include;
3168
+ if (Array.isArray(include)) {
3169
+ const first = include.find((entry) => typeof entry === "string");
3170
+ migrated.delivery = asString2(first) ?? null;
3171
+ } else if (typeof include === "string" && include.trim().length > 0) {
3172
+ migrated.delivery = include.trim();
3173
+ } else {
3174
+ migrated.delivery = null;
3175
+ }
3176
+ }
3177
+ const estimate = migrated.estimate;
3178
+ if (typeof estimate === "number" && Number.isFinite(estimate) && estimate > 0) {
3179
+ migrated.estimate = estimate_to_three_point(estimate);
3180
+ } else if (asRecord(estimate)) {
3181
+ const estimateRecord = estimate;
3182
+ const hasExpectedHours = typeof estimateRecord.expected_hours === "number" && Number.isFinite(estimateRecord.expected_hours);
3183
+ const hasOptimisticHours = typeof estimateRecord.optimistic_hours === "number" && Number.isFinite(estimateRecord.optimistic_hours);
3184
+ const hasPessimisticHours = typeof estimateRecord.pessimistic_hours === "number" && Number.isFinite(estimateRecord.pessimistic_hours);
3185
+ if (hasExpectedHours && (!hasOptimisticHours || !hasPessimisticHours)) {
3186
+ migrated.estimate = {
3187
+ ...estimateRecord,
3188
+ optimistic_hours: hasOptimisticHours ? estimateRecord.optimistic_hours : Number((Number(estimateRecord.expected_hours) * 0.7).toFixed(2)),
3189
+ pessimistic_hours: hasPessimisticHours ? estimateRecord.pessimistic_hours : Number((Number(estimateRecord.expected_hours) * 1.8).toFixed(2))
3190
+ };
3191
+ }
3192
+ }
3193
+ return migrated;
3194
+ }
3195
+ var MIGRATIONS = [
3196
+ {
3197
+ from: 1,
3198
+ to: 2,
3199
+ description: "COOP schema v1 to v2 task migration.",
3200
+ migrate: migration_v1_to_v2
3201
+ }
3202
+ ];
3203
+ function migration_for_step(fromVersion, toVersion) {
3204
+ const found = MIGRATIONS.find((migration) => migration.from === fromVersion && migration.to === toVersion);
3205
+ if (!found) {
3206
+ throw new Error(`No schema migration registered for v${fromVersion} -> v${toVersion}.`);
3207
+ }
3208
+ return found;
3209
+ }
3210
+ function migrate_task(rawTask, fromVersion, toVersion, context = {}) {
3211
+ if (!Number.isInteger(fromVersion) || fromVersion <= 0) {
3212
+ throw new Error(`Invalid fromVersion '${String(fromVersion)}'.`);
3213
+ }
3214
+ if (!Number.isInteger(toVersion) || toVersion <= 0) {
3215
+ throw new Error(`Invalid toVersion '${String(toVersion)}'.`);
3216
+ }
3217
+ if (fromVersion > toVersion) {
3218
+ throw new Error(`Downgrade migration is not supported: v${fromVersion} -> v${toVersion}.`);
3219
+ }
3220
+ if (fromVersion === toVersion) {
3221
+ return cloneRaw(rawTask);
3222
+ }
3223
+ let current = cloneRaw(rawTask);
3224
+ for (let version = fromVersion; version < toVersion; version += 1) {
3225
+ const migration = migration_for_step(version, version + 1);
3226
+ current = migration.migrate(current, context);
3227
+ }
3228
+ return current;
3229
+ }
3230
+ function migrate_repository(coopDir, targetVersion = CURRENT_SCHEMA_VERSION, options = {}) {
3231
+ if (!Number.isInteger(targetVersion) || targetVersion <= 0) {
3232
+ throw new Error(`Target version must be a positive integer. Received: ${String(targetVersion)}.`);
3233
+ }
3234
+ const dry_run = Boolean(options.dry_run);
3235
+ const from_version = read_schema_version(coopDir);
3236
+ if (from_version > targetVersion) {
3237
+ throw new Error(`Cannot migrate backwards from v${from_version} to v${targetVersion}.`);
3238
+ }
3239
+ const taskFiles = walk_files(path7.join(coopDir, "tasks"), /* @__PURE__ */ new Set([".md"]));
3240
+ const changed_files = [];
3241
+ for (const filePath of taskFiles) {
3242
+ const { frontmatter, body } = parseFrontmatterFile(filePath);
3243
+ const migrated = migrate_task(frontmatter, from_version, targetVersion, {
3244
+ file_path: filePath,
3245
+ file_mtime_iso: file_mtime_iso(filePath)
3246
+ });
3247
+ const before = JSON.stringify(frontmatter);
3248
+ const after = JSON.stringify(migrated);
3249
+ if (before === after) continue;
3250
+ changed_files.push(filePath);
3251
+ if (!dry_run) {
3252
+ fs12.writeFileSync(filePath, stringifyFrontmatter(migrated, body), "utf8");
3253
+ }
3254
+ }
3255
+ if (!dry_run && from_version !== targetVersion) {
3256
+ write_schema_version(coopDir, targetVersion);
3257
+ }
3258
+ return {
3259
+ from_version,
3260
+ to_version: targetVersion,
3261
+ dry_run,
3262
+ files_scanned: taskFiles.length,
3263
+ files_changed: changed_files.length,
3264
+ changed_files
3265
+ };
3266
+ }
3267
+
872
3268
  // src/state/auto-transitions.ts
873
3269
  function toStatusMap(value) {
874
3270
  if (!value) return /* @__PURE__ */ new Map();
@@ -946,6 +3342,14 @@ function toIsoDate(now) {
946
3342
  const date = now instanceof Date ? now : /* @__PURE__ */ new Date();
947
3343
  return date.toISOString().slice(0, 10);
948
3344
  }
3345
+ function toIsoTimestamp2(now) {
3346
+ if (typeof now === "string") {
3347
+ if (now.includes("T")) return now;
3348
+ return `${now}T00:00:00.000Z`;
3349
+ }
3350
+ const date = now instanceof Date ? now : /* @__PURE__ */ new Date();
3351
+ return date.toISOString();
3352
+ }
949
3353
  function validateGovernanceAndDeps(task, targetStatus, context) {
950
3354
  if (task.status === "in_review" && targetStatus === "done" && task.governance?.approval_required) {
951
3355
  const reviewer = task.governance.reviewer;
@@ -982,13 +3386,26 @@ function transition(task, targetStatus, context = {}) {
982
3386
  error: governanceError
983
3387
  };
984
3388
  }
3389
+ const nextTask = {
3390
+ ...task,
3391
+ status: targetStatus,
3392
+ updated: toIsoDate(context.now)
3393
+ };
3394
+ if (targetStatus !== task.status) {
3395
+ context.eventEmitter?.emit({
3396
+ type: "task.transitioned",
3397
+ timestamp: toIsoTimestamp2(context.now),
3398
+ payload: {
3399
+ task_id: task.id,
3400
+ from: task.status,
3401
+ to: targetStatus,
3402
+ actor: context.actor
3403
+ }
3404
+ });
3405
+ }
985
3406
  return {
986
3407
  success: true,
987
- task: {
988
- ...task,
989
- status: targetStatus,
990
- updated: toIsoDate(context.now)
991
- }
3408
+ task: nextTask
992
3409
  };
993
3410
  }
994
3411
 
@@ -1206,8 +3623,9 @@ function validateSemantic(task, context = {}) {
1206
3623
  }
1207
3624
 
1208
3625
  // src/validator/structural.ts
1209
- import path3 from "path";
1210
- var ID_PATTERN = /^[A-Z]+-\d+$/;
3626
+ import path8 from "path";
3627
+ var ID_PATTERN = /^[A-Z0-9]+(?:-[A-Z0-9]+)+$/;
3628
+ var ALIAS_PATTERN = /^[A-Z0-9]+(?:[.-][A-Z0-9]+)*$/;
1211
3629
  var ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
1212
3630
  function error4(field, rule, message) {
1213
3631
  return { level: "error", field, rule, message };
@@ -1249,13 +3667,43 @@ function validateStructural(task, context = {}) {
1249
3667
  errors.push(error4("updated", "struct.updated_iso_date", `Field 'updated' must be an ISO date (YYYY-MM-DD).`));
1250
3668
  }
1251
3669
  if (context.filePath) {
1252
- const expected = path3.basename(context.filePath, path3.extname(context.filePath));
3670
+ const expected = path8.basename(context.filePath, path8.extname(context.filePath));
1253
3671
  if (typeof task.id === "string" && task.id !== expected) {
1254
3672
  errors.push(
1255
3673
  error4("id", "struct.id_matches_filename", `Task id '${task.id}' must match filename '${expected}'.`)
1256
3674
  );
1257
3675
  }
1258
3676
  }
3677
+ const aliases = task.aliases;
3678
+ if (aliases !== void 0) {
3679
+ if (!Array.isArray(aliases)) {
3680
+ errors.push(error4("aliases", "struct.aliases_array", "Field 'aliases' must be an array of strings."));
3681
+ } else {
3682
+ const seen = /* @__PURE__ */ new Set();
3683
+ for (const rawAlias of aliases) {
3684
+ if (typeof rawAlias !== "string" || rawAlias.trim().length === 0) {
3685
+ errors.push(error4("aliases", "struct.aliases_string", "Field 'aliases' entries must be non-empty strings."));
3686
+ continue;
3687
+ }
3688
+ const normalized = rawAlias.trim().toUpperCase().replace(/_/g, ".");
3689
+ if (!ALIAS_PATTERN.test(normalized)) {
3690
+ errors.push(
3691
+ error4(
3692
+ "aliases",
3693
+ "struct.aliases_pattern",
3694
+ `Alias '${rawAlias}' is invalid. Use letters/numbers with '.' or '-' separators.`
3695
+ )
3696
+ );
3697
+ continue;
3698
+ }
3699
+ if (seen.has(normalized)) {
3700
+ errors.push(error4("aliases", "struct.aliases_unique", `Alias '${rawAlias}' is duplicated.`));
3701
+ continue;
3702
+ }
3703
+ seen.add(normalized);
3704
+ }
3705
+ }
3706
+ }
1259
3707
  return errors;
1260
3708
  }
1261
3709
 
@@ -1343,9 +3791,131 @@ function validate(task, context = {}) {
1343
3791
  };
1344
3792
  }
1345
3793
 
3794
+ // src/workspace.ts
3795
+ import fs13 from "fs";
3796
+ import path9 from "path";
3797
+ var COOP_DIR_NAME = ".coop";
3798
+ function sanitizeProjectId(value, fallback) {
3799
+ const normalized = value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-");
3800
+ return normalized || fallback;
3801
+ }
3802
+ function coop_workspace_dir(repoRoot) {
3803
+ return path9.join(path9.resolve(repoRoot), COOP_DIR_NAME);
3804
+ }
3805
+ function coop_projects_dir(repoRoot) {
3806
+ return path9.join(coop_workspace_dir(repoRoot), "projects");
3807
+ }
3808
+ function coop_workspace_config_path(repoRoot) {
3809
+ return path9.join(coop_workspace_dir(repoRoot), "config.yml");
3810
+ }
3811
+ function coop_project_root(repoRoot, projectId) {
3812
+ return path9.join(coop_projects_dir(repoRoot), projectId);
3813
+ }
3814
+ function coop_project_config_path(projectRoot) {
3815
+ return path9.join(projectRoot, "config.yml");
3816
+ }
3817
+ function repo_default_project_id(repoRoot) {
3818
+ return sanitizeProjectId(path9.basename(path9.resolve(repoRoot)), "workspace");
3819
+ }
3820
+ function repo_default_project_name(repoRoot) {
3821
+ const base = path9.basename(path9.resolve(repoRoot)).trim();
3822
+ return base || "COOP Workspace";
3823
+ }
3824
+ function has_v2_projects_layout(repoRoot) {
3825
+ return fs13.existsSync(coop_projects_dir(repoRoot));
3826
+ }
3827
+ function has_legacy_project_layout(repoRoot) {
3828
+ const workspaceDir = coop_workspace_dir(repoRoot);
3829
+ return fs13.existsSync(workspaceDir) && fs13.existsSync(path9.join(workspaceDir, "config.yml")) && !fs13.existsSync(coop_projects_dir(repoRoot));
3830
+ }
3831
+ function read_workspace_config(repoRoot) {
3832
+ const configPath = coop_workspace_config_path(repoRoot);
3833
+ if (!fs13.existsSync(configPath) || has_legacy_project_layout(repoRoot)) {
3834
+ return { version: 2 };
3835
+ }
3836
+ return parseYamlFile(configPath);
3837
+ }
3838
+ function write_workspace_config(repoRoot, config) {
3839
+ fs13.mkdirSync(coop_workspace_dir(repoRoot), { recursive: true });
3840
+ writeYamlFile(coop_workspace_config_path(repoRoot), {
3841
+ version: config.version ?? 2,
3842
+ ...config.current_project ? { current_project: config.current_project } : {}
3843
+ });
3844
+ }
3845
+ function read_project_config(projectRoot) {
3846
+ return parseYamlFile(coop_project_config_path(projectRoot));
3847
+ }
3848
+ function project_ref_from_config(repoRoot, projectRoot, layout) {
3849
+ const config = read_project_config(projectRoot);
3850
+ const repoName = repo_default_project_name(repoRoot);
3851
+ const fallbackId = repo_default_project_id(repoRoot);
3852
+ return {
3853
+ id: sanitizeProjectId(config.project?.id ?? fallbackId, fallbackId),
3854
+ name: config.project?.name?.trim() || repoName,
3855
+ aliases: Array.isArray(config.project?.aliases) ? config.project.aliases.filter((entry) => typeof entry === "string" && entry.trim().length > 0) : [],
3856
+ root: projectRoot,
3857
+ repo_root: path9.resolve(repoRoot),
3858
+ layout
3859
+ };
3860
+ }
3861
+ function list_projects(repoRoot) {
3862
+ if (has_v2_projects_layout(repoRoot)) {
3863
+ const projectsDir = coop_projects_dir(repoRoot);
3864
+ return fs13.readdirSync(projectsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => path9.join(projectsDir, entry.name)).filter((projectRoot) => fs13.existsSync(coop_project_config_path(projectRoot))).map((projectRoot) => project_ref_from_config(repoRoot, projectRoot, "v2")).sort((a, b) => a.id.localeCompare(b.id));
3865
+ }
3866
+ if (has_legacy_project_layout(repoRoot)) {
3867
+ return [project_ref_from_config(repoRoot, coop_workspace_dir(repoRoot), "legacy")];
3868
+ }
3869
+ return [];
3870
+ }
3871
+ function resolve_project(repoRoot, options = {}) {
3872
+ const projects = list_projects(repoRoot);
3873
+ const requested = options.project?.trim().toLowerCase();
3874
+ if (requested) {
3875
+ const match = projects.find(
3876
+ (project) => project.id.toLowerCase() === requested || project.name.toLowerCase() === requested || project.aliases.some((alias) => alias.toLowerCase() === requested)
3877
+ );
3878
+ if (!match) {
3879
+ throw new Error(`Project '${options.project}' not found.`);
3880
+ }
3881
+ return match;
3882
+ }
3883
+ if (projects.length === 1) {
3884
+ return projects[0];
3885
+ }
3886
+ const workspaceConfig = read_workspace_config(repoRoot);
3887
+ if (workspaceConfig.current_project) {
3888
+ const match = projects.find((project) => project.id === workspaceConfig.current_project);
3889
+ if (match) return match;
3890
+ }
3891
+ if (!options.require && projects.length === 0) {
3892
+ return {
3893
+ id: repo_default_project_id(repoRoot),
3894
+ name: repo_default_project_name(repoRoot),
3895
+ aliases: [],
3896
+ root: coop_project_root(repoRoot, repo_default_project_id(repoRoot)),
3897
+ repo_root: path9.resolve(repoRoot),
3898
+ layout: "v2"
3899
+ };
3900
+ }
3901
+ if (projects.length === 0) {
3902
+ throw new Error("No COOP project found. Run 'coop init'.");
3903
+ }
3904
+ throw new Error("Multiple COOP projects found. Pass --project <id> or run 'coop project use <id>'.");
3905
+ }
3906
+ function ensure_workspace_layout(repoRoot) {
3907
+ const workspaceDir = coop_workspace_dir(repoRoot);
3908
+ fs13.mkdirSync(workspaceDir, { recursive: true });
3909
+ fs13.mkdirSync(coop_projects_dir(repoRoot), { recursive: true });
3910
+ return workspaceDir;
3911
+ }
3912
+ function is_project_initialized(projectRoot) {
3913
+ return fs13.existsSync(coop_project_config_path(projectRoot));
3914
+ }
3915
+
1346
3916
  // src/core.ts
1347
- import fs9 from "fs";
1348
- import path4 from "path";
3917
+ import fs14 from "fs";
3918
+ import path10 from "path";
1349
3919
  import matter from "gray-matter";
1350
3920
 
1351
3921
  // src/types.ts
@@ -1362,7 +3932,7 @@ var ITEM_DIRS = {
1362
3932
  spike: "spikes"
1363
3933
  };
1364
3934
  var COOP_DIR = ".coop";
1365
- var DEFAULT_CONFIG = {
3935
+ var DEFAULT_CONFIG2 = {
1366
3936
  spec_version: 1,
1367
3937
  id_prefix: "COOP",
1368
3938
  id_strategy: "text"
@@ -1371,17 +3941,17 @@ function toIdKey(value) {
1371
3941
  return value.trim().toUpperCase();
1372
3942
  }
1373
3943
  function repoRootByPackage(cwd) {
1374
- let current = path4.resolve(cwd);
3944
+ let current = path10.resolve(cwd);
1375
3945
  let lastWorkspaceRoot = null;
1376
3946
  while (true) {
1377
- const packageJson = path4.join(current, "package.json");
1378
- const workspaceYaml = path4.join(current, "pnpm-workspace.yaml");
1379
- if (fs9.existsSync(packageJson) && fs9.existsSync(workspaceYaml)) {
3947
+ const packageJson = path10.join(current, "package.json");
3948
+ const workspaceYaml = path10.join(current, "pnpm-workspace.yaml");
3949
+ if (fs14.existsSync(packageJson) && fs14.existsSync(workspaceYaml)) {
1380
3950
  lastWorkspaceRoot = current;
1381
- const hasCoop = fs9.existsSync(path4.join(current, COOP_DIR, "config.yml"));
3951
+ const hasCoop = fs14.existsSync(path10.join(current, COOP_DIR, "config.yml"));
1382
3952
  if (hasCoop) return current;
1383
3953
  }
1384
- const parent = path4.dirname(current);
3954
+ const parent = path10.dirname(current);
1385
3955
  if (parent === current) return lastWorkspaceRoot;
1386
3956
  current = parent;
1387
3957
  }
@@ -1390,24 +3960,24 @@ function findRepoRoot(cwd = process.cwd()) {
1390
3960
  return repoRootByPackage(cwd);
1391
3961
  }
1392
3962
  function configPathFor(rootDir, workspaceDir) {
1393
- return path4.join(rootDir, workspaceDir, "config.yml");
3963
+ return path10.join(rootDir, workspaceDir, "config.yml");
1394
3964
  }
1395
3965
  function backlogPathFor(rootDir, workspaceDir) {
1396
- return path4.join(rootDir, workspaceDir, "backlog");
3966
+ return path10.join(rootDir, workspaceDir, "backlog");
1397
3967
  }
1398
3968
  function releasesPathFor(rootDir, workspaceDir) {
1399
- return path4.join(rootDir, workspaceDir, "releases");
3969
+ return path10.join(rootDir, workspaceDir, "releases");
1400
3970
  }
1401
3971
  function detectWorkspaceDir(rootDir) {
1402
- if (fs9.existsSync(configPathFor(rootDir, COOP_DIR))) return COOP_DIR;
1403
- if (fs9.existsSync(path4.join(rootDir, COOP_DIR))) return COOP_DIR;
3972
+ if (fs14.existsSync(configPathFor(rootDir, COOP_DIR))) return COOP_DIR;
3973
+ if (fs14.existsSync(path10.join(rootDir, COOP_DIR))) return COOP_DIR;
1404
3974
  return null;
1405
3975
  }
1406
3976
  function preferredWorkspaceDir(rootDir) {
1407
3977
  return detectWorkspaceDir(rootDir) ?? COOP_DIR;
1408
3978
  }
1409
3979
  function missingConfigError(rootDir) {
1410
- const coopConfig = path4.relative(rootDir, configPathFor(rootDir, COOP_DIR));
3980
+ const coopConfig = path10.relative(rootDir, configPathFor(rootDir, COOP_DIR));
1411
3981
  return new Error(`COOP config missing at ${coopConfig}. Run: coop init`);
1412
3982
  }
1413
3983
  function parseConfig(raw) {
@@ -1418,9 +3988,9 @@ function parseConfig(raw) {
1418
3988
  if (ix <= 0) continue;
1419
3989
  map.set(line.slice(0, ix).trim(), line.slice(ix + 1).trim());
1420
3990
  }
1421
- const spec = Number(map.get("spec_version") ?? DEFAULT_CONFIG.spec_version);
1422
- const idPrefix = map.get("id_prefix") ?? DEFAULT_CONFIG.id_prefix;
1423
- const strategy = String(map.get("id_strategy") ?? DEFAULT_CONFIG.id_strategy ?? "text").trim().toLowerCase();
3991
+ const spec = Number(map.get("spec_version") ?? DEFAULT_CONFIG2.spec_version);
3992
+ const idPrefix = map.get("id_prefix") ?? DEFAULT_CONFIG2.id_prefix;
3993
+ const strategy = String(map.get("id_strategy") ?? DEFAULT_CONFIG2.id_strategy ?? "text").trim().toLowerCase();
1424
3994
  const rawNextId = map.get("next_id");
1425
3995
  const nextId = rawNextId != null ? Number(rawNextId) : void 0;
1426
3996
  if (!Number.isInteger(spec) || spec <= 0) throw new Error("config.yml must define a numeric spec_version.");
@@ -1453,10 +4023,10 @@ function configToString(config) {
1453
4023
  return lines.join("\n");
1454
4024
  }
1455
4025
  function toPortablePath(value) {
1456
- return value.split(path4.sep).join("/");
4026
+ return value.split(path10.sep).join("/");
1457
4027
  }
1458
4028
  function ensureReleasesDir(rootDir, workspaceDir) {
1459
- fs9.mkdirSync(releasesPathFor(rootDir, workspaceDir), { recursive: true });
4029
+ fs14.mkdirSync(releasesPathFor(rootDir, workspaceDir), { recursive: true });
1460
4030
  }
1461
4031
  function releaseHeader(date) {
1462
4032
  return `## ${date}`;
@@ -1478,12 +4048,12 @@ function appendReleaseEntry(rootDir, workspaceDir, item, previousStatus, nextSta
1478
4048
  ensureReleasesDir(rootDir, workspaceDir);
1479
4049
  const now = /* @__PURE__ */ new Date();
1480
4050
  const date = now.toISOString().slice(0, 10);
1481
- const releasePath = path4.join(releasesPathFor(rootDir, workspaceDir), `${date}.md`);
4051
+ const releasePath = path10.join(releasesPathFor(rootDir, workspaceDir), `${date}.md`);
1482
4052
  const heading = "# COOP Release Notes";
1483
4053
  const dayHeader = releaseHeader(date);
1484
4054
  const entry = releaseEntryLine(item, previousStatus, nextStatus);
1485
- if (!fs9.existsSync(releasePath)) {
1486
- fs9.writeFileSync(
4055
+ if (!fs14.existsSync(releasePath)) {
4056
+ fs14.writeFileSync(
1487
4057
  releasePath,
1488
4058
  [
1489
4059
  `${heading}
@@ -1496,10 +4066,10 @@ function appendReleaseEntry(rootDir, workspaceDir, item, previousStatus, nextSta
1496
4066
  ].join("\n"),
1497
4067
  "utf8"
1498
4068
  );
1499
- return toPortablePath(path4.relative(rootDir, releasePath));
4069
+ return toPortablePath(path10.relative(rootDir, releasePath));
1500
4070
  }
1501
- const existing = fs9.readFileSync(releasePath, "utf8");
1502
- if (hasReleaseEntry(existing, item.id)) return toPortablePath(path4.relative(rootDir, releasePath));
4071
+ const existing = fs14.readFileSync(releasePath, "utf8");
4072
+ if (hasReleaseEntry(existing, item.id)) return toPortablePath(path10.relative(rootDir, releasePath));
1503
4073
  let nextContent = existing;
1504
4074
  if (!existing.includes(`## ${date}`)) {
1505
4075
  if (!nextContent.endsWith("\n")) nextContent += "\n";
@@ -1509,9 +4079,9 @@ function appendReleaseEntry(rootDir, workspaceDir, item, previousStatus, nextSta
1509
4079
  if (!nextContent.endsWith("\n")) nextContent += "\n";
1510
4080
  nextContent += `${entry}
1511
4081
  `;
1512
- fs9.writeFileSync(releasePath, `${nextContent}
4082
+ fs14.writeFileSync(releasePath, `${nextContent}
1513
4083
  `, "utf8");
1514
- return toPortablePath(path4.relative(rootDir, releasePath));
4084
+ return toPortablePath(path10.relative(rootDir, releasePath));
1515
4085
  }
1516
4086
  function completeItem(rootDir, id) {
1517
4087
  const state = loadState(rootDir);
@@ -1589,28 +4159,28 @@ function validateAndNormalize(data, sourceFile) {
1589
4159
  };
1590
4160
  }
1591
4161
  function parseItem(filePath, rootDir) {
1592
- const raw = fs9.readFileSync(filePath, "utf8");
4162
+ const raw = fs14.readFileSync(filePath, "utf8");
1593
4163
  const parsed = matter(raw);
1594
- const data = validateAndNormalize(parsed.data, path4.relative(rootDir, filePath));
4164
+ const data = validateAndNormalize(parsed.data, path10.relative(rootDir, filePath));
1595
4165
  return {
1596
4166
  ...data,
1597
4167
  body: parsed.content || "",
1598
- filePath: path4.relative(rootDir, filePath)
4168
+ filePath: path10.relative(rootDir, filePath)
1599
4169
  };
1600
4170
  }
1601
4171
  function walk(dir) {
1602
4172
  const out = [];
1603
- if (!fs9.existsSync(dir)) return out;
1604
- const entries = fs9.readdirSync(dir, { withFileTypes: true });
4173
+ if (!fs14.existsSync(dir)) return out;
4174
+ const entries = fs14.readdirSync(dir, { withFileTypes: true });
1605
4175
  for (const entry of entries) {
1606
- const file = path4.join(dir, entry.name);
4176
+ const file = path10.join(dir, entry.name);
1607
4177
  if (entry.isDirectory()) out.push(...walk(file));
1608
4178
  if (entry.isFile() && file.endsWith(".md")) out.push(file);
1609
4179
  }
1610
4180
  return out;
1611
4181
  }
1612
4182
  function itemPath(type, id, rootDir, workspaceDir) {
1613
- return path4.join(backlogPathFor(rootDir, workspaceDir), ITEM_DIRS[type], `${id}.md`);
4183
+ return path10.join(backlogPathFor(rootDir, workspaceDir), ITEM_DIRS[type], `${id}.md`);
1614
4184
  }
1615
4185
  function normalizeFrontmatterValue(value) {
1616
4186
  if (value == null) return void 0;
@@ -1697,30 +4267,30 @@ function nextGeneratedId(config, title, existing) {
1697
4267
  }
1698
4268
  function ensureCoopLayout(rootDir) {
1699
4269
  const workspaceDir = preferredWorkspaceDir(rootDir);
1700
- const root = path4.join(rootDir, workspaceDir);
1701
- fs9.mkdirSync(root, { recursive: true });
1702
- fs9.mkdirSync(path4.join(root, "releases"), { recursive: true });
1703
- fs9.mkdirSync(path4.join(root, "plans"), { recursive: true });
1704
- fs9.mkdirSync(path4.join(root, "views"), { recursive: true });
1705
- fs9.mkdirSync(path4.join(root, "templates"), { recursive: true });
4270
+ const root = path10.join(rootDir, workspaceDir);
4271
+ fs14.mkdirSync(root, { recursive: true });
4272
+ fs14.mkdirSync(path10.join(root, "releases"), { recursive: true });
4273
+ fs14.mkdirSync(path10.join(root, "plans"), { recursive: true });
4274
+ fs14.mkdirSync(path10.join(root, "views"), { recursive: true });
4275
+ fs14.mkdirSync(path10.join(root, "templates"), { recursive: true });
1706
4276
  for (const dir of Object.values(ITEM_DIRS)) {
1707
- fs9.mkdirSync(path4.join(root, "backlog", dir), { recursive: true });
4277
+ fs14.mkdirSync(path10.join(root, "backlog", dir), { recursive: true });
1708
4278
  }
1709
- const configFile = path4.join(root, "config.yml");
1710
- if (!fs9.existsSync(configFile)) {
1711
- fs9.writeFileSync(configFile, configToString(DEFAULT_CONFIG), "utf8");
4279
+ const configFile = path10.join(root, "config.yml");
4280
+ if (!fs14.existsSync(configFile)) {
4281
+ fs14.writeFileSync(configFile, configToString(DEFAULT_CONFIG2), "utf8");
1712
4282
  }
1713
4283
  }
1714
4284
  function loadState(rootDir) {
1715
4285
  const workspaceDir = detectWorkspaceDir(rootDir);
1716
4286
  if (!workspaceDir) throw missingConfigError(rootDir);
1717
4287
  const configPath = configPathFor(rootDir, workspaceDir);
1718
- if (!fs9.existsSync(configPath)) throw missingConfigError(rootDir);
1719
- const config = parseConfig(fs9.readFileSync(configPath, "utf8"));
4288
+ if (!fs14.existsSync(configPath)) throw missingConfigError(rootDir);
4289
+ const config = parseConfig(fs14.readFileSync(configPath, "utf8"));
1720
4290
  const items = [];
1721
4291
  const itemsById = /* @__PURE__ */ new Map();
1722
4292
  for (const type of ITEM_TYPES) {
1723
- const dir = path4.join(backlogPathFor(rootDir, workspaceDir), ITEM_DIRS[type]);
4293
+ const dir = path10.join(backlogPathFor(rootDir, workspaceDir), ITEM_DIRS[type]);
1724
4294
  const files = walk(dir);
1725
4295
  for (const file of files) {
1726
4296
  const item = parseItem(file, rootDir);
@@ -1804,21 +4374,21 @@ function createItem(rootDir, params) {
1804
4374
  parent_id: params.parent_id
1805
4375
  };
1806
4376
  const itemPathName = itemPath(params.type, item.id, rootDir, state.workspaceDir);
1807
- fs9.writeFileSync(itemPathName, serialize(item, params.body || ""), "utf8");
4377
+ fs14.writeFileSync(itemPathName, serialize(item, params.body || ""), "utf8");
1808
4378
  if ((config.id_strategy ?? "text") === "counter") {
1809
4379
  const numericMatch = /-(\d+)$/.exec(item.id);
1810
4380
  if (numericMatch) {
1811
4381
  const numericValue = Number(numericMatch[1]);
1812
4382
  if (Number.isInteger(numericValue) && numericValue >= (config.next_id ?? 1)) {
1813
4383
  config.next_id = numericValue + 1;
1814
- fs9.writeFileSync(configPathFor(rootDir, state.workspaceDir), configToString(config), "utf8");
4384
+ fs14.writeFileSync(configPathFor(rootDir, state.workspaceDir), configToString(config), "utf8");
1815
4385
  }
1816
4386
  }
1817
4387
  }
1818
4388
  return {
1819
4389
  ...item,
1820
4390
  body: params.body || "",
1821
- filePath: path4.relative(rootDir, itemPathName)
4391
+ filePath: path10.relative(rootDir, itemPathName)
1822
4392
  };
1823
4393
  }
1824
4394
  function updateItem(rootDir, id, patch) {
@@ -1844,8 +4414,8 @@ function updateItem(rootDir, id, patch) {
1844
4414
  };
1845
4415
  if (!ITEM_TYPES.includes(next.type)) throw new Error(`Unknown type ${next.type}.`);
1846
4416
  if (!ITEM_STATUSES.includes(next.status)) throw new Error(`Unknown status ${next.status}.`);
1847
- const filePath = path4.join(rootDir, existing.filePath);
1848
- fs9.writeFileSync(filePath, serialize(next, patch.body || existing.body), "utf8");
4417
+ const filePath = path10.join(rootDir, existing.filePath);
4418
+ fs14.writeFileSync(filePath, serialize(next, patch.body || existing.body), "utf8");
1849
4419
  return {
1850
4420
  ...next,
1851
4421
  body: patch.body || existing.body,
@@ -1861,7 +4431,7 @@ function deleteItem(rootDir, id) {
1861
4431
  if (children.length > 0) {
1862
4432
  throw new Error(`Cannot delete ${existing.id} because it has ${children.length} child item(s). Remove children first.`);
1863
4433
  }
1864
- fs9.unlinkSync(path4.join(rootDir, existing.filePath));
4434
+ fs14.unlinkSync(path10.join(rootDir, existing.filePath));
1865
4435
  return existing;
1866
4436
  }
1867
4437
  function renderAgentPrompt(item) {
@@ -1900,8 +4470,8 @@ function validateRepo(rootDir) {
1900
4470
  const errors = [];
1901
4471
  const warnings = [];
1902
4472
  const workspaceDir = detectWorkspaceDir(rootDir);
1903
- if (!workspaceDir || !fs9.existsSync(configPathFor(rootDir, workspaceDir))) {
1904
- errors.push("Missing .coop/config.yml. Run coop init first.");
4473
+ if (!workspaceDir || !fs14.existsSync(configPathFor(rootDir, workspaceDir))) {
4474
+ errors.push("Missing COOP config. Run coop init first.");
1905
4475
  return { valid: false, errors, warnings };
1906
4476
  }
1907
4477
  let state;
@@ -1933,13 +4503,20 @@ function validateRepo(rootDir) {
1933
4503
  }
1934
4504
  export {
1935
4505
  ArtifactType,
4506
+ COOP_EVENT_TYPES,
1936
4507
  CURRENT_SCHEMA_VERSION,
4508
+ CoopEventEmitter,
4509
+ DEFAULT_SCORE_WEIGHTS,
1937
4510
  DeliveryStatus,
1938
4511
  ExecutorType,
1939
4512
  ITEM_STATUSES,
1940
4513
  ITEM_TYPES,
1941
4514
  IdeaStatus,
4515
+ IndexManager,
4516
+ MIGRATIONS,
1942
4517
  RiskLevel,
4518
+ RunStatus,
4519
+ RunStepStatus,
1943
4520
  RunbookAction,
1944
4521
  TaskComplexity,
1945
4522
  TaskDeterminism,
@@ -1948,23 +4525,63 @@ export {
1948
4525
  TaskType,
1949
4526
  VALID_TASK_TRANSITIONS,
1950
4527
  VALID_TRANSITIONS,
4528
+ allocate,
4529
+ allocate_ai,
4530
+ allocate_ai_tokens,
4531
+ analyze_feasibility,
4532
+ analyze_what_if,
4533
+ build_capacity_ledger,
1951
4534
  build_graph,
1952
4535
  check_blocked,
4536
+ check_permission,
1953
4537
  check_unblocked,
4538
+ check_wip,
1954
4539
  completeItem,
4540
+ complexity_penalty,
1955
4541
  compute_all_readiness,
4542
+ compute_critical_path,
1956
4543
  compute_readiness,
1957
4544
  compute_readiness_with_corrections,
4545
+ compute_score,
4546
+ compute_velocity,
4547
+ coop_project_config_path,
4548
+ coop_project_root,
4549
+ coop_projects_dir,
4550
+ coop_workspace_config_path,
4551
+ coop_workspace_dir,
1958
4552
  createItem,
4553
+ create_seeded_rng,
4554
+ critical_path_weight,
1959
4555
  deleteItem,
4556
+ dependency_unlock_weight,
1960
4557
  detect_cycle,
4558
+ detect_delivery_risks,
4559
+ determinism_weight,
4560
+ effective_weekly_hours,
4561
+ effort_or_default,
1961
4562
  ensureCoopLayout,
4563
+ ensure_workspace_layout,
4564
+ executor_fit_weight,
4565
+ external_dependencies_for_task,
1962
4566
  extract_subgraph,
1963
4567
  findRepoRoot,
1964
4568
  find_external_dependencies,
1965
4569
  getItemById,
4570
+ get_remaining_tokens,
4571
+ get_user_role,
4572
+ has_legacy_project_layout,
4573
+ has_v2_projects_layout,
4574
+ is_external_dependency,
4575
+ is_project_initialized,
4576
+ list_projects,
1966
4577
  loadState,
4578
+ load_auth_config,
4579
+ load_completed_runs,
1967
4580
  load_graph,
4581
+ load_plugins,
4582
+ migrate_repository,
4583
+ migrate_task,
4584
+ monte_carlo_forecast,
1968
4585
  parseDeliveryContent,
1969
4586
  parseDeliveryFile,
1970
4587
  parseFrontmatterContent,
@@ -1975,16 +4592,38 @@ export {
1975
4592
  parseTaskFile,
1976
4593
  parseYamlContent,
1977
4594
  parseYamlFile,
4595
+ parse_external_dependency,
1978
4596
  partition_by_readiness,
4597
+ pert_hours,
4598
+ pert_stddev,
4599
+ priority_weight,
1979
4600
  queryItems,
4601
+ read_project_config,
1980
4602
  read_schema_version,
4603
+ read_workspace_config,
1981
4604
  renderAgentPrompt,
4605
+ repo_default_project_id,
4606
+ repo_default_project_name,
4607
+ resolve_external_dependencies,
4608
+ resolve_project,
4609
+ risk_penalty,
4610
+ run_hook,
4611
+ run_monte_carlo_chunk,
4612
+ run_plugins_for_event,
4613
+ sample_pert_beta,
4614
+ sample_task_hours,
4615
+ schedule_next,
4616
+ simulate_schedule,
1982
4617
  stringifyFrontmatter,
4618
+ stringifyYamlContent,
4619
+ task_effort_hours,
1983
4620
  topological_sort,
1984
4621
  transition,
1985
4622
  transitive_dependencies,
1986
4623
  transitive_dependents,
4624
+ type_weight,
1987
4625
  updateItem,
4626
+ urgency_weight,
1988
4627
  validate,
1989
4628
  validateReferential,
1990
4629
  validateRepo,
@@ -1994,5 +4633,7 @@ export {
1994
4633
  validate_graph,
1995
4634
  validate_transition,
1996
4635
  writeTask,
1997
- write_schema_version
4636
+ writeYamlFile,
4637
+ write_schema_version,
4638
+ write_workspace_config
1998
4639
  };