@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.cjs CHANGED
@@ -31,13 +31,20 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
33
  ArtifactType: () => ArtifactType,
34
+ COOP_EVENT_TYPES: () => COOP_EVENT_TYPES,
34
35
  CURRENT_SCHEMA_VERSION: () => CURRENT_SCHEMA_VERSION,
36
+ CoopEventEmitter: () => CoopEventEmitter,
37
+ DEFAULT_SCORE_WEIGHTS: () => DEFAULT_SCORE_WEIGHTS,
35
38
  DeliveryStatus: () => DeliveryStatus,
36
39
  ExecutorType: () => ExecutorType,
37
40
  ITEM_STATUSES: () => ITEM_STATUSES,
38
41
  ITEM_TYPES: () => ITEM_TYPES,
39
42
  IdeaStatus: () => IdeaStatus,
43
+ IndexManager: () => IndexManager,
44
+ MIGRATIONS: () => MIGRATIONS,
40
45
  RiskLevel: () => RiskLevel,
46
+ RunStatus: () => RunStatus,
47
+ RunStepStatus: () => RunStepStatus,
41
48
  RunbookAction: () => RunbookAction,
42
49
  TaskComplexity: () => TaskComplexity,
43
50
  TaskDeterminism: () => TaskDeterminism,
@@ -46,23 +53,63 @@ __export(index_exports, {
46
53
  TaskType: () => TaskType,
47
54
  VALID_TASK_TRANSITIONS: () => VALID_TASK_TRANSITIONS,
48
55
  VALID_TRANSITIONS: () => VALID_TRANSITIONS,
56
+ allocate: () => allocate,
57
+ allocate_ai: () => allocate_ai,
58
+ allocate_ai_tokens: () => allocate_ai_tokens,
59
+ analyze_feasibility: () => analyze_feasibility,
60
+ analyze_what_if: () => analyze_what_if,
61
+ build_capacity_ledger: () => build_capacity_ledger,
49
62
  build_graph: () => build_graph,
50
63
  check_blocked: () => check_blocked,
64
+ check_permission: () => check_permission,
51
65
  check_unblocked: () => check_unblocked,
66
+ check_wip: () => check_wip,
52
67
  completeItem: () => completeItem,
68
+ complexity_penalty: () => complexity_penalty,
53
69
  compute_all_readiness: () => compute_all_readiness,
70
+ compute_critical_path: () => compute_critical_path,
54
71
  compute_readiness: () => compute_readiness,
55
72
  compute_readiness_with_corrections: () => compute_readiness_with_corrections,
73
+ compute_score: () => compute_score,
74
+ compute_velocity: () => compute_velocity,
75
+ coop_project_config_path: () => coop_project_config_path,
76
+ coop_project_root: () => coop_project_root,
77
+ coop_projects_dir: () => coop_projects_dir,
78
+ coop_workspace_config_path: () => coop_workspace_config_path,
79
+ coop_workspace_dir: () => coop_workspace_dir,
56
80
  createItem: () => createItem,
81
+ create_seeded_rng: () => create_seeded_rng,
82
+ critical_path_weight: () => critical_path_weight,
57
83
  deleteItem: () => deleteItem,
84
+ dependency_unlock_weight: () => dependency_unlock_weight,
58
85
  detect_cycle: () => detect_cycle,
86
+ detect_delivery_risks: () => detect_delivery_risks,
87
+ determinism_weight: () => determinism_weight,
88
+ effective_weekly_hours: () => effective_weekly_hours,
89
+ effort_or_default: () => effort_or_default,
59
90
  ensureCoopLayout: () => ensureCoopLayout,
91
+ ensure_workspace_layout: () => ensure_workspace_layout,
92
+ executor_fit_weight: () => executor_fit_weight,
93
+ external_dependencies_for_task: () => external_dependencies_for_task,
60
94
  extract_subgraph: () => extract_subgraph,
61
95
  findRepoRoot: () => findRepoRoot,
62
96
  find_external_dependencies: () => find_external_dependencies,
63
97
  getItemById: () => getItemById,
98
+ get_remaining_tokens: () => get_remaining_tokens,
99
+ get_user_role: () => get_user_role,
100
+ has_legacy_project_layout: () => has_legacy_project_layout,
101
+ has_v2_projects_layout: () => has_v2_projects_layout,
102
+ is_external_dependency: () => is_external_dependency,
103
+ is_project_initialized: () => is_project_initialized,
104
+ list_projects: () => list_projects,
64
105
  loadState: () => loadState,
106
+ load_auth_config: () => load_auth_config,
107
+ load_completed_runs: () => load_completed_runs,
65
108
  load_graph: () => load_graph,
109
+ load_plugins: () => load_plugins,
110
+ migrate_repository: () => migrate_repository,
111
+ migrate_task: () => migrate_task,
112
+ monte_carlo_forecast: () => monte_carlo_forecast,
66
113
  parseDeliveryContent: () => parseDeliveryContent,
67
114
  parseDeliveryFile: () => parseDeliveryFile,
68
115
  parseFrontmatterContent: () => parseFrontmatterContent,
@@ -73,16 +120,38 @@ __export(index_exports, {
73
120
  parseTaskFile: () => parseTaskFile,
74
121
  parseYamlContent: () => parseYamlContent,
75
122
  parseYamlFile: () => parseYamlFile,
123
+ parse_external_dependency: () => parse_external_dependency,
76
124
  partition_by_readiness: () => partition_by_readiness,
125
+ pert_hours: () => pert_hours,
126
+ pert_stddev: () => pert_stddev,
127
+ priority_weight: () => priority_weight,
77
128
  queryItems: () => queryItems,
129
+ read_project_config: () => read_project_config,
78
130
  read_schema_version: () => read_schema_version,
131
+ read_workspace_config: () => read_workspace_config,
79
132
  renderAgentPrompt: () => renderAgentPrompt,
133
+ repo_default_project_id: () => repo_default_project_id,
134
+ repo_default_project_name: () => repo_default_project_name,
135
+ resolve_external_dependencies: () => resolve_external_dependencies,
136
+ resolve_project: () => resolve_project,
137
+ risk_penalty: () => risk_penalty,
138
+ run_hook: () => run_hook,
139
+ run_monte_carlo_chunk: () => run_monte_carlo_chunk,
140
+ run_plugins_for_event: () => run_plugins_for_event,
141
+ sample_pert_beta: () => sample_pert_beta,
142
+ sample_task_hours: () => sample_task_hours,
143
+ schedule_next: () => schedule_next,
144
+ simulate_schedule: () => simulate_schedule,
80
145
  stringifyFrontmatter: () => stringifyFrontmatter,
146
+ stringifyYamlContent: () => stringifyYamlContent,
147
+ task_effort_hours: () => task_effort_hours,
81
148
  topological_sort: () => topological_sort,
82
149
  transition: () => transition,
83
150
  transitive_dependencies: () => transitive_dependencies,
84
151
  transitive_dependents: () => transitive_dependents,
152
+ type_weight: () => type_weight,
85
153
  updateItem: () => updateItem,
154
+ urgency_weight: () => urgency_weight,
86
155
  validate: () => validate,
87
156
  validateReferential: () => validateReferential,
88
157
  validateRepo: () => validateRepo,
@@ -92,10 +161,166 @@ __export(index_exports, {
92
161
  validate_graph: () => validate_graph,
93
162
  validate_transition: () => validate_transition,
94
163
  writeTask: () => writeTask,
95
- write_schema_version: () => write_schema_version
164
+ writeYamlFile: () => writeYamlFile,
165
+ write_schema_version: () => write_schema_version,
166
+ write_workspace_config: () => write_workspace_config
96
167
  });
97
168
  module.exports = __toCommonJS(index_exports);
98
169
 
170
+ // src/planning/estimation.ts
171
+ var COMPLEXITY_FALLBACK_HOURS = {
172
+ trivial: 1,
173
+ small: 4,
174
+ medium: 12,
175
+ large: 32,
176
+ unknown: Number.NaN
177
+ };
178
+ function validateEstimateValue(value, field) {
179
+ if (!Number.isFinite(value) || value <= 0) {
180
+ throw new Error(`estimate.${field} must be a positive number.`);
181
+ }
182
+ }
183
+ function complexity_fallback_hours(complexity) {
184
+ if (!complexity) {
185
+ throw new Error("Task effort cannot be computed: missing complexity and estimate.");
186
+ }
187
+ if (complexity === "unknown") {
188
+ throw new Error("Task effort cannot be computed: complexity is 'unknown' and estimate is missing.");
189
+ }
190
+ return COMPLEXITY_FALLBACK_HOURS[complexity];
191
+ }
192
+ function pert_hours(estimate) {
193
+ validateEstimateValue(estimate.optimistic_hours, "optimistic_hours");
194
+ validateEstimateValue(estimate.expected_hours, "expected_hours");
195
+ validateEstimateValue(estimate.pessimistic_hours, "pessimistic_hours");
196
+ return (estimate.optimistic_hours + 4 * estimate.expected_hours + estimate.pessimistic_hours) / 6;
197
+ }
198
+ function pert_stddev(estimate) {
199
+ validateEstimateValue(estimate.optimistic_hours, "optimistic_hours");
200
+ validateEstimateValue(estimate.pessimistic_hours, "pessimistic_hours");
201
+ return (estimate.pessimistic_hours - estimate.optimistic_hours) / 6;
202
+ }
203
+ function task_effort_hours(task) {
204
+ const humanHours = task.resources?.human_hours;
205
+ if (typeof humanHours === "number" && Number.isFinite(humanHours)) {
206
+ if (humanHours < 0) {
207
+ throw new Error("resources.human_hours must be >= 0.");
208
+ }
209
+ return humanHours;
210
+ }
211
+ if (task.estimate) {
212
+ return pert_hours(task.estimate);
213
+ }
214
+ return complexity_fallback_hours(task.complexity);
215
+ }
216
+ function effort_or_default(task, config) {
217
+ const humanHours = task.resources?.human_hours;
218
+ if (typeof humanHours === "number" && Number.isFinite(humanHours)) {
219
+ if (humanHours < 0) {
220
+ throw new Error("resources.human_hours must be >= 0.");
221
+ }
222
+ return humanHours;
223
+ }
224
+ if (task.estimate) {
225
+ return pert_hours(task.estimate);
226
+ }
227
+ const defaultComplexity = config.defaults?.task?.complexity;
228
+ return complexity_fallback_hours(task.complexity ?? defaultComplexity);
229
+ }
230
+
231
+ // src/graph/external.ts
232
+ var EXTERNAL_PREFIX = "external:";
233
+ function is_external_dependency(value) {
234
+ return value.trim().toLowerCase().startsWith(EXTERNAL_PREFIX);
235
+ }
236
+ function parse_external_dependency(value) {
237
+ const normalized = value.trim();
238
+ if (!is_external_dependency(normalized)) {
239
+ return null;
240
+ }
241
+ const rest = normalized.slice(EXTERNAL_PREFIX.length);
242
+ const separator = rest.indexOf("/");
243
+ if (separator <= 0 || separator === rest.length - 1) {
244
+ return null;
245
+ }
246
+ const repo = rest.slice(0, separator).trim();
247
+ const taskId = rest.slice(separator + 1).trim();
248
+ if (!repo || !taskId) {
249
+ return null;
250
+ }
251
+ return {
252
+ raw: normalized,
253
+ repo,
254
+ task_id: taskId
255
+ };
256
+ }
257
+ function external_dependencies_for_task(graph, taskId) {
258
+ return [...graph.external_dependencies?.get(taskId) ?? []];
259
+ }
260
+ function joinUrl(baseUrl, pathName) {
261
+ return `${baseUrl.replace(/\/+$/g, "")}${pathName.startsWith("/") ? pathName : `/${pathName}`}`;
262
+ }
263
+ function resolvedStatus(taskStatus) {
264
+ if (!taskStatus) return "unknown";
265
+ if (taskStatus === "done" || taskStatus === "canceled") return "resolved";
266
+ return "blocked";
267
+ }
268
+ async function resolve_external_dependencies(graph, options) {
269
+ const fetchImpl = options.fetch_impl ?? fetch;
270
+ const resolutions = /* @__PURE__ */ new Map();
271
+ const externalMap = graph.external_dependencies ?? /* @__PURE__ */ new Map();
272
+ for (const [taskId, deps] of externalMap.entries()) {
273
+ const results = [];
274
+ for (const dep of deps) {
275
+ const remote = options.remotes[dep.repo];
276
+ if (!remote?.base_url) {
277
+ results.push({
278
+ dependency: dep,
279
+ status: "unknown",
280
+ warning: `No API base URL configured for external repo '${dep.repo}'.`
281
+ });
282
+ continue;
283
+ }
284
+ try {
285
+ const response = await fetchImpl(joinUrl(remote.base_url, `/api/tasks/${encodeURIComponent(dep.task_id)}`));
286
+ if (response.status === 404) {
287
+ results.push({
288
+ dependency: dep,
289
+ status: "missing",
290
+ warning: `External task '${dep.raw}' was not found at ${remote.base_url}.`
291
+ });
292
+ continue;
293
+ }
294
+ if (!response.ok) {
295
+ results.push({
296
+ dependency: dep,
297
+ status: "unreachable",
298
+ warning: `External task '${dep.raw}' returned HTTP ${response.status}.`
299
+ });
300
+ continue;
301
+ }
302
+ const payload = await response.json();
303
+ const taskRecord = payload && typeof payload === "object" && payload.task && typeof payload.task === "object" ? payload.task : payload;
304
+ const taskStatus = typeof taskRecord.status === "string" ? taskRecord.status : null;
305
+ results.push({
306
+ dependency: dep,
307
+ status: resolvedStatus(taskStatus),
308
+ task_status: taskStatus,
309
+ warning: resolvedStatus(taskStatus) === "resolved" ? void 0 : `External task '${dep.raw}' is ${taskStatus ?? "unknown"}.`
310
+ });
311
+ } catch {
312
+ results.push({
313
+ dependency: dep,
314
+ status: "unreachable",
315
+ warning: `External task '${dep.raw}' could not be reached.`
316
+ });
317
+ }
318
+ }
319
+ resolutions.set(taskId, results);
320
+ }
321
+ return resolutions;
322
+ }
323
+
99
324
  // src/graph/dag.ts
100
325
  function existingDeps(graph, nodeId) {
101
326
  const deps = graph.forward.get(nodeId) ?? /* @__PURE__ */ new Set();
@@ -216,13 +441,25 @@ function build_graph(tasks, context = {}) {
216
441
  }
217
442
  const forward = /* @__PURE__ */ new Map();
218
443
  const reverse = /* @__PURE__ */ new Map();
444
+ const externalDependencies = /* @__PURE__ */ new Map();
219
445
  for (const nodeId of nodes.keys()) {
220
446
  forward.set(nodeId, /* @__PURE__ */ new Set());
221
447
  reverse.set(nodeId, /* @__PURE__ */ new Set());
448
+ externalDependencies.set(nodeId, []);
222
449
  }
223
450
  for (const task of tasks) {
224
- const deps = new Set(task.depends_on ?? []);
451
+ const deps = /* @__PURE__ */ new Set();
452
+ const externalDeps = [];
453
+ for (const depId of task.depends_on ?? []) {
454
+ const external = parse_external_dependency(depId);
455
+ if (external) {
456
+ externalDeps.push(external);
457
+ continue;
458
+ }
459
+ deps.add(depId);
460
+ }
225
461
  forward.set(task.id, deps);
462
+ externalDependencies.set(task.id, externalDeps);
226
463
  for (const depId of deps) {
227
464
  if (!nodes.has(depId)) continue;
228
465
  reverse.get(depId)?.add(task.id);
@@ -235,16 +472,266 @@ function build_graph(tasks, context = {}) {
235
472
  topological_order: [],
236
473
  tracks: context.tracks ? new Map(context.tracks) : /* @__PURE__ */ new Map(),
237
474
  resources: context.resources ? new Map(context.resources) : /* @__PURE__ */ new Map(),
238
- deliveries: context.deliveries ? new Map(context.deliveries) : /* @__PURE__ */ new Map()
475
+ deliveries: context.deliveries ? new Map(context.deliveries) : /* @__PURE__ */ new Map(),
476
+ external_dependencies: externalDependencies
239
477
  };
240
478
  graph.topological_order = topological_sort(graph);
241
479
  return graph;
242
480
  }
243
481
 
244
- // src/graph/loader.ts
482
+ // src/graph/subgraph.ts
483
+ function extract_subgraph(graph, taskIds) {
484
+ const selected = new Set(taskIds.filter((taskId) => graph.nodes.has(taskId)));
485
+ const nodes = /* @__PURE__ */ new Map();
486
+ for (const taskId of selected) {
487
+ const task = graph.nodes.get(taskId);
488
+ if (task) nodes.set(taskId, task);
489
+ }
490
+ const forward = /* @__PURE__ */ new Map();
491
+ const reverse = /* @__PURE__ */ new Map();
492
+ for (const taskId of selected) {
493
+ forward.set(taskId, /* @__PURE__ */ new Set());
494
+ reverse.set(taskId, /* @__PURE__ */ new Set());
495
+ }
496
+ for (const taskId of selected) {
497
+ const deps = graph.forward.get(taskId) ?? /* @__PURE__ */ new Set();
498
+ const internalDeps = new Set(Array.from(deps).filter((depId) => selected.has(depId)));
499
+ forward.set(taskId, internalDeps);
500
+ for (const depId of internalDeps) {
501
+ reverse.get(depId)?.add(taskId);
502
+ }
503
+ }
504
+ const subgraph = {
505
+ nodes,
506
+ forward,
507
+ reverse,
508
+ topological_order: [],
509
+ tracks: new Map(graph.tracks),
510
+ resources: new Map(graph.resources),
511
+ deliveries: new Map(graph.deliveries),
512
+ external_dependencies: new Map(
513
+ Array.from(selected).map((taskId) => [taskId, external_dependencies_for_task(graph, taskId)])
514
+ )
515
+ };
516
+ subgraph.topological_order = topological_sort(subgraph);
517
+ return subgraph;
518
+ }
519
+ function find_external_dependencies(subgraph, fullGraph) {
520
+ const external = /* @__PURE__ */ new Set();
521
+ for (const taskId of subgraph.nodes.keys()) {
522
+ const deps = fullGraph.forward.get(taskId) ?? /* @__PURE__ */ new Set();
523
+ for (const depId of deps) {
524
+ if (!subgraph.nodes.has(depId) && fullGraph.nodes.has(depId)) {
525
+ external.add(depId);
526
+ }
527
+ }
528
+ for (const dep of external_dependencies_for_task(fullGraph, taskId)) {
529
+ external.add(dep.raw);
530
+ }
531
+ }
532
+ return Array.from(external).sort((a, b) => a.localeCompare(b));
533
+ }
534
+
535
+ // src/graph/critical-path.ts
536
+ var DEFAULT_CONFIG = {
537
+ version: 2,
538
+ project: {
539
+ name: "COOP",
540
+ id: "coop"
541
+ },
542
+ defaults: {
543
+ task: {
544
+ complexity: "medium"
545
+ }
546
+ }
547
+ };
548
+ var EPSILON = 1e-9;
549
+ function durationHours(task) {
550
+ return effort_or_default(task, DEFAULT_CONFIG);
551
+ }
552
+ function rootsOf(graph) {
553
+ const roots = [];
554
+ for (const id of graph.nodes.keys()) {
555
+ if ((graph.forward.get(id)?.size ?? 0) === 0) {
556
+ roots.push(id);
557
+ }
558
+ }
559
+ return roots;
560
+ }
561
+ function leavesOf(graph) {
562
+ const leaves = [];
563
+ for (const id of graph.nodes.keys()) {
564
+ if ((graph.reverse.get(id)?.size ?? 0) === 0) {
565
+ leaves.push(id);
566
+ }
567
+ }
568
+ return leaves;
569
+ }
570
+ function compute_critical_path(delivery, graph) {
571
+ const include = new Set(delivery.scope.include ?? []);
572
+ for (const excluded of delivery.scope.exclude ?? []) {
573
+ include.delete(excluded);
574
+ }
575
+ const subgraph = extract_subgraph(graph, [...include]);
576
+ if (subgraph.nodes.size === 0) {
577
+ return {
578
+ critical_path: [],
579
+ project_duration_hours: 0,
580
+ per_task: []
581
+ };
582
+ }
583
+ const roots = new Set(rootsOf(subgraph));
584
+ const leaves = new Set(leavesOf(subgraph));
585
+ const es = /* @__PURE__ */ new Map();
586
+ const ef = /* @__PURE__ */ new Map();
587
+ const ls = /* @__PURE__ */ new Map();
588
+ const lf = /* @__PURE__ */ new Map();
589
+ const durations = /* @__PURE__ */ new Map();
590
+ for (const taskId of subgraph.topological_order) {
591
+ const task = subgraph.nodes.get(taskId);
592
+ if (!task) continue;
593
+ const duration = durationHours(task);
594
+ durations.set(taskId, duration);
595
+ let earliestStart = 0;
596
+ if (!roots.has(taskId)) {
597
+ const deps = [...subgraph.forward.get(taskId) ?? /* @__PURE__ */ new Set()];
598
+ earliestStart = deps.length === 0 ? 0 : Math.max(...deps.map((depId) => ef.get(depId) ?? 0));
599
+ }
600
+ es.set(taskId, earliestStart);
601
+ ef.set(taskId, earliestStart + duration);
602
+ }
603
+ const project_duration_hours = Math.max(...[...leaves].map((id) => ef.get(id) ?? 0));
604
+ const reverseTopo = [...subgraph.topological_order].reverse();
605
+ for (const taskId of reverseTopo) {
606
+ const duration = durations.get(taskId) ?? 0;
607
+ let latestFinish = project_duration_hours;
608
+ if (!leaves.has(taskId)) {
609
+ const successors = [...subgraph.reverse.get(taskId) ?? /* @__PURE__ */ new Set()];
610
+ latestFinish = successors.length === 0 ? project_duration_hours : Math.min(...successors.map((id) => ls.get(id) ?? project_duration_hours));
611
+ }
612
+ lf.set(taskId, latestFinish);
613
+ ls.set(taskId, latestFinish - duration);
614
+ }
615
+ const per_task = [];
616
+ for (const id of subgraph.nodes.keys()) {
617
+ const ES = es.get(id) ?? 0;
618
+ const EF = ef.get(id) ?? 0;
619
+ const LS = ls.get(id) ?? 0;
620
+ const LF = lf.get(id) ?? 0;
621
+ const slack = LS - ES;
622
+ per_task.push({
623
+ id,
624
+ ES,
625
+ EF,
626
+ LS,
627
+ LF,
628
+ slack,
629
+ on_critical_path: Math.abs(slack) <= EPSILON
630
+ });
631
+ }
632
+ per_task.sort((a, b) => {
633
+ if (a.ES !== b.ES) return a.ES - b.ES;
634
+ return a.id.localeCompare(b.id);
635
+ });
636
+ const critical_path = per_task.filter((task) => task.on_critical_path).map((task) => task.id);
637
+ return {
638
+ critical_path,
639
+ project_duration_hours,
640
+ per_task
641
+ };
642
+ }
643
+
644
+ // src/index/index-manager.ts
245
645
  var import_node_fs5 = __toESM(require("fs"), 1);
246
646
  var import_node_path = __toESM(require("path"), 1);
247
647
 
648
+ // src/planning/readiness.ts
649
+ function isResolvedDependencyStatus(status) {
650
+ return status === "done" || status === "canceled";
651
+ }
652
+ function compute_readiness(task, graph) {
653
+ if (task.status === "done" || task.status === "canceled") {
654
+ return "done";
655
+ }
656
+ if (task.status === "in_review") {
657
+ return "waiting_review";
658
+ }
659
+ if (task.status === "in_progress") {
660
+ return "in_progress";
661
+ }
662
+ for (const depId of task.depends_on ?? []) {
663
+ const dep = graph.nodes.get(depId);
664
+ if (!dep || !isResolvedDependencyStatus(dep.status)) {
665
+ return "blocked";
666
+ }
667
+ }
668
+ return "ready";
669
+ }
670
+ function compute_all_readiness(graph) {
671
+ const readiness = /* @__PURE__ */ new Map();
672
+ for (const [taskId, task] of graph.nodes.entries()) {
673
+ readiness.set(taskId, compute_readiness(task, graph));
674
+ }
675
+ return readiness;
676
+ }
677
+ function partition_by_readiness(graph) {
678
+ const partitions = {
679
+ ready: [],
680
+ blocked: [],
681
+ in_progress: [],
682
+ waiting_review: [],
683
+ done: []
684
+ };
685
+ for (const task of graph.nodes.values()) {
686
+ const state = compute_readiness(task, graph);
687
+ partitions[state].push(task);
688
+ }
689
+ return partitions;
690
+ }
691
+ function compute_readiness_with_corrections(graph) {
692
+ const readiness = /* @__PURE__ */ new Map();
693
+ const corrections = [];
694
+ const warnings = [];
695
+ const partitions = {
696
+ ready: [],
697
+ blocked: [],
698
+ in_progress: [],
699
+ waiting_review: [],
700
+ done: []
701
+ };
702
+ for (const [taskId, task] of graph.nodes.entries()) {
703
+ const state = compute_readiness(task, graph);
704
+ readiness.set(taskId, state);
705
+ partitions[state].push(task);
706
+ if (task.status === "blocked" && state === "ready") {
707
+ corrections.push({
708
+ type: "task.transitioned",
709
+ task_id: taskId,
710
+ from: "blocked",
711
+ to: "todo",
712
+ reason: "All dependencies are resolved."
713
+ });
714
+ }
715
+ if (task.status === "todo" && state === "blocked") {
716
+ const unresolved = (task.depends_on ?? []).filter((depId) => {
717
+ const depTask = graph.nodes.get(depId);
718
+ return !depTask || !isResolvedDependencyStatus(depTask.status);
719
+ });
720
+ warnings.push({
721
+ code: "todo_with_unresolved_dependencies",
722
+ task_id: taskId,
723
+ message: `Task is todo but has unresolved dependencies: ${unresolved.join(", ")}.`
724
+ });
725
+ }
726
+ }
727
+ return {
728
+ readiness,
729
+ partitions,
730
+ corrections,
731
+ warnings
732
+ };
733
+ }
734
+
248
735
  // src/parser/delivery-parser.ts
249
736
  var import_node_fs3 = __toESM(require("fs"), 1);
250
737
 
@@ -412,6 +899,12 @@ function parseYamlFile(filePath) {
412
899
  const content = import_node_fs2.default.readFileSync(filePath, "utf8");
413
900
  return parseYamlContent(content, filePath);
414
901
  }
902
+ function stringifyYamlContent(content) {
903
+ return import_yaml2.default.stringify(content);
904
+ }
905
+ function writeYamlFile(filePath, content) {
906
+ import_node_fs2.default.writeFileSync(filePath, stringifyYamlContent(content), "utf8");
907
+ }
415
908
 
416
909
  // src/parser/delivery-parser.ts
417
910
  function asStringArray(value) {
@@ -508,7 +1001,7 @@ function parseTaskFile(filePath) {
508
1001
  return parseTaskContent(content, filePath);
509
1002
  }
510
1003
 
511
- // src/graph/loader.ts
1004
+ // src/index/index-manager.ts
512
1005
  function walkFiles(dirPath, extensions) {
513
1006
  if (!import_node_fs5.default.existsSync(dirPath)) return [];
514
1007
  const out = [];
@@ -527,106 +1020,447 @@ function walkFiles(dirPath, extensions) {
527
1020
  }
528
1021
  return out.sort((a, b) => a.localeCompare(b));
529
1022
  }
530
- function loadTasks(tasksDir) {
531
- const files = walkFiles(tasksDir, /* @__PURE__ */ new Set([".md"]));
532
- const tasks = [];
533
- const seen = /* @__PURE__ */ new Set();
1023
+ function toPosixPath(input) {
1024
+ return input.replace(/\\/g, "/");
1025
+ }
1026
+ function fromRecordMap(value) {
1027
+ return new Map(Object.entries(value));
1028
+ }
1029
+ function toRecordMap(value) {
1030
+ return Object.fromEntries(value.entries());
1031
+ }
1032
+ function round2(value) {
1033
+ return Number(value.toFixed(2));
1034
+ }
1035
+ function parseTaskCollection(files, baseDir) {
1036
+ const tasks = /* @__PURE__ */ new Map();
1037
+ const fileToId = {};
534
1038
  for (const filePath of files) {
535
- const parsed = parseTaskFile(filePath);
536
- if (seen.has(parsed.task.id)) {
537
- throw new Error(`Duplicate task id '${parsed.task.id}' found at ${filePath}.`);
1039
+ const parsed = parseTaskFile(filePath).task;
1040
+ if (tasks.has(parsed.id)) {
1041
+ throw new Error(`Duplicate task id '${parsed.id}' found at ${filePath}.`);
538
1042
  }
539
- seen.add(parsed.task.id);
540
- tasks.push(parsed.task);
1043
+ tasks.set(parsed.id, parsed);
1044
+ fileToId[toPosixPath(import_node_path.default.relative(baseDir, filePath))] = parsed.id;
541
1045
  }
542
- return tasks;
1046
+ return { tasks, fileToId };
543
1047
  }
544
- function loadTracks(tracksDir) {
545
- const files = walkFiles(tracksDir, /* @__PURE__ */ new Set([".yml", ".yaml"]));
1048
+ function parseTrackCollection(files, baseDir) {
546
1049
  const tracks = /* @__PURE__ */ new Map();
1050
+ const fileToId = {};
547
1051
  for (const filePath of files) {
548
- const data = parseYamlFile(filePath);
549
- if (!data.id) continue;
550
- tracks.set(data.id, data);
1052
+ const parsed = parseYamlFile(filePath);
1053
+ if (!parsed.id) continue;
1054
+ tracks.set(parsed.id, parsed);
1055
+ fileToId[toPosixPath(import_node_path.default.relative(baseDir, filePath))] = parsed.id;
551
1056
  }
552
- return tracks;
1057
+ return { tracks, fileToId };
553
1058
  }
554
- function loadResources(resourcesDir) {
555
- const files = walkFiles(resourcesDir, /* @__PURE__ */ new Set([".yml", ".yaml"]));
1059
+ function parseResourceCollection(files, baseDir) {
556
1060
  const resources = /* @__PURE__ */ new Map();
1061
+ const fileToId = {};
557
1062
  for (const filePath of files) {
558
- const data = parseYamlFile(filePath);
559
- if (!data.id) continue;
560
- resources.set(data.id, data);
1063
+ const parsed = parseYamlFile(filePath);
1064
+ if (!parsed.id) continue;
1065
+ resources.set(parsed.id, parsed);
1066
+ fileToId[toPosixPath(import_node_path.default.relative(baseDir, filePath))] = parsed.id;
561
1067
  }
562
- return resources;
1068
+ return { resources, fileToId };
563
1069
  }
564
- function loadDeliveries(deliveriesDir) {
565
- const files = walkFiles(deliveriesDir, /* @__PURE__ */ new Set([".yml", ".yaml", ".md"]));
1070
+ function parseDeliveryCollection(files, baseDir) {
566
1071
  const deliveries = /* @__PURE__ */ new Map();
1072
+ const fileToId = {};
567
1073
  for (const filePath of files) {
568
- const parsed = parseDeliveryFile(filePath);
569
- deliveries.set(parsed.delivery.id, parsed.delivery);
1074
+ const parsed = parseDeliveryFile(filePath).delivery;
1075
+ deliveries.set(parsed.id, parsed);
1076
+ fileToId[toPosixPath(import_node_path.default.relative(baseDir, filePath))] = parsed.id;
570
1077
  }
571
- return deliveries;
572
- }
573
- function load_graph(coopDir) {
574
- const tasks = loadTasks(import_node_path.default.join(coopDir, "tasks"));
575
- const tracks = loadTracks(import_node_path.default.join(coopDir, "tracks"));
576
- const resources = loadResources(import_node_path.default.join(coopDir, "resources"));
577
- const deliveries = loadDeliveries(import_node_path.default.join(coopDir, "deliveries"));
578
- return build_graph(tasks, {
579
- tracks,
580
- resources,
581
- deliveries
582
- });
1078
+ return { deliveries, fileToId };
583
1079
  }
584
-
585
- // src/graph/subgraph.ts
586
- function extract_subgraph(graph, taskIds) {
587
- const selected = new Set(taskIds.filter((taskId) => graph.nodes.has(taskId)));
588
- const nodes = /* @__PURE__ */ new Map();
589
- for (const taskId of selected) {
590
- const task = graph.nodes.get(taskId);
591
- if (task) nodes.set(taskId, task);
592
- }
593
- const forward = /* @__PURE__ */ new Map();
594
- const reverse = /* @__PURE__ */ new Map();
595
- for (const taskId of selected) {
596
- forward.set(taskId, /* @__PURE__ */ new Set());
597
- reverse.set(taskId, /* @__PURE__ */ new Set());
598
- }
599
- for (const taskId of selected) {
600
- const deps = graph.forward.get(taskId) ?? /* @__PURE__ */ new Set();
601
- const internalDeps = new Set(Array.from(deps).filter((depId) => selected.has(depId)));
602
- forward.set(taskId, internalDeps);
603
- for (const depId of internalDeps) {
604
- reverse.get(depId)?.add(taskId);
1080
+ var IndexManager = class {
1081
+ coopDir;
1082
+ indexDir;
1083
+ graphPath;
1084
+ tasksPath;
1085
+ capacityPath;
1086
+ constructor(coopDir) {
1087
+ this.coopDir = import_node_path.default.resolve(coopDir);
1088
+ this.indexDir = import_node_path.default.join(this.coopDir, ".index");
1089
+ this.graphPath = import_node_path.default.join(this.indexDir, "graph.json");
1090
+ this.tasksPath = import_node_path.default.join(this.indexDir, "tasks.json");
1091
+ this.capacityPath = import_node_path.default.join(this.indexDir, "capacity.json");
1092
+ }
1093
+ is_stale() {
1094
+ return this.status().stale;
1095
+ }
1096
+ status() {
1097
+ const scan = this.scanSources();
1098
+ if (!import_node_fs5.default.existsSync(this.graphPath)) {
1099
+ return {
1100
+ exists: false,
1101
+ stale: true,
1102
+ graph_path: this.graphPath,
1103
+ source_max_mtime_ms: scan.sourceMaxMtimeMs,
1104
+ changed_files: Object.keys(scan.sourceFiles).sort((a, b) => a.localeCompare(b)),
1105
+ reason: "graph index missing"
1106
+ };
605
1107
  }
606
- }
607
- const subgraph = {
608
- nodes,
609
- forward,
610
- reverse,
611
- topological_order: [],
612
- tracks: new Map(graph.tracks),
613
- resources: new Map(graph.resources),
614
- deliveries: new Map(graph.deliveries)
615
- };
616
- subgraph.topological_order = topological_sort(subgraph);
617
- return subgraph;
618
- }
619
- function find_external_dependencies(subgraph, fullGraph) {
620
- const external = /* @__PURE__ */ new Set();
621
- for (const taskId of subgraph.nodes.keys()) {
622
- const deps = fullGraph.forward.get(taskId) ?? /* @__PURE__ */ new Set();
623
- for (const depId of deps) {
624
- if (!subgraph.nodes.has(depId) && fullGraph.nodes.has(depId)) {
625
- external.add(depId);
1108
+ const indexed = this.readGraphIndexFile();
1109
+ if (!indexed) {
1110
+ return {
1111
+ exists: true,
1112
+ stale: true,
1113
+ graph_path: this.graphPath,
1114
+ source_max_mtime_ms: scan.sourceMaxMtimeMs,
1115
+ changed_files: Object.keys(scan.sourceFiles).sort((a, b) => a.localeCompare(b)),
1116
+ reason: "graph index unreadable"
1117
+ };
1118
+ }
1119
+ const changed = /* @__PURE__ */ new Set();
1120
+ for (const [relativePath, mtime] of Object.entries(scan.sourceFiles)) {
1121
+ const indexedMtime = indexed.source_files[relativePath];
1122
+ if (indexedMtime === void 0 || indexedMtime !== mtime) {
1123
+ changed.add(relativePath);
626
1124
  }
627
1125
  }
628
- }
629
- return Array.from(external).sort((a, b) => a.localeCompare(b));
1126
+ for (const relativePath of Object.keys(indexed.source_files)) {
1127
+ if (scan.sourceFiles[relativePath] === void 0) {
1128
+ changed.add(relativePath);
1129
+ }
1130
+ }
1131
+ return {
1132
+ exists: true,
1133
+ stale: changed.size > 0 || indexed.version !== 1,
1134
+ graph_path: this.graphPath,
1135
+ source_max_mtime_ms: scan.sourceMaxMtimeMs,
1136
+ changed_files: Array.from(changed).sort((a, b) => a.localeCompare(b)),
1137
+ generated_at: indexed.generated_at,
1138
+ reason: indexed.version === 1 ? void 0 : "index version mismatch"
1139
+ };
1140
+ }
1141
+ load_indexed_graph() {
1142
+ const indexed = this.readGraphIndexFile();
1143
+ if (!indexed) return null;
1144
+ try {
1145
+ return this.deserializeGraph(indexed.graph);
1146
+ } catch {
1147
+ return null;
1148
+ }
1149
+ }
1150
+ build_full_index() {
1151
+ const scan = this.scanSources();
1152
+ const taskData = parseTaskCollection(scan.taskFiles, this.coopDir);
1153
+ const trackData = parseTrackCollection(scan.trackFiles, this.coopDir);
1154
+ const resourceData = parseResourceCollection(scan.resourceFiles, this.coopDir);
1155
+ const deliveryData = parseDeliveryCollection(scan.deliveryFiles, this.coopDir);
1156
+ const graph = build_graph(Array.from(taskData.tasks.values()), {
1157
+ tracks: trackData.tracks,
1158
+ resources: resourceData.resources,
1159
+ deliveries: deliveryData.deliveries
1160
+ });
1161
+ this.writeIndexFiles(
1162
+ graph,
1163
+ scan,
1164
+ {
1165
+ tasks: taskData.fileToId,
1166
+ tracks: trackData.fileToId,
1167
+ resources: resourceData.fileToId,
1168
+ deliveries: deliveryData.fileToId
1169
+ }
1170
+ );
1171
+ return graph;
1172
+ }
1173
+ update_incremental(changedFiles) {
1174
+ const indexed = this.readGraphIndexFile();
1175
+ if (!indexed) {
1176
+ return this.build_full_index();
1177
+ }
1178
+ const scan = this.scanSources();
1179
+ const changed = changedFiles.length > 0 ? changedFiles : this.status().changed_files;
1180
+ const normalized = new Set(
1181
+ changed.map((entry) => this.normalizeChangedFile(entry)).filter((entry) => Boolean(entry))
1182
+ );
1183
+ if (normalized.size === 0) {
1184
+ const fromIndex = this.load_indexed_graph();
1185
+ return fromIndex ?? this.build_full_index();
1186
+ }
1187
+ const taskMap = fromRecordMap(indexed.graph.nodes);
1188
+ const trackMap = fromRecordMap(indexed.graph.tracks);
1189
+ const resourceMap = fromRecordMap(indexed.graph.resources);
1190
+ const deliveryMap = fromRecordMap(indexed.graph.deliveries);
1191
+ const fileMaps = {
1192
+ tasks: { ...indexed.files.tasks },
1193
+ tracks: { ...indexed.files.tracks },
1194
+ resources: { ...indexed.files.resources },
1195
+ deliveries: { ...indexed.files.deliveries }
1196
+ };
1197
+ for (const relativePath of normalized) {
1198
+ const kind = this.fileKind(relativePath);
1199
+ if (!kind) {
1200
+ return this.build_full_index();
1201
+ }
1202
+ const absolutePath = import_node_path.default.join(this.coopDir, relativePath);
1203
+ const exists = import_node_fs5.default.existsSync(absolutePath);
1204
+ if (!exists) {
1205
+ const oldId = fileMaps[kind][relativePath];
1206
+ if (!oldId) {
1207
+ return this.build_full_index();
1208
+ }
1209
+ delete fileMaps[kind][relativePath];
1210
+ if (kind === "tasks") taskMap.delete(oldId);
1211
+ if (kind === "tracks") trackMap.delete(oldId);
1212
+ if (kind === "resources") resourceMap.delete(oldId);
1213
+ if (kind === "deliveries") deliveryMap.delete(oldId);
1214
+ continue;
1215
+ }
1216
+ try {
1217
+ if (kind === "tasks") {
1218
+ const oldId2 = fileMaps.tasks[relativePath];
1219
+ if (oldId2) taskMap.delete(oldId2);
1220
+ const parsed2 = parseTaskFile(absolutePath).task;
1221
+ taskMap.set(parsed2.id, parsed2);
1222
+ fileMaps.tasks[relativePath] = parsed2.id;
1223
+ continue;
1224
+ }
1225
+ if (kind === "tracks") {
1226
+ const oldId2 = fileMaps.tracks[relativePath];
1227
+ if (oldId2) trackMap.delete(oldId2);
1228
+ const parsed2 = parseYamlFile(absolutePath);
1229
+ if (parsed2.id) {
1230
+ trackMap.set(parsed2.id, parsed2);
1231
+ fileMaps.tracks[relativePath] = parsed2.id;
1232
+ }
1233
+ continue;
1234
+ }
1235
+ if (kind === "resources") {
1236
+ const oldId2 = fileMaps.resources[relativePath];
1237
+ if (oldId2) resourceMap.delete(oldId2);
1238
+ const parsed2 = parseYamlFile(absolutePath);
1239
+ if (parsed2.id) {
1240
+ resourceMap.set(parsed2.id, parsed2);
1241
+ fileMaps.resources[relativePath] = parsed2.id;
1242
+ }
1243
+ continue;
1244
+ }
1245
+ const oldId = fileMaps.deliveries[relativePath];
1246
+ if (oldId) deliveryMap.delete(oldId);
1247
+ const parsed = parseDeliveryFile(absolutePath).delivery;
1248
+ deliveryMap.set(parsed.id, parsed);
1249
+ fileMaps.deliveries[relativePath] = parsed.id;
1250
+ } catch {
1251
+ return this.build_full_index();
1252
+ }
1253
+ }
1254
+ const graph = build_graph(Array.from(taskMap.values()), {
1255
+ tracks: trackMap,
1256
+ resources: resourceMap,
1257
+ deliveries: deliveryMap
1258
+ });
1259
+ this.writeIndexFiles(graph, scan, fileMaps);
1260
+ return graph;
1261
+ }
1262
+ ensureIndexDir() {
1263
+ import_node_fs5.default.mkdirSync(this.indexDir, { recursive: true });
1264
+ }
1265
+ writeIndexFiles(graph, scan, fileMaps) {
1266
+ this.ensureIndexDir();
1267
+ const graphIndex = {
1268
+ version: 1,
1269
+ generated_at: (/* @__PURE__ */ new Date()).toISOString(),
1270
+ source_max_mtime_ms: scan.sourceMaxMtimeMs,
1271
+ source_files: scan.sourceFiles,
1272
+ files: fileMaps,
1273
+ graph: this.serializeGraph(graph)
1274
+ };
1275
+ import_node_fs5.default.writeFileSync(this.graphPath, `${JSON.stringify(graphIndex, null, 2)}
1276
+ `, "utf8");
1277
+ import_node_fs5.default.writeFileSync(this.tasksPath, `${JSON.stringify(this.computeTaskIndexRows(graph), null, 2)}
1278
+ `, "utf8");
1279
+ import_node_fs5.default.writeFileSync(
1280
+ this.capacityPath,
1281
+ `${JSON.stringify(this.computeCapacitySnapshot(graph.resources), null, 2)}
1282
+ `,
1283
+ "utf8"
1284
+ );
1285
+ }
1286
+ readGraphIndexFile() {
1287
+ if (!import_node_fs5.default.existsSync(this.graphPath)) return null;
1288
+ try {
1289
+ const parsed = JSON.parse(import_node_fs5.default.readFileSync(this.graphPath, "utf8"));
1290
+ if (!parsed || typeof parsed !== "object") return null;
1291
+ if (!parsed.graph || typeof parsed.graph !== "object") return null;
1292
+ if (!parsed.source_files || typeof parsed.source_files !== "object") return null;
1293
+ if (!parsed.files || typeof parsed.files !== "object") return null;
1294
+ return parsed;
1295
+ } catch {
1296
+ return null;
1297
+ }
1298
+ }
1299
+ serializeGraph(graph) {
1300
+ return {
1301
+ nodes: toRecordMap(graph.nodes),
1302
+ forward: Object.fromEntries(
1303
+ Array.from(graph.forward.entries()).map(([id, deps]) => [id, Array.from(deps).sort((a, b) => a.localeCompare(b))])
1304
+ ),
1305
+ reverse: Object.fromEntries(
1306
+ Array.from(graph.reverse.entries()).map(([id, deps]) => [id, Array.from(deps).sort((a, b) => a.localeCompare(b))])
1307
+ ),
1308
+ topological_order: [...graph.topological_order],
1309
+ tracks: toRecordMap(graph.tracks),
1310
+ resources: toRecordMap(graph.resources),
1311
+ deliveries: toRecordMap(graph.deliveries),
1312
+ external_dependencies: Object.fromEntries(
1313
+ Array.from(graph.external_dependencies?.entries() ?? []).map(([id, deps]) => [id, deps])
1314
+ )
1315
+ };
1316
+ }
1317
+ deserializeGraph(data) {
1318
+ return {
1319
+ nodes: fromRecordMap(data.nodes ?? {}),
1320
+ forward: new Map(
1321
+ Object.entries(data.forward ?? {}).map(([id, deps]) => [id, new Set(deps ?? [])])
1322
+ ),
1323
+ reverse: new Map(
1324
+ Object.entries(data.reverse ?? {}).map(([id, deps]) => [id, new Set(deps ?? [])])
1325
+ ),
1326
+ topological_order: Array.isArray(data.topological_order) ? [...data.topological_order] : [],
1327
+ tracks: fromRecordMap(data.tracks ?? {}),
1328
+ resources: fromRecordMap(data.resources ?? {}),
1329
+ deliveries: fromRecordMap(data.deliveries ?? {}),
1330
+ external_dependencies: new Map(Object.entries(data.external_dependencies ?? {}))
1331
+ };
1332
+ }
1333
+ scanSources() {
1334
+ const taskFiles = walkFiles(import_node_path.default.join(this.coopDir, "tasks"), /* @__PURE__ */ new Set([".md"]));
1335
+ const trackFiles = walkFiles(import_node_path.default.join(this.coopDir, "tracks"), /* @__PURE__ */ new Set([".yml", ".yaml"]));
1336
+ const resourceFiles = walkFiles(import_node_path.default.join(this.coopDir, "resources"), /* @__PURE__ */ new Set([".yml", ".yaml"]));
1337
+ const deliveryFiles = walkFiles(import_node_path.default.join(this.coopDir, "deliveries"), /* @__PURE__ */ new Set([".yml", ".yaml", ".md"]));
1338
+ const sourceFiles = {};
1339
+ let sourceMaxMtimeMs = 0;
1340
+ const allFiles = [...taskFiles, ...trackFiles, ...resourceFiles, ...deliveryFiles];
1341
+ for (const filePath of allFiles) {
1342
+ const stat = import_node_fs5.default.statSync(filePath);
1343
+ const relative = toPosixPath(import_node_path.default.relative(this.coopDir, filePath));
1344
+ sourceFiles[relative] = stat.mtimeMs;
1345
+ if (stat.mtimeMs > sourceMaxMtimeMs) {
1346
+ sourceMaxMtimeMs = stat.mtimeMs;
1347
+ }
1348
+ }
1349
+ return {
1350
+ taskFiles,
1351
+ trackFiles,
1352
+ resourceFiles,
1353
+ deliveryFiles,
1354
+ sourceFiles,
1355
+ sourceMaxMtimeMs
1356
+ };
1357
+ }
1358
+ normalizeChangedFile(value) {
1359
+ if (!value || !value.trim()) return null;
1360
+ const normalized = import_node_path.default.normalize(value.trim());
1361
+ const absolute = import_node_path.default.isAbsolute(normalized) ? normalized : import_node_path.default.resolve(this.coopDir, normalized);
1362
+ const relative = import_node_path.default.relative(this.coopDir, absolute);
1363
+ if (relative.startsWith("..")) return null;
1364
+ return toPosixPath(relative);
1365
+ }
1366
+ fileKind(relativePath) {
1367
+ if (relativePath.startsWith("tasks/")) return "tasks";
1368
+ if (relativePath.startsWith("tracks/")) return "tracks";
1369
+ if (relativePath.startsWith("resources/")) return "resources";
1370
+ if (relativePath.startsWith("deliveries/")) return "deliveries";
1371
+ return null;
1372
+ }
1373
+ computeTaskIndexRows(graph) {
1374
+ const depth = /* @__PURE__ */ new Map();
1375
+ for (const id of graph.topological_order) {
1376
+ const deps = Array.from(graph.forward.get(id) ?? /* @__PURE__ */ new Set()).filter((dep) => graph.nodes.has(dep));
1377
+ if (deps.length === 0) {
1378
+ depth.set(id, 0);
1379
+ continue;
1380
+ }
1381
+ const maxDepDepth = Math.max(...deps.map((dep) => depth.get(dep) ?? 0));
1382
+ depth.set(id, maxDepDepth + 1);
1383
+ }
1384
+ const rows = {};
1385
+ for (const [id, task] of graph.nodes.entries()) {
1386
+ const estimate = task.estimate;
1387
+ let pertHours;
1388
+ let pertStddev;
1389
+ if (estimate && typeof estimate.optimistic_hours === "number" && typeof estimate.expected_hours === "number" && typeof estimate.pessimistic_hours === "number") {
1390
+ pertHours = round2((estimate.optimistic_hours + 4 * estimate.expected_hours + estimate.pessimistic_hours) / 6);
1391
+ pertStddev = round2((estimate.pessimistic_hours - estimate.optimistic_hours) / 6);
1392
+ }
1393
+ const blocks = Array.from(graph.reverse.get(id) ?? /* @__PURE__ */ new Set()).sort((a, b) => a.localeCompare(b));
1394
+ const row = {
1395
+ id,
1396
+ status: task.status,
1397
+ priority: task.priority ?? null,
1398
+ track: task.track ?? null,
1399
+ blocks,
1400
+ readiness: compute_readiness(task, graph),
1401
+ depth: depth.get(id) ?? 0
1402
+ };
1403
+ if (pertHours !== void 0) row.pert_hours = pertHours;
1404
+ if (pertStddev !== void 0) row.pert_stddev = pertStddev;
1405
+ rows[id] = row;
1406
+ }
1407
+ return rows;
1408
+ }
1409
+ computeCapacitySnapshot(resources) {
1410
+ const profiles = {};
1411
+ for (const [id, profile] of resources.entries()) {
1412
+ if (profile.type === "human") {
1413
+ const members = profile.members ?? [];
1414
+ const baseHours = members.reduce((sum, member) => sum + (member.hours_per_week ?? 0), 0);
1415
+ const meetings = profile.overhead?.meetings_percent ?? 0;
1416
+ const context = profile.overhead?.context_switch_percent ?? 0;
1417
+ const overheadPercent = meetings + context;
1418
+ const effectiveHours = baseHours * Math.max(0, 1 - overheadPercent / 100);
1419
+ profiles[id] = {
1420
+ id,
1421
+ type: profile.type,
1422
+ members: members.length,
1423
+ total_weekly_hours: round2(baseHours),
1424
+ effective_weekly_hours: round2(effectiveHours),
1425
+ overhead_percent: round2(overheadPercent)
1426
+ };
1427
+ continue;
1428
+ }
1429
+ if (profile.type === "ai") {
1430
+ profiles[id] = {
1431
+ id,
1432
+ type: profile.type,
1433
+ agents: profile.agents.length,
1434
+ effective_capacity: profile.effective_capacity ?? null
1435
+ };
1436
+ continue;
1437
+ }
1438
+ profiles[id] = {
1439
+ id,
1440
+ type: profile.type,
1441
+ nodes: profile.nodes.length,
1442
+ effective_capacity: profile.effective_capacity ?? null
1443
+ };
1444
+ }
1445
+ return {
1446
+ generated_at: (/* @__PURE__ */ new Date()).toISOString(),
1447
+ profiles
1448
+ };
1449
+ }
1450
+ };
1451
+
1452
+ // src/graph/loader.ts
1453
+ function load_graph(coopDir) {
1454
+ const index = new IndexManager(coopDir);
1455
+ const status = index.status();
1456
+ if (status.exists && !status.stale) {
1457
+ const cached = index.load_indexed_graph();
1458
+ if (cached) return cached;
1459
+ }
1460
+ if (!status.exists) {
1461
+ return index.build_full_index();
1462
+ }
1463
+ return index.update_incremental(status.changed_files);
630
1464
  }
631
1465
 
632
1466
  // src/graph/validator.ts
@@ -692,10 +1526,17 @@ function checkTerminalConvergence(graph) {
692
1526
  }
693
1527
  return issues;
694
1528
  }
695
- function validate_graph(graph) {
1529
+ function validate_graph(graph, context = {}) {
696
1530
  const issues = [];
697
1531
  const cycle = detect_cycle(graph);
698
1532
  if (cycle) {
1533
+ context.eventEmitter?.emit({
1534
+ type: "graph.cycle_detected",
1535
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1536
+ payload: {
1537
+ cycle
1538
+ }
1539
+ });
699
1540
  issues.push(error("acyclicity", `Dependency cycle detected: ${cycle.join(" -> ")}.`, cycle));
700
1541
  }
701
1542
  for (const [taskId, deps] of graph.forward.entries()) {
@@ -733,8 +1574,540 @@ function validate_graph(graph) {
733
1574
  return issues;
734
1575
  }
735
1576
 
736
- // src/parser/idea-parser.ts
1577
+ // src/events/emitter.ts
1578
+ var CoopEventEmitter = class {
1579
+ handlers = /* @__PURE__ */ new Map();
1580
+ on(eventType, handler) {
1581
+ const existing = this.handlers.get(eventType) ?? /* @__PURE__ */ new Set();
1582
+ existing.add(handler);
1583
+ this.handlers.set(eventType, existing);
1584
+ return () => {
1585
+ const bucket = this.handlers.get(eventType);
1586
+ if (!bucket) return;
1587
+ bucket.delete(handler);
1588
+ if (bucket.size === 0) {
1589
+ this.handlers.delete(eventType);
1590
+ }
1591
+ };
1592
+ }
1593
+ emit(event) {
1594
+ const listeners = this.handlers.get(event.type);
1595
+ if (!listeners || listeners.size === 0) return;
1596
+ for (const listener of listeners) {
1597
+ listener(event);
1598
+ }
1599
+ }
1600
+ };
1601
+
1602
+ // src/events/events.ts
1603
+ var COOP_EVENT_TYPES = [
1604
+ "task.created",
1605
+ "task.transitioned",
1606
+ "task.assigned",
1607
+ "delivery.committed",
1608
+ "delivery.at_risk",
1609
+ "run.started",
1610
+ "run.completed",
1611
+ "run.failed",
1612
+ "graph.cycle_detected"
1613
+ ];
1614
+
1615
+ // src/events/hook-runner.ts
1616
+ var import_node_child_process = require("child_process");
1617
+ var import_node_path2 = __toESM(require("path"), 1);
1618
+ function commandFor(hookPath) {
1619
+ const ext = import_node_path2.default.extname(hookPath).toLowerCase();
1620
+ if (ext === ".js" || ext === ".mjs" || ext === ".cjs") {
1621
+ return {
1622
+ command: process.execPath,
1623
+ args: [hookPath]
1624
+ };
1625
+ }
1626
+ return {
1627
+ command: hookPath,
1628
+ args: []
1629
+ };
1630
+ }
1631
+ function run_hook(hookPath, event, timeoutMs = 1e4) {
1632
+ return new Promise((resolve) => {
1633
+ const payload = JSON.stringify(event);
1634
+ const runner = commandFor(hookPath);
1635
+ const child = (0, import_node_child_process.spawn)(runner.command, runner.args, {
1636
+ stdio: ["pipe", "pipe", "pipe"],
1637
+ shell: runner.command === hookPath,
1638
+ windowsHide: true
1639
+ });
1640
+ let stdout = "";
1641
+ let stderr = "";
1642
+ let settled = false;
1643
+ let timedOut = false;
1644
+ const settle = (result) => {
1645
+ if (settled) return;
1646
+ settled = true;
1647
+ resolve(result);
1648
+ };
1649
+ const timeout = setTimeout(() => {
1650
+ timedOut = true;
1651
+ child.kill("SIGTERM");
1652
+ settle({
1653
+ exitCode: null,
1654
+ stdout,
1655
+ stderr: `${stderr}${stderr ? "\n" : ""}Hook timed out after ${timeoutMs}ms.`,
1656
+ timedOut: true
1657
+ });
1658
+ }, timeoutMs);
1659
+ child.stdout.on("data", (chunk) => {
1660
+ stdout += chunk.toString();
1661
+ });
1662
+ child.stderr.on("data", (chunk) => {
1663
+ stderr += chunk.toString();
1664
+ });
1665
+ child.on("error", (error6) => {
1666
+ clearTimeout(timeout);
1667
+ settle({
1668
+ exitCode: -1,
1669
+ stdout,
1670
+ stderr: `${stderr}${stderr ? "\n" : ""}${error6.message}`,
1671
+ timedOut: false
1672
+ });
1673
+ });
1674
+ child.on("close", (code) => {
1675
+ clearTimeout(timeout);
1676
+ settle({
1677
+ exitCode: timedOut ? null : code,
1678
+ stdout,
1679
+ stderr,
1680
+ timedOut
1681
+ });
1682
+ });
1683
+ child.stdin.write(payload, "utf8");
1684
+ child.stdin.end();
1685
+ });
1686
+ }
1687
+
1688
+ // src/auth/auth.ts
1689
+ var DEFAULT_AUTH_CONFIG = {
1690
+ enabled: false,
1691
+ roles: {
1692
+ admin: { permissions: ["all"] },
1693
+ lead: {
1694
+ permissions: ["create_task", "transition_task", "create_delivery", "plan_delivery", "assign_task"]
1695
+ },
1696
+ contributor: { permissions: ["transition_own_task", "comment"] },
1697
+ viewer: { permissions: ["read"] }
1698
+ },
1699
+ assignments: {},
1700
+ policies: [
1701
+ { action: "delete_task", requires: ["admin"] },
1702
+ { action: "commit_delivery", requires: ["admin", "lead"] },
1703
+ { action: "override_blocked", requires: ["admin", "lead"] }
1704
+ ]
1705
+ };
1706
+ function normalizeRole(value) {
1707
+ if (typeof value !== "string") return null;
1708
+ if (value === "admin" || value === "lead" || value === "contributor" || value === "viewer") return value;
1709
+ return null;
1710
+ }
1711
+ function asPermissions(value) {
1712
+ if (!Array.isArray(value)) return [];
1713
+ return value.map((entry) => String(entry)).filter((entry) => {
1714
+ 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";
1715
+ });
1716
+ }
1717
+ function normalizeUser(value) {
1718
+ return value.trim().toLowerCase();
1719
+ }
1720
+ function loadPolicies(raw) {
1721
+ if (!Array.isArray(raw)) return [...DEFAULT_AUTH_CONFIG.policies];
1722
+ const out = [];
1723
+ for (const entry of raw) {
1724
+ if (!entry || typeof entry !== "object") continue;
1725
+ const record = entry;
1726
+ const action = typeof record.action === "string" ? record.action : "";
1727
+ if (action !== "delete_task" && action !== "commit_delivery" && action !== "override_blocked" && action !== "transition_task" && action !== "create_delivery") {
1728
+ continue;
1729
+ }
1730
+ const requiresRaw = record.requires;
1731
+ const requiresList = Array.isArray(requiresRaw) ? requiresRaw : [requiresRaw];
1732
+ const requires = requiresList.map((role) => normalizeRole(role)).filter((role) => Boolean(role));
1733
+ if (requires.length === 0) continue;
1734
+ out.push({ action, requires });
1735
+ }
1736
+ return out.length > 0 ? out : [...DEFAULT_AUTH_CONFIG.policies];
1737
+ }
1738
+ function load_auth_config(config) {
1739
+ const source = config && typeof config === "object" ? config : {};
1740
+ const rawAuth = source.authorization && typeof source.authorization === "object" ? source.authorization : {};
1741
+ const enabled = Boolean(rawAuth.enabled ?? false);
1742
+ const rawRoles = rawAuth.roles && typeof rawAuth.roles === "object" ? rawAuth.roles : {};
1743
+ const roles = {
1744
+ admin: { permissions: [...DEFAULT_AUTH_CONFIG.roles.admin.permissions] },
1745
+ lead: { permissions: [...DEFAULT_AUTH_CONFIG.roles.lead.permissions] },
1746
+ contributor: { permissions: [...DEFAULT_AUTH_CONFIG.roles.contributor.permissions] },
1747
+ viewer: { permissions: [...DEFAULT_AUTH_CONFIG.roles.viewer.permissions] }
1748
+ };
1749
+ for (const role of Object.keys(roles)) {
1750
+ const roleConfig = rawRoles[role];
1751
+ if (!roleConfig || typeof roleConfig !== "object") continue;
1752
+ const permissions = asPermissions(roleConfig.permissions);
1753
+ if (permissions.length > 0) {
1754
+ roles[role].permissions = permissions;
1755
+ }
1756
+ }
1757
+ const rawAssignments = rawAuth.assignments && typeof rawAuth.assignments === "object" ? rawAuth.assignments : {};
1758
+ const assignments = {};
1759
+ for (const [user, roleRaw] of Object.entries(rawAssignments)) {
1760
+ const role = normalizeRole(roleRaw);
1761
+ if (!role) continue;
1762
+ assignments[normalizeUser(user)] = role;
1763
+ }
1764
+ const policies = loadPolicies(rawAuth.policies);
1765
+ return {
1766
+ enabled,
1767
+ roles,
1768
+ assignments,
1769
+ policies
1770
+ };
1771
+ }
1772
+ function get_user_role(user, config) {
1773
+ const key = normalizeUser(user);
1774
+ return config.assignments[key] ?? "viewer";
1775
+ }
1776
+ function hasRolePolicyOverride(role, action, config) {
1777
+ const policy = config.policies.find((entry) => entry.action === action);
1778
+ if (!policy) return true;
1779
+ return policy.requires.includes(role);
1780
+ }
1781
+ function check_permission(user, action, context) {
1782
+ const { config } = context;
1783
+ if (!config.enabled) return true;
1784
+ const role = get_user_role(user, config);
1785
+ const rolePermissions = new Set(config.roles[role]?.permissions ?? []);
1786
+ let allowed = rolePermissions.has("all") || rolePermissions.has(action);
1787
+ if (!allowed && action === "transition_task" && rolePermissions.has("transition_own_task")) {
1788
+ const owner = context.taskOwner?.trim() ?? "";
1789
+ allowed = owner.length > 0 && normalizeUser(owner) === normalizeUser(user);
1790
+ }
1791
+ if (!allowed) return false;
1792
+ return hasRolePolicyOverride(role, action, config);
1793
+ }
1794
+
1795
+ // src/plugins/plugin-loader.ts
737
1796
  var import_node_fs6 = __toESM(require("fs"), 1);
1797
+ var import_node_path3 = __toESM(require("path"), 1);
1798
+ var EVENT_TYPES = new Set(COOP_EVENT_TYPES);
1799
+ var PLUGIN_ID_PATTERN = /^[a-z0-9]+(?:[-_][a-z0-9]+)*$/i;
1800
+ function pluginDir(coopDir) {
1801
+ return import_node_path3.default.join(coopDir, "plugins");
1802
+ }
1803
+ function listPluginFiles(coopDir) {
1804
+ const dir = pluginDir(coopDir);
1805
+ if (!import_node_fs6.default.existsSync(dir)) return [];
1806
+ const entries = import_node_fs6.default.readdirSync(dir, { withFileTypes: true });
1807
+ return entries.filter((entry) => entry.isFile() && /\.(yml|yaml)$/i.test(entry.name)).map((entry) => import_node_path3.default.join(dir, entry.name)).sort((a, b) => a.localeCompare(b));
1808
+ }
1809
+ function asString(value, field, source) {
1810
+ if (typeof value !== "string" || value.trim().length === 0) {
1811
+ throw new Error(`${source}: ${field} must be a non-empty string.`);
1812
+ }
1813
+ return value.trim();
1814
+ }
1815
+ function asStringMap(value, field, source) {
1816
+ if (value === void 0) return {};
1817
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1818
+ throw new Error(`${source}: ${field} must be a mapping of string values.`);
1819
+ }
1820
+ const out = {};
1821
+ for (const [key, entry] of Object.entries(value)) {
1822
+ if (typeof entry !== "string") {
1823
+ throw new Error(`${source}: ${field}.${key} must be a string.`);
1824
+ }
1825
+ out[key] = entry;
1826
+ }
1827
+ return out;
1828
+ }
1829
+ function parseAction(raw, source) {
1830
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
1831
+ throw new Error(`${source}: trigger action must be a mapping object.`);
1832
+ }
1833
+ const record = raw;
1834
+ const actionType = asString(record.type, "action.type", source).toLowerCase();
1835
+ if (actionType === "webhook") {
1836
+ const url = asString(record.url, "action.url", source);
1837
+ const template = record.template === void 0 ? void 0 : asString(record.template, "action.template", source);
1838
+ const headers = asStringMap(record.headers, "action.headers", source);
1839
+ return { type: "webhook", url, template, headers };
1840
+ }
1841
+ if (actionType === "console") {
1842
+ const template = asString(record.template, "action.template", source);
1843
+ return { type: "console", template };
1844
+ }
1845
+ if (actionType === "github_pr") {
1846
+ const operation = asString(record.operation, "action.operation", source).toLowerCase();
1847
+ if (operation !== "create_or_update" && operation !== "merge") {
1848
+ throw new Error(`${source}: unsupported action.operation '${operation}' for github_pr.`);
1849
+ }
1850
+ const branch = record.branch === void 0 ? void 0 : asString(record.branch, "action.branch", source);
1851
+ const baseBranch = record.base_branch === void 0 ? void 0 : asString(record.base_branch, "action.base_branch", source);
1852
+ const draft = record.draft === void 0 ? void 0 : Boolean(record.draft);
1853
+ return {
1854
+ type: "github_pr",
1855
+ operation,
1856
+ branch,
1857
+ base_branch: baseBranch,
1858
+ draft
1859
+ };
1860
+ }
1861
+ throw new Error(`${source}: unsupported action.type '${actionType}'.`);
1862
+ }
1863
+ function parseTrigger(raw, source, index) {
1864
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
1865
+ throw new Error(`${source}: triggers[${index}] must be a mapping object.`);
1866
+ }
1867
+ const record = raw;
1868
+ const event = asString(record.event, `triggers[${index}].event`, source);
1869
+ if (!EVENT_TYPES.has(event)) {
1870
+ throw new Error(`${source}: unsupported event '${event}' in triggers[${index}].`);
1871
+ }
1872
+ const filterRaw = record.filter;
1873
+ if (filterRaw !== void 0 && (!filterRaw || typeof filterRaw !== "object" || Array.isArray(filterRaw))) {
1874
+ throw new Error(`${source}: triggers[${index}].filter must be a mapping object.`);
1875
+ }
1876
+ const action = parseAction(record.action, `${source} triggers[${index}]`);
1877
+ return {
1878
+ event,
1879
+ filter: filterRaw,
1880
+ action
1881
+ };
1882
+ }
1883
+ function parseSecrets(value, source) {
1884
+ if (value === void 0) return [];
1885
+ if (!Array.isArray(value)) {
1886
+ throw new Error(`${source}: secrets must be a list of environment variable names.`);
1887
+ }
1888
+ return value.map((entry, index) => asString(entry, `secrets[${index}]`, source));
1889
+ }
1890
+ function parsePlugin(filePath) {
1891
+ const source = import_node_path3.default.relative(import_node_path3.default.dirname(import_node_path3.default.dirname(filePath)), filePath);
1892
+ const raw = parseYamlFile(filePath);
1893
+ const id = asString(raw.id, "id", source);
1894
+ if (!PLUGIN_ID_PATTERN.test(id)) {
1895
+ throw new Error(`${source}: id '${id}' contains invalid characters.`);
1896
+ }
1897
+ const name = asString(raw.name, "name", source);
1898
+ const version = asString(raw.version, "version", source);
1899
+ if (!Array.isArray(raw.triggers) || raw.triggers.length === 0) {
1900
+ throw new Error(`${source}: triggers must be a non-empty list.`);
1901
+ }
1902
+ const triggers = raw.triggers.map((entry, index) => parseTrigger(entry, source, index));
1903
+ const secrets = parseSecrets(raw.secrets, source);
1904
+ return {
1905
+ id,
1906
+ name,
1907
+ version,
1908
+ triggers,
1909
+ secrets,
1910
+ file_path: filePath
1911
+ };
1912
+ }
1913
+ function load_plugins(coopDir) {
1914
+ const files = listPluginFiles(coopDir);
1915
+ const plugins = files.map((filePath) => parsePlugin(filePath));
1916
+ const seen = /* @__PURE__ */ new Set();
1917
+ for (const plugin of plugins) {
1918
+ if (seen.has(plugin.id)) {
1919
+ throw new Error(`Duplicate plugin id '${plugin.id}'.`);
1920
+ }
1921
+ seen.add(plugin.id);
1922
+ }
1923
+ return plugins;
1924
+ }
1925
+
1926
+ // src/plugins/plugin-runner.ts
1927
+ var import_node_fs7 = __toESM(require("fs"), 1);
1928
+ var import_node_path4 = __toESM(require("path"), 1);
1929
+ function isObject(value) {
1930
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
1931
+ }
1932
+ function resolvePathValue(input, pathExpr) {
1933
+ const tokens = pathExpr.split(".").map((entry) => entry.trim()).filter(Boolean);
1934
+ if (tokens.length === 0) return void 0;
1935
+ let cursor = input;
1936
+ for (const token of tokens) {
1937
+ if (!isObject(cursor)) return void 0;
1938
+ cursor = cursor[token];
1939
+ }
1940
+ return cursor;
1941
+ }
1942
+ function renderTemplate(template, context) {
1943
+ return template.replace(/{{\s*([^}]+?)\s*}}/g, (_, expr) => {
1944
+ const value = resolvePathValue(context, expr);
1945
+ if (value === null || value === void 0) return "";
1946
+ if (typeof value === "object") return JSON.stringify(value);
1947
+ return String(value);
1948
+ });
1949
+ }
1950
+ function resolveSecrets(template, env, plugin) {
1951
+ return template.replace(/\$\{([A-Z0-9_]+)\}/g, (_, secretName) => {
1952
+ if (!plugin.secrets.includes(secretName)) {
1953
+ throw new Error(`Plugin '${plugin.id}' references undeclared secret '${secretName}'.`);
1954
+ }
1955
+ const value = env[secretName];
1956
+ if (!value) {
1957
+ throw new Error(`Plugin '${plugin.id}' missing environment value for '${secretName}'.`);
1958
+ }
1959
+ return value;
1960
+ });
1961
+ }
1962
+ function filterMatches(filter, eventView) {
1963
+ if (!filter) return true;
1964
+ for (const [key, expected] of Object.entries(filter)) {
1965
+ const actual = resolvePathValue(eventView, key);
1966
+ if (Array.isArray(expected)) {
1967
+ const allowed = expected.map((entry) => String(entry));
1968
+ if (!allowed.includes(String(actual))) return false;
1969
+ continue;
1970
+ }
1971
+ if (String(actual) !== String(expected)) return false;
1972
+ }
1973
+ return true;
1974
+ }
1975
+ function eventTemplateView(event) {
1976
+ return {
1977
+ type: event.type,
1978
+ timestamp: event.timestamp,
1979
+ ...event.payload
1980
+ };
1981
+ }
1982
+ function buildContext(event, graph) {
1983
+ const context = {
1984
+ event: eventTemplateView(event)
1985
+ };
1986
+ if (graph) {
1987
+ const payload = event.payload;
1988
+ const taskId = typeof payload.task_id === "string" ? payload.task_id : void 0;
1989
+ const deliveryId = typeof payload.delivery_id === "string" ? payload.delivery_id : void 0;
1990
+ if (taskId) {
1991
+ const task = graph.nodes.get(taskId);
1992
+ if (task) {
1993
+ context.task = task;
1994
+ }
1995
+ }
1996
+ if (deliveryId) {
1997
+ const delivery = graph.deliveries.get(deliveryId);
1998
+ if (delivery) {
1999
+ context.delivery = delivery;
2000
+ }
2001
+ }
2002
+ }
2003
+ return context;
2004
+ }
2005
+ function appendRunLog(coopDir, record) {
2006
+ const runsDir = import_node_path4.default.join(coopDir, "runs");
2007
+ import_node_fs7.default.mkdirSync(runsDir, { recursive: true });
2008
+ const logPath = import_node_path4.default.join(runsDir, "plugin-events.log");
2009
+ import_node_fs7.default.appendFileSync(logPath, `${JSON.stringify(record)}
2010
+ `, "utf8");
2011
+ }
2012
+ async function executeWebhookAction(plugin, trigger, action, context, env, fetchImpl) {
2013
+ const resolvedUrl = resolveSecrets(action.url, env, plugin);
2014
+ const template = action.template ?? "{{ event.type }}";
2015
+ const body = renderTemplate(resolveSecrets(template, env, plugin), context);
2016
+ const headers = {
2017
+ "content-type": "text/plain; charset=utf-8"
2018
+ };
2019
+ for (const [key, value] of Object.entries(action.headers ?? {})) {
2020
+ headers[key] = resolveSecrets(value, env, plugin);
2021
+ }
2022
+ const response = await fetchImpl(resolvedUrl, {
2023
+ method: "POST",
2024
+ headers,
2025
+ body
2026
+ });
2027
+ return {
2028
+ statusCode: response.status,
2029
+ message: `${trigger.event} -> ${response.status}`
2030
+ };
2031
+ }
2032
+ function executeConsoleAction(plugin, action, context, env) {
2033
+ const template = resolveSecrets(action.template, env, plugin);
2034
+ const message = renderTemplate(template, context);
2035
+ console.log(`[COOP][plugin:${plugin.id}] ${message}`);
2036
+ return { message };
2037
+ }
2038
+ async function executeCustomAction(plugin, trigger, action, context, env, actionHandlers) {
2039
+ const handler = actionHandlers[action.type];
2040
+ if (!handler) {
2041
+ throw new Error(`Plugin action '${action.type}' requires a registered action handler.`);
2042
+ }
2043
+ return handler(plugin, trigger, action, context, env);
2044
+ }
2045
+ async function run_plugins_for_event(coopDir, event, options = {}) {
2046
+ const env = options.env ?? process.env;
2047
+ const fetchImpl = options.fetch_impl ?? fetch;
2048
+ const graph = options.graph;
2049
+ const actionHandlers = options.action_handlers ?? {};
2050
+ const plugins = load_plugins(coopDir);
2051
+ const context = buildContext(event, graph);
2052
+ const eventView = context.event;
2053
+ const results = [];
2054
+ for (const plugin of plugins) {
2055
+ for (const trigger of plugin.triggers) {
2056
+ if (trigger.event !== event.type) continue;
2057
+ if (!filterMatches(trigger.filter, eventView)) continue;
2058
+ const base = {
2059
+ plugin_id: plugin.id,
2060
+ plugin_name: plugin.name,
2061
+ trigger_event: trigger.event,
2062
+ action_type: trigger.action.type,
2063
+ success: false,
2064
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2065
+ };
2066
+ try {
2067
+ if (trigger.action.type === "webhook") {
2068
+ const result = await executeWebhookAction(plugin, trigger, trigger.action, context, env, fetchImpl);
2069
+ base.success = true;
2070
+ base.status_code = result.statusCode;
2071
+ base.message = result.message;
2072
+ } else if (trigger.action.type === "console") {
2073
+ const result = executeConsoleAction(plugin, trigger.action, context, env);
2074
+ base.success = true;
2075
+ base.message = result.message;
2076
+ } else {
2077
+ const result = await executeCustomAction(plugin, trigger, trigger.action, context, env, actionHandlers);
2078
+ base.success = true;
2079
+ base.status_code = result.statusCode;
2080
+ base.message = result.message;
2081
+ }
2082
+ } catch (error6) {
2083
+ base.success = false;
2084
+ base.error = error6 instanceof Error ? error6.message : String(error6);
2085
+ }
2086
+ appendRunLog(coopDir, base);
2087
+ results.push(base);
2088
+ }
2089
+ }
2090
+ return results;
2091
+ }
2092
+
2093
+ // src/models/run.ts
2094
+ var RunStatus = {
2095
+ pending: "pending",
2096
+ running: "running",
2097
+ paused: "paused",
2098
+ completed: "completed",
2099
+ failed: "failed",
2100
+ canceled: "canceled"
2101
+ };
2102
+ var RunStepStatus = {
2103
+ completed: "completed",
2104
+ failed: "failed",
2105
+ paused: "paused",
2106
+ skipped: "skipped"
2107
+ };
2108
+
2109
+ // src/parser/idea-parser.ts
2110
+ var import_node_fs8 = __toESM(require("fs"), 1);
738
2111
  function asStringArray2(value) {
739
2112
  if (!Array.isArray(value)) {
740
2113
  return [];
@@ -766,6 +2139,7 @@ function parseIdeaFromRaw(raw, source) {
766
2139
  id,
767
2140
  title,
768
2141
  created,
2142
+ aliases: asStringArray2(raw.aliases),
769
2143
  author,
770
2144
  status,
771
2145
  tags: asStringArray2(raw.tags),
@@ -782,12 +2156,12 @@ function parseIdeaContent(content, source = "<content>") {
782
2156
  };
783
2157
  }
784
2158
  function parseIdeaFile(filePath) {
785
- const content = import_node_fs6.default.readFileSync(filePath, "utf8");
2159
+ const content = import_node_fs8.default.readFileSync(filePath, "utf8");
786
2160
  return parseIdeaContent(content, filePath);
787
2161
  }
788
2162
 
789
2163
  // src/parser/task-writer.ts
790
- var import_node_fs7 = __toESM(require("fs"), 1);
2164
+ var import_node_fs9 = __toESM(require("fs"), 1);
791
2165
  var TASK_FIELD_ORDER = [
792
2166
  "id",
793
2167
  "title",
@@ -795,6 +2169,7 @@ var TASK_FIELD_ORDER = [
795
2169
  "status",
796
2170
  "created",
797
2171
  "updated",
2172
+ "aliases",
798
2173
  "priority",
799
2174
  "track",
800
2175
  "assignee",
@@ -832,123 +2207,1971 @@ function buildOrderedFrontmatter(task, raw) {
832
2207
  if (!raw) {
833
2208
  return ordered;
834
2209
  }
835
- for (const key of Object.keys(raw)) {
836
- if (ORDERED_TASK_FIELDS.has(key)) {
837
- continue;
2210
+ for (const key of Object.keys(raw)) {
2211
+ if (ORDERED_TASK_FIELDS.has(key)) {
2212
+ continue;
2213
+ }
2214
+ ordered[key] = raw[key];
2215
+ }
2216
+ return ordered;
2217
+ }
2218
+ function writeTask(task, options = {}) {
2219
+ const frontmatter = buildOrderedFrontmatter(task, options.raw);
2220
+ const output = stringifyFrontmatter(frontmatter, options.body ?? "");
2221
+ if (options.filePath) {
2222
+ const existedBeforeWrite = import_node_fs9.default.existsSync(options.filePath);
2223
+ import_node_fs9.default.writeFileSync(options.filePath, output, "utf8");
2224
+ if (!existedBeforeWrite) {
2225
+ options.eventEmitter?.emit({
2226
+ type: "task.created",
2227
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2228
+ payload: {
2229
+ task_id: task.id,
2230
+ title: task.title,
2231
+ task_type: task.type,
2232
+ file_path: options.filePath
2233
+ }
2234
+ });
2235
+ }
2236
+ }
2237
+ return output;
2238
+ }
2239
+
2240
+ // src/planning/capacity.ts
2241
+ function toDate(value) {
2242
+ if (value instanceof Date) return new Date(value.getTime());
2243
+ const parsed = /* @__PURE__ */ new Date(`${value}T00:00:00.000Z`);
2244
+ if (Number.isNaN(parsed.valueOf())) {
2245
+ throw new Error(`Invalid date value '${value}'.`);
2246
+ }
2247
+ return parsed;
2248
+ }
2249
+ function isoDate(value) {
2250
+ return value.toISOString().slice(0, 10);
2251
+ }
2252
+ function startOfWeek(date) {
2253
+ const copy = new Date(date.getTime());
2254
+ const day = copy.getUTCDay();
2255
+ const offset = day === 0 ? -6 : 1 - day;
2256
+ copy.setUTCDate(copy.getUTCDate() + offset);
2257
+ copy.setUTCHours(0, 0, 0, 0);
2258
+ return copy;
2259
+ }
2260
+ function endOfWeek(date) {
2261
+ const start = startOfWeek(date);
2262
+ const end = new Date(start.getTime());
2263
+ end.setUTCDate(end.getUTCDate() + 6);
2264
+ end.setUTCHours(0, 0, 0, 0);
2265
+ return end;
2266
+ }
2267
+ function addDays(date, days) {
2268
+ const next = new Date(date.getTime());
2269
+ next.setUTCDate(next.getUTCDate() + days);
2270
+ return next;
2271
+ }
2272
+ function isBusinessDay(date) {
2273
+ const day = date.getUTCDay();
2274
+ return day >= 1 && day <= 5;
2275
+ }
2276
+ function overlapBusinessDays(from, to, weekStart, weekEnd) {
2277
+ const start = from > weekStart ? from : weekStart;
2278
+ const end = to < weekEnd ? to : weekEnd;
2279
+ if (start > end) return 0;
2280
+ let count = 0;
2281
+ const cursor = new Date(start.getTime());
2282
+ while (cursor <= end) {
2283
+ if (isBusinessDay(cursor)) {
2284
+ count += 1;
2285
+ }
2286
+ cursor.setUTCDate(cursor.getUTCDate() + 1);
2287
+ }
2288
+ return count;
2289
+ }
2290
+ function clampPercent(input) {
2291
+ if (!Number.isFinite(input)) return 0;
2292
+ return Math.min(100, Math.max(0, Number(input)));
2293
+ }
2294
+ function normalizeTrackKey(input) {
2295
+ return input.trim().toLowerCase();
2296
+ }
2297
+ function normalizeAgentKey(input) {
2298
+ return input.trim().toLowerCase();
2299
+ }
2300
+ function simplifiedTrackKeyFromProfile(profileId) {
2301
+ const lower = profileId.toLowerCase();
2302
+ if (lower.endsWith("_team")) return lower.slice(0, -5);
2303
+ if (lower.endsWith("-team")) return lower.slice(0, -5);
2304
+ if (lower.endsWith("_cluster")) return lower.slice(0, -8);
2305
+ if (lower.endsWith("-cluster")) return lower.slice(0, -8);
2306
+ return null;
2307
+ }
2308
+ function valuesOfResources(resources) {
2309
+ if (Array.isArray(resources)) return resources;
2310
+ return [...resources.values()];
2311
+ }
2312
+ function selectedProfiles(delivery, resources) {
2313
+ if (!delivery.capacity_profiles || delivery.capacity_profiles.length === 0) {
2314
+ return resources;
2315
+ }
2316
+ const wanted = new Set(delivery.capacity_profiles.map((id) => id.toLowerCase()));
2317
+ return resources.filter((profile) => wanted.has(profile.id.toLowerCase()));
2318
+ }
2319
+ function selectedHumanProfiles(delivery, resources) {
2320
+ return selectedProfiles(delivery, resources).filter((profile) => profile.type === "human");
2321
+ }
2322
+ function dailyHours(memberHoursPerWeek) {
2323
+ return memberHoursPerWeek / 5;
2324
+ }
2325
+ function effective_weekly_hours(profile, week_start, week_end) {
2326
+ const weekStart = toDate(week_start);
2327
+ const weekEnd = toDate(week_end);
2328
+ let gross = 0;
2329
+ for (const member of profile.members) {
2330
+ const baseHours = member.hours_per_week ?? profile.defaults?.hours_per_week ?? 0;
2331
+ let available = baseHours;
2332
+ for (const window of member.availability ?? []) {
2333
+ const from = toDate(window.from);
2334
+ const to = toDate(window.to);
2335
+ const overlapDays = overlapBusinessDays(from, to, weekStart, weekEnd);
2336
+ if (overlapDays <= 0) continue;
2337
+ const unavailablePerDay = Math.max(0, dailyHours(baseHours) - Math.max(0, window.hours_per_day));
2338
+ available -= overlapDays * unavailablePerDay;
2339
+ }
2340
+ gross += Math.max(0, available);
2341
+ }
2342
+ const meetings = clampPercent(profile.overhead?.meetings_percent);
2343
+ const contextSwitch = clampPercent(profile.overhead?.context_switch_percent);
2344
+ const meetingFactor = 1 - meetings / 100;
2345
+ const contextFactor = 1 - contextSwitch / 100;
2346
+ return gross * meetingFactor * contextFactor;
2347
+ }
2348
+ function effective_ai_tokens_per_day(profile) {
2349
+ if (profile.type !== "ai") return 0;
2350
+ return profile.agents.reduce((sum, agent) => sum + Math.max(0, agent.token_limit_per_day ?? 0), 0);
2351
+ }
2352
+ function ai_tokens_per_agent(profile) {
2353
+ if (profile.type !== "ai") return [];
2354
+ return profile.agents.map((agent) => ({
2355
+ id: normalizeAgentKey(agent.id),
2356
+ limit: Math.max(0, agent.token_limit_per_day ?? 0)
2357
+ }));
2358
+ }
2359
+ function addSlot(slots, trackId, week, hours) {
2360
+ const key = normalizeTrackKey(trackId);
2361
+ const byWeek = slots.get(key) ?? /* @__PURE__ */ new Map();
2362
+ byWeek.set(week, (byWeek.get(week) ?? 0) + hours);
2363
+ slots.set(key, byWeek);
2364
+ }
2365
+ function build_capacity_ledger(delivery, resources, today) {
2366
+ const allResources = valuesOfResources(resources);
2367
+ const start = startOfWeek(toDate(today));
2368
+ const target = delivery.target_date ? toDate(delivery.target_date) : addDays(start, 7 * 8);
2369
+ const finalWeekStart = startOfWeek(target);
2370
+ const weeks = /* @__PURE__ */ new Map();
2371
+ const slots = /* @__PURE__ */ new Map();
2372
+ const ai_tokens = /* @__PURE__ */ new Map();
2373
+ const ai_tokens_by_agent = /* @__PURE__ */ new Map();
2374
+ const ai_tokens_consumed_by_agent = /* @__PURE__ */ new Map();
2375
+ const humans = selectedHumanProfiles(delivery, allResources);
2376
+ const selected = selectedProfiles(delivery, allResources);
2377
+ let weekIndex = 0;
2378
+ for (let cursor = new Date(start.getTime()); cursor <= finalWeekStart; cursor = addDays(cursor, 7), weekIndex += 1) {
2379
+ const weekStart = new Date(cursor.getTime());
2380
+ const weekEnd = endOfWeek(weekStart);
2381
+ weeks.set(weekIndex, { start: isoDate(weekStart), end: isoDate(weekEnd) });
2382
+ const totalHours = humans.reduce((sum, profile) => sum + effective_weekly_hours(profile, weekStart, weekEnd), 0);
2383
+ addSlot(slots, "unassigned", weekIndex, totalHours);
2384
+ for (const profile of humans) {
2385
+ const hours = effective_weekly_hours(profile, weekStart, weekEnd);
2386
+ addSlot(slots, profile.id, weekIndex, hours);
2387
+ const simplified = simplifiedTrackKeyFromProfile(profile.id);
2388
+ if (simplified) {
2389
+ addSlot(slots, simplified, weekIndex, hours);
2390
+ }
2391
+ }
2392
+ }
2393
+ const aiProfiles = selected.filter((profile) => profile.type === "ai");
2394
+ const defaultTokensPerDay = aiProfiles.reduce((sum, profile) => sum + effective_ai_tokens_per_day(profile), 0);
2395
+ const tokenWindowEnd = delivery.target_date ? toDate(delivery.target_date) : addDays(start, 7 * 8);
2396
+ for (let day = new Date(start.getTime()); day <= tokenWindowEnd; day = addDays(day, 1)) {
2397
+ const dayKey = isoDate(day);
2398
+ ai_tokens.set(dayKey, defaultTokensPerDay);
2399
+ for (const profile of aiProfiles) {
2400
+ for (const agent of ai_tokens_per_agent(profile)) {
2401
+ const byDay = ai_tokens_by_agent.get(agent.id) ?? /* @__PURE__ */ new Map();
2402
+ byDay.set(dayKey, (byDay.get(dayKey) ?? 0) + agent.limit);
2403
+ ai_tokens_by_agent.set(agent.id, byDay);
2404
+ }
2405
+ }
2406
+ }
2407
+ return {
2408
+ slots,
2409
+ weeks,
2410
+ ai_tokens,
2411
+ ai_tokens_by_agent,
2412
+ ai_tokens_consumed_by_agent
2413
+ };
2414
+ }
2415
+ function findTrackSlots(ledger, trackId) {
2416
+ const normalized = normalizeTrackKey(trackId ?? "unassigned");
2417
+ return ledger.slots.get(normalized) ?? ledger.slots.get("unassigned") ?? null;
2418
+ }
2419
+ function allocate(ledger, task, start_week) {
2420
+ const trackSlots = findTrackSlots(ledger, task.track);
2421
+ if (!trackSlots) {
2422
+ return { success: false, reason: "capacity_exhausted" };
2423
+ }
2424
+ let hoursRemaining = task_effort_hours(task);
2425
+ let week = start_week;
2426
+ while (hoursRemaining > 0) {
2427
+ const available = trackSlots.get(week);
2428
+ if (available == null) {
2429
+ return { success: false, reason: "capacity_exhausted" };
2430
+ }
2431
+ const take = Math.min(hoursRemaining, available);
2432
+ trackSlots.set(week, available - take);
2433
+ hoursRemaining -= take;
2434
+ if (hoursRemaining > 0) {
2435
+ week += 1;
2436
+ }
2437
+ }
2438
+ return {
2439
+ success: true,
2440
+ start_week,
2441
+ end_week: week,
2442
+ duration_weeks: week - start_week + 1
2443
+ };
2444
+ }
2445
+ function check_wip(track, graph) {
2446
+ if (!track) return true;
2447
+ const limit = track.constraints?.max_concurrent_tasks;
2448
+ if (!Number.isFinite(limit) || Number(limit) <= 0) return true;
2449
+ let inProgressCount = 0;
2450
+ for (const task of graph.nodes.values()) {
2451
+ if ((task.track ?? "unassigned") !== track.id) continue;
2452
+ if (task.status === "in_progress" || task.status === "in_review") {
2453
+ inProgressCount += 1;
2454
+ }
2455
+ }
2456
+ return inProgressCount < Number(limit);
2457
+ }
2458
+ function allocate_ai(ledger, task, day, fallback_tokens = 5e4) {
2459
+ const tokensNeeded = task.resources?.ai_tokens ?? task.execution?.constraints?.max_tokens ?? fallback_tokens;
2460
+ const preferredAgent = task.execution?.agent?.trim().toLowerCase();
2461
+ const dayKey = day;
2462
+ if (preferredAgent && ledger.ai_tokens_by_agent.size > 0) {
2463
+ const allocated = allocate_ai_tokens(ledger, preferredAgent, tokensNeeded, dayKey);
2464
+ if (allocated) {
2465
+ return { success: true };
2466
+ }
2467
+ }
2468
+ if (ledger.ai_tokens_by_agent.size > 0) {
2469
+ const candidates = Array.from(ledger.ai_tokens_by_agent.keys()).sort((a, b) => a.localeCompare(b));
2470
+ for (const agentId of candidates) {
2471
+ if (preferredAgent && agentId === preferredAgent) continue;
2472
+ const allocated = allocate_ai_tokens(ledger, agentId, tokensNeeded, dayKey);
2473
+ if (allocated) {
2474
+ return { success: true };
2475
+ }
2476
+ }
2477
+ }
2478
+ const available = ledger.ai_tokens.get(dayKey) ?? 0;
2479
+ if (tokensNeeded > available) {
2480
+ return { success: false, reason: "ai_capacity_exhausted" };
2481
+ }
2482
+ ledger.ai_tokens.set(dayKey, available - tokensNeeded);
2483
+ return { success: true };
2484
+ }
2485
+ function allocate_ai_tokens(ledger, agent, tokens, day) {
2486
+ const normalizedAgent = normalizeAgentKey(agent);
2487
+ if (tokens <= 0) return true;
2488
+ const byDay = ledger.ai_tokens_by_agent.get(normalizedAgent);
2489
+ if (!byDay) return false;
2490
+ const available = byDay.get(day) ?? 0;
2491
+ if (tokens > available) return false;
2492
+ byDay.set(day, available - tokens);
2493
+ ledger.ai_tokens_by_agent.set(normalizedAgent, byDay);
2494
+ const consumed = ledger.ai_tokens_consumed_by_agent.get(normalizedAgent) ?? /* @__PURE__ */ new Map();
2495
+ consumed.set(day, (consumed.get(day) ?? 0) + tokens);
2496
+ ledger.ai_tokens_consumed_by_agent.set(normalizedAgent, consumed);
2497
+ const totalAvailable = ledger.ai_tokens.get(day) ?? 0;
2498
+ ledger.ai_tokens.set(day, Math.max(0, totalAvailable - tokens));
2499
+ return true;
2500
+ }
2501
+ function get_remaining_tokens(ledger, agent, day) {
2502
+ const normalizedAgent = normalizeAgentKey(agent);
2503
+ const byDay = ledger.ai_tokens_by_agent.get(normalizedAgent);
2504
+ if (!byDay) return 0;
2505
+ return byDay.get(day) ?? 0;
2506
+ }
2507
+
2508
+ // src/planning/simulator.ts
2509
+ var import_node_fs10 = __toESM(require("fs"), 1);
2510
+ var import_node_os = __toESM(require("os"), 1);
2511
+ var import_node_worker_threads = require("worker_threads");
2512
+ var import_node_url = require("url");
2513
+
2514
+ // src/planning/sampling.ts
2515
+ var DEFAULT_CONFIG2 = {
2516
+ version: 2,
2517
+ project: {
2518
+ name: "COOP",
2519
+ id: "coop"
2520
+ },
2521
+ defaults: {
2522
+ task: {
2523
+ complexity: "medium"
2524
+ }
2525
+ }
2526
+ };
2527
+ function sample_standard_normal(rng) {
2528
+ let u = 0;
2529
+ let v = 0;
2530
+ while (u === 0) u = rng();
2531
+ while (v === 0) v = rng();
2532
+ return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v);
2533
+ }
2534
+ function sample_gamma(shape, rng) {
2535
+ if (shape <= 0) {
2536
+ throw new Error("Gamma shape must be > 0.");
2537
+ }
2538
+ if (shape < 1) {
2539
+ const u = rng();
2540
+ return sample_gamma(shape + 1, rng) * Math.pow(u, 1 / shape);
2541
+ }
2542
+ const d = shape - 1 / 3;
2543
+ const c = 1 / Math.sqrt(9 * d);
2544
+ while (true) {
2545
+ const x = sample_standard_normal(rng);
2546
+ const v = Math.pow(1 + c * x, 3);
2547
+ if (v <= 0) continue;
2548
+ const u = rng();
2549
+ if (u < 1 - 0.0331 * Math.pow(x, 4)) {
2550
+ return d * v;
2551
+ }
2552
+ if (Math.log(u) < 0.5 * x * x + d * (1 - v + Math.log(v))) {
2553
+ return d * v;
2554
+ }
2555
+ }
2556
+ }
2557
+ function sample_beta(alpha, beta, rng) {
2558
+ const x = sample_gamma(alpha, rng);
2559
+ const y = sample_gamma(beta, rng);
2560
+ return x / (x + y);
2561
+ }
2562
+ function sample_pert_beta(optimistic, expected, pessimistic, rng = Math.random) {
2563
+ if (!Number.isFinite(optimistic) || !Number.isFinite(expected) || !Number.isFinite(pessimistic)) {
2564
+ throw new Error("PERT inputs must be finite numbers.");
2565
+ }
2566
+ if (optimistic <= 0 || expected <= 0 || pessimistic <= 0) {
2567
+ throw new Error("PERT inputs must be positive.");
2568
+ }
2569
+ if (optimistic > pessimistic) {
2570
+ throw new Error("PERT optimistic must be <= pessimistic.");
2571
+ }
2572
+ if (optimistic === pessimistic) {
2573
+ return optimistic;
2574
+ }
2575
+ const expectedClamped = Math.min(pessimistic, Math.max(optimistic, expected));
2576
+ const range = pessimistic - optimistic;
2577
+ const lambda = 4;
2578
+ const alpha = 1 + lambda * ((expectedClamped - optimistic) / range);
2579
+ const beta = 1 + lambda * ((pessimistic - expectedClamped) / range);
2580
+ const sample = sample_beta(alpha, beta, rng);
2581
+ return optimistic + sample * range;
2582
+ }
2583
+ function sample_task_hours(task, rng = Math.random) {
2584
+ if (task.estimate) {
2585
+ return sample_pert_beta(
2586
+ task.estimate.optimistic_hours,
2587
+ task.estimate.expected_hours,
2588
+ task.estimate.pessimistic_hours,
2589
+ rng
2590
+ );
2591
+ }
2592
+ const base = effort_or_default(task, DEFAULT_CONFIG2);
2593
+ const min = base * 0.7;
2594
+ const max = base * 1.3;
2595
+ return min + (max - min) * rng();
2596
+ }
2597
+
2598
+ // src/planning/simulator.ts
2599
+ var DEFAULT_COMPLEXITY = "medium";
2600
+ var DEFAULT_MONTE_CARLO_WORKERS = 4;
2601
+ function asDate(value) {
2602
+ if (value instanceof Date) return new Date(value.getTime());
2603
+ const parsed = /* @__PURE__ */ new Date(`${value}T00:00:00.000Z`);
2604
+ if (Number.isNaN(parsed.valueOf())) {
2605
+ throw new Error(`Invalid date '${value}'.`);
2606
+ }
2607
+ return parsed;
2608
+ }
2609
+ function isoDate2(value) {
2610
+ return value.toISOString().slice(0, 10);
2611
+ }
2612
+ function startOfWeek2(date) {
2613
+ const copy = new Date(date.getTime());
2614
+ const day = copy.getUTCDay();
2615
+ const offset = day === 0 ? -6 : 1 - day;
2616
+ copy.setUTCDate(copy.getUTCDate() + offset);
2617
+ copy.setUTCHours(0, 0, 0, 0);
2618
+ return copy;
2619
+ }
2620
+ function addDays2(date, days) {
2621
+ const out = new Date(date.getTime());
2622
+ out.setUTCDate(out.getUTCDate() + days);
2623
+ return out;
2624
+ }
2625
+ function addWeeks(date, weeks) {
2626
+ return addDays2(date, weeks * 7);
2627
+ }
2628
+ function week_to_date(today, week) {
2629
+ const origin = startOfWeek2(asDate(today));
2630
+ const date = addDays2(origin, week * 7 + 6);
2631
+ return isoDate2(date);
2632
+ }
2633
+ function clone_ledger(ledger) {
2634
+ return {
2635
+ slots: new Map(
2636
+ Array.from(ledger.slots.entries(), ([track, byWeek]) => [track, new Map(byWeek)])
2637
+ ),
2638
+ weeks: new Map(ledger.weeks),
2639
+ ai_tokens: new Map(ledger.ai_tokens),
2640
+ ai_tokens_by_agent: new Map(
2641
+ Array.from(ledger.ai_tokens_by_agent.entries(), ([agent, byDay]) => [agent, new Map(byDay)])
2642
+ ),
2643
+ ai_tokens_consumed_by_agent: new Map(
2644
+ Array.from(ledger.ai_tokens_consumed_by_agent.entries(), ([agent, byDay]) => [
2645
+ agent,
2646
+ new Map(byDay)
2647
+ ])
2648
+ )
2649
+ };
2650
+ }
2651
+ function normalize_track(track) {
2652
+ return (track ?? "unassigned").trim().toLowerCase();
2653
+ }
2654
+ function find_track_slots(ledger, track) {
2655
+ const key = normalize_track(track);
2656
+ return ledger.slots.get(key) ?? ledger.slots.get("unassigned") ?? null;
2657
+ }
2658
+ function max_week(ledger) {
2659
+ const keys = Array.from(ledger.weeks.keys());
2660
+ if (keys.length === 0) return 0;
2661
+ return Math.max(...keys);
2662
+ }
2663
+ function track_limit(trackId, graph) {
2664
+ const limit = graph.tracks.get(trackId)?.constraints?.max_concurrent_tasks;
2665
+ if (typeof limit !== "number" || !Number.isFinite(limit) || limit <= 0) {
2666
+ return Number.POSITIVE_INFINITY;
2667
+ }
2668
+ return limit;
2669
+ }
2670
+ function ordered_tasks(tasks, graph) {
2671
+ const wanted = new Map(tasks.map((task) => [task.id, task]));
2672
+ const ordered = [];
2673
+ for (const id of graph.topological_order) {
2674
+ const task = wanted.get(id);
2675
+ if (task) {
2676
+ ordered.push(task);
2677
+ }
2678
+ }
2679
+ if (ordered.length !== wanted.size) {
2680
+ const missing = Array.from(wanted.keys()).filter((id) => !ordered.some((task) => task.id === id));
2681
+ throw new Error(`Tasks missing from graph topological order: ${missing.join(", ")}.`);
2682
+ }
2683
+ return ordered;
2684
+ }
2685
+ function task_with_default_complexity(task) {
2686
+ const hasHumanHours = typeof task.resources?.human_hours === "number";
2687
+ if (hasHumanHours || task.estimate || task.complexity) return task;
2688
+ return {
2689
+ ...task,
2690
+ complexity: DEFAULT_COMPLEXITY
2691
+ };
2692
+ }
2693
+ function sum_hours(slots) {
2694
+ if (!slots) return 0;
2695
+ let total = 0;
2696
+ for (const value of slots.values()) {
2697
+ total += value;
2698
+ }
2699
+ return total;
2700
+ }
2701
+ function compute_utilization(initial, current, tracks) {
2702
+ const utilization = [];
2703
+ for (const track of tracks) {
2704
+ const normalized = normalize_track(track);
2705
+ const initialSlots = find_track_slots(initial, normalized);
2706
+ const currentSlots = find_track_slots(current, normalized);
2707
+ const capacity = sum_hours(initialSlots ?? void 0);
2708
+ const remaining = sum_hours(currentSlots ?? void 0);
2709
+ const allocated = Math.max(0, capacity - remaining);
2710
+ utilization.push({
2711
+ track: normalized,
2712
+ allocated_hours: allocated,
2713
+ capacity_hours: capacity,
2714
+ utilization: capacity > 0 ? allocated / capacity : 0
2715
+ });
2716
+ }
2717
+ utilization.sort((a, b) => a.track.localeCompare(b.track));
2718
+ return utilization;
2719
+ }
2720
+ function plan_allocation(trackSlots, effort, start_week) {
2721
+ let hoursRemaining = effort;
2722
+ let week = start_week;
2723
+ while (hoursRemaining > 0) {
2724
+ const available = trackSlots.get(week);
2725
+ if (available == null || available <= 0) {
2726
+ return { success: false, reason: "capacity_exhausted" };
2727
+ }
2728
+ hoursRemaining -= Math.min(hoursRemaining, available);
2729
+ if (hoursRemaining > 0) {
2730
+ week += 1;
2731
+ }
2732
+ }
2733
+ return {
2734
+ success: true,
2735
+ start_week,
2736
+ end_week: week,
2737
+ duration_weeks: week - start_week + 1
2738
+ };
2739
+ }
2740
+ function apply_allocation(trackSlots, effort, allocation) {
2741
+ let hoursRemaining = effort;
2742
+ for (let week = allocation.start_week; week <= allocation.end_week && hoursRemaining > 0; week += 1) {
2743
+ const available = trackSlots.get(week) ?? 0;
2744
+ const take = Math.min(hoursRemaining, available);
2745
+ trackSlots.set(week, available - take);
2746
+ hoursRemaining -= take;
2747
+ }
2748
+ }
2749
+ function simulate_ordered_schedule(ordered, includedTaskIds, graph, delivery, today) {
2750
+ const ledger = build_capacity_ledger(delivery, graph.resources, today);
2751
+ const initialLedger = clone_ledger(ledger);
2752
+ const schedule = {};
2753
+ const wip = /* @__PURE__ */ new Map();
2754
+ const maxWeek = max_week(ledger);
2755
+ const tracks = /* @__PURE__ */ new Set();
2756
+ for (const rawTask of ordered) {
2757
+ const task = task_with_default_complexity(rawTask);
2758
+ const track = normalize_track(task.track);
2759
+ tracks.add(track);
2760
+ if (task.status === "done" || task.status === "canceled") {
2761
+ schedule[task.id] = {
2762
+ task_id: task.id,
2763
+ track,
2764
+ start_week: null,
2765
+ end_week: null,
2766
+ already_complete: true
2767
+ };
2768
+ continue;
2769
+ }
2770
+ let earliest = 0;
2771
+ for (const depId of task.depends_on ?? []) {
2772
+ const depSchedule = schedule[depId];
2773
+ if (depSchedule && typeof depSchedule.end_week === "number") {
2774
+ earliest = Math.max(earliest, depSchedule.end_week + 1);
2775
+ continue;
2776
+ }
2777
+ const depTask = graph.nodes.get(depId);
2778
+ if (depTask && depTask.status !== "done" && depTask.status !== "canceled" && !includedTaskIds.has(depId)) {
2779
+ return {
2780
+ schedule,
2781
+ projected_completion: null,
2782
+ total_weeks: 0,
2783
+ utilization_by_track: compute_utilization(initialLedger, ledger, /* @__PURE__ */ new Set([track])),
2784
+ error: {
2785
+ code: "schedule_overflow",
2786
+ task: task.id
2787
+ }
2788
+ };
2789
+ }
2790
+ }
2791
+ const trackSlots = find_track_slots(ledger, track);
2792
+ const trackWip = wip.get(track) ?? /* @__PURE__ */ new Map();
2793
+ const limit = track_limit(track, graph);
2794
+ const effort = task_effort_hours(task);
2795
+ let placed = false;
2796
+ for (let week = earliest; week <= maxWeek; week += 1) {
2797
+ const hasCapacity = (trackSlots?.get(week) ?? 0) > 0;
2798
+ const hasWipRoom = (trackWip.get(week) ?? 0) < limit;
2799
+ if (!hasCapacity || !hasWipRoom || !trackSlots) continue;
2800
+ const allocation = plan_allocation(trackSlots, effort, week);
2801
+ if (!allocation.success || allocation.start_week == null || allocation.end_week == null) {
2802
+ continue;
2803
+ }
2804
+ let violatesWip = false;
2805
+ for (let w = allocation.start_week; w <= allocation.end_week; w += 1) {
2806
+ const nextWip = (trackWip.get(w) ?? 0) + 1;
2807
+ if (nextWip > limit) {
2808
+ violatesWip = true;
2809
+ break;
2810
+ }
2811
+ }
2812
+ if (violatesWip) continue;
2813
+ apply_allocation(trackSlots, effort, {
2814
+ start_week: allocation.start_week,
2815
+ end_week: allocation.end_week
2816
+ });
2817
+ schedule[task.id] = {
2818
+ task_id: task.id,
2819
+ track,
2820
+ start_week: allocation.start_week,
2821
+ end_week: allocation.end_week
2822
+ };
2823
+ for (let w = allocation.start_week; w <= allocation.end_week; w += 1) {
2824
+ trackWip.set(w, (trackWip.get(w) ?? 0) + 1);
2825
+ }
2826
+ wip.set(track, trackWip);
2827
+ placed = true;
2828
+ break;
2829
+ }
2830
+ if (!placed) {
2831
+ return {
2832
+ schedule,
2833
+ projected_completion: null,
2834
+ total_weeks: 0,
2835
+ utilization_by_track: compute_utilization(initialLedger, ledger, /* @__PURE__ */ new Set([track])),
2836
+ error: {
2837
+ code: "schedule_overflow",
2838
+ task: task.id
2839
+ }
2840
+ };
2841
+ }
2842
+ }
2843
+ let latestEnd = -1;
2844
+ for (const entry of Object.values(schedule)) {
2845
+ if (entry.already_complete) continue;
2846
+ if (typeof entry.end_week === "number") {
2847
+ latestEnd = Math.max(latestEnd, entry.end_week);
2848
+ }
2849
+ }
2850
+ return {
2851
+ schedule,
2852
+ projected_completion: latestEnd >= 0 ? week_to_date(today, latestEnd) : isoDate2(asDate(today)),
2853
+ total_weeks: latestEnd >= 0 ? latestEnd + 1 : 0,
2854
+ utilization_by_track: compute_utilization(initialLedger, ledger, tracks)
2855
+ };
2856
+ }
2857
+ function simulate_schedule(tasks, graph, delivery, today) {
2858
+ const ordered = ordered_tasks(tasks, graph);
2859
+ return simulate_ordered_schedule(ordered, new Set(ordered.map((task) => task.id)), graph, delivery, today);
2860
+ }
2861
+ function percentile(sorted, ratio) {
2862
+ if (sorted.length === 0) return Number.NaN;
2863
+ const index = Math.min(sorted.length - 1, Math.max(0, Math.floor((sorted.length - 1) * ratio)));
2864
+ return sorted[index] ?? Number.NaN;
2865
+ }
2866
+ function mean(values) {
2867
+ if (values.length === 0) return Number.NaN;
2868
+ return values.reduce((sum, value) => sum + value, 0) / values.length;
2869
+ }
2870
+ function stddev(values, avg) {
2871
+ if (values.length === 0) return Number.NaN;
2872
+ const variance = values.reduce((sum, value) => sum + Math.pow(value - avg, 2), 0) / values.length;
2873
+ return Math.sqrt(variance);
2874
+ }
2875
+ function toDayOffset(origin, value) {
2876
+ const target = asDate(value);
2877
+ return Math.round((target.getTime() - origin.getTime()) / (24 * 60 * 60 * 1e3));
2878
+ }
2879
+ function fromDayOffset(origin, offset) {
2880
+ return isoDate2(addDays2(origin, offset));
2881
+ }
2882
+ function resolve_scoped_tasks(delivery, graph) {
2883
+ const include = new Set(delivery.scope.include);
2884
+ for (const excluded of delivery.scope.exclude) {
2885
+ include.delete(excluded);
2886
+ }
2887
+ const ordered = [];
2888
+ for (const taskId of graph.topological_order) {
2889
+ if (!include.has(taskId)) continue;
2890
+ const task = graph.nodes.get(taskId);
2891
+ if (!task) continue;
2892
+ if (task.status === "done" || task.status === "canceled") continue;
2893
+ ordered.push(task);
2894
+ }
2895
+ return ordered;
2896
+ }
2897
+ function sample_ordered_tasks(tasks, rng) {
2898
+ const sampled = new Array(tasks.length);
2899
+ for (let i = 0; i < tasks.length; i += 1) {
2900
+ const task = tasks[i];
2901
+ sampled[i] = {
2902
+ ...task,
2903
+ resources: {
2904
+ ...task.resources ?? {},
2905
+ human_hours: sample_task_hours(task, rng)
2906
+ }
2907
+ };
2908
+ }
2909
+ return sampled;
2910
+ }
2911
+ function create_seeded_rng(seed) {
2912
+ let state = seed >>> 0 || 1831565813;
2913
+ return () => {
2914
+ state = state + 1831565813 | 0;
2915
+ let t = Math.imul(state ^ state >>> 15, 1 | state);
2916
+ t ^= t + Math.imul(t ^ t >>> 7, 61 | t);
2917
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
2918
+ };
2919
+ }
2920
+ function normalize_seed(seed) {
2921
+ if (typeof seed === "number" && Number.isFinite(seed)) {
2922
+ return Math.trunc(seed) >>> 0;
2923
+ }
2924
+ return Math.floor(Math.random() * 4294967296) >>> 0;
2925
+ }
2926
+ function derive_iteration_seed(seed, iterationIndex) {
2927
+ return (seed ^ Math.imul(iterationIndex + 1, 2654435761)) >>> 0;
2928
+ }
2929
+ function run_monte_carlo_chunk(delivery, graph, options) {
2930
+ const scopedTasks = resolve_scoped_tasks(delivery, graph);
2931
+ if (scopedTasks.length === 0) {
2932
+ return Array.from({ length: options.iterations }, () => 0);
2933
+ }
2934
+ const includedTaskIds = new Set(scopedTasks.map((task) => task.id));
2935
+ const completionOffsets = [];
2936
+ const startIndex = options.start_index ?? 0;
2937
+ const seed = normalize_seed(options.seed);
2938
+ for (let i = 0; i < options.iterations; i += 1) {
2939
+ const rng = options.rng ?? create_seeded_rng(derive_iteration_seed(seed, startIndex + i));
2940
+ const sampledTasks = sample_ordered_tasks(scopedTasks, rng);
2941
+ const simulated = simulate_ordered_schedule(sampledTasks, includedTaskIds, graph, delivery, options.today);
2942
+ if (simulated.error) {
2943
+ continue;
2944
+ }
2945
+ const offset = simulated.total_weeks > 0 ? (simulated.total_weeks - 1) * 7 + 6 : 0;
2946
+ completionOffsets.push(offset);
2947
+ }
2948
+ if (completionOffsets.length === 0 && scopedTasks.length > 0) {
2949
+ return [];
2950
+ }
2951
+ return completionOffsets;
2952
+ }
2953
+ function buildHistogram(offsets, origin, total) {
2954
+ if (offsets.length === 0 || total <= 0) return [];
2955
+ const buckets = /* @__PURE__ */ new Map();
2956
+ for (const offset of offsets) {
2957
+ const week = Math.max(0, Math.floor(offset / 7));
2958
+ buckets.set(week, (buckets.get(week) ?? 0) + 1);
2959
+ }
2960
+ return Array.from(buckets.entries()).sort((a, b) => a[0] - b[0]).map(([week, count]) => ({
2961
+ week_index: week,
2962
+ start_date: isoDate2(addWeeks(origin, week)),
2963
+ end_date: isoDate2(addDays2(addWeeks(origin, week + 1), -1)),
2964
+ count,
2965
+ probability: count / total
2966
+ }));
2967
+ }
2968
+ function build_monte_carlo_result(offsets, origin, iterations, targetOffset) {
2969
+ const sorted = [...offsets].sort((a, b) => a - b);
2970
+ const p50Value = percentile(sorted, 0.5);
2971
+ const p75Value = percentile(sorted, 0.75);
2972
+ const p85Value = percentile(sorted, 0.85);
2973
+ const p95Value = percentile(sorted, 0.95);
2974
+ const avg = mean(sorted);
2975
+ const sigma = stddev(sorted, avg);
2976
+ let probabilityByTarget = null;
2977
+ if (targetOffset != null && sorted.length > 0) {
2978
+ let onTime = 0;
2979
+ for (const offset of sorted) {
2980
+ if (offset <= targetOffset) {
2981
+ onTime += 1;
2982
+ } else {
2983
+ break;
2984
+ }
2985
+ }
2986
+ probabilityByTarget = onTime / sorted.length;
2987
+ }
2988
+ return {
2989
+ iterations,
2990
+ successful_iterations: sorted.length,
2991
+ p50: Number.isFinite(p50Value) ? fromDayOffset(origin, p50Value) : null,
2992
+ p75: Number.isFinite(p75Value) ? fromDayOffset(origin, p75Value) : null,
2993
+ p85: Number.isFinite(p85Value) ? fromDayOffset(origin, p85Value) : null,
2994
+ p95: Number.isFinite(p95Value) ? fromDayOffset(origin, p95Value) : null,
2995
+ mean_date: Number.isFinite(avg) ? fromDayOffset(origin, Math.round(avg)) : null,
2996
+ stddev_days: Number.isFinite(sigma) ? sigma : 0,
2997
+ probability_by_target: probabilityByTarget,
2998
+ histogram: buildHistogram(sorted, origin, sorted.length)
2999
+ };
3000
+ }
3001
+ function default_worker_count(workers) {
3002
+ if (typeof workers === "number" && Number.isInteger(workers) && workers > 0) {
3003
+ return workers;
3004
+ }
3005
+ return DEFAULT_MONTE_CARLO_WORKERS;
3006
+ }
3007
+ function current_module_url() {
3008
+ try {
3009
+ return (0, eval)("import.meta.url");
3010
+ } catch {
3011
+ if (typeof __filename === "string") {
3012
+ return (0, import_node_url.pathToFileURL)(__filename).href;
3013
+ }
3014
+ return null;
3015
+ }
3016
+ }
3017
+ function resolve_worker_url() {
3018
+ const moduleUrl = current_module_url();
3019
+ if (!moduleUrl) {
3020
+ return null;
3021
+ }
3022
+ if (moduleUrl.endsWith(".ts")) {
3023
+ return null;
3024
+ }
3025
+ const candidates = [
3026
+ new URL("./planning/monte-carlo-worker.js", moduleUrl),
3027
+ new URL("./planning/monte-carlo-worker.cjs", moduleUrl),
3028
+ new URL("./monte-carlo-worker.js", moduleUrl),
3029
+ new URL("./monte-carlo-worker.cjs", moduleUrl)
3030
+ ];
3031
+ for (const candidate of candidates) {
3032
+ if (import_node_fs10.default.existsSync((0, import_node_url.fileURLToPath)(candidate))) {
3033
+ return candidate;
3034
+ }
3035
+ }
3036
+ return null;
3037
+ }
3038
+ function resolve_worker_exec_argv(workerUrl) {
3039
+ void workerUrl;
3040
+ return void 0;
3041
+ }
3042
+ function should_use_worker_threads(iterations, options, workerUrl) {
3043
+ return Boolean(workerUrl) && !options.rng && iterations > 1e3 && default_worker_count(options.workers) > 1;
3044
+ }
3045
+ async function run_monte_carlo_in_workers(delivery, graph, options) {
3046
+ const workerUrl = resolve_worker_url();
3047
+ if (!workerUrl) {
3048
+ return run_monte_carlo_chunk(delivery, graph, options);
3049
+ }
3050
+ const desiredWorkers = Math.min(default_worker_count(options.workers), options.iterations, import_node_os.default.availableParallelism());
3051
+ if (desiredWorkers <= 1) {
3052
+ return run_monte_carlo_chunk(delivery, graph, options);
3053
+ }
3054
+ const execArgv = resolve_worker_exec_argv(workerUrl);
3055
+ const baseChunk = Math.floor(options.iterations / desiredWorkers);
3056
+ const remainder = options.iterations % desiredWorkers;
3057
+ let startIndex = 0;
3058
+ const jobs = [];
3059
+ for (let index = 0; index < desiredWorkers; index += 1) {
3060
+ const chunkIterations = baseChunk + (index < remainder ? 1 : 0);
3061
+ if (chunkIterations <= 0) continue;
3062
+ const payload = {
3063
+ delivery,
3064
+ graph,
3065
+ today: typeof options.today === "string" ? options.today : isoDate2(asDate(options.today)),
3066
+ iterations: chunkIterations,
3067
+ start_index: startIndex,
3068
+ seed: options.seed
3069
+ };
3070
+ startIndex += chunkIterations;
3071
+ jobs.push(
3072
+ new Promise((resolve, reject) => {
3073
+ const worker = new import_node_worker_threads.Worker(workerUrl, {
3074
+ workerData: payload,
3075
+ execArgv
3076
+ });
3077
+ worker.once("message", (message) => {
3078
+ resolve(message);
3079
+ });
3080
+ worker.once("error", reject);
3081
+ worker.once("exit", (code) => {
3082
+ if (code !== 0) {
3083
+ reject(new Error(`Monte Carlo worker exited with code ${code}.`));
3084
+ }
3085
+ });
3086
+ })
3087
+ );
3088
+ }
3089
+ const chunks = await Promise.all(jobs);
3090
+ return chunks.flat();
3091
+ }
3092
+ async function monte_carlo_forecast(delivery, graph, options = {}) {
3093
+ const iterations = typeof options.iterations === "number" && Number.isInteger(options.iterations) && options.iterations > 0 ? options.iterations : 1e4;
3094
+ const today = options.today ?? /* @__PURE__ */ new Date();
3095
+ const origin = startOfWeek2(asDate(today));
3096
+ const seed = normalize_seed(options.seed);
3097
+ const targetOffset = delivery.target_date ? toDayOffset(origin, delivery.target_date) : null;
3098
+ const workerUrl = resolve_worker_url();
3099
+ const completionOffsets = should_use_worker_threads(iterations, options, workerUrl) ? await run_monte_carlo_in_workers(delivery, graph, {
3100
+ iterations,
3101
+ today,
3102
+ seed,
3103
+ workers: options.workers
3104
+ }) : run_monte_carlo_chunk(delivery, graph, {
3105
+ iterations,
3106
+ today,
3107
+ seed,
3108
+ rng: options.rng
3109
+ });
3110
+ return build_monte_carlo_result(completionOffsets, origin, iterations, targetOffset);
3111
+ }
3112
+
3113
+ // src/planning/feasibility.ts
3114
+ var DEFAULT_ESTIMATION_CONFIG = {
3115
+ version: 2,
3116
+ project: {
3117
+ name: "COOP",
3118
+ id: "coop"
3119
+ },
3120
+ defaults: {
3121
+ task: {
3122
+ complexity: "medium"
3123
+ }
3124
+ }
3125
+ };
3126
+ function asDate2(value) {
3127
+ if (value instanceof Date) return new Date(value.getTime());
3128
+ const parsed = /* @__PURE__ */ new Date(`${value}T00:00:00.000Z`);
3129
+ if (Number.isNaN(parsed.valueOf())) {
3130
+ throw new Error(`Invalid date '${value}'.`);
3131
+ }
3132
+ return parsed;
3133
+ }
3134
+ function normalize_track2(track) {
3135
+ return (track ?? "unassigned").trim().toLowerCase();
3136
+ }
3137
+ function isBusinessDay2(date) {
3138
+ const day = date.getUTCDay();
3139
+ return day >= 1 && day <= 5;
3140
+ }
3141
+ function business_days(from, to) {
3142
+ const start = asDate2(from);
3143
+ const end = asDate2(to);
3144
+ if (start.getTime() === end.getTime()) return 0;
3145
+ const forward = start.getTime() < end.getTime();
3146
+ const low = forward ? start : end;
3147
+ const high = forward ? end : start;
3148
+ let count = 0;
3149
+ const cursor = new Date(low.getTime());
3150
+ while (cursor < high) {
3151
+ if (isBusinessDay2(cursor)) count += 1;
3152
+ cursor.setUTCDate(cursor.getUTCDate() + 1);
3153
+ }
3154
+ return forward ? count : -count;
3155
+ }
3156
+ function sumTaskEffort(tasks) {
3157
+ let total = 0;
3158
+ for (const task of tasks) {
3159
+ total += effort_or_default(task, DEFAULT_ESTIMATION_CONFIG);
3160
+ }
3161
+ return total;
3162
+ }
3163
+ function sumPert(tasks) {
3164
+ let mean3 = 0;
3165
+ let variance = 0;
3166
+ for (const task of tasks) {
3167
+ if (task.estimate) {
3168
+ mean3 += pert_hours(task.estimate);
3169
+ const sigma = pert_stddev(task.estimate);
3170
+ variance += sigma * sigma;
3171
+ } else {
3172
+ mean3 += effort_or_default(task, DEFAULT_ESTIMATION_CONFIG);
3173
+ }
3174
+ }
3175
+ return {
3176
+ mean: mean3,
3177
+ stddev: Math.sqrt(variance)
3178
+ };
3179
+ }
3180
+ function find_track_slots_hours(slots, track) {
3181
+ const direct = slots.get(track);
3182
+ if (direct) {
3183
+ let sum2 = 0;
3184
+ for (const value of direct.values()) sum2 += value;
3185
+ return sum2;
3186
+ }
3187
+ const fallback = slots.get("unassigned");
3188
+ if (!fallback) return 0;
3189
+ let sum = 0;
3190
+ for (const value of fallback.values()) sum += value;
3191
+ return sum;
3192
+ }
3193
+ function track_effort(tasks) {
3194
+ const out = /* @__PURE__ */ new Map();
3195
+ for (const task of tasks) {
3196
+ const track = normalize_track2(task.track);
3197
+ out.set(track, (out.get(track) ?? 0) + effort_or_default(task, DEFAULT_ESTIMATION_CONFIG));
3198
+ }
3199
+ return out;
3200
+ }
3201
+ function analyze_feasibility(deliveryId, graph, today) {
3202
+ const delivery = graph.deliveries.get(deliveryId);
3203
+ if (!delivery) {
3204
+ throw new Error(`Delivery '${deliveryId}' not found.`);
3205
+ }
3206
+ const include = new Set(delivery.scope.include);
3207
+ for (const excluded of delivery.scope.exclude) {
3208
+ include.delete(excluded);
3209
+ }
3210
+ const scope_tasks = Array.from(include).map((id) => graph.nodes.get(id)).filter((task) => Boolean(task));
3211
+ const subgraph = extract_subgraph(
3212
+ graph,
3213
+ scope_tasks.map((task) => task.id)
3214
+ );
3215
+ const external_deps = find_external_dependencies(subgraph, graph);
3216
+ const done_tasks = scope_tasks.filter((task) => task.status === "done");
3217
+ const remaining_tasks = scope_tasks.filter(
3218
+ (task) => task.status !== "done" && task.status !== "canceled"
3219
+ );
3220
+ const total_effort = sumTaskEffort(remaining_tasks);
3221
+ const pert = sumPert(remaining_tasks);
3222
+ const cpm = compute_critical_path(delivery, graph);
3223
+ const ledger = build_capacity_ledger(delivery, graph.resources, today);
3224
+ const simulation = simulate_schedule(remaining_tasks, graph, delivery, today);
3225
+ const tracks = new Set(remaining_tasks.map((task) => normalize_track2(task.track)));
3226
+ const capacity_by_track = {};
3227
+ for (const track of tracks) {
3228
+ capacity_by_track[track] = find_track_slots_hours(ledger.slots, track);
3229
+ }
3230
+ const total_capacity = Object.values(capacity_by_track).reduce((sum, value) => sum + value, 0);
3231
+ const budget_hours = delivery.budget.engineering_hours ?? Number.POSITIVE_INFINITY;
3232
+ const budget_cost = delivery.budget.cost_usd ?? Number.POSITIVE_INFINITY;
3233
+ const total_cost = remaining_tasks.reduce((sum, task) => sum + (task.resources?.cost_usd ?? 0), 0);
3234
+ let feasible = true;
3235
+ const risks = [];
3236
+ if (total_effort > budget_hours) {
3237
+ feasible = false;
3238
+ risks.push({
3239
+ type: "budget_exceeded",
3240
+ message: `Required ${total_effort.toFixed(1)}h exceeds budget ${budget_hours.toFixed(1)}h.`,
3241
+ overage_hours: total_effort - budget_hours
3242
+ });
3243
+ }
3244
+ if (total_cost > budget_cost) {
3245
+ feasible = false;
3246
+ risks.push({
3247
+ type: "cost_budget_exceeded",
3248
+ message: `Required $${total_cost.toFixed(2)} exceeds budget $${budget_cost.toFixed(2)}.`,
3249
+ overage_cost_usd: total_cost - budget_cost
3250
+ });
3251
+ }
3252
+ if (total_effort > total_capacity) {
3253
+ feasible = false;
3254
+ risks.push({
3255
+ type: "capacity_exceeded",
3256
+ message: `Required ${total_effort.toFixed(1)}h exceeds capacity ${total_capacity.toFixed(1)}h.`,
3257
+ overage_hours: total_effort - total_capacity
3258
+ });
3259
+ }
3260
+ if (simulation.error) {
3261
+ feasible = false;
3262
+ risks.push({
3263
+ type: "schedule_overflow",
3264
+ message: `Simulation overflow while scheduling task ${simulation.error.task}.`,
3265
+ tasks: [simulation.error.task]
3266
+ });
3267
+ }
3268
+ if (delivery.target_date && simulation.projected_completion && simulation.projected_completion > delivery.target_date) {
3269
+ feasible = false;
3270
+ risks.push({
3271
+ type: "schedule_exceeded",
3272
+ message: `Projected ${simulation.projected_completion} exceeds target ${delivery.target_date}.`,
3273
+ overage_days: business_days(delivery.target_date, simulation.projected_completion)
3274
+ });
3275
+ }
3276
+ const effort_by_track = track_effort(remaining_tasks);
3277
+ for (const [track, effort] of effort_by_track.entries()) {
3278
+ const capacity = capacity_by_track[track] ?? 0;
3279
+ if (capacity <= 0) continue;
3280
+ const utilization = effort / capacity;
3281
+ if (utilization > 0.85) {
3282
+ risks.push({
3283
+ type: "high_utilization",
3284
+ message: `Track '${track}' at ${Math.round(utilization * 100)}% utilization.`,
3285
+ track,
3286
+ utilization
3287
+ });
3288
+ }
3289
+ }
3290
+ if (external_deps.length > 0) {
3291
+ risks.push({
3292
+ type: "external_dependencies",
3293
+ message: `${external_deps.length} tasks depend on work outside this delivery.`,
3294
+ tasks: external_deps
3295
+ });
3296
+ }
3297
+ const buffer_days = delivery.target_date && simulation.projected_completion ? business_days(simulation.projected_completion, delivery.target_date) : null;
3298
+ return {
3299
+ delivery: deliveryId,
3300
+ status: feasible ? "FEASIBLE" : "NOT_FEASIBLE",
3301
+ risks,
3302
+ summary: {
3303
+ total_tasks: scope_tasks.length,
3304
+ completed: done_tasks.length,
3305
+ remaining: remaining_tasks.length,
3306
+ effort_hours: {
3307
+ required: total_effort,
3308
+ budget: budget_hours
3309
+ },
3310
+ capacity_hours: {
3311
+ available: total_capacity,
3312
+ by_track: capacity_by_track
3313
+ },
3314
+ pert,
3315
+ dates: {
3316
+ target: delivery.target_date,
3317
+ projected: simulation.projected_completion,
3318
+ buffer_days
3319
+ },
3320
+ critical_path: cpm.critical_path,
3321
+ critical_path_hours: cpm.project_duration_hours
3322
+ },
3323
+ simulation
3324
+ };
3325
+ }
3326
+
3327
+ // src/planning/risk-detector.ts
3328
+ function toIsoTimestamp(now) {
3329
+ if (typeof now === "string") {
3330
+ if (now.includes("T")) return now;
3331
+ return `${now}T00:00:00.000Z`;
3332
+ }
3333
+ const date = now instanceof Date ? now : /* @__PURE__ */ new Date();
3334
+ return date.toISOString();
3335
+ }
3336
+ function detect_delivery_risks(delivery, graph, velocity, options = {}) {
3337
+ const today = options.today ?? /* @__PURE__ */ new Date();
3338
+ const feasibility = analyze_feasibility(delivery.id, graph, today);
3339
+ const criticalPath = compute_critical_path(delivery, graph);
3340
+ const risks = [];
3341
+ 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)) {
3342
+ risks.push({
3343
+ type: "behind_schedule",
3344
+ message: `Delivery '${delivery.id}' is slipping while velocity is decelerating.`
3345
+ });
3346
+ }
3347
+ for (const utilization of feasibility.simulation.utilization_by_track) {
3348
+ if (utilization.utilization >= 0.9) {
3349
+ risks.push({
3350
+ type: "capacity_crunch",
3351
+ message: `Track '${utilization.track}' is at ${Math.round(utilization.utilization * 100)}% utilization.`,
3352
+ track: utilization.track
3353
+ });
3354
+ }
3355
+ }
3356
+ const notStarted = criticalPath.critical_path.filter((taskId) => {
3357
+ const task = graph.nodes.get(taskId);
3358
+ return task?.status === "todo" || task?.status === "blocked";
3359
+ });
3360
+ if (notStarted.length > 0) {
3361
+ risks.push({
3362
+ type: "critical_path_not_started",
3363
+ message: `${notStarted.length} critical path tasks are not started.`,
3364
+ task_ids: notStarted
3365
+ });
3366
+ }
3367
+ const highRiskCritical = criticalPath.critical_path.filter((taskId) => {
3368
+ const task = graph.nodes.get(taskId);
3369
+ return task?.risk?.level === "high" || task?.risk?.level === "critical";
3370
+ });
3371
+ if (highRiskCritical.length > 0) {
3372
+ risks.push({
3373
+ type: "high_risk_critical_path",
3374
+ message: `${highRiskCritical.length} high-risk tasks are on the critical path.`,
3375
+ task_ids: highRiskCritical
3376
+ });
3377
+ }
3378
+ if (risks.length > 0) {
3379
+ options.eventEmitter?.emit({
3380
+ type: "delivery.at_risk",
3381
+ timestamp: toIsoTimestamp(today),
3382
+ payload: {
3383
+ delivery_id: delivery.id,
3384
+ status: feasibility.status,
3385
+ risks: risks.map((risk) => risk.message)
3386
+ }
3387
+ });
3388
+ }
3389
+ return risks;
3390
+ }
3391
+
3392
+ // src/planning/scorer.ts
3393
+ var DEFAULT_SCORE_WEIGHTS = {
3394
+ priority: {
3395
+ p0: 100,
3396
+ p1: 75,
3397
+ p2: 50,
3398
+ p3: 25
3399
+ },
3400
+ urgency: {
3401
+ days_7: 40,
3402
+ days_14: 25,
3403
+ days_28: 10
3404
+ },
3405
+ dependency_unlock: {
3406
+ gte_5: 30,
3407
+ gte_3: 20,
3408
+ gte_1: 10,
3409
+ transitive_cap: 15
3410
+ },
3411
+ critical_path: 35,
3412
+ determinism: {
3413
+ high: 30,
3414
+ medium: 15,
3415
+ low: 0,
3416
+ experimental: -10
3417
+ },
3418
+ executor_fit: {
3419
+ match: 15,
3420
+ mismatch: -15,
3421
+ ci_match: 10
3422
+ },
3423
+ type: {
3424
+ bug: 10,
3425
+ spike: 5,
3426
+ feature: 0,
3427
+ chore: 0,
3428
+ epic: 0
3429
+ },
3430
+ complexity_penalty: {
3431
+ trivial: 0,
3432
+ small: 0,
3433
+ medium: -10,
3434
+ large: -25,
3435
+ unknown: -40
3436
+ },
3437
+ risk_penalty: {
3438
+ low: 0,
3439
+ medium: -10,
3440
+ high: -25,
3441
+ critical: -40
3442
+ }
3443
+ };
3444
+ function asDate3(value) {
3445
+ if (value instanceof Date) {
3446
+ return new Date(value.getTime());
3447
+ }
3448
+ const parsed = /* @__PURE__ */ new Date(`${value}T00:00:00.000Z`);
3449
+ if (Number.isNaN(parsed.valueOf())) {
3450
+ throw new Error(`Invalid date '${value}'.`);
3451
+ }
3452
+ return parsed;
3453
+ }
3454
+ function calendar_days(today, target) {
3455
+ const from = asDate3(today);
3456
+ const to = asDate3(target);
3457
+ const millis = to.getTime() - from.getTime();
3458
+ return Math.floor(millis / (1e3 * 60 * 60 * 24));
3459
+ }
3460
+ function toMap(deliveries) {
3461
+ if (!deliveries) return /* @__PURE__ */ new Map();
3462
+ if (deliveries instanceof Map) return deliveries;
3463
+ return new Map(deliveries.map((delivery) => [delivery.id, delivery]));
3464
+ }
3465
+ function isRecord(value) {
3466
+ return typeof value === "object" && value !== null;
3467
+ }
3468
+ function number_or_default(value, fallback) {
3469
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
3470
+ }
3471
+ function configured_weights(config) {
3472
+ const rawWeights = config?.scheduling?.weights;
3473
+ if (!isRecord(rawWeights)) {
3474
+ return DEFAULT_SCORE_WEIGHTS;
3475
+ }
3476
+ const priority = isRecord(rawWeights.priority) ? rawWeights.priority : {};
3477
+ const urgency = isRecord(rawWeights.urgency) ? rawWeights.urgency : {};
3478
+ const dependency_unlock = isRecord(rawWeights.dependency_unlock) ? rawWeights.dependency_unlock : {};
3479
+ const determinism = isRecord(rawWeights.determinism) ? rawWeights.determinism : {};
3480
+ const executor_fit = isRecord(rawWeights.executor_fit) ? rawWeights.executor_fit : {};
3481
+ const type = isRecord(rawWeights.type) ? rawWeights.type : {};
3482
+ const complexity_penalty2 = isRecord(rawWeights.complexity_penalty) ? rawWeights.complexity_penalty : {};
3483
+ const risk_penalty2 = isRecord(rawWeights.risk_penalty) ? rawWeights.risk_penalty : {};
3484
+ return {
3485
+ priority: {
3486
+ p0: number_or_default(priority.p0, DEFAULT_SCORE_WEIGHTS.priority.p0),
3487
+ p1: number_or_default(priority.p1, DEFAULT_SCORE_WEIGHTS.priority.p1),
3488
+ p2: number_or_default(priority.p2, DEFAULT_SCORE_WEIGHTS.priority.p2),
3489
+ p3: number_or_default(priority.p3, DEFAULT_SCORE_WEIGHTS.priority.p3)
3490
+ },
3491
+ urgency: {
3492
+ days_7: number_or_default(urgency.days_7, DEFAULT_SCORE_WEIGHTS.urgency.days_7),
3493
+ days_14: number_or_default(urgency.days_14, DEFAULT_SCORE_WEIGHTS.urgency.days_14),
3494
+ days_28: number_or_default(urgency.days_28, DEFAULT_SCORE_WEIGHTS.urgency.days_28)
3495
+ },
3496
+ dependency_unlock: {
3497
+ gte_5: number_or_default(dependency_unlock.gte_5, DEFAULT_SCORE_WEIGHTS.dependency_unlock.gte_5),
3498
+ gte_3: number_or_default(dependency_unlock.gte_3, DEFAULT_SCORE_WEIGHTS.dependency_unlock.gte_3),
3499
+ gte_1: number_or_default(dependency_unlock.gte_1, DEFAULT_SCORE_WEIGHTS.dependency_unlock.gte_1),
3500
+ transitive_cap: number_or_default(
3501
+ dependency_unlock.transitive_cap,
3502
+ DEFAULT_SCORE_WEIGHTS.dependency_unlock.transitive_cap
3503
+ )
3504
+ },
3505
+ critical_path: number_or_default(rawWeights.critical_path, DEFAULT_SCORE_WEIGHTS.critical_path),
3506
+ determinism: {
3507
+ high: number_or_default(determinism.high, DEFAULT_SCORE_WEIGHTS.determinism.high),
3508
+ medium: number_or_default(determinism.medium, DEFAULT_SCORE_WEIGHTS.determinism.medium),
3509
+ low: number_or_default(determinism.low, DEFAULT_SCORE_WEIGHTS.determinism.low),
3510
+ experimental: number_or_default(determinism.experimental, DEFAULT_SCORE_WEIGHTS.determinism.experimental)
3511
+ },
3512
+ executor_fit: {
3513
+ match: number_or_default(executor_fit.match, DEFAULT_SCORE_WEIGHTS.executor_fit.match),
3514
+ mismatch: number_or_default(executor_fit.mismatch, DEFAULT_SCORE_WEIGHTS.executor_fit.mismatch),
3515
+ ci_match: number_or_default(executor_fit.ci_match, DEFAULT_SCORE_WEIGHTS.executor_fit.ci_match)
3516
+ },
3517
+ type: {
3518
+ bug: number_or_default(type.bug, DEFAULT_SCORE_WEIGHTS.type.bug),
3519
+ spike: number_or_default(type.spike, DEFAULT_SCORE_WEIGHTS.type.spike),
3520
+ feature: number_or_default(type.feature, DEFAULT_SCORE_WEIGHTS.type.feature),
3521
+ chore: number_or_default(type.chore, DEFAULT_SCORE_WEIGHTS.type.chore),
3522
+ epic: number_or_default(type.epic, DEFAULT_SCORE_WEIGHTS.type.epic)
3523
+ },
3524
+ complexity_penalty: {
3525
+ trivial: number_or_default(complexity_penalty2.trivial, DEFAULT_SCORE_WEIGHTS.complexity_penalty.trivial),
3526
+ small: number_or_default(complexity_penalty2.small, DEFAULT_SCORE_WEIGHTS.complexity_penalty.small),
3527
+ medium: number_or_default(complexity_penalty2.medium, DEFAULT_SCORE_WEIGHTS.complexity_penalty.medium),
3528
+ large: number_or_default(complexity_penalty2.large, DEFAULT_SCORE_WEIGHTS.complexity_penalty.large),
3529
+ unknown: number_or_default(complexity_penalty2.unknown, DEFAULT_SCORE_WEIGHTS.complexity_penalty.unknown)
3530
+ },
3531
+ risk_penalty: {
3532
+ low: number_or_default(risk_penalty2.low, DEFAULT_SCORE_WEIGHTS.risk_penalty.low),
3533
+ medium: number_or_default(risk_penalty2.medium, DEFAULT_SCORE_WEIGHTS.risk_penalty.medium),
3534
+ high: number_or_default(risk_penalty2.high, DEFAULT_SCORE_WEIGHTS.risk_penalty.high),
3535
+ critical: number_or_default(risk_penalty2.critical, DEFAULT_SCORE_WEIGHTS.risk_penalty.critical)
3536
+ }
3537
+ };
3538
+ }
3539
+ function unresolved_dependencies(task, graph) {
3540
+ return (task.depends_on ?? []).filter((depId) => {
3541
+ const dep = graph.nodes.get(depId);
3542
+ return !dep || dep.status !== "done" && dep.status !== "canceled";
3543
+ });
3544
+ }
3545
+ function priority_weight(task, config) {
3546
+ const weights = configured_weights(config);
3547
+ const priority = task.priority ?? "p2";
3548
+ return weights.priority[priority];
3549
+ }
3550
+ function urgency_weight(task, deliveries, today, config) {
3551
+ if (!task.delivery) return 0;
3552
+ const delivery = toMap(deliveries).get(task.delivery);
3553
+ if (!delivery?.target_date) return 0;
3554
+ const weights = configured_weights(config);
3555
+ const daysRemaining = calendar_days(today, delivery.target_date);
3556
+ if (daysRemaining < 7) return weights.urgency.days_7;
3557
+ if (daysRemaining < 14) return weights.urgency.days_14;
3558
+ if (daysRemaining < 28) return weights.urgency.days_28;
3559
+ return 0;
3560
+ }
3561
+ function dependency_unlock_weight(taskId, graph, config) {
3562
+ const weights = configured_weights(config);
3563
+ const reverse = graph.reverse.get(taskId) ?? /* @__PURE__ */ new Set();
3564
+ const directBlocked = [];
3565
+ for (const dependentId of reverse) {
3566
+ const dependent = graph.nodes.get(dependentId);
3567
+ if (!dependent) continue;
3568
+ if (compute_readiness(dependent, graph) !== "blocked") continue;
3569
+ const unresolved = unresolved_dependencies(dependent, graph);
3570
+ if (unresolved.length === 1 && unresolved[0] === taskId) {
3571
+ directBlocked.push(dependentId);
3572
+ }
3573
+ }
3574
+ const directCount = directBlocked.length;
3575
+ let directWeight = 0;
3576
+ if (directCount >= 5) {
3577
+ directWeight = weights.dependency_unlock.gte_5;
3578
+ } else if (directCount >= 3) {
3579
+ directWeight = weights.dependency_unlock.gte_3;
3580
+ } else if (directCount >= 1) {
3581
+ directWeight = weights.dependency_unlock.gte_1;
3582
+ }
3583
+ const transitive = /* @__PURE__ */ new Set();
3584
+ for (const directId of directBlocked) {
3585
+ const reverseDepth2 = graph.reverse.get(directId) ?? /* @__PURE__ */ new Set();
3586
+ for (const transitiveId of reverseDepth2) {
3587
+ const task = graph.nodes.get(transitiveId);
3588
+ if (!task) continue;
3589
+ if (compute_readiness(task, graph) === "blocked") {
3590
+ transitive.add(transitiveId);
3591
+ }
3592
+ }
3593
+ }
3594
+ const transitiveWeight = Math.min(transitive.size * 2, weights.dependency_unlock.transitive_cap);
3595
+ return directWeight + transitiveWeight;
3596
+ }
3597
+ function critical_path_weight(taskId, cpm, config) {
3598
+ if (!cpm) return 0;
3599
+ const weights = configured_weights(config);
3600
+ const isCritical = cpm.critical_path.includes(taskId);
3601
+ return isCritical ? weights.critical_path : 0;
3602
+ }
3603
+ function determinism_weight(task, config) {
3604
+ const weights = configured_weights(config);
3605
+ const defaultDeterminism = config?.defaults?.task?.determinism ?? "medium";
3606
+ const determinism = task.determinism ?? defaultDeterminism;
3607
+ return weights.determinism[determinism];
3608
+ }
3609
+ function executor_fit_weight(task, targetExecutor, config) {
3610
+ if (!targetExecutor) return 0;
3611
+ const weights = configured_weights(config);
3612
+ const defaultDeterminism = config?.defaults?.task?.determinism ?? "medium";
3613
+ const determinism = task.determinism ?? defaultDeterminism;
3614
+ if (targetExecutor === "ai") {
3615
+ return determinism === "high" || determinism === "medium" ? weights.executor_fit.match : weights.executor_fit.mismatch;
3616
+ }
3617
+ if (targetExecutor === "human") {
3618
+ return determinism === "low" || determinism === "experimental" ? weights.executor_fit.match : weights.executor_fit.mismatch;
3619
+ }
3620
+ if (targetExecutor === "ci") {
3621
+ const runbook = task.execution?.runbook ?? [];
3622
+ const ciReady = task.type === "chore" && runbook.length > 0 && runbook.every((step) => step.action === "run");
3623
+ return ciReady ? weights.executor_fit.ci_match : weights.executor_fit.mismatch;
3624
+ }
3625
+ return 0;
3626
+ }
3627
+ function type_weight(task, config) {
3628
+ const weights = configured_weights(config);
3629
+ return weights.type[task.type];
3630
+ }
3631
+ function complexity_penalty(task, config) {
3632
+ const weights = configured_weights(config);
3633
+ const complexity = task.complexity ?? config?.defaults?.task?.complexity ?? "medium";
3634
+ return weights.complexity_penalty[complexity];
3635
+ }
3636
+ function risk_penalty(task, config) {
3637
+ const weights = configured_weights(config);
3638
+ const level = task.risk?.level ?? "low";
3639
+ return weights.risk_penalty[level];
3640
+ }
3641
+ function compute_score(task, graph, context = {}) {
3642
+ const deliveries = context.deliveries ?? graph.deliveries;
3643
+ const today = context.today ?? /* @__PURE__ */ new Date();
3644
+ const config = context.config;
3645
+ 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);
3646
+ }
3647
+
3648
+ // src/planning/scheduler.ts
3649
+ function asDate4(value) {
3650
+ if (value instanceof Date) return new Date(value.getTime());
3651
+ const parsed = /* @__PURE__ */ new Date(`${value}T00:00:00.000Z`);
3652
+ if (Number.isNaN(parsed.valueOf())) {
3653
+ throw new Error(`Invalid date '${value}'.`);
3654
+ }
3655
+ return parsed;
3656
+ }
3657
+ function isoDate3(value) {
3658
+ return value.toISOString().slice(0, 10);
3659
+ }
3660
+ function addDays3(value, days) {
3661
+ const out = new Date(value.getTime());
3662
+ out.setUTCDate(out.getUTCDate() + days);
3663
+ return out;
3664
+ }
3665
+ function clone_ledger2(ledger) {
3666
+ return {
3667
+ slots: new Map(
3668
+ Array.from(ledger.slots.entries(), ([track, byWeek]) => [track, new Map(byWeek)])
3669
+ ),
3670
+ weeks: new Map(ledger.weeks),
3671
+ ai_tokens: new Map(ledger.ai_tokens),
3672
+ ai_tokens_by_agent: new Map(
3673
+ Array.from(ledger.ai_tokens_by_agent.entries(), ([agent, byDay]) => [agent, new Map(byDay)])
3674
+ ),
3675
+ ai_tokens_consumed_by_agent: new Map(
3676
+ Array.from(ledger.ai_tokens_consumed_by_agent.entries(), ([agent, byDay]) => [
3677
+ agent,
3678
+ new Map(byDay)
3679
+ ])
3680
+ )
3681
+ };
3682
+ }
3683
+ function normalize_track3(track) {
3684
+ return (track ?? "unassigned").trim().toLowerCase();
3685
+ }
3686
+ function find_track_slots2(ledger, track) {
3687
+ const normalized = normalize_track3(track);
3688
+ return ledger.slots.get(normalized) ?? ledger.slots.get("unassigned") ?? null;
3689
+ }
3690
+ function check_capacity(task, ledger) {
3691
+ try {
3692
+ const slots = find_track_slots2(ledger, task.track);
3693
+ if (!slots) return false;
3694
+ const week0 = slots.get(0) ?? 0;
3695
+ if (week0 <= 0) return false;
3696
+ const attempt = clone_ledger2(ledger);
3697
+ return allocate(attempt, task, 0).success;
3698
+ } catch {
3699
+ return false;
3700
+ }
3701
+ }
3702
+ function advisory_delivery(graph, tasks, options) {
3703
+ if (options.delivery) {
3704
+ const existing = graph.deliveries.get(options.delivery);
3705
+ if (existing) {
3706
+ return {
3707
+ id: existing.id,
3708
+ name: existing.name,
3709
+ status: "planning",
3710
+ target_date: existing.target_date ?? isoDate3(addDays3(asDate4(options.today ?? /* @__PURE__ */ new Date()), 56)),
3711
+ started_date: existing.started_date,
3712
+ delivered_date: existing.delivered_date,
3713
+ budget: existing.budget,
3714
+ capacity_profiles: existing.capacity_profiles,
3715
+ scope: existing.scope
3716
+ };
838
3717
  }
839
- ordered[key] = raw[key];
840
3718
  }
841
- return ordered;
3719
+ const targetDate = isoDate3(addDays3(asDate4(options.today ?? /* @__PURE__ */ new Date()), 56));
3720
+ return {
3721
+ id: "__SCHEDULE__",
3722
+ name: "Advisory Schedule",
3723
+ status: "planning",
3724
+ target_date: targetDate,
3725
+ started_date: null,
3726
+ delivered_date: null,
3727
+ budget: {},
3728
+ capacity_profiles: [],
3729
+ scope: {
3730
+ include: tasks.map((task) => task.id),
3731
+ exclude: []
3732
+ }
3733
+ };
842
3734
  }
843
- function writeTask(task, options = {}) {
844
- const frontmatter = buildOrderedFrontmatter(task, options.raw);
845
- const output = stringifyFrontmatter(frontmatter, options.body ?? "");
846
- if (options.filePath) {
847
- import_node_fs7.default.writeFileSync(options.filePath, output, "utf8");
3735
+ function resolve_cpm(graph, options) {
3736
+ if (options.delivery) {
3737
+ const delivery = graph.deliveries.get(options.delivery);
3738
+ if (!delivery) return void 0;
3739
+ return compute_critical_path(delivery, graph);
3740
+ }
3741
+ const preferred = Array.from(graph.deliveries.values()).find(
3742
+ (delivery) => delivery.status === "committed" || delivery.status === "in_progress"
3743
+ );
3744
+ if (!preferred) return void 0;
3745
+ return compute_critical_path(preferred, graph);
3746
+ }
3747
+ function schedule_next(graph, options = {}) {
3748
+ const ready_tasks = Array.from(graph.nodes.values()).filter(
3749
+ (task) => compute_readiness(task, graph) === "ready"
3750
+ );
3751
+ let filtered = ready_tasks;
3752
+ if (options.track) {
3753
+ filtered = filtered.filter((task) => (task.track ?? "unassigned") === options.track);
3754
+ }
3755
+ const deliveryFilter = options.delivery;
3756
+ if (deliveryFilter) {
3757
+ const scope = graph.deliveries.get(deliveryFilter)?.scope.include ?? [];
3758
+ const scopeSet = new Set(scope);
3759
+ filtered = filtered.filter(
3760
+ (task) => task.delivery === deliveryFilter || scopeSet.has(task.id)
3761
+ );
3762
+ }
3763
+ if (options.executor) {
3764
+ filtered = filtered.filter(
3765
+ (task) => task.execution?.executor === options.executor || task.execution?.executor == null
3766
+ );
3767
+ }
3768
+ const cpm = resolve_cpm(graph, options);
3769
+ const scored = filtered.map((task) => ({
3770
+ task,
3771
+ score: compute_score(task, graph, {
3772
+ deliveries: graph.deliveries,
3773
+ cpm,
3774
+ today: options.today ?? /* @__PURE__ */ new Date(),
3775
+ target_executor: options.executor,
3776
+ config: options.config
3777
+ }),
3778
+ readiness: compute_readiness(task, graph),
3779
+ fits_capacity: true,
3780
+ fits_wip: true
3781
+ }));
3782
+ scored.sort((a, b) => {
3783
+ if (a.score !== b.score) return b.score - a.score;
3784
+ return a.task.id.localeCompare(b.task.id);
3785
+ });
3786
+ const limit = typeof options.limit === "number" && Number.isInteger(options.limit) && options.limit > 0 ? options.limit : void 0;
3787
+ const output = typeof limit === "number" ? scored.slice(0, limit) : scored;
3788
+ if (output.length === 0) return output;
3789
+ const delivery = advisory_delivery(graph, output.map((entry) => entry.task), options);
3790
+ const ledger = build_capacity_ledger(delivery, graph.resources, options.today ?? /* @__PURE__ */ new Date());
3791
+ for (const entry of output) {
3792
+ entry.fits_capacity = check_capacity(entry.task, ledger);
3793
+ entry.fits_wip = check_wip(
3794
+ graph.tracks.get(entry.task.track ?? "unassigned"),
3795
+ graph
3796
+ );
848
3797
  }
849
3798
  return output;
850
3799
  }
851
3800
 
852
- // src/planning/readiness.ts
853
- function isResolvedDependencyStatus(status) {
854
- return status === "done" || status === "canceled";
3801
+ // src/planning/velocity.ts
3802
+ var import_node_fs11 = __toESM(require("fs"), 1);
3803
+ var import_node_path5 = __toESM(require("path"), 1);
3804
+ var DEFAULT_ESTIMATION_CONFIG2 = {
3805
+ version: 2,
3806
+ project: {
3807
+ name: "COOP",
3808
+ id: "coop"
3809
+ },
3810
+ defaults: {
3811
+ task: {
3812
+ complexity: "medium"
3813
+ }
3814
+ }
3815
+ };
3816
+ function asDate5(value) {
3817
+ if (value instanceof Date) return new Date(value.getTime());
3818
+ const parsed = new Date(value);
3819
+ if (Number.isNaN(parsed.valueOf())) {
3820
+ throw new Error(`Invalid date '${value}'.`);
3821
+ }
3822
+ return parsed;
855
3823
  }
856
- function compute_readiness(task, graph) {
857
- if (task.status === "done" || task.status === "canceled") {
858
- return "done";
3824
+ function isoDate4(value) {
3825
+ return value.toISOString().slice(0, 10);
3826
+ }
3827
+ function startOfWeek3(date) {
3828
+ const copy = new Date(date.getTime());
3829
+ const day = copy.getUTCDay();
3830
+ const offset = day === 0 ? -6 : 1 - day;
3831
+ copy.setUTCDate(copy.getUTCDate() + offset);
3832
+ copy.setUTCHours(0, 0, 0, 0);
3833
+ return copy;
3834
+ }
3835
+ function addDays4(date, days) {
3836
+ const out = new Date(date.getTime());
3837
+ out.setUTCDate(out.getUTCDate() + days);
3838
+ return out;
3839
+ }
3840
+ function durationHours2(run) {
3841
+ const fromSteps = run.steps.reduce((sum, step) => sum + (step.duration_seconds ?? 0), 0) / 3600;
3842
+ if (fromSteps > 0) {
3843
+ return fromSteps;
859
3844
  }
860
- if (task.status === "in_review") {
861
- return "waiting_review";
3845
+ if (run.completed) {
3846
+ const started = asDate5(run.started);
3847
+ const completed = asDate5(run.completed);
3848
+ return Math.max(0, completed.getTime() - started.getTime()) / (60 * 60 * 1e3);
862
3849
  }
863
- if (task.status === "in_progress") {
864
- return "in_progress";
3850
+ return 0;
3851
+ }
3852
+ function mean2(values) {
3853
+ if (values.length === 0) return 0;
3854
+ return values.reduce((sum, value) => sum + value, 0) / values.length;
3855
+ }
3856
+ function detectTrend(points) {
3857
+ if (points.length < 4) {
3858
+ return "stable";
865
3859
  }
866
- for (const depId of task.depends_on ?? []) {
867
- const dep = graph.nodes.get(depId);
868
- if (!dep || !isResolvedDependencyStatus(dep.status)) {
869
- return "blocked";
870
- }
3860
+ const midpoint = Math.floor(points.length / 2);
3861
+ const firstHalf = points.slice(0, midpoint).map((point) => point.completed_tasks);
3862
+ const secondHalf = points.slice(midpoint).map((point) => point.completed_tasks);
3863
+ const firstAverage = mean2(firstHalf);
3864
+ const secondAverage = mean2(secondHalf);
3865
+ if (firstAverage === 0 && secondAverage === 0) {
3866
+ return "stable";
871
3867
  }
872
- return "ready";
3868
+ if (firstAverage === 0 && secondAverage > 0) {
3869
+ return "accelerating";
3870
+ }
3871
+ const ratio = secondAverage / Math.max(firstAverage, 1e-4);
3872
+ if (ratio >= 1.15) {
3873
+ return "accelerating";
3874
+ }
3875
+ if (ratio <= 0.85) {
3876
+ return "decelerating";
3877
+ }
3878
+ return "stable";
873
3879
  }
874
- function compute_all_readiness(graph) {
875
- const readiness = /* @__PURE__ */ new Map();
876
- for (const [taskId, task] of graph.nodes.entries()) {
877
- readiness.set(taskId, compute_readiness(task, graph));
3880
+ function load_completed_runs(coopDir) {
3881
+ const runsDir = import_node_path5.default.join(coopDir, "runs");
3882
+ if (!import_node_fs11.default.existsSync(runsDir)) {
3883
+ return [];
878
3884
  }
879
- return readiness;
3885
+ return import_node_fs11.default.readdirSync(runsDir).filter((entry) => entry.toLowerCase().endsWith(".yml") || entry.toLowerCase().endsWith(".yaml")).sort((a, b) => a.localeCompare(b)).map((entry) => parseYamlFile(import_node_path5.default.join(runsDir, entry))).filter((run) => run.status === "completed" && typeof run.completed === "string");
880
3886
  }
881
- function partition_by_readiness(graph) {
882
- const partitions = {
883
- ready: [],
884
- blocked: [],
885
- in_progress: [],
886
- waiting_review: [],
887
- done: []
3887
+ function compute_velocity(runs, windowWeeks, options = {}) {
3888
+ const normalizedWindow = Number.isInteger(windowWeeks) && windowWeeks > 0 ? windowWeeks : 4;
3889
+ const today = startOfWeek3(asDate5(options.today ?? /* @__PURE__ */ new Date()));
3890
+ const windowStart = addDays4(today, -7 * (normalizedWindow - 1));
3891
+ const pointMap = /* @__PURE__ */ new Map();
3892
+ for (let weekIndex = 0; weekIndex < normalizedWindow; weekIndex += 1) {
3893
+ const weekStart = addDays4(windowStart, weekIndex * 7);
3894
+ const weekEnd = addDays4(weekStart, 6);
3895
+ pointMap.set(isoDate4(weekStart), {
3896
+ tasks: /* @__PURE__ */ new Set(),
3897
+ hours: 0,
3898
+ index: weekIndex,
3899
+ end: isoDate4(weekEnd)
3900
+ });
3901
+ }
3902
+ let completedRuns = 0;
3903
+ let estimatedHours = 0;
3904
+ let actualHours = 0;
3905
+ for (const run of runs) {
3906
+ if (!run.completed) continue;
3907
+ const completedAt = asDate5(run.completed);
3908
+ const weekStart = startOfWeek3(completedAt);
3909
+ const weekKey = isoDate4(weekStart);
3910
+ const bucket = pointMap.get(weekKey);
3911
+ if (!bucket) continue;
3912
+ completedRuns += 1;
3913
+ const hours = durationHours2(run);
3914
+ actualHours += hours;
3915
+ bucket.tasks.add(run.task);
3916
+ bucket.hours += hours;
3917
+ const task = options.graph?.nodes.get(run.task);
3918
+ if (task) {
3919
+ estimatedHours += effort_or_default(task, DEFAULT_ESTIMATION_CONFIG2);
3920
+ }
3921
+ }
3922
+ const points = Array.from(pointMap.entries()).sort((a, b) => a[1].index - b[1].index).map(([weekStart, bucket]) => ({
3923
+ week_index: bucket.index,
3924
+ week_start: weekStart,
3925
+ week_end: bucket.end,
3926
+ completed_tasks: bucket.tasks.size,
3927
+ delivered_hours: Number(bucket.hours.toFixed(2))
3928
+ }));
3929
+ const tasksCompletedTotal = points.reduce((sum, point) => sum + point.completed_tasks, 0);
3930
+ const deliveredHoursTotal = points.reduce((sum, point) => sum + point.delivered_hours, 0);
3931
+ return {
3932
+ window_weeks: normalizedWindow,
3933
+ completed_runs: completedRuns,
3934
+ tasks_completed_total: tasksCompletedTotal,
3935
+ delivered_hours_total: Number(deliveredHoursTotal.toFixed(2)),
3936
+ tasks_completed_per_week: Number((tasksCompletedTotal / normalizedWindow).toFixed(2)),
3937
+ hours_delivered_per_week: Number((deliveredHoursTotal / normalizedWindow).toFixed(2)),
3938
+ accuracy_ratio: estimatedHours > 0 ? Number((actualHours / estimatedHours).toFixed(3)) : null,
3939
+ trend: detectTrend(points),
3940
+ points
888
3941
  };
889
- for (const task of graph.nodes.values()) {
890
- const state = compute_readiness(task, graph);
891
- partitions[state].push(task);
3942
+ }
3943
+
3944
+ // src/planning/what-if.ts
3945
+ function asDate6(value) {
3946
+ if (value instanceof Date) return new Date(value.getTime());
3947
+ const parsed = /* @__PURE__ */ new Date(`${value}T00:00:00.000Z`);
3948
+ if (Number.isNaN(parsed.valueOf())) {
3949
+ throw new Error(`Invalid date '${value}'.`);
892
3950
  }
893
- return partitions;
3951
+ return parsed;
894
3952
  }
895
- function compute_readiness_with_corrections(graph) {
896
- const readiness = /* @__PURE__ */ new Map();
897
- const corrections = [];
898
- const warnings = [];
899
- const partitions = {
900
- ready: [],
901
- blocked: [],
902
- in_progress: [],
903
- waiting_review: [],
904
- done: []
905
- };
906
- for (const [taskId, task] of graph.nodes.entries()) {
907
- const state = compute_readiness(task, graph);
908
- readiness.set(taskId, state);
909
- partitions[state].push(task);
910
- if (task.status === "blocked" && state === "ready") {
911
- corrections.push({
912
- type: "task.transitioned",
913
- task_id: taskId,
914
- from: "blocked",
915
- to: "todo",
916
- reason: "All dependencies are resolved."
917
- });
3953
+ function calendar_days2(from, to) {
3954
+ const start = asDate6(from);
3955
+ const end = asDate6(to);
3956
+ return Math.round((end.getTime() - start.getTime()) / (24 * 60 * 60 * 1e3));
3957
+ }
3958
+ function signed_number(value, suffix = "") {
3959
+ if (!Number.isFinite(value) || value === 0) return "-";
3960
+ return `${value > 0 ? "+" : ""}${value}${suffix}`;
3961
+ }
3962
+ function signed_hours(value) {
3963
+ if (!Number.isFinite(value) || value === 0) return "-";
3964
+ return `${value > 0 ? "+" : ""}${value.toFixed(1)}h`;
3965
+ }
3966
+ function format_hours(value) {
3967
+ if (!Number.isFinite(value)) return "unbounded";
3968
+ return `${value.toFixed(1)}h`;
3969
+ }
3970
+ function format_headroom(result) {
3971
+ const budget = result.summary.effort_hours.budget;
3972
+ if (!Number.isFinite(budget)) return "unbounded";
3973
+ return `${(budget - result.summary.effort_hours.required).toFixed(1)}h`;
3974
+ }
3975
+ function modification_label(modification) {
3976
+ switch (modification.kind) {
3977
+ case "without":
3978
+ return `--without ${modification.task_id}`;
3979
+ case "add_member":
3980
+ return `--add-member ${modification.target}`;
3981
+ case "target":
3982
+ return `--target ${modification.target_date}`;
3983
+ case "set_priority":
3984
+ return `--set ${modification.task_id}:priority=${modification.priority}`;
3985
+ }
3986
+ }
3987
+ function build_rows(baseline, scenario) {
3988
+ const baselineTarget = baseline.summary.dates.target ?? "-";
3989
+ const scenarioTarget = scenario.summary.dates.target ?? "-";
3990
+ const baselineProjected = baseline.summary.dates.projected ?? "-";
3991
+ const scenarioProjected = scenario.summary.dates.projected ?? "-";
3992
+ const baselineHeadroom = Number.isFinite(baseline.summary.effort_hours.budget) ? baseline.summary.effort_hours.budget - baseline.summary.effort_hours.required : Number.NaN;
3993
+ const scenarioHeadroom = Number.isFinite(scenario.summary.effort_hours.budget) ? scenario.summary.effort_hours.budget - scenario.summary.effort_hours.required : Number.NaN;
3994
+ return [
3995
+ {
3996
+ metric: "Target date",
3997
+ original: baselineTarget,
3998
+ scenario: scenarioTarget,
3999
+ delta: baseline.summary.dates.target && scenario.summary.dates.target ? signed_number(calendar_days2(baseline.summary.dates.target, scenario.summary.dates.target), " days") : "-"
4000
+ },
4001
+ {
4002
+ metric: "Projected date",
4003
+ original: baselineProjected,
4004
+ scenario: scenarioProjected,
4005
+ delta: baseline.summary.dates.projected && scenario.summary.dates.projected ? signed_number(
4006
+ calendar_days2(baseline.summary.dates.projected, scenario.summary.dates.projected),
4007
+ " days"
4008
+ ) : "-"
4009
+ },
4010
+ {
4011
+ metric: "Effort (hours)",
4012
+ original: format_hours(baseline.summary.effort_hours.required),
4013
+ scenario: format_hours(scenario.summary.effort_hours.required),
4014
+ delta: signed_hours(scenario.summary.effort_hours.required - baseline.summary.effort_hours.required)
4015
+ },
4016
+ {
4017
+ metric: "Headroom",
4018
+ original: format_headroom(baseline),
4019
+ scenario: format_headroom(scenario),
4020
+ delta: signed_hours(scenarioHeadroom - baselineHeadroom)
4021
+ },
4022
+ {
4023
+ metric: "Critical path tasks",
4024
+ original: String(baseline.summary.critical_path.length),
4025
+ scenario: String(scenario.summary.critical_path.length),
4026
+ delta: signed_number(scenario.summary.critical_path.length - baseline.summary.critical_path.length)
4027
+ },
4028
+ {
4029
+ metric: "Status",
4030
+ original: baseline.status,
4031
+ scenario: scenario.status,
4032
+ delta: baseline.status === scenario.status ? "-" : `${baseline.status} -> ${scenario.status}`
918
4033
  }
919
- if (task.status === "todo" && state === "blocked") {
920
- const unresolved = (task.depends_on ?? []).filter((depId) => {
921
- const depTask = graph.nodes.get(depId);
922
- return !depTask || !isResolvedDependencyStatus(depTask.status);
923
- });
924
- warnings.push({
925
- code: "todo_with_unresolved_dependencies",
926
- task_id: taskId,
927
- message: `Task is todo but has unresolved dependencies: ${unresolved.join(", ")}.`
928
- });
4034
+ ];
4035
+ }
4036
+ function clone_graph(graph) {
4037
+ return structuredClone(graph);
4038
+ }
4039
+ function resolve_delivery(graph, deliveryId) {
4040
+ const delivery = graph.deliveries.get(deliveryId);
4041
+ if (!delivery) {
4042
+ throw new Error(`Delivery '${deliveryId}' not found.`);
4043
+ }
4044
+ return delivery;
4045
+ }
4046
+ function remove_task_and_exclusive_dependencies(graph, delivery, taskId) {
4047
+ const scoped = new Set(delivery.scope.include);
4048
+ for (const excluded of delivery.scope.exclude) {
4049
+ scoped.delete(excluded);
4050
+ }
4051
+ if (!scoped.has(taskId)) {
4052
+ throw new Error(`Task '${taskId}' is not in delivery '${delivery.id}' scope.`);
4053
+ }
4054
+ const toRemove = /* @__PURE__ */ new Set([taskId]);
4055
+ const queue = [taskId];
4056
+ while (queue.length > 0) {
4057
+ const current = queue.shift();
4058
+ if (!current) continue;
4059
+ for (const dependencyId of graph.forward.get(current) ?? /* @__PURE__ */ new Set()) {
4060
+ if (!scoped.has(dependencyId) || toRemove.has(dependencyId)) continue;
4061
+ const dependents = graph.reverse.get(dependencyId) ?? /* @__PURE__ */ new Set();
4062
+ const hasRemainingDependent = Array.from(dependents).some(
4063
+ (dependentId) => scoped.has(dependentId) && !toRemove.has(dependentId)
4064
+ );
4065
+ if (hasRemainingDependent) continue;
4066
+ toRemove.add(dependencyId);
4067
+ queue.push(dependencyId);
4068
+ }
4069
+ }
4070
+ delivery.scope.include = delivery.scope.include.filter((id) => !toRemove.has(id));
4071
+ delivery.scope.exclude = delivery.scope.exclude.filter((id) => !toRemove.has(id));
4072
+ }
4073
+ function resolve_human_profile(graph, delivery, target) {
4074
+ const direct = graph.resources.get(target);
4075
+ if (direct?.type === "human") {
4076
+ return direct;
4077
+ }
4078
+ const track = graph.tracks.get(target);
4079
+ if (track) {
4080
+ for (const profileId of track.capacity_profiles) {
4081
+ const profile = graph.resources.get(profileId);
4082
+ if (profile?.type === "human") {
4083
+ return profile;
4084
+ }
4085
+ }
4086
+ }
4087
+ for (const profileId of delivery.capacity_profiles) {
4088
+ const profile = graph.resources.get(profileId);
4089
+ if (profile?.type === "human") {
4090
+ return profile;
4091
+ }
4092
+ }
4093
+ throw new Error(`No human capacity profile found for '${target}'.`);
4094
+ }
4095
+ function add_member(graph, delivery, target, hoursPerWeek) {
4096
+ const profile = resolve_human_profile(graph, delivery, target);
4097
+ const defaultHours = hoursPerWeek ?? profile.defaults?.hours_per_week ?? profile.members[0]?.hours_per_week ?? 40;
4098
+ const nextMemberIndex = profile.members.length + 1;
4099
+ profile.members = [
4100
+ ...profile.members,
4101
+ {
4102
+ id: `what-if-${profile.id}-${nextMemberIndex}`,
4103
+ hours_per_week: defaultHours
929
4104
  }
4105
+ ];
4106
+ graph.resources.set(profile.id, profile);
4107
+ }
4108
+ function set_target(delivery, targetDate) {
4109
+ asDate6(targetDate);
4110
+ delivery.target_date = targetDate;
4111
+ }
4112
+ function set_priority(graph, taskId, priority) {
4113
+ const task = graph.nodes.get(taskId);
4114
+ if (!task) {
4115
+ throw new Error(`Task '${taskId}' not found.`);
4116
+ }
4117
+ task.priority = priority;
4118
+ graph.nodes.set(taskId, task);
4119
+ }
4120
+ function apply_modification(graph, deliveryId, modification) {
4121
+ const delivery = resolve_delivery(graph, deliveryId);
4122
+ switch (modification.kind) {
4123
+ case "without":
4124
+ remove_task_and_exclusive_dependencies(graph, delivery, modification.task_id);
4125
+ return;
4126
+ case "add_member":
4127
+ add_member(graph, delivery, modification.target, modification.hours_per_week);
4128
+ return;
4129
+ case "target":
4130
+ set_target(delivery, modification.target_date);
4131
+ graph.deliveries.set(delivery.id, delivery);
4132
+ return;
4133
+ case "set_priority":
4134
+ set_priority(graph, modification.task_id, modification.priority);
4135
+ return;
4136
+ }
4137
+ }
4138
+ function analyze_what_if(baseline, modification) {
4139
+ const modifications = Array.isArray(modification) ? modification : [modification];
4140
+ if (modifications.length === 0) {
4141
+ throw new Error("At least one what-if modification is required.");
4142
+ }
4143
+ const baselineResult = analyze_feasibility(baseline.delivery_id, baseline.graph, baseline.today);
4144
+ const scenarioGraph = clone_graph(baseline.graph);
4145
+ for (const next of modifications) {
4146
+ apply_modification(scenarioGraph, baseline.delivery_id, next);
930
4147
  }
4148
+ const scenarioResult = analyze_feasibility(baseline.delivery_id, scenarioGraph, baseline.today);
931
4149
  return {
932
- readiness,
933
- partitions,
934
- corrections,
935
- warnings
4150
+ delivery: baseline.delivery_id,
4151
+ label: modifications.map(modification_label).join(" "),
4152
+ baseline: baselineResult,
4153
+ scenario: scenarioResult,
4154
+ rows: build_rows(baselineResult, scenarioResult)
936
4155
  };
937
4156
  }
938
4157
 
4158
+ // src/schema/migration.ts
4159
+ var import_node_fs13 = __toESM(require("fs"), 1);
4160
+ var import_node_path7 = __toESM(require("path"), 1);
4161
+
939
4162
  // src/schema/version.ts
940
- var import_node_fs8 = __toESM(require("fs"), 1);
941
- var import_node_path2 = __toESM(require("path"), 1);
4163
+ var import_node_fs12 = __toESM(require("fs"), 1);
4164
+ var import_node_path6 = __toESM(require("path"), 1);
942
4165
  var CURRENT_SCHEMA_VERSION = 2;
943
4166
  function schemaVersionFile(coopDir) {
944
- return import_node_path2.default.join(coopDir, "schema-version");
4167
+ return import_node_path6.default.join(coopDir, "schema-version");
945
4168
  }
946
4169
  function read_schema_version(coopDir) {
947
4170
  const filePath = schemaVersionFile(coopDir);
948
- if (!import_node_fs8.default.existsSync(filePath)) {
4171
+ if (!import_node_fs12.default.existsSync(filePath)) {
949
4172
  throw new Error(`Missing schema-version file at ${filePath}.`);
950
4173
  }
951
- const raw = import_node_fs8.default.readFileSync(filePath, "utf8").trim();
4174
+ const raw = import_node_fs12.default.readFileSync(filePath, "utf8").trim();
952
4175
  if (!raw) {
953
4176
  throw new Error(`Schema version file is empty at ${filePath}.`);
954
4177
  }
@@ -963,10 +4186,168 @@ function write_schema_version(coopDir, version) {
963
4186
  throw new Error(`Schema version must be a positive integer. Received: ${String(version)}.`);
964
4187
  }
965
4188
  const filePath = schemaVersionFile(coopDir);
966
- import_node_fs8.default.writeFileSync(filePath, `${version}
4189
+ import_node_fs12.default.writeFileSync(filePath, `${version}
967
4190
  `, "utf8");
968
4191
  }
969
4192
 
4193
+ // src/schema/migration.ts
4194
+ function asString2(value) {
4195
+ return typeof value === "string" && value.trim().length > 0 ? value : null;
4196
+ }
4197
+ function asRecord(value) {
4198
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
4199
+ }
4200
+ function cloneRaw(raw) {
4201
+ return JSON.parse(JSON.stringify(raw));
4202
+ }
4203
+ function estimate_to_three_point(value) {
4204
+ return {
4205
+ optimistic_hours: Number((value * 0.7).toFixed(2)),
4206
+ expected_hours: Number(value.toFixed(2)),
4207
+ pessimistic_hours: Number((value * 1.8).toFixed(2))
4208
+ };
4209
+ }
4210
+ function walk_files(dirPath, extensions) {
4211
+ if (!import_node_fs13.default.existsSync(dirPath)) return [];
4212
+ const out = [];
4213
+ const entries = import_node_fs13.default.readdirSync(dirPath, { withFileTypes: true });
4214
+ for (const entry of entries) {
4215
+ const fullPath = import_node_path7.default.join(dirPath, entry.name);
4216
+ if (entry.isDirectory()) {
4217
+ out.push(...walk_files(fullPath, extensions));
4218
+ continue;
4219
+ }
4220
+ if (!entry.isFile()) continue;
4221
+ const ext = import_node_path7.default.extname(entry.name).toLowerCase();
4222
+ if (extensions.has(ext)) {
4223
+ out.push(fullPath);
4224
+ }
4225
+ }
4226
+ return out.sort((a, b) => a.localeCompare(b));
4227
+ }
4228
+ function file_mtime_iso(filePath) {
4229
+ const stats = import_node_fs13.default.statSync(filePath);
4230
+ return stats.mtime.toISOString().slice(0, 10);
4231
+ }
4232
+ function migration_v1_to_v2(rawTask, context = {}) {
4233
+ const migrated = { ...rawTask };
4234
+ if (migrated.status === "dropped") {
4235
+ migrated.status = "canceled";
4236
+ }
4237
+ if (migrated.type === "task") {
4238
+ migrated.type = "chore";
4239
+ } else if (migrated.type === "research") {
4240
+ migrated.type = "spike";
4241
+ }
4242
+ if (!Object.prototype.hasOwnProperty.call(migrated, "updated")) {
4243
+ migrated.updated = context.file_mtime_iso ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
4244
+ }
4245
+ if (!Object.prototype.hasOwnProperty.call(migrated, "assignee")) {
4246
+ migrated.assignee = null;
4247
+ }
4248
+ const deliveryRecord = asRecord(migrated.delivery);
4249
+ if (deliveryRecord) {
4250
+ const include = deliveryRecord.include;
4251
+ if (Array.isArray(include)) {
4252
+ const first = include.find((entry) => typeof entry === "string");
4253
+ migrated.delivery = asString2(first) ?? null;
4254
+ } else if (typeof include === "string" && include.trim().length > 0) {
4255
+ migrated.delivery = include.trim();
4256
+ } else {
4257
+ migrated.delivery = null;
4258
+ }
4259
+ }
4260
+ const estimate = migrated.estimate;
4261
+ if (typeof estimate === "number" && Number.isFinite(estimate) && estimate > 0) {
4262
+ migrated.estimate = estimate_to_three_point(estimate);
4263
+ } else if (asRecord(estimate)) {
4264
+ const estimateRecord = estimate;
4265
+ const hasExpectedHours = typeof estimateRecord.expected_hours === "number" && Number.isFinite(estimateRecord.expected_hours);
4266
+ const hasOptimisticHours = typeof estimateRecord.optimistic_hours === "number" && Number.isFinite(estimateRecord.optimistic_hours);
4267
+ const hasPessimisticHours = typeof estimateRecord.pessimistic_hours === "number" && Number.isFinite(estimateRecord.pessimistic_hours);
4268
+ if (hasExpectedHours && (!hasOptimisticHours || !hasPessimisticHours)) {
4269
+ migrated.estimate = {
4270
+ ...estimateRecord,
4271
+ optimistic_hours: hasOptimisticHours ? estimateRecord.optimistic_hours : Number((Number(estimateRecord.expected_hours) * 0.7).toFixed(2)),
4272
+ pessimistic_hours: hasPessimisticHours ? estimateRecord.pessimistic_hours : Number((Number(estimateRecord.expected_hours) * 1.8).toFixed(2))
4273
+ };
4274
+ }
4275
+ }
4276
+ return migrated;
4277
+ }
4278
+ var MIGRATIONS = [
4279
+ {
4280
+ from: 1,
4281
+ to: 2,
4282
+ description: "COOP schema v1 to v2 task migration.",
4283
+ migrate: migration_v1_to_v2
4284
+ }
4285
+ ];
4286
+ function migration_for_step(fromVersion, toVersion) {
4287
+ const found = MIGRATIONS.find((migration) => migration.from === fromVersion && migration.to === toVersion);
4288
+ if (!found) {
4289
+ throw new Error(`No schema migration registered for v${fromVersion} -> v${toVersion}.`);
4290
+ }
4291
+ return found;
4292
+ }
4293
+ function migrate_task(rawTask, fromVersion, toVersion, context = {}) {
4294
+ if (!Number.isInteger(fromVersion) || fromVersion <= 0) {
4295
+ throw new Error(`Invalid fromVersion '${String(fromVersion)}'.`);
4296
+ }
4297
+ if (!Number.isInteger(toVersion) || toVersion <= 0) {
4298
+ throw new Error(`Invalid toVersion '${String(toVersion)}'.`);
4299
+ }
4300
+ if (fromVersion > toVersion) {
4301
+ throw new Error(`Downgrade migration is not supported: v${fromVersion} -> v${toVersion}.`);
4302
+ }
4303
+ if (fromVersion === toVersion) {
4304
+ return cloneRaw(rawTask);
4305
+ }
4306
+ let current = cloneRaw(rawTask);
4307
+ for (let version = fromVersion; version < toVersion; version += 1) {
4308
+ const migration = migration_for_step(version, version + 1);
4309
+ current = migration.migrate(current, context);
4310
+ }
4311
+ return current;
4312
+ }
4313
+ function migrate_repository(coopDir, targetVersion = CURRENT_SCHEMA_VERSION, options = {}) {
4314
+ if (!Number.isInteger(targetVersion) || targetVersion <= 0) {
4315
+ throw new Error(`Target version must be a positive integer. Received: ${String(targetVersion)}.`);
4316
+ }
4317
+ const dry_run = Boolean(options.dry_run);
4318
+ const from_version = read_schema_version(coopDir);
4319
+ if (from_version > targetVersion) {
4320
+ throw new Error(`Cannot migrate backwards from v${from_version} to v${targetVersion}.`);
4321
+ }
4322
+ const taskFiles = walk_files(import_node_path7.default.join(coopDir, "tasks"), /* @__PURE__ */ new Set([".md"]));
4323
+ const changed_files = [];
4324
+ for (const filePath of taskFiles) {
4325
+ const { frontmatter, body } = parseFrontmatterFile(filePath);
4326
+ const migrated = migrate_task(frontmatter, from_version, targetVersion, {
4327
+ file_path: filePath,
4328
+ file_mtime_iso: file_mtime_iso(filePath)
4329
+ });
4330
+ const before = JSON.stringify(frontmatter);
4331
+ const after = JSON.stringify(migrated);
4332
+ if (before === after) continue;
4333
+ changed_files.push(filePath);
4334
+ if (!dry_run) {
4335
+ import_node_fs13.default.writeFileSync(filePath, stringifyFrontmatter(migrated, body), "utf8");
4336
+ }
4337
+ }
4338
+ if (!dry_run && from_version !== targetVersion) {
4339
+ write_schema_version(coopDir, targetVersion);
4340
+ }
4341
+ return {
4342
+ from_version,
4343
+ to_version: targetVersion,
4344
+ dry_run,
4345
+ files_scanned: taskFiles.length,
4346
+ files_changed: changed_files.length,
4347
+ changed_files
4348
+ };
4349
+ }
4350
+
970
4351
  // src/state/auto-transitions.ts
971
4352
  function toStatusMap(value) {
972
4353
  if (!value) return /* @__PURE__ */ new Map();
@@ -1044,6 +4425,14 @@ function toIsoDate(now) {
1044
4425
  const date = now instanceof Date ? now : /* @__PURE__ */ new Date();
1045
4426
  return date.toISOString().slice(0, 10);
1046
4427
  }
4428
+ function toIsoTimestamp2(now) {
4429
+ if (typeof now === "string") {
4430
+ if (now.includes("T")) return now;
4431
+ return `${now}T00:00:00.000Z`;
4432
+ }
4433
+ const date = now instanceof Date ? now : /* @__PURE__ */ new Date();
4434
+ return date.toISOString();
4435
+ }
1047
4436
  function validateGovernanceAndDeps(task, targetStatus, context) {
1048
4437
  if (task.status === "in_review" && targetStatus === "done" && task.governance?.approval_required) {
1049
4438
  const reviewer = task.governance.reviewer;
@@ -1080,13 +4469,26 @@ function transition(task, targetStatus, context = {}) {
1080
4469
  error: governanceError
1081
4470
  };
1082
4471
  }
4472
+ const nextTask = {
4473
+ ...task,
4474
+ status: targetStatus,
4475
+ updated: toIsoDate(context.now)
4476
+ };
4477
+ if (targetStatus !== task.status) {
4478
+ context.eventEmitter?.emit({
4479
+ type: "task.transitioned",
4480
+ timestamp: toIsoTimestamp2(context.now),
4481
+ payload: {
4482
+ task_id: task.id,
4483
+ from: task.status,
4484
+ to: targetStatus,
4485
+ actor: context.actor
4486
+ }
4487
+ });
4488
+ }
1083
4489
  return {
1084
4490
  success: true,
1085
- task: {
1086
- ...task,
1087
- status: targetStatus,
1088
- updated: toIsoDate(context.now)
1089
- }
4491
+ task: nextTask
1090
4492
  };
1091
4493
  }
1092
4494
 
@@ -1304,8 +4706,9 @@ function validateSemantic(task, context = {}) {
1304
4706
  }
1305
4707
 
1306
4708
  // src/validator/structural.ts
1307
- var import_node_path3 = __toESM(require("path"), 1);
1308
- var ID_PATTERN = /^[A-Z]+-\d+$/;
4709
+ var import_node_path8 = __toESM(require("path"), 1);
4710
+ var ID_PATTERN = /^[A-Z0-9]+(?:-[A-Z0-9]+)+$/;
4711
+ var ALIAS_PATTERN = /^[A-Z0-9]+(?:[.-][A-Z0-9]+)*$/;
1309
4712
  var ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
1310
4713
  function error4(field, rule, message) {
1311
4714
  return { level: "error", field, rule, message };
@@ -1347,13 +4750,43 @@ function validateStructural(task, context = {}) {
1347
4750
  errors.push(error4("updated", "struct.updated_iso_date", `Field 'updated' must be an ISO date (YYYY-MM-DD).`));
1348
4751
  }
1349
4752
  if (context.filePath) {
1350
- const expected = import_node_path3.default.basename(context.filePath, import_node_path3.default.extname(context.filePath));
4753
+ const expected = import_node_path8.default.basename(context.filePath, import_node_path8.default.extname(context.filePath));
1351
4754
  if (typeof task.id === "string" && task.id !== expected) {
1352
4755
  errors.push(
1353
4756
  error4("id", "struct.id_matches_filename", `Task id '${task.id}' must match filename '${expected}'.`)
1354
4757
  );
1355
4758
  }
1356
4759
  }
4760
+ const aliases = task.aliases;
4761
+ if (aliases !== void 0) {
4762
+ if (!Array.isArray(aliases)) {
4763
+ errors.push(error4("aliases", "struct.aliases_array", "Field 'aliases' must be an array of strings."));
4764
+ } else {
4765
+ const seen = /* @__PURE__ */ new Set();
4766
+ for (const rawAlias of aliases) {
4767
+ if (typeof rawAlias !== "string" || rawAlias.trim().length === 0) {
4768
+ errors.push(error4("aliases", "struct.aliases_string", "Field 'aliases' entries must be non-empty strings."));
4769
+ continue;
4770
+ }
4771
+ const normalized = rawAlias.trim().toUpperCase().replace(/_/g, ".");
4772
+ if (!ALIAS_PATTERN.test(normalized)) {
4773
+ errors.push(
4774
+ error4(
4775
+ "aliases",
4776
+ "struct.aliases_pattern",
4777
+ `Alias '${rawAlias}' is invalid. Use letters/numbers with '.' or '-' separators.`
4778
+ )
4779
+ );
4780
+ continue;
4781
+ }
4782
+ if (seen.has(normalized)) {
4783
+ errors.push(error4("aliases", "struct.aliases_unique", `Alias '${rawAlias}' is duplicated.`));
4784
+ continue;
4785
+ }
4786
+ seen.add(normalized);
4787
+ }
4788
+ }
4789
+ }
1357
4790
  return errors;
1358
4791
  }
1359
4792
 
@@ -1441,9 +4874,131 @@ function validate(task, context = {}) {
1441
4874
  };
1442
4875
  }
1443
4876
 
4877
+ // src/workspace.ts
4878
+ var import_node_fs14 = __toESM(require("fs"), 1);
4879
+ var import_node_path9 = __toESM(require("path"), 1);
4880
+ var COOP_DIR_NAME = ".coop";
4881
+ function sanitizeProjectId(value, fallback) {
4882
+ const normalized = value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-");
4883
+ return normalized || fallback;
4884
+ }
4885
+ function coop_workspace_dir(repoRoot) {
4886
+ return import_node_path9.default.join(import_node_path9.default.resolve(repoRoot), COOP_DIR_NAME);
4887
+ }
4888
+ function coop_projects_dir(repoRoot) {
4889
+ return import_node_path9.default.join(coop_workspace_dir(repoRoot), "projects");
4890
+ }
4891
+ function coop_workspace_config_path(repoRoot) {
4892
+ return import_node_path9.default.join(coop_workspace_dir(repoRoot), "config.yml");
4893
+ }
4894
+ function coop_project_root(repoRoot, projectId) {
4895
+ return import_node_path9.default.join(coop_projects_dir(repoRoot), projectId);
4896
+ }
4897
+ function coop_project_config_path(projectRoot) {
4898
+ return import_node_path9.default.join(projectRoot, "config.yml");
4899
+ }
4900
+ function repo_default_project_id(repoRoot) {
4901
+ return sanitizeProjectId(import_node_path9.default.basename(import_node_path9.default.resolve(repoRoot)), "workspace");
4902
+ }
4903
+ function repo_default_project_name(repoRoot) {
4904
+ const base = import_node_path9.default.basename(import_node_path9.default.resolve(repoRoot)).trim();
4905
+ return base || "COOP Workspace";
4906
+ }
4907
+ function has_v2_projects_layout(repoRoot) {
4908
+ return import_node_fs14.default.existsSync(coop_projects_dir(repoRoot));
4909
+ }
4910
+ function has_legacy_project_layout(repoRoot) {
4911
+ const workspaceDir = coop_workspace_dir(repoRoot);
4912
+ return import_node_fs14.default.existsSync(workspaceDir) && import_node_fs14.default.existsSync(import_node_path9.default.join(workspaceDir, "config.yml")) && !import_node_fs14.default.existsSync(coop_projects_dir(repoRoot));
4913
+ }
4914
+ function read_workspace_config(repoRoot) {
4915
+ const configPath = coop_workspace_config_path(repoRoot);
4916
+ if (!import_node_fs14.default.existsSync(configPath) || has_legacy_project_layout(repoRoot)) {
4917
+ return { version: 2 };
4918
+ }
4919
+ return parseYamlFile(configPath);
4920
+ }
4921
+ function write_workspace_config(repoRoot, config) {
4922
+ import_node_fs14.default.mkdirSync(coop_workspace_dir(repoRoot), { recursive: true });
4923
+ writeYamlFile(coop_workspace_config_path(repoRoot), {
4924
+ version: config.version ?? 2,
4925
+ ...config.current_project ? { current_project: config.current_project } : {}
4926
+ });
4927
+ }
4928
+ function read_project_config(projectRoot) {
4929
+ return parseYamlFile(coop_project_config_path(projectRoot));
4930
+ }
4931
+ function project_ref_from_config(repoRoot, projectRoot, layout) {
4932
+ const config = read_project_config(projectRoot);
4933
+ const repoName = repo_default_project_name(repoRoot);
4934
+ const fallbackId = repo_default_project_id(repoRoot);
4935
+ return {
4936
+ id: sanitizeProjectId(config.project?.id ?? fallbackId, fallbackId),
4937
+ name: config.project?.name?.trim() || repoName,
4938
+ aliases: Array.isArray(config.project?.aliases) ? config.project.aliases.filter((entry) => typeof entry === "string" && entry.trim().length > 0) : [],
4939
+ root: projectRoot,
4940
+ repo_root: import_node_path9.default.resolve(repoRoot),
4941
+ layout
4942
+ };
4943
+ }
4944
+ function list_projects(repoRoot) {
4945
+ if (has_v2_projects_layout(repoRoot)) {
4946
+ const projectsDir = coop_projects_dir(repoRoot);
4947
+ return import_node_fs14.default.readdirSync(projectsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => import_node_path9.default.join(projectsDir, entry.name)).filter((projectRoot) => import_node_fs14.default.existsSync(coop_project_config_path(projectRoot))).map((projectRoot) => project_ref_from_config(repoRoot, projectRoot, "v2")).sort((a, b) => a.id.localeCompare(b.id));
4948
+ }
4949
+ if (has_legacy_project_layout(repoRoot)) {
4950
+ return [project_ref_from_config(repoRoot, coop_workspace_dir(repoRoot), "legacy")];
4951
+ }
4952
+ return [];
4953
+ }
4954
+ function resolve_project(repoRoot, options = {}) {
4955
+ const projects = list_projects(repoRoot);
4956
+ const requested = options.project?.trim().toLowerCase();
4957
+ if (requested) {
4958
+ const match = projects.find(
4959
+ (project) => project.id.toLowerCase() === requested || project.name.toLowerCase() === requested || project.aliases.some((alias) => alias.toLowerCase() === requested)
4960
+ );
4961
+ if (!match) {
4962
+ throw new Error(`Project '${options.project}' not found.`);
4963
+ }
4964
+ return match;
4965
+ }
4966
+ if (projects.length === 1) {
4967
+ return projects[0];
4968
+ }
4969
+ const workspaceConfig = read_workspace_config(repoRoot);
4970
+ if (workspaceConfig.current_project) {
4971
+ const match = projects.find((project) => project.id === workspaceConfig.current_project);
4972
+ if (match) return match;
4973
+ }
4974
+ if (!options.require && projects.length === 0) {
4975
+ return {
4976
+ id: repo_default_project_id(repoRoot),
4977
+ name: repo_default_project_name(repoRoot),
4978
+ aliases: [],
4979
+ root: coop_project_root(repoRoot, repo_default_project_id(repoRoot)),
4980
+ repo_root: import_node_path9.default.resolve(repoRoot),
4981
+ layout: "v2"
4982
+ };
4983
+ }
4984
+ if (projects.length === 0) {
4985
+ throw new Error("No COOP project found. Run 'coop init'.");
4986
+ }
4987
+ throw new Error("Multiple COOP projects found. Pass --project <id> or run 'coop project use <id>'.");
4988
+ }
4989
+ function ensure_workspace_layout(repoRoot) {
4990
+ const workspaceDir = coop_workspace_dir(repoRoot);
4991
+ import_node_fs14.default.mkdirSync(workspaceDir, { recursive: true });
4992
+ import_node_fs14.default.mkdirSync(coop_projects_dir(repoRoot), { recursive: true });
4993
+ return workspaceDir;
4994
+ }
4995
+ function is_project_initialized(projectRoot) {
4996
+ return import_node_fs14.default.existsSync(coop_project_config_path(projectRoot));
4997
+ }
4998
+
1444
4999
  // src/core.ts
1445
- var import_node_fs9 = __toESM(require("fs"), 1);
1446
- var import_node_path4 = __toESM(require("path"), 1);
5000
+ var import_node_fs15 = __toESM(require("fs"), 1);
5001
+ var import_node_path10 = __toESM(require("path"), 1);
1447
5002
  var import_gray_matter = __toESM(require("gray-matter"), 1);
1448
5003
 
1449
5004
  // src/types.ts
@@ -1460,7 +5015,7 @@ var ITEM_DIRS = {
1460
5015
  spike: "spikes"
1461
5016
  };
1462
5017
  var COOP_DIR = ".coop";
1463
- var DEFAULT_CONFIG = {
5018
+ var DEFAULT_CONFIG3 = {
1464
5019
  spec_version: 1,
1465
5020
  id_prefix: "COOP",
1466
5021
  id_strategy: "text"
@@ -1469,17 +5024,17 @@ function toIdKey(value) {
1469
5024
  return value.trim().toUpperCase();
1470
5025
  }
1471
5026
  function repoRootByPackage(cwd) {
1472
- let current = import_node_path4.default.resolve(cwd);
5027
+ let current = import_node_path10.default.resolve(cwd);
1473
5028
  let lastWorkspaceRoot = null;
1474
5029
  while (true) {
1475
- const packageJson = import_node_path4.default.join(current, "package.json");
1476
- const workspaceYaml = import_node_path4.default.join(current, "pnpm-workspace.yaml");
1477
- if (import_node_fs9.default.existsSync(packageJson) && import_node_fs9.default.existsSync(workspaceYaml)) {
5030
+ const packageJson = import_node_path10.default.join(current, "package.json");
5031
+ const workspaceYaml = import_node_path10.default.join(current, "pnpm-workspace.yaml");
5032
+ if (import_node_fs15.default.existsSync(packageJson) && import_node_fs15.default.existsSync(workspaceYaml)) {
1478
5033
  lastWorkspaceRoot = current;
1479
- const hasCoop = import_node_fs9.default.existsSync(import_node_path4.default.join(current, COOP_DIR, "config.yml"));
5034
+ const hasCoop = import_node_fs15.default.existsSync(import_node_path10.default.join(current, COOP_DIR, "config.yml"));
1480
5035
  if (hasCoop) return current;
1481
5036
  }
1482
- const parent = import_node_path4.default.dirname(current);
5037
+ const parent = import_node_path10.default.dirname(current);
1483
5038
  if (parent === current) return lastWorkspaceRoot;
1484
5039
  current = parent;
1485
5040
  }
@@ -1488,24 +5043,24 @@ function findRepoRoot(cwd = process.cwd()) {
1488
5043
  return repoRootByPackage(cwd);
1489
5044
  }
1490
5045
  function configPathFor(rootDir, workspaceDir) {
1491
- return import_node_path4.default.join(rootDir, workspaceDir, "config.yml");
5046
+ return import_node_path10.default.join(rootDir, workspaceDir, "config.yml");
1492
5047
  }
1493
5048
  function backlogPathFor(rootDir, workspaceDir) {
1494
- return import_node_path4.default.join(rootDir, workspaceDir, "backlog");
5049
+ return import_node_path10.default.join(rootDir, workspaceDir, "backlog");
1495
5050
  }
1496
5051
  function releasesPathFor(rootDir, workspaceDir) {
1497
- return import_node_path4.default.join(rootDir, workspaceDir, "releases");
5052
+ return import_node_path10.default.join(rootDir, workspaceDir, "releases");
1498
5053
  }
1499
5054
  function detectWorkspaceDir(rootDir) {
1500
- if (import_node_fs9.default.existsSync(configPathFor(rootDir, COOP_DIR))) return COOP_DIR;
1501
- if (import_node_fs9.default.existsSync(import_node_path4.default.join(rootDir, COOP_DIR))) return COOP_DIR;
5055
+ if (import_node_fs15.default.existsSync(configPathFor(rootDir, COOP_DIR))) return COOP_DIR;
5056
+ if (import_node_fs15.default.existsSync(import_node_path10.default.join(rootDir, COOP_DIR))) return COOP_DIR;
1502
5057
  return null;
1503
5058
  }
1504
5059
  function preferredWorkspaceDir(rootDir) {
1505
5060
  return detectWorkspaceDir(rootDir) ?? COOP_DIR;
1506
5061
  }
1507
5062
  function missingConfigError(rootDir) {
1508
- const coopConfig = import_node_path4.default.relative(rootDir, configPathFor(rootDir, COOP_DIR));
5063
+ const coopConfig = import_node_path10.default.relative(rootDir, configPathFor(rootDir, COOP_DIR));
1509
5064
  return new Error(`COOP config missing at ${coopConfig}. Run: coop init`);
1510
5065
  }
1511
5066
  function parseConfig(raw) {
@@ -1516,9 +5071,9 @@ function parseConfig(raw) {
1516
5071
  if (ix <= 0) continue;
1517
5072
  map.set(line.slice(0, ix).trim(), line.slice(ix + 1).trim());
1518
5073
  }
1519
- const spec = Number(map.get("spec_version") ?? DEFAULT_CONFIG.spec_version);
1520
- const idPrefix = map.get("id_prefix") ?? DEFAULT_CONFIG.id_prefix;
1521
- const strategy = String(map.get("id_strategy") ?? DEFAULT_CONFIG.id_strategy ?? "text").trim().toLowerCase();
5074
+ const spec = Number(map.get("spec_version") ?? DEFAULT_CONFIG3.spec_version);
5075
+ const idPrefix = map.get("id_prefix") ?? DEFAULT_CONFIG3.id_prefix;
5076
+ const strategy = String(map.get("id_strategy") ?? DEFAULT_CONFIG3.id_strategy ?? "text").trim().toLowerCase();
1522
5077
  const rawNextId = map.get("next_id");
1523
5078
  const nextId = rawNextId != null ? Number(rawNextId) : void 0;
1524
5079
  if (!Number.isInteger(spec) || spec <= 0) throw new Error("config.yml must define a numeric spec_version.");
@@ -1551,10 +5106,10 @@ function configToString(config) {
1551
5106
  return lines.join("\n");
1552
5107
  }
1553
5108
  function toPortablePath(value) {
1554
- return value.split(import_node_path4.default.sep).join("/");
5109
+ return value.split(import_node_path10.default.sep).join("/");
1555
5110
  }
1556
5111
  function ensureReleasesDir(rootDir, workspaceDir) {
1557
- import_node_fs9.default.mkdirSync(releasesPathFor(rootDir, workspaceDir), { recursive: true });
5112
+ import_node_fs15.default.mkdirSync(releasesPathFor(rootDir, workspaceDir), { recursive: true });
1558
5113
  }
1559
5114
  function releaseHeader(date) {
1560
5115
  return `## ${date}`;
@@ -1576,12 +5131,12 @@ function appendReleaseEntry(rootDir, workspaceDir, item, previousStatus, nextSta
1576
5131
  ensureReleasesDir(rootDir, workspaceDir);
1577
5132
  const now = /* @__PURE__ */ new Date();
1578
5133
  const date = now.toISOString().slice(0, 10);
1579
- const releasePath = import_node_path4.default.join(releasesPathFor(rootDir, workspaceDir), `${date}.md`);
5134
+ const releasePath = import_node_path10.default.join(releasesPathFor(rootDir, workspaceDir), `${date}.md`);
1580
5135
  const heading = "# COOP Release Notes";
1581
5136
  const dayHeader = releaseHeader(date);
1582
5137
  const entry = releaseEntryLine(item, previousStatus, nextStatus);
1583
- if (!import_node_fs9.default.existsSync(releasePath)) {
1584
- import_node_fs9.default.writeFileSync(
5138
+ if (!import_node_fs15.default.existsSync(releasePath)) {
5139
+ import_node_fs15.default.writeFileSync(
1585
5140
  releasePath,
1586
5141
  [
1587
5142
  `${heading}
@@ -1594,10 +5149,10 @@ function appendReleaseEntry(rootDir, workspaceDir, item, previousStatus, nextSta
1594
5149
  ].join("\n"),
1595
5150
  "utf8"
1596
5151
  );
1597
- return toPortablePath(import_node_path4.default.relative(rootDir, releasePath));
5152
+ return toPortablePath(import_node_path10.default.relative(rootDir, releasePath));
1598
5153
  }
1599
- const existing = import_node_fs9.default.readFileSync(releasePath, "utf8");
1600
- if (hasReleaseEntry(existing, item.id)) return toPortablePath(import_node_path4.default.relative(rootDir, releasePath));
5154
+ const existing = import_node_fs15.default.readFileSync(releasePath, "utf8");
5155
+ if (hasReleaseEntry(existing, item.id)) return toPortablePath(import_node_path10.default.relative(rootDir, releasePath));
1601
5156
  let nextContent = existing;
1602
5157
  if (!existing.includes(`## ${date}`)) {
1603
5158
  if (!nextContent.endsWith("\n")) nextContent += "\n";
@@ -1607,9 +5162,9 @@ function appendReleaseEntry(rootDir, workspaceDir, item, previousStatus, nextSta
1607
5162
  if (!nextContent.endsWith("\n")) nextContent += "\n";
1608
5163
  nextContent += `${entry}
1609
5164
  `;
1610
- import_node_fs9.default.writeFileSync(releasePath, `${nextContent}
5165
+ import_node_fs15.default.writeFileSync(releasePath, `${nextContent}
1611
5166
  `, "utf8");
1612
- return toPortablePath(import_node_path4.default.relative(rootDir, releasePath));
5167
+ return toPortablePath(import_node_path10.default.relative(rootDir, releasePath));
1613
5168
  }
1614
5169
  function completeItem(rootDir, id) {
1615
5170
  const state = loadState(rootDir);
@@ -1687,28 +5242,28 @@ function validateAndNormalize(data, sourceFile) {
1687
5242
  };
1688
5243
  }
1689
5244
  function parseItem(filePath, rootDir) {
1690
- const raw = import_node_fs9.default.readFileSync(filePath, "utf8");
5245
+ const raw = import_node_fs15.default.readFileSync(filePath, "utf8");
1691
5246
  const parsed = (0, import_gray_matter.default)(raw);
1692
- const data = validateAndNormalize(parsed.data, import_node_path4.default.relative(rootDir, filePath));
5247
+ const data = validateAndNormalize(parsed.data, import_node_path10.default.relative(rootDir, filePath));
1693
5248
  return {
1694
5249
  ...data,
1695
5250
  body: parsed.content || "",
1696
- filePath: import_node_path4.default.relative(rootDir, filePath)
5251
+ filePath: import_node_path10.default.relative(rootDir, filePath)
1697
5252
  };
1698
5253
  }
1699
5254
  function walk(dir) {
1700
5255
  const out = [];
1701
- if (!import_node_fs9.default.existsSync(dir)) return out;
1702
- const entries = import_node_fs9.default.readdirSync(dir, { withFileTypes: true });
5256
+ if (!import_node_fs15.default.existsSync(dir)) return out;
5257
+ const entries = import_node_fs15.default.readdirSync(dir, { withFileTypes: true });
1703
5258
  for (const entry of entries) {
1704
- const file = import_node_path4.default.join(dir, entry.name);
5259
+ const file = import_node_path10.default.join(dir, entry.name);
1705
5260
  if (entry.isDirectory()) out.push(...walk(file));
1706
5261
  if (entry.isFile() && file.endsWith(".md")) out.push(file);
1707
5262
  }
1708
5263
  return out;
1709
5264
  }
1710
5265
  function itemPath(type, id, rootDir, workspaceDir) {
1711
- return import_node_path4.default.join(backlogPathFor(rootDir, workspaceDir), ITEM_DIRS[type], `${id}.md`);
5266
+ return import_node_path10.default.join(backlogPathFor(rootDir, workspaceDir), ITEM_DIRS[type], `${id}.md`);
1712
5267
  }
1713
5268
  function normalizeFrontmatterValue(value) {
1714
5269
  if (value == null) return void 0;
@@ -1795,30 +5350,30 @@ function nextGeneratedId(config, title, existing) {
1795
5350
  }
1796
5351
  function ensureCoopLayout(rootDir) {
1797
5352
  const workspaceDir = preferredWorkspaceDir(rootDir);
1798
- const root = import_node_path4.default.join(rootDir, workspaceDir);
1799
- import_node_fs9.default.mkdirSync(root, { recursive: true });
1800
- import_node_fs9.default.mkdirSync(import_node_path4.default.join(root, "releases"), { recursive: true });
1801
- import_node_fs9.default.mkdirSync(import_node_path4.default.join(root, "plans"), { recursive: true });
1802
- import_node_fs9.default.mkdirSync(import_node_path4.default.join(root, "views"), { recursive: true });
1803
- import_node_fs9.default.mkdirSync(import_node_path4.default.join(root, "templates"), { recursive: true });
5353
+ const root = import_node_path10.default.join(rootDir, workspaceDir);
5354
+ import_node_fs15.default.mkdirSync(root, { recursive: true });
5355
+ import_node_fs15.default.mkdirSync(import_node_path10.default.join(root, "releases"), { recursive: true });
5356
+ import_node_fs15.default.mkdirSync(import_node_path10.default.join(root, "plans"), { recursive: true });
5357
+ import_node_fs15.default.mkdirSync(import_node_path10.default.join(root, "views"), { recursive: true });
5358
+ import_node_fs15.default.mkdirSync(import_node_path10.default.join(root, "templates"), { recursive: true });
1804
5359
  for (const dir of Object.values(ITEM_DIRS)) {
1805
- import_node_fs9.default.mkdirSync(import_node_path4.default.join(root, "backlog", dir), { recursive: true });
5360
+ import_node_fs15.default.mkdirSync(import_node_path10.default.join(root, "backlog", dir), { recursive: true });
1806
5361
  }
1807
- const configFile = import_node_path4.default.join(root, "config.yml");
1808
- if (!import_node_fs9.default.existsSync(configFile)) {
1809
- import_node_fs9.default.writeFileSync(configFile, configToString(DEFAULT_CONFIG), "utf8");
5362
+ const configFile = import_node_path10.default.join(root, "config.yml");
5363
+ if (!import_node_fs15.default.existsSync(configFile)) {
5364
+ import_node_fs15.default.writeFileSync(configFile, configToString(DEFAULT_CONFIG3), "utf8");
1810
5365
  }
1811
5366
  }
1812
5367
  function loadState(rootDir) {
1813
5368
  const workspaceDir = detectWorkspaceDir(rootDir);
1814
5369
  if (!workspaceDir) throw missingConfigError(rootDir);
1815
5370
  const configPath = configPathFor(rootDir, workspaceDir);
1816
- if (!import_node_fs9.default.existsSync(configPath)) throw missingConfigError(rootDir);
1817
- const config = parseConfig(import_node_fs9.default.readFileSync(configPath, "utf8"));
5371
+ if (!import_node_fs15.default.existsSync(configPath)) throw missingConfigError(rootDir);
5372
+ const config = parseConfig(import_node_fs15.default.readFileSync(configPath, "utf8"));
1818
5373
  const items = [];
1819
5374
  const itemsById = /* @__PURE__ */ new Map();
1820
5375
  for (const type of ITEM_TYPES) {
1821
- const dir = import_node_path4.default.join(backlogPathFor(rootDir, workspaceDir), ITEM_DIRS[type]);
5376
+ const dir = import_node_path10.default.join(backlogPathFor(rootDir, workspaceDir), ITEM_DIRS[type]);
1822
5377
  const files = walk(dir);
1823
5378
  for (const file of files) {
1824
5379
  const item = parseItem(file, rootDir);
@@ -1902,21 +5457,21 @@ function createItem(rootDir, params) {
1902
5457
  parent_id: params.parent_id
1903
5458
  };
1904
5459
  const itemPathName = itemPath(params.type, item.id, rootDir, state.workspaceDir);
1905
- import_node_fs9.default.writeFileSync(itemPathName, serialize(item, params.body || ""), "utf8");
5460
+ import_node_fs15.default.writeFileSync(itemPathName, serialize(item, params.body || ""), "utf8");
1906
5461
  if ((config.id_strategy ?? "text") === "counter") {
1907
5462
  const numericMatch = /-(\d+)$/.exec(item.id);
1908
5463
  if (numericMatch) {
1909
5464
  const numericValue = Number(numericMatch[1]);
1910
5465
  if (Number.isInteger(numericValue) && numericValue >= (config.next_id ?? 1)) {
1911
5466
  config.next_id = numericValue + 1;
1912
- import_node_fs9.default.writeFileSync(configPathFor(rootDir, state.workspaceDir), configToString(config), "utf8");
5467
+ import_node_fs15.default.writeFileSync(configPathFor(rootDir, state.workspaceDir), configToString(config), "utf8");
1913
5468
  }
1914
5469
  }
1915
5470
  }
1916
5471
  return {
1917
5472
  ...item,
1918
5473
  body: params.body || "",
1919
- filePath: import_node_path4.default.relative(rootDir, itemPathName)
5474
+ filePath: import_node_path10.default.relative(rootDir, itemPathName)
1920
5475
  };
1921
5476
  }
1922
5477
  function updateItem(rootDir, id, patch) {
@@ -1942,8 +5497,8 @@ function updateItem(rootDir, id, patch) {
1942
5497
  };
1943
5498
  if (!ITEM_TYPES.includes(next.type)) throw new Error(`Unknown type ${next.type}.`);
1944
5499
  if (!ITEM_STATUSES.includes(next.status)) throw new Error(`Unknown status ${next.status}.`);
1945
- const filePath = import_node_path4.default.join(rootDir, existing.filePath);
1946
- import_node_fs9.default.writeFileSync(filePath, serialize(next, patch.body || existing.body), "utf8");
5500
+ const filePath = import_node_path10.default.join(rootDir, existing.filePath);
5501
+ import_node_fs15.default.writeFileSync(filePath, serialize(next, patch.body || existing.body), "utf8");
1947
5502
  return {
1948
5503
  ...next,
1949
5504
  body: patch.body || existing.body,
@@ -1959,7 +5514,7 @@ function deleteItem(rootDir, id) {
1959
5514
  if (children.length > 0) {
1960
5515
  throw new Error(`Cannot delete ${existing.id} because it has ${children.length} child item(s). Remove children first.`);
1961
5516
  }
1962
- import_node_fs9.default.unlinkSync(import_node_path4.default.join(rootDir, existing.filePath));
5517
+ import_node_fs15.default.unlinkSync(import_node_path10.default.join(rootDir, existing.filePath));
1963
5518
  return existing;
1964
5519
  }
1965
5520
  function renderAgentPrompt(item) {
@@ -1998,8 +5553,8 @@ function validateRepo(rootDir) {
1998
5553
  const errors = [];
1999
5554
  const warnings = [];
2000
5555
  const workspaceDir = detectWorkspaceDir(rootDir);
2001
- if (!workspaceDir || !import_node_fs9.default.existsSync(configPathFor(rootDir, workspaceDir))) {
2002
- errors.push("Missing .coop/config.yml. Run coop init first.");
5556
+ if (!workspaceDir || !import_node_fs15.default.existsSync(configPathFor(rootDir, workspaceDir))) {
5557
+ errors.push("Missing COOP config. Run coop init first.");
2003
5558
  return { valid: false, errors, warnings };
2004
5559
  }
2005
5560
  let state;
@@ -2032,13 +5587,20 @@ function validateRepo(rootDir) {
2032
5587
  // Annotate the CommonJS export names for ESM import in node:
2033
5588
  0 && (module.exports = {
2034
5589
  ArtifactType,
5590
+ COOP_EVENT_TYPES,
2035
5591
  CURRENT_SCHEMA_VERSION,
5592
+ CoopEventEmitter,
5593
+ DEFAULT_SCORE_WEIGHTS,
2036
5594
  DeliveryStatus,
2037
5595
  ExecutorType,
2038
5596
  ITEM_STATUSES,
2039
5597
  ITEM_TYPES,
2040
5598
  IdeaStatus,
5599
+ IndexManager,
5600
+ MIGRATIONS,
2041
5601
  RiskLevel,
5602
+ RunStatus,
5603
+ RunStepStatus,
2042
5604
  RunbookAction,
2043
5605
  TaskComplexity,
2044
5606
  TaskDeterminism,
@@ -2047,23 +5609,63 @@ function validateRepo(rootDir) {
2047
5609
  TaskType,
2048
5610
  VALID_TASK_TRANSITIONS,
2049
5611
  VALID_TRANSITIONS,
5612
+ allocate,
5613
+ allocate_ai,
5614
+ allocate_ai_tokens,
5615
+ analyze_feasibility,
5616
+ analyze_what_if,
5617
+ build_capacity_ledger,
2050
5618
  build_graph,
2051
5619
  check_blocked,
5620
+ check_permission,
2052
5621
  check_unblocked,
5622
+ check_wip,
2053
5623
  completeItem,
5624
+ complexity_penalty,
2054
5625
  compute_all_readiness,
5626
+ compute_critical_path,
2055
5627
  compute_readiness,
2056
5628
  compute_readiness_with_corrections,
5629
+ compute_score,
5630
+ compute_velocity,
5631
+ coop_project_config_path,
5632
+ coop_project_root,
5633
+ coop_projects_dir,
5634
+ coop_workspace_config_path,
5635
+ coop_workspace_dir,
2057
5636
  createItem,
5637
+ create_seeded_rng,
5638
+ critical_path_weight,
2058
5639
  deleteItem,
5640
+ dependency_unlock_weight,
2059
5641
  detect_cycle,
5642
+ detect_delivery_risks,
5643
+ determinism_weight,
5644
+ effective_weekly_hours,
5645
+ effort_or_default,
2060
5646
  ensureCoopLayout,
5647
+ ensure_workspace_layout,
5648
+ executor_fit_weight,
5649
+ external_dependencies_for_task,
2061
5650
  extract_subgraph,
2062
5651
  findRepoRoot,
2063
5652
  find_external_dependencies,
2064
5653
  getItemById,
5654
+ get_remaining_tokens,
5655
+ get_user_role,
5656
+ has_legacy_project_layout,
5657
+ has_v2_projects_layout,
5658
+ is_external_dependency,
5659
+ is_project_initialized,
5660
+ list_projects,
2065
5661
  loadState,
5662
+ load_auth_config,
5663
+ load_completed_runs,
2066
5664
  load_graph,
5665
+ load_plugins,
5666
+ migrate_repository,
5667
+ migrate_task,
5668
+ monte_carlo_forecast,
2067
5669
  parseDeliveryContent,
2068
5670
  parseDeliveryFile,
2069
5671
  parseFrontmatterContent,
@@ -2074,16 +5676,38 @@ function validateRepo(rootDir) {
2074
5676
  parseTaskFile,
2075
5677
  parseYamlContent,
2076
5678
  parseYamlFile,
5679
+ parse_external_dependency,
2077
5680
  partition_by_readiness,
5681
+ pert_hours,
5682
+ pert_stddev,
5683
+ priority_weight,
2078
5684
  queryItems,
5685
+ read_project_config,
2079
5686
  read_schema_version,
5687
+ read_workspace_config,
2080
5688
  renderAgentPrompt,
5689
+ repo_default_project_id,
5690
+ repo_default_project_name,
5691
+ resolve_external_dependencies,
5692
+ resolve_project,
5693
+ risk_penalty,
5694
+ run_hook,
5695
+ run_monte_carlo_chunk,
5696
+ run_plugins_for_event,
5697
+ sample_pert_beta,
5698
+ sample_task_hours,
5699
+ schedule_next,
5700
+ simulate_schedule,
2081
5701
  stringifyFrontmatter,
5702
+ stringifyYamlContent,
5703
+ task_effort_hours,
2082
5704
  topological_sort,
2083
5705
  transition,
2084
5706
  transitive_dependencies,
2085
5707
  transitive_dependents,
5708
+ type_weight,
2086
5709
  updateItem,
5710
+ urgency_weight,
2087
5711
  validate,
2088
5712
  validateReferential,
2089
5713
  validateRepo,
@@ -2093,5 +5717,7 @@ function validateRepo(rootDir) {
2093
5717
  validate_graph,
2094
5718
  validate_transition,
2095
5719
  writeTask,
2096
- write_schema_version
5720
+ writeYamlFile,
5721
+ write_schema_version,
5722
+ write_workspace_config
2097
5723
  });