@kitsy/coop 1.1.0 → 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.
Files changed (3) hide show
  1. package/README.md +11 -6
  2. package/dist/index.js +732 -188
  3. package/package.json +4 -4
package/dist/index.js CHANGED
@@ -1,15 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import fs13 from "fs";
5
- import path18 from "path";
4
+ import fs15 from "fs";
5
+ import path19 from "path";
6
6
  import { fileURLToPath as fileURLToPath2 } from "url";
7
7
  import { Command } from "commander";
8
8
 
9
9
  // src/utils/aliases.ts
10
10
  import fs2 from "fs";
11
11
  import path2 from "path";
12
- import { parseIdeaFile, parseTaskFile as parseTaskFile2, parseYamlFile as parseYamlFile2, stringifyFrontmatter, writeTask, writeYamlFile as writeYamlFile2 } from "@kitsy/coop-core";
12
+ import { parseIdeaFile, parseTaskFile as parseTaskFile2, parseYamlFile, stringifyFrontmatter, writeTask, writeYamlFile as writeYamlFile2 } from "@kitsy/coop-core";
13
13
 
14
14
  // src/utils/repo.ts
15
15
  import fs from "fs";
@@ -17,7 +17,17 @@ import os from "os";
17
17
  import path from "path";
18
18
  import { spawnSync } from "child_process";
19
19
  import crypto from "crypto";
20
- import { parseTaskFile, parseYamlFile, writeYamlFile } from "@kitsy/coop-core";
20
+ import {
21
+ coop_project_config_path,
22
+ coop_workspace_config_path,
23
+ list_projects as listWorkspaceProjects,
24
+ parseTaskFile,
25
+ read_project_config,
26
+ read_workspace_config,
27
+ resolve_project,
28
+ write_workspace_config,
29
+ writeYamlFile
30
+ } from "@kitsy/coop-core";
21
31
  var SEQ_MARKER = "COOPSEQTOKEN";
22
32
  function resolveCoopHome() {
23
33
  const configured = process.env.COOP_HOME?.trim();
@@ -39,21 +49,54 @@ function resolveRepoRoot(start = process.cwd()) {
39
49
  current = parent;
40
50
  }
41
51
  }
42
- function coopDir(root) {
52
+ function resolveRequestedProject(argv = process.argv) {
53
+ for (let i = 0; i < argv.length; i += 1) {
54
+ const current = argv[i]?.trim();
55
+ if (current === "--project" || current === "-p") {
56
+ const next = argv[i + 1]?.trim();
57
+ return next || void 0;
58
+ }
59
+ if (current?.startsWith("--project=")) {
60
+ return current.slice("--project=".length).trim() || void 0;
61
+ }
62
+ }
63
+ return void 0;
64
+ }
65
+ function coopWorkspaceDir(root) {
43
66
  return path.join(root, ".coop");
44
67
  }
45
- function ensureCoopInitialized(root) {
46
- const dir = coopDir(root);
47
- if (!fs.existsSync(dir)) {
48
- throw new Error(`Missing .coop directory at ${dir}. Run 'coop init'.`);
68
+ function coopDir(root, projectId = resolveRequestedProject()) {
69
+ return resolve_project(root, { project: projectId, require: true }).root;
70
+ }
71
+ function ensureCoopInitialized(root, projectId = resolveRequestedProject()) {
72
+ const workspaceDir = coopWorkspaceDir(root);
73
+ if (!fs.existsSync(workspaceDir)) {
74
+ throw new Error(`Missing .coop directory at ${workspaceDir}. Run 'coop init'.`);
75
+ }
76
+ const project = resolve_project(root, { project: projectId, require: true });
77
+ if (!fs.existsSync(project.root)) {
78
+ throw new Error(`Missing COOP project directory at ${project.root}. Run 'coop init'.`);
49
79
  }
50
- return dir;
80
+ return project.root;
51
81
  }
52
- function coopConfigPath(root) {
53
- return path.join(coopDir(root), "config.yml");
82
+ function coopConfigPath(root, projectId = resolveRequestedProject()) {
83
+ return coop_project_config_path(ensureCoopInitialized(root, projectId));
54
84
  }
55
- function readCoopConfig(root) {
56
- const configPath = coopConfigPath(root);
85
+ function readWorkspaceConfig(root) {
86
+ return read_workspace_config(root);
87
+ }
88
+ function writeWorkspaceConfig(root, config) {
89
+ write_workspace_config(root, config);
90
+ }
91
+ function listProjects(root) {
92
+ return listWorkspaceProjects(root);
93
+ }
94
+ function resolveProject(root, projectId = resolveRequestedProject()) {
95
+ return resolve_project(root, { project: projectId, require: true });
96
+ }
97
+ function readCoopConfig(root, projectId = resolveRequestedProject()) {
98
+ const project = resolve_project(root, { project: projectId, require: false });
99
+ const configPath = coop_project_config_path(project.root);
57
100
  const repoName = path.basename(path.resolve(root));
58
101
  if (!fs.existsSync(configPath)) {
59
102
  return {
@@ -69,10 +112,10 @@ function readCoopConfig(root) {
69
112
  filePath: configPath
70
113
  };
71
114
  }
72
- const config = parseYamlFile(configPath);
115
+ const config = read_project_config(project.root);
73
116
  const projectRaw = typeof config.project === "object" && config.project !== null ? config.project : {};
74
117
  const projectName = typeof projectRaw.name === "string" && projectRaw.name.trim().length > 0 ? projectRaw.name.trim() : repoName;
75
- const projectId = typeof projectRaw.id === "string" && projectRaw.id.trim().length > 0 ? projectRaw.id.trim() : repoName;
118
+ const resolvedProjectId = typeof projectRaw.id === "string" && projectRaw.id.trim().length > 0 ? projectRaw.id.trim() : repoName;
76
119
  const projectAliases = Array.isArray(projectRaw.aliases) ? projectRaw.aliases.filter((entry) => typeof entry === "string" && entry.trim().length > 0) : [];
77
120
  const idPrefixesRaw = config.id_prefixes;
78
121
  const idPrefixes = typeof idPrefixesRaw === "object" && idPrefixesRaw !== null ? idPrefixesRaw : {};
@@ -93,7 +136,7 @@ function readCoopConfig(root) {
93
136
  idNamingTemplate,
94
137
  idSeqPadding,
95
138
  projectName: projectName || "COOP Workspace",
96
- projectId: projectId || "workspace",
139
+ projectId: resolvedProjectId || "workspace",
97
140
  projectAliases,
98
141
  raw: config,
99
142
  filePath: configPath
@@ -158,15 +201,15 @@ function findTaskFileById(root, id) {
158
201
  const target = `${id}.md`.toLowerCase();
159
202
  const match = listTaskFiles(root).find((filePath) => path.basename(filePath).toLowerCase() === target);
160
203
  if (!match) {
161
- throw new Error(`Task '${id}' not found in .coop/tasks.`);
204
+ throw new Error(`Task '${id}' not found in the active COOP project.`);
162
205
  }
163
206
  return match;
164
207
  }
165
208
  function todayIsoDate() {
166
209
  return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
167
210
  }
168
- function normalizeIdPart(input, fallback, maxLength = 12) {
169
- const normalized = input.toUpperCase().replace(/[^A-Z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-").replace(/-/g, "");
211
+ function normalizeIdPart(input3, fallback, maxLength = 12) {
212
+ const normalized = input3.toUpperCase().replace(/[^A-Z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-").replace(/-/g, "");
170
213
  if (!normalized) return fallback;
171
214
  return normalized.slice(0, maxLength);
172
215
  }
@@ -202,8 +245,8 @@ function shortDateToken(now = /* @__PURE__ */ new Date()) {
202
245
  function randomToken() {
203
246
  return crypto.randomBytes(4).toString("hex").toUpperCase();
204
247
  }
205
- function sanitizeTemplateValue(input, fallback = "X") {
206
- const normalized = input.toUpperCase().replace(/[^A-Z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-");
248
+ function sanitizeTemplateValue(input3, fallback = "X") {
249
+ const normalized = input3.toUpperCase().replace(/[^A-Z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-");
207
250
  return normalized || fallback;
208
251
  }
209
252
  function sequenceForPattern(existingIds, prefix, suffix) {
@@ -311,27 +354,27 @@ function generateConfiguredId(root, existingIds, context) {
311
354
 
312
355
  // src/utils/aliases.ts
313
356
  var ALIAS_PATTERN = /^[A-Z0-9]+(?:[.-][A-Z0-9]+)*$/;
314
- function toPosixPath(input) {
315
- return input.replace(/\\/g, "/");
357
+ function toPosixPath(input3) {
358
+ return input3.replace(/\\/g, "/");
316
359
  }
317
360
  function indexFilePath(root) {
318
361
  const { indexDataFormat } = readCoopConfig(root);
319
362
  const extension = indexDataFormat === "json" ? "json" : "yml";
320
- return path2.join(root, ".coop", ".index", `aliases.${extension}`);
363
+ return path2.join(ensureCoopInitialized(root), ".index", `aliases.${extension}`);
321
364
  }
322
- function normalizeAliasValue(input) {
323
- return input.trim().toUpperCase().replace(/_/g, ".").replace(/\.+/g, ".");
365
+ function normalizeAliasValue(input3) {
366
+ return input3.trim().toUpperCase().replace(/_/g, ".").replace(/\.+/g, ".");
324
367
  }
325
- function normalizePatternValue(input) {
326
- return input.trim().toUpperCase().replace(/_/g, ".");
368
+ function normalizePatternValue(input3) {
369
+ return input3.trim().toUpperCase().replace(/_/g, ".");
327
370
  }
328
- function normalizeAlias(input) {
329
- const normalized = normalizeAliasValue(input);
371
+ function normalizeAlias(input3) {
372
+ const normalized = normalizeAliasValue(input3);
330
373
  if (!normalized) {
331
374
  throw new Error("Alias cannot be empty.");
332
375
  }
333
376
  if (!ALIAS_PATTERN.test(normalized)) {
334
- throw new Error(`Invalid alias '${input}'. Use letters/numbers with '.' and '-'.`);
377
+ throw new Error(`Invalid alias '${input3}'. Use letters/numbers with '.' and '-'.`);
335
378
  }
336
379
  return normalized;
337
380
  }
@@ -383,7 +426,7 @@ function readIndexFile(root, aliasIndexPath) {
383
426
  const parsed = JSON.parse(fs2.readFileSync(aliasIndexPath, "utf8"));
384
427
  return parsed;
385
428
  }
386
- return parseYamlFile2(aliasIndexPath);
429
+ return parseYamlFile(aliasIndexPath);
387
430
  } catch {
388
431
  return null;
389
432
  }
@@ -394,14 +437,14 @@ function writeIndexFile(root, data) {
394
437
  if (aliasIndexPath.endsWith(".json")) {
395
438
  fs2.writeFileSync(aliasIndexPath, `${JSON.stringify(data, null, 2)}
396
439
  `, "utf8");
397
- const yamlPath = path2.join(root, ".coop", ".index", "aliases.yml");
440
+ const yamlPath = path2.join(ensureCoopInitialized(root), ".index", "aliases.yml");
398
441
  if (fs2.existsSync(yamlPath)) {
399
442
  fs2.rmSync(yamlPath, { force: true });
400
443
  }
401
444
  return;
402
445
  }
403
446
  writeYamlFile2(aliasIndexPath, data);
404
- const jsonPath = path2.join(root, ".coop", ".index", "aliases.json");
447
+ const jsonPath = path2.join(ensureCoopInitialized(root), ".index", "aliases.json");
405
448
  if (fs2.existsSync(jsonPath)) {
406
449
  fs2.rmSync(jsonPath, { force: true });
407
450
  }
@@ -518,8 +561,8 @@ function updateTaskAliases(filePath, aliases) {
518
561
  function updateIdeaAliases(filePath, aliases) {
519
562
  const parsed = parseIdeaFile(filePath);
520
563
  const nextRaw = { ...parsed.raw, aliases };
521
- const output = stringifyFrontmatter(nextRaw, parsed.body);
522
- fs2.writeFileSync(filePath, output, "utf8");
564
+ const output3 = stringifyFrontmatter(nextRaw, parsed.body);
565
+ fs2.writeFileSync(filePath, output3, "utf8");
523
566
  }
524
567
  function resolveFilePath(root, target) {
525
568
  return path2.join(root, ...target.file.split("/"));
@@ -1061,14 +1104,14 @@ function updateIdeaLinkedTasks(filePath, idea, raw, body, linked) {
1061
1104
  };
1062
1105
  fs3.writeFileSync(filePath, stringifyFrontmatter2(nextRaw, body), "utf8");
1063
1106
  }
1064
- function makeTaskDraft(input) {
1107
+ function makeTaskDraft(input3) {
1065
1108
  return {
1066
- title: input.title,
1067
- type: input.type,
1068
- status: input.status,
1069
- track: input.track,
1070
- priority: input.priority,
1071
- body: input.body
1109
+ title: input3.title,
1110
+ type: input3.type,
1111
+ status: input3.status,
1112
+ track: input3.track,
1113
+ priority: input3.priority,
1114
+ body: input3.body
1072
1115
  };
1073
1116
  }
1074
1117
  function registerCreateCommand(program) {
@@ -1419,10 +1462,10 @@ import {
1419
1462
  function normalize(value) {
1420
1463
  return value.trim().toLowerCase();
1421
1464
  }
1422
- function resolveDelivery(graph, input) {
1423
- const direct = graph.deliveries.get(input);
1465
+ function resolveDelivery(graph, input3) {
1466
+ const direct = graph.deliveries.get(input3);
1424
1467
  if (direct) return direct;
1425
- const target = normalize(input);
1468
+ const target = normalize(input3);
1426
1469
  const byId = Array.from(graph.deliveries.values()).find((delivery) => normalize(delivery.id) === target);
1427
1470
  if (byId) return byId;
1428
1471
  const byName = Array.from(graph.deliveries.values()).filter((delivery) => normalize(delivery.name) === target);
@@ -1430,9 +1473,9 @@ function resolveDelivery(graph, input) {
1430
1473
  return byName[0];
1431
1474
  }
1432
1475
  if (byName.length > 1) {
1433
- throw new Error(`Multiple deliveries match '${input}'. Use delivery id instead.`);
1476
+ throw new Error(`Multiple deliveries match '${input3}'. Use delivery id instead.`);
1434
1477
  }
1435
- throw new Error(`Delivery '${input}' not found.`);
1478
+ throw new Error(`Delivery '${input3}' not found.`);
1436
1479
  }
1437
1480
 
1438
1481
  // src/commands/graph.ts
@@ -1623,6 +1666,8 @@ function registerIndexCommand(program) {
1623
1666
  import fs6 from "fs";
1624
1667
  import path8 from "path";
1625
1668
  import { spawnSync as spawnSync3 } from "child_process";
1669
+ import { createInterface } from "readline/promises";
1670
+ import { stdin as input, stdout as output } from "process";
1626
1671
  import { CURRENT_SCHEMA_VERSION, write_schema_version } from "@kitsy/coop-core";
1627
1672
 
1628
1673
  // src/hooks/pre-commit.ts
@@ -1650,7 +1695,41 @@ function toPosixPath2(filePath) {
1650
1695
  }
1651
1696
  function stagedTaskFiles(repoRoot) {
1652
1697
  const { stdout } = runGit(repoRoot, ["diff", "--cached", "--name-only", "--diff-filter=ACMR"]);
1653
- return stdout.split(/\r?\n/).map((entry) => entry.trim()).filter(Boolean).map(toPosixPath2).filter((entry) => entry.startsWith(".coop/tasks/") && entry.endsWith(".md"));
1698
+ return stdout.split(/\r?\n/).map((entry) => entry.trim()).filter(Boolean).map(toPosixPath2).filter(
1699
+ (entry) => (entry.startsWith(".coop/tasks/") || /^\.coop\/projects\/[^/]+\/tasks\//.test(entry)) && entry.endsWith(".md")
1700
+ );
1701
+ }
1702
+ function projectRootFromRelativePath(repoRoot, relativePath) {
1703
+ const normalized = toPosixPath2(relativePath);
1704
+ const projectMatch = /^\.coop\/projects\/([^/]+)\/tasks\/.+\.md$/i.exec(normalized);
1705
+ if (projectMatch?.[1]) {
1706
+ return path6.join(repoRoot, ".coop", "projects", projectMatch[1]);
1707
+ }
1708
+ if (normalized.startsWith(".coop/tasks/")) {
1709
+ return path6.join(repoRoot, ".coop");
1710
+ }
1711
+ throw new Error(`Unsupported staged COOP task path '${relativePath}'.`);
1712
+ }
1713
+ function listTaskFilesForProject(projectRoot) {
1714
+ const tasksDir = path6.join(projectRoot, "tasks");
1715
+ if (!fs4.existsSync(tasksDir)) return [];
1716
+ const out = [];
1717
+ const stack = [tasksDir];
1718
+ while (stack.length > 0) {
1719
+ const current = stack.pop();
1720
+ const entries = fs4.readdirSync(current, { withFileTypes: true });
1721
+ for (const entry of entries) {
1722
+ const fullPath = path6.join(current, entry.name);
1723
+ if (entry.isDirectory()) {
1724
+ stack.push(fullPath);
1725
+ continue;
1726
+ }
1727
+ if (entry.isFile() && path6.extname(entry.name).toLowerCase() === ".md") {
1728
+ out.push(fullPath);
1729
+ }
1730
+ }
1731
+ }
1732
+ return out.sort((a, b) => a.localeCompare(b));
1654
1733
  }
1655
1734
  function readGitBlob(repoRoot, spec) {
1656
1735
  const result = runGit(repoRoot, ["show", spec], true);
@@ -1665,6 +1744,7 @@ function parseStagedTasks(repoRoot, relativePaths) {
1665
1744
  const staged = [];
1666
1745
  for (const relativePath of relativePaths) {
1667
1746
  const absolutePath = path6.join(repoRoot, ...relativePath.split("/"));
1747
+ const projectRoot = projectRootFromRelativePath(repoRoot, relativePath);
1668
1748
  const stagedBlob = readGitBlob(repoRoot, `:${relativePath}`);
1669
1749
  if (!stagedBlob) {
1670
1750
  errors.push(`[COOP] Unable to read staged task '${relativePath}' from git index.`);
@@ -1685,6 +1765,7 @@ function parseStagedTasks(repoRoot, relativePaths) {
1685
1765
  staged.push({
1686
1766
  relativePath,
1687
1767
  absolutePath,
1768
+ projectRoot,
1688
1769
  task
1689
1770
  });
1690
1771
  }
@@ -1719,13 +1800,13 @@ function buildGraphForCycleCheck(tasks) {
1719
1800
  deliveries: /* @__PURE__ */ new Map()
1720
1801
  };
1721
1802
  }
1722
- function collectTasksForCycleCheck(repoRoot, stagedTasks) {
1803
+ function collectTasksForCycleCheck(projectRoot, stagedTasks) {
1723
1804
  const stagedByPath = /* @__PURE__ */ new Map();
1724
1805
  for (const staged of stagedTasks) {
1725
1806
  stagedByPath.set(toPosixPath2(staged.absolutePath), staged.task);
1726
1807
  }
1727
1808
  const tasks = [];
1728
- for (const filePath of listTaskFiles(repoRoot)) {
1809
+ for (const filePath of listTaskFilesForProject(projectRoot)) {
1729
1810
  const normalized = toPosixPath2(path6.resolve(filePath));
1730
1811
  const stagedTask = stagedByPath.get(normalized);
1731
1812
  if (stagedTask) {
@@ -1744,16 +1825,25 @@ function runPreCommitChecks(repoRoot) {
1744
1825
  const parsed = parseStagedTasks(repoRoot, relativeTaskFiles);
1745
1826
  const errors = [...parsed.errors];
1746
1827
  if (parsed.staged.length > 0 && errors.length === 0) {
1747
- try {
1748
- const tasks = collectTasksForCycleCheck(repoRoot, parsed.staged);
1749
- const graph = buildGraphForCycleCheck(tasks);
1750
- const cycle = detect_cycle(graph);
1751
- if (cycle) {
1752
- errors.push(`[COOP] Dependency cycle detected: ${cycle.join(" -> ")}.`);
1828
+ const stagedByProject = /* @__PURE__ */ new Map();
1829
+ for (const staged of parsed.staged) {
1830
+ const existing = stagedByProject.get(staged.projectRoot) ?? [];
1831
+ existing.push(staged);
1832
+ stagedByProject.set(staged.projectRoot, existing);
1833
+ }
1834
+ for (const [projectRoot, stagedTasks] of stagedByProject.entries()) {
1835
+ try {
1836
+ const tasks = collectTasksForCycleCheck(projectRoot, stagedTasks);
1837
+ const graph = buildGraphForCycleCheck(tasks);
1838
+ const cycle = detect_cycle(graph);
1839
+ if (cycle) {
1840
+ const projectLabel = toPosixPath2(path6.relative(repoRoot, projectRoot));
1841
+ errors.push(`[COOP] Dependency cycle detected in ${projectLabel}: ${cycle.join(" -> ")}.`);
1842
+ }
1843
+ } catch (error) {
1844
+ const message = error instanceof Error ? error.message : String(error);
1845
+ errors.push(`[COOP] Failed to run dependency cycle check: ${message}`);
1753
1846
  }
1754
- } catch (error) {
1755
- const message = error instanceof Error ? error.message : String(error);
1756
- errors.push(`[COOP] Failed to run dependency cycle check: ${message}`);
1757
1847
  }
1758
1848
  }
1759
1849
  return {
@@ -1813,28 +1903,37 @@ function installPreCommitHook(repoRoot) {
1813
1903
  // src/hooks/post-merge-validate.ts
1814
1904
  import fs5 from "fs";
1815
1905
  import path7 from "path";
1906
+ import { list_projects } from "@kitsy/coop-core";
1816
1907
  import { load_graph as load_graph3, validate_graph as validate_graph2 } from "@kitsy/coop-core";
1817
1908
  var HOOK_BLOCK_START2 = "# COOP_POST_MERGE_START";
1818
1909
  var HOOK_BLOCK_END2 = "# COOP_POST_MERGE_END";
1819
1910
  function runPostMergeValidate(repoRoot) {
1820
- const coopDir2 = path7.join(repoRoot, ".coop");
1821
- if (!fs5.existsSync(coopDir2)) {
1911
+ const workspaceDir = path7.join(repoRoot, ".coop");
1912
+ if (!fs5.existsSync(workspaceDir)) {
1822
1913
  return {
1823
1914
  ok: true,
1824
1915
  warnings: ["[COOP] Skipped post-merge validation (.coop not found)."]
1825
1916
  };
1826
1917
  }
1827
1918
  try {
1828
- const graph = load_graph3(coopDir2);
1829
- const issues = validate_graph2(graph);
1830
- if (issues.length === 0) {
1831
- return { ok: true, warnings: [] };
1919
+ const projects = list_projects(repoRoot);
1920
+ if (projects.length === 0) {
1921
+ return {
1922
+ ok: true,
1923
+ warnings: ["[COOP] Skipped post-merge validation (no COOP projects found)."]
1924
+ };
1925
+ }
1926
+ const warnings = [];
1927
+ for (const project of projects) {
1928
+ const graph = load_graph3(project.root);
1929
+ const issues = validate_graph2(graph);
1930
+ for (const issue of issues) {
1931
+ warnings.push(`[COOP] post-merge warning [${project.id}] [${issue.invariant}] ${issue.message}`);
1932
+ }
1832
1933
  }
1833
1934
  return {
1834
1935
  ok: true,
1835
- warnings: issues.map(
1836
- (issue) => `[COOP] post-merge warning [${issue.invariant}] ${issue.message}`
1837
- )
1936
+ warnings
1838
1937
  };
1839
1938
  } catch (error) {
1840
1939
  return {
@@ -1893,14 +1992,32 @@ function installPostMergeHook(repoRoot) {
1893
1992
  }
1894
1993
 
1895
1994
  // src/commands/init.ts
1896
- function buildDefaultConfig(root) {
1897
- const repoName = repoDisplayName(root);
1898
- const repoId = repoIdentityId(root);
1995
+ function normalizeProjectId(value) {
1996
+ return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-");
1997
+ }
1998
+ function parseAliases2(value) {
1999
+ if (!value) return [];
2000
+ return Array.from(
2001
+ new Set(
2002
+ value.split(",").map((entry) => entry.trim()).filter(Boolean).map(
2003
+ (entry) => entry.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-")
2004
+ ).filter(Boolean)
2005
+ )
2006
+ );
2007
+ }
2008
+ function aliasesYaml(aliases) {
2009
+ if (aliases.length === 0) {
2010
+ return " []";
2011
+ }
2012
+ return `
2013
+ ${aliases.map((alias) => ` - "${alias}"`).join("\n")}`;
2014
+ }
2015
+ function buildProjectConfig(projectId, projectName, projectAliases) {
1899
2016
  return `version: 2
1900
2017
  project:
1901
- name: "${repoName}"
1902
- id: "${repoId}"
1903
- aliases: []
2018
+ name: ${JSON.stringify(projectName)}
2019
+ id: "${projectId}"
2020
+ aliases:${aliasesYaml(projectAliases)}
1904
2021
 
1905
2022
  id_prefixes:
1906
2023
  idea: "IDEA"
@@ -1950,8 +2067,8 @@ api:
1950
2067
  remotes: {}
1951
2068
 
1952
2069
  hooks:
1953
- on_task_transition: .coop/hooks/on-task-transition.sh
1954
- on_delivery_complete: .coop/hooks/on-delivery-complete.sh
2070
+ on_task_transition: .coop/projects/${projectId}/hooks/on-task-transition.sh
2071
+ on_delivery_complete: .coop/projects/${projectId}/hooks/on-delivery-complete.sh
1955
2072
 
1956
2073
  index:
1957
2074
  data: yaml
@@ -2077,8 +2194,8 @@ function setGitConfig(root, key, value) {
2077
2194
  }
2078
2195
  function installMergeDrivers(root) {
2079
2196
  const entries = [
2080
- ".coop/tasks/*.md merge=coop-task",
2081
- ".coop/deliveries/*.yml merge=coop-delivery"
2197
+ ".coop/projects/*/tasks/*.md merge=coop-task",
2198
+ ".coop/projects/*/deliveries/*.yml merge=coop-delivery"
2082
2199
  ];
2083
2200
  for (const entry of entries) {
2084
2201
  ensureGitattributesEntry(root, entry);
@@ -2094,10 +2211,58 @@ function installMergeDrivers(root) {
2094
2211
  }
2095
2212
  return "Updated .gitattributes but could not register merge drivers in git config.";
2096
2213
  }
2214
+ async function promptInitIdentity(root, options) {
2215
+ const rl = createInterface({ input, output });
2216
+ const ask2 = async (question, fallback) => {
2217
+ try {
2218
+ const answer = await rl.question(`${question} [${fallback}]: `);
2219
+ return answer.trim() || fallback;
2220
+ } catch {
2221
+ return fallback;
2222
+ }
2223
+ };
2224
+ const defaultName = repoDisplayName(root);
2225
+ try {
2226
+ const initialName = options.name?.trim() || defaultName;
2227
+ const projectName = options.name?.trim() || await ask2("Project name", initialName);
2228
+ const defaultId = options.id?.trim() || normalizeProjectId(projectName) || repoIdentityId(root);
2229
+ const projectIdRaw = options.id?.trim() || await ask2("Project id", defaultId);
2230
+ const projectId = normalizeProjectId(projectIdRaw);
2231
+ if (!projectId) {
2232
+ throw new Error("Invalid project id. Use letters, numbers, and hyphens.");
2233
+ }
2234
+ const aliasesInput = options.aliases !== void 0 ? options.aliases : await ask2("Aliases (csv, optional)", "");
2235
+ return {
2236
+ projectName,
2237
+ projectId,
2238
+ projectAliases: parseAliases2(aliasesInput)
2239
+ };
2240
+ } finally {
2241
+ rl.close();
2242
+ }
2243
+ }
2244
+ async function resolveInitIdentity(root, options) {
2245
+ const defaultName = repoDisplayName(root);
2246
+ const fallbackName = options.name?.trim() || defaultName;
2247
+ const fallbackId = normalizeProjectId(options.id?.trim() || fallbackName) || repoIdentityId(root);
2248
+ const fallbackAliases = parseAliases2(options.aliases);
2249
+ const interactive = Boolean(options.interactive || !options.yes && input.isTTY && output.isTTY);
2250
+ if (interactive) {
2251
+ return promptInitIdentity(root, options);
2252
+ }
2253
+ return {
2254
+ projectName: fallbackName,
2255
+ projectId: fallbackId,
2256
+ projectAliases: fallbackAliases
2257
+ };
2258
+ }
2097
2259
  function registerInitCommand(program) {
2098
- program.command("init").description("Initialize .coop/ structure in the current repository").action(() => {
2260
+ program.command("init").description("Initialize .coop/ structure in the current repository").option("--name <name>", "Project display name").option("--id <id>", "Project id").option("--aliases <csv>", "Comma-separated project aliases").option("-y, --yes", "Use defaults without prompting").option("--interactive", "Prompt for missing project identity values").action(async (options) => {
2099
2261
  const root = resolveRepoRoot();
2100
- const coop = coopDir(root);
2262
+ const workspaceDir = coopWorkspaceDir(root);
2263
+ const identity = await resolveInitIdentity(root, options);
2264
+ const projectId = identity.projectId;
2265
+ const projectRoot = path8.join(workspaceDir, "projects", projectId);
2101
2266
  const dirs = [
2102
2267
  "ideas",
2103
2268
  "tasks",
@@ -2113,25 +2278,36 @@ function registerInitCommand(program) {
2113
2278
  "history/deliveries",
2114
2279
  ".index"
2115
2280
  ];
2281
+ ensureDir(path8.join(workspaceDir, "projects"));
2116
2282
  for (const dir of dirs) {
2117
- ensureDir(path8.join(coop, dir));
2118
- }
2119
- writeIfMissing(path8.join(coop, "config.yml"), buildDefaultConfig(root));
2120
- if (!fs6.existsSync(path8.join(coop, "schema-version"))) {
2121
- write_schema_version(coop, CURRENT_SCHEMA_VERSION);
2122
- }
2123
- writeIfMissing(path8.join(coop, "templates/task.md"), TASK_TEMPLATE);
2124
- writeIfMissing(path8.join(coop, "templates/idea.md"), IDEA_TEMPLATE);
2125
- writeIfMissing(path8.join(coop, "plugins/console-log.yml"), PLUGIN_CONSOLE_TEMPLATE);
2126
- writeIfMissing(path8.join(coop, "plugins/github-pr.yml"), PLUGIN_GITHUB_TEMPLATE);
2127
- writeIfMissing(path8.join(coop, ".ignore"), COOP_IGNORE_TEMPLATE);
2128
- writeIfMissing(path8.join(coop, ".gitignore"), COOP_IGNORE_TEMPLATE);
2129
- ensureGitignoreEntry(root, ".coop/.index/");
2283
+ ensureDir(path8.join(projectRoot, dir));
2284
+ }
2285
+ writeIfMissing(
2286
+ path8.join(projectRoot, "config.yml"),
2287
+ buildProjectConfig(projectId, identity.projectName, identity.projectAliases)
2288
+ );
2289
+ if (!fs6.existsSync(path8.join(projectRoot, "schema-version"))) {
2290
+ write_schema_version(projectRoot, CURRENT_SCHEMA_VERSION);
2291
+ }
2292
+ writeIfMissing(path8.join(projectRoot, "templates/task.md"), TASK_TEMPLATE);
2293
+ writeIfMissing(path8.join(projectRoot, "templates/idea.md"), IDEA_TEMPLATE);
2294
+ writeIfMissing(path8.join(projectRoot, "plugins/console-log.yml"), PLUGIN_CONSOLE_TEMPLATE);
2295
+ writeIfMissing(path8.join(projectRoot, "plugins/github-pr.yml"), PLUGIN_GITHUB_TEMPLATE);
2296
+ writeIfMissing(path8.join(workspaceDir, ".ignore"), COOP_IGNORE_TEMPLATE);
2297
+ writeIfMissing(path8.join(workspaceDir, ".gitignore"), COOP_IGNORE_TEMPLATE);
2298
+ writeWorkspaceConfig(root, { version: 2, current_project: projectId });
2299
+ ensureGitignoreEntry(root, ".coop/logs/");
2300
+ ensureGitignoreEntry(root, ".coop/tmp/");
2130
2301
  const preCommitHook = installPreCommitHook(root);
2131
2302
  const postMergeHook = installPostMergeHook(root);
2132
2303
  const mergeDrivers = installMergeDrivers(root);
2304
+ const project = resolveProject(root, projectId);
2133
2305
  console.log("Initialized COOP workspace.");
2134
2306
  console.log(`- Root: ${root}`);
2307
+ console.log(`- Workspace: ${path8.relative(root, workspaceDir)}`);
2308
+ console.log(`- Project: ${project.id} (${path8.relative(root, project.root)})`);
2309
+ console.log(`- Name: ${identity.projectName}`);
2310
+ console.log(`- Aliases: ${identity.projectAliases.length > 0 ? identity.projectAliases.join(", ") : "(none)"}`);
2135
2311
  console.log(`- ${preCommitHook.message}`);
2136
2312
  if (preCommitHook.installed) {
2137
2313
  console.log(`- Hook: ${path8.relative(root, preCommitHook.hookPath)}`);
@@ -2290,9 +2466,9 @@ function resolveRepoSafe(start = process.cwd()) {
2290
2466
  }
2291
2467
  function resolveCliLogFile(start = process.cwd()) {
2292
2468
  const root = resolveRepoSafe(start);
2293
- const coop = coopDir(root);
2294
- if (fs7.existsSync(path10.join(coop, "config.yml"))) {
2295
- return path10.join(coop, "logs", "cli.log");
2469
+ const workspace = coopWorkspaceDir(root);
2470
+ if (fs7.existsSync(workspace)) {
2471
+ return path10.join(workspace, "logs", "cli.log");
2296
2472
  }
2297
2473
  return path10.join(resolveCoopHome(), "logs", "cli.log");
2298
2474
  }
@@ -2373,8 +2549,11 @@ function registerLogCommand(program) {
2373
2549
  }
2374
2550
 
2375
2551
  // src/commands/migrate.ts
2552
+ import fs8 from "fs";
2376
2553
  import path11 from "path";
2377
- import { CURRENT_SCHEMA_VERSION as CURRENT_SCHEMA_VERSION2, migrate_repository } from "@kitsy/coop-core";
2554
+ import { createInterface as createInterface2 } from "readline/promises";
2555
+ import { stdin as input2, stdout as output2 } from "process";
2556
+ import { CURRENT_SCHEMA_VERSION as CURRENT_SCHEMA_VERSION2, IndexManager as IndexManager2, migrate_repository, parseYamlFile as parseYamlFile2, writeYamlFile as writeYamlFile4 } from "@kitsy/coop-core";
2378
2557
  function parseTargetVersion(raw) {
2379
2558
  if (!raw) return CURRENT_SCHEMA_VERSION2;
2380
2559
  const parsed = Number(raw);
@@ -2383,8 +2562,160 @@ function parseTargetVersion(raw) {
2383
2562
  }
2384
2563
  return parsed;
2385
2564
  }
2565
+ function legacyWorkspaceProjectEntries(root) {
2566
+ const workspaceDir = coopWorkspaceDir(root);
2567
+ return [
2568
+ "config.yml",
2569
+ "schema-version",
2570
+ "ideas",
2571
+ "tasks",
2572
+ "tracks",
2573
+ "deliveries",
2574
+ "resources",
2575
+ "templates",
2576
+ "plugins",
2577
+ "hooks",
2578
+ "runs",
2579
+ "decisions",
2580
+ "history",
2581
+ ".index",
2582
+ "views",
2583
+ "backlog",
2584
+ "plans",
2585
+ "releases"
2586
+ ].filter((entry) => fs8.existsSync(path11.join(workspaceDir, entry)));
2587
+ }
2588
+ function normalizeProjectId2(value) {
2589
+ return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-");
2590
+ }
2591
+ function parseAliases3(value) {
2592
+ if (!value) return [];
2593
+ return Array.from(
2594
+ new Set(
2595
+ value.split(",").map((entry) => entry.trim()).filter(Boolean).map(
2596
+ (entry) => entry.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-")
2597
+ ).filter(Boolean)
2598
+ )
2599
+ );
2600
+ }
2601
+ async function promptProjectIdentity(defaults, options) {
2602
+ const rl = createInterface2({ input: input2, output: output2 });
2603
+ const ask2 = async (question, fallback) => {
2604
+ try {
2605
+ const answer = await rl.question(`${question} [${fallback}]: `);
2606
+ return answer.trim() || fallback;
2607
+ } catch {
2608
+ return fallback;
2609
+ }
2610
+ };
2611
+ try {
2612
+ const projectName = options.name?.trim() || await ask2("Project name", defaults.projectName);
2613
+ const projectIdRaw = options.id?.trim() || await ask2("Project id", defaults.projectId);
2614
+ const projectId = normalizeProjectId2(projectIdRaw);
2615
+ if (!projectId) {
2616
+ throw new Error("Invalid project id. Use letters, numbers, and hyphens.");
2617
+ }
2618
+ const aliasesInput = options.aliases !== void 0 ? options.aliases : await ask2("Aliases (csv, optional)", defaults.projectAliases.join(","));
2619
+ return {
2620
+ projectName,
2621
+ projectId,
2622
+ projectAliases: parseAliases3(aliasesInput)
2623
+ };
2624
+ } finally {
2625
+ rl.close();
2626
+ }
2627
+ }
2628
+ async function resolveMigrationIdentity(root, options) {
2629
+ const existing = readCoopConfig(root);
2630
+ const defaults = {
2631
+ projectName: existing.projectName || path11.basename(root),
2632
+ projectId: normalizeProjectId2(existing.projectId || repoIdentityId(root)) || repoIdentityId(root),
2633
+ projectAliases: options.aliases !== void 0 ? parseAliases3(options.aliases) : existing.projectAliases
2634
+ };
2635
+ const interactive = Boolean(options.interactive || !options.yes && input2.isTTY && output2.isTTY);
2636
+ if (interactive) {
2637
+ return promptProjectIdentity(defaults, options);
2638
+ }
2639
+ return {
2640
+ projectName: options.name?.trim() || defaults.projectName,
2641
+ projectId: normalizeProjectId2(options.id?.trim() || defaults.projectId) || defaults.projectId,
2642
+ projectAliases: options.aliases !== void 0 ? parseAliases3(options.aliases) : defaults.projectAliases
2643
+ };
2644
+ }
2645
+ async function migrateWorkspaceLayout(root, options) {
2646
+ const target = (options.to ?? "").trim().toLowerCase();
2647
+ if (target !== "v2") {
2648
+ throw new Error(`Unsupported workspace-layout target '${options.to ?? ""}'. Expected 'v2'.`);
2649
+ }
2650
+ const workspaceDir = coopWorkspaceDir(root);
2651
+ if (!fs8.existsSync(workspaceDir)) {
2652
+ throw new Error("Missing .coop directory. Run 'coop init' first.");
2653
+ }
2654
+ const projectsDir = path11.join(workspaceDir, "projects");
2655
+ const legacyEntries = legacyWorkspaceProjectEntries(root);
2656
+ if (legacyEntries.length === 0 && fs8.existsSync(projectsDir)) {
2657
+ console.log("[COOP] workspace layout already uses v2.");
2658
+ return;
2659
+ }
2660
+ const identity = await resolveMigrationIdentity(root, options);
2661
+ const projectId = identity.projectId;
2662
+ const projectRoot = path11.join(projectsDir, projectId);
2663
+ if (fs8.existsSync(projectRoot) && !options.force) {
2664
+ throw new Error(`Project destination '${path11.relative(root, projectRoot)}' already exists. Re-run with --force.`);
2665
+ }
2666
+ const changed = legacyEntries.map((entry) => `${path11.join(".coop", entry)} -> ${path11.join(".coop", "projects", projectId, entry)}`);
2667
+ changed.push(`.coop/config.yml -> workspace current_project=${projectId}`);
2668
+ console.log(`Workspace layout migration (${options.dryRun ? "DRY RUN" : "APPLY"})`);
2669
+ console.log(`- from: v1 flat layout`);
2670
+ console.log(`- to: v2 multi-project layout`);
2671
+ console.log(`- project: ${projectId}`);
2672
+ console.log(`- name: ${identity.projectName}`);
2673
+ console.log(`- aliases: ${identity.projectAliases.length > 0 ? identity.projectAliases.join(", ") : "(none)"}`);
2674
+ console.log(`- entries: ${legacyEntries.length}`);
2675
+ for (const entry of changed) {
2676
+ console.log(` - ${entry}`);
2677
+ }
2678
+ if (options.dryRun) {
2679
+ console.log("- no files were modified.");
2680
+ return;
2681
+ }
2682
+ fs8.mkdirSync(projectsDir, { recursive: true });
2683
+ fs8.mkdirSync(projectRoot, { recursive: true });
2684
+ for (const entry of legacyEntries) {
2685
+ const source = path11.join(workspaceDir, entry);
2686
+ const destination = path11.join(projectRoot, entry);
2687
+ if (fs8.existsSync(destination)) {
2688
+ if (!options.force) {
2689
+ throw new Error(`Destination '${path11.relative(root, destination)}' already exists.`);
2690
+ }
2691
+ fs8.rmSync(destination, { recursive: true, force: true });
2692
+ }
2693
+ fs8.renameSync(source, destination);
2694
+ }
2695
+ const movedConfigPath = path11.join(projectRoot, "config.yml");
2696
+ if (fs8.existsSync(movedConfigPath)) {
2697
+ const movedConfig = parseYamlFile2(movedConfigPath);
2698
+ const nextProject = typeof movedConfig.project === "object" && movedConfig.project !== null ? { ...movedConfig.project } : {};
2699
+ nextProject.name = identity.projectName;
2700
+ nextProject.id = projectId;
2701
+ nextProject.aliases = identity.projectAliases;
2702
+ const nextHooks = typeof movedConfig.hooks === "object" && movedConfig.hooks !== null ? { ...movedConfig.hooks } : {};
2703
+ nextHooks.on_task_transition = `.coop/projects/${projectId}/hooks/on-task-transition.sh`;
2704
+ nextHooks.on_delivery_complete = `.coop/projects/${projectId}/hooks/on-delivery-complete.sh`;
2705
+ writeYamlFile4(movedConfigPath, {
2706
+ ...movedConfig,
2707
+ project: nextProject,
2708
+ hooks: nextHooks
2709
+ });
2710
+ }
2711
+ const workspace = readWorkspaceConfig(root);
2712
+ writeWorkspaceConfig(root, { ...workspace, version: 2, current_project: projectId });
2713
+ const manager = new IndexManager2(projectRoot);
2714
+ manager.build_full_index();
2715
+ console.log(`[COOP] migrated workspace to v2 at ${path11.relative(root, projectRoot)}`);
2716
+ }
2386
2717
  function registerMigrateCommand(program) {
2387
- program.command("migrate").description("Migrate .coop data to a schema version").option("--dry-run", "Preview migration without writing files").option("--to <version>", "Target schema version", String(CURRENT_SCHEMA_VERSION2)).action((options) => {
2718
+ const migrate = program.command("migrate").description("Migrate COOP data and workspace layouts").option("--dry-run", "Preview migration without writing files").option("--to <version>", "Target schema version", String(CURRENT_SCHEMA_VERSION2)).action((options) => {
2388
2719
  const root = resolveRepoRoot();
2389
2720
  ensureCoopInitialized(root);
2390
2721
  const target = parseTargetVersion(options.to);
@@ -2409,6 +2740,15 @@ function registerMigrateCommand(program) {
2409
2740
  console.log(`- schema-version updated to v${report.to_version}`);
2410
2741
  }
2411
2742
  });
2743
+ migrate.command("workspace-layout").description("Migrate a flat v1 workspace layout to the v2 multi-project layout").option("--dry-run", "Preview migration without writing files").option("--to <version>", "Target workspace layout version", "v2").option("--force", "Overwrite an existing destination project directory").option("--name <name>", "Project display name").option("--id <id>", "Project id").option("--aliases <csv>", "Comma-separated project aliases").option("-y, --yes", "Use existing project identity defaults without prompting").option("--interactive", "Prompt for project identity during migration").action(async (options) => {
2744
+ const root = resolveRepoRoot();
2745
+ const parentOptions = migrate.opts();
2746
+ await migrateWorkspaceLayout(root, {
2747
+ ...parentOptions,
2748
+ ...options,
2749
+ dryRun: Boolean(options.dryRun || parentOptions.dryRun)
2750
+ });
2751
+ });
2412
2752
  }
2413
2753
 
2414
2754
  // src/commands/plan.ts
@@ -2644,9 +2984,206 @@ function registerPlanCommand(program) {
2644
2984
  });
2645
2985
  }
2646
2986
 
2647
- // src/commands/run.ts
2648
- import fs8 from "fs";
2987
+ // src/commands/project.ts
2988
+ import fs9 from "fs";
2649
2989
  import path12 from "path";
2990
+ import { CURRENT_SCHEMA_VERSION as CURRENT_SCHEMA_VERSION3, write_schema_version as write_schema_version2 } from "@kitsy/coop-core";
2991
+ var TASK_TEMPLATE2 = `---
2992
+ id: TASK-001
2993
+ title: "Implement feature"
2994
+ type: feature
2995
+ status: todo
2996
+ created: 2026-03-06
2997
+ updated: 2026-03-06
2998
+ aliases: []
2999
+ priority: p2
3000
+ track: unassigned
3001
+ depends_on: []
3002
+ ---
3003
+
3004
+ ## Context
3005
+ Why this task exists.
3006
+ `;
3007
+ var IDEA_TEMPLATE2 = `---
3008
+ id: IDEA-001
3009
+ title: "New idea"
3010
+ created: 2026-03-06
3011
+ aliases: []
3012
+ author: your-name
3013
+ status: captured
3014
+ tags: []
3015
+ source: manual
3016
+ linked_tasks: []
3017
+ ---
3018
+
3019
+ ## Problem
3020
+ What problem are you solving?
3021
+ `;
3022
+ var PROJECT_CONFIG_TEMPLATE = (projectId, projectName) => `version: 2
3023
+ project:
3024
+ name: "${projectName}"
3025
+ id: "${projectId}"
3026
+ aliases: []
3027
+
3028
+ id_prefixes:
3029
+ idea: "IDEA"
3030
+ task: "PM"
3031
+ delivery: "DEL"
3032
+ run: "RUN"
3033
+
3034
+ id:
3035
+ naming: "<TYPE>-<USER>-<YYMMDD>-<RAND>"
3036
+ seq_padding: 0
3037
+
3038
+ defaults:
3039
+ task:
3040
+ type: feature
3041
+ priority: p2
3042
+ complexity: medium
3043
+ determinism: medium
3044
+ track: unassigned
3045
+
3046
+ scheduling:
3047
+ algorithm: critical-path
3048
+ velocity_window_weeks: 4
3049
+ overhead_factor: 1.2
3050
+
3051
+ ai:
3052
+ provider: mock
3053
+ model: mock-local
3054
+ default_executor: claude-sonnet
3055
+ sandbox: true
3056
+ max_concurrent_agents: 4
3057
+ token_budget_per_task: 50000
3058
+
3059
+ plugins:
3060
+ enabled: []
3061
+
3062
+ github:
3063
+ owner: ""
3064
+ repo: ""
3065
+ base_branch: main
3066
+ token_env: GITHUB_TOKEN
3067
+ webhook_secret_env: GITHUB_WEBHOOK_SECRET
3068
+ merge_method: squash
3069
+
3070
+ api:
3071
+ host: 127.0.0.1
3072
+ port: 3847
3073
+ remotes: {}
3074
+
3075
+ hooks:
3076
+ on_task_transition: .coop/projects/${projectId}/hooks/on-task-transition.sh
3077
+ on_delivery_complete: .coop/projects/${projectId}/hooks/on-delivery-complete.sh
3078
+
3079
+ index:
3080
+ data: yaml
3081
+ `;
3082
+ var PROJECT_DIRS = [
3083
+ "ideas",
3084
+ "tasks",
3085
+ "tracks",
3086
+ "deliveries",
3087
+ "resources",
3088
+ "templates",
3089
+ "plugins",
3090
+ "hooks",
3091
+ "runs",
3092
+ "decisions",
3093
+ "history/tasks",
3094
+ "history/deliveries",
3095
+ ".index"
3096
+ ];
3097
+ function ensureDir2(dirPath) {
3098
+ fs9.mkdirSync(dirPath, { recursive: true });
3099
+ }
3100
+ function writeIfMissing2(filePath, content) {
3101
+ if (!fs9.existsSync(filePath)) {
3102
+ fs9.writeFileSync(filePath, content, "utf8");
3103
+ }
3104
+ }
3105
+ function normalizeProjectId3(value) {
3106
+ return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-");
3107
+ }
3108
+ function createProject(root, projectId, projectName) {
3109
+ const workspaceDir = coopWorkspaceDir(root);
3110
+ const projectRoot = path12.join(workspaceDir, "projects", projectId);
3111
+ ensureDir2(path12.join(workspaceDir, "projects"));
3112
+ for (const dir of PROJECT_DIRS) {
3113
+ ensureDir2(path12.join(projectRoot, dir));
3114
+ }
3115
+ writeIfMissing2(path12.join(projectRoot, "config.yml"), PROJECT_CONFIG_TEMPLATE(projectId, projectName));
3116
+ if (!fs9.existsSync(path12.join(projectRoot, "schema-version"))) {
3117
+ write_schema_version2(projectRoot, CURRENT_SCHEMA_VERSION3);
3118
+ }
3119
+ writeIfMissing2(path12.join(projectRoot, "templates/task.md"), TASK_TEMPLATE2);
3120
+ writeIfMissing2(path12.join(projectRoot, "templates/idea.md"), IDEA_TEMPLATE2);
3121
+ return projectRoot;
3122
+ }
3123
+ function registerProjectCommand(program) {
3124
+ const project = program.command("project").description("Manage COOP projects");
3125
+ project.command("list").description("List COOP projects in the current workspace").action(() => {
3126
+ const root = resolveRepoRoot();
3127
+ const projects = listProjects(root);
3128
+ const workspace = readWorkspaceConfig(root);
3129
+ if (projects.length === 0) {
3130
+ console.log("No COOP projects found.");
3131
+ return;
3132
+ }
3133
+ for (const item of projects) {
3134
+ const current = workspace.current_project === item.id ? " *" : "";
3135
+ console.log(`${item.id}${current} ${item.name}`);
3136
+ }
3137
+ });
3138
+ project.command("show").description("Show the active or selected COOP project").argument("[id]", "Optional project id").action((id) => {
3139
+ const root = resolveRepoRoot();
3140
+ const projects = listProjects(root);
3141
+ const workspace = readWorkspaceConfig(root);
3142
+ const active = (id ? projects.find((item) => item.id === id) : void 0) ?? projects.find((item) => item.id === workspace.current_project) ?? projects[0];
3143
+ if (!active) {
3144
+ throw new Error("No COOP projects found.");
3145
+ }
3146
+ console.log(`id=${active.id}`);
3147
+ console.log(`name=${active.name}`);
3148
+ console.log(`path=${path12.relative(root, active.root)}`);
3149
+ console.log(`layout=${active.layout}`);
3150
+ });
3151
+ project.command("use").description("Set the active COOP project").argument("<id>", "Project id").action((id) => {
3152
+ const root = resolveRepoRoot();
3153
+ const projects = listProjects(root);
3154
+ const match = projects.find((item) => item.id === id);
3155
+ if (!match) {
3156
+ throw new Error(`Project '${id}' not found.`);
3157
+ }
3158
+ const workspace = readWorkspaceConfig(root);
3159
+ writeWorkspaceConfig(root, { ...workspace, version: 2, current_project: match.id });
3160
+ console.log(`current_project=${match.id}`);
3161
+ });
3162
+ project.command("create").description("Create a new COOP project in the current workspace").argument("<id>", "Project id").option("--name <name>", "Project display name").action((id, options) => {
3163
+ const root = resolveRepoRoot();
3164
+ const projectId = normalizeProjectId3(id);
3165
+ if (!projectId) {
3166
+ throw new Error("Invalid project id. Use letters, numbers, and hyphens.");
3167
+ }
3168
+ const existing = listProjects(root).find((item) => item.id === projectId);
3169
+ if (existing) {
3170
+ throw new Error(`Project '${projectId}' already exists.`);
3171
+ }
3172
+ const projectName = options.name?.trim() || repoDisplayName(root);
3173
+ const projectRoot = createProject(root, projectId, projectName);
3174
+ const workspace = readWorkspaceConfig(root);
3175
+ writeWorkspaceConfig(root, {
3176
+ ...workspace,
3177
+ version: 2,
3178
+ current_project: workspace.current_project || projectId
3179
+ });
3180
+ console.log(`Created project '${projectId}' at ${path12.relative(root, projectRoot)}`);
3181
+ });
3182
+ }
3183
+
3184
+ // src/commands/run.ts
3185
+ import fs10 from "fs";
3186
+ import path13 from "path";
2650
3187
  import { load_graph as load_graph5, parseTaskFile as parseTaskFile7 } from "@kitsy/coop-core";
2651
3188
  import {
2652
3189
  build_contract,
@@ -2656,8 +3193,8 @@ import {
2656
3193
  } from "@kitsy/coop-ai";
2657
3194
  function loadTask(root, idOrAlias) {
2658
3195
  const target = resolveReference(root, idOrAlias, "task");
2659
- const taskFile = path12.join(root, ...target.file.split("/"));
2660
- if (!fs8.existsSync(taskFile)) {
3196
+ const taskFile = path13.join(root, ...target.file.split("/"));
3197
+ if (!fs10.existsSync(taskFile)) {
2661
3198
  throw new Error(`Task file not found: ${target.file}`);
2662
3199
  }
2663
3200
  return parseTaskFile7(taskFile).task;
@@ -2696,22 +3233,22 @@ function registerRunCommand(program) {
2696
3233
  on_progress: (message) => console.log(`[COOP] ${message}`)
2697
3234
  });
2698
3235
  if (result.status === "failed") {
2699
- throw new Error(`Run failed: ${result.run.id}. Log: ${path12.relative(root, result.log_path)}`);
3236
+ throw new Error(`Run failed: ${result.run.id}. Log: ${path13.relative(root, result.log_path)}`);
2700
3237
  }
2701
3238
  if (result.status === "paused") {
2702
3239
  console.log(`[COOP] run paused: ${result.run.id}`);
2703
- console.log(`[COOP] log: ${path12.relative(root, result.log_path)}`);
3240
+ console.log(`[COOP] log: ${path13.relative(root, result.log_path)}`);
2704
3241
  return;
2705
3242
  }
2706
3243
  console.log(`[COOP] run completed: ${result.run.id}`);
2707
- console.log(`[COOP] log: ${path12.relative(root, result.log_path)}`);
3244
+ console.log(`[COOP] log: ${path13.relative(root, result.log_path)}`);
2708
3245
  });
2709
3246
  }
2710
3247
 
2711
3248
  // src/server/api.ts
2712
- import fs9 from "fs";
3249
+ import fs11 from "fs";
2713
3250
  import http from "http";
2714
- import path13 from "path";
3251
+ import path14 from "path";
2715
3252
  import {
2716
3253
  analyze_feasibility as analyze_feasibility2,
2717
3254
  load_graph as load_graph6,
@@ -2757,12 +3294,12 @@ function taskSummary(graph, task, external = []) {
2757
3294
  };
2758
3295
  }
2759
3296
  function taskFileById(root, id) {
2760
- const tasksDir = path13.join(root, ".coop", "tasks");
2761
- if (!fs9.existsSync(tasksDir)) return null;
2762
- const entries = fs9.readdirSync(tasksDir, { withFileTypes: true });
3297
+ const tasksDir = path14.join(resolveProject(root).root, "tasks");
3298
+ if (!fs11.existsSync(tasksDir)) return null;
3299
+ const entries = fs11.readdirSync(tasksDir, { withFileTypes: true });
2763
3300
  const target = `${id}.md`.toLowerCase();
2764
3301
  const match = entries.find((entry) => entry.isFile() && entry.name.toLowerCase() === target);
2765
- return match ? path13.join(tasksDir, match.name) : null;
3302
+ return match ? path14.join(tasksDir, match.name) : null;
2766
3303
  }
2767
3304
  function loadRemoteConfig(root) {
2768
3305
  const raw = readCoopConfig(root).raw;
@@ -2826,7 +3363,8 @@ function createApiServer(root, options = {}) {
2826
3363
  json(res, 200, workspaceMeta(repoRoot));
2827
3364
  return;
2828
3365
  }
2829
- const coopPath = coopDir(repoRoot);
3366
+ const project = resolveProject(repoRoot);
3367
+ const coopPath = project.root;
2830
3368
  const graph = load_graph6(coopPath);
2831
3369
  const resolutions = await externalResolutions(repoRoot, graph, options);
2832
3370
  if (pathname === "/api/tasks") {
@@ -2851,7 +3389,7 @@ function createApiServer(root, options = {}) {
2851
3389
  created: task.created,
2852
3390
  updated: task.updated,
2853
3391
  body: parsed.body,
2854
- file_path: path13.relative(repoRoot, filePath).replace(/\\/g, "/")
3392
+ file_path: path14.relative(repoRoot, filePath).replace(/\\/g, "/")
2855
3393
  }
2856
3394
  });
2857
3395
  return;
@@ -2939,8 +3477,8 @@ function registerServeCommand(program) {
2939
3477
  }
2940
3478
 
2941
3479
  // src/commands/show.ts
2942
- import fs10 from "fs";
2943
- import path14 from "path";
3480
+ import fs12 from "fs";
3481
+ import path15 from "path";
2944
3482
  import { parseIdeaFile as parseIdeaFile4, parseTaskFile as parseTaskFile9 } from "@kitsy/coop-core";
2945
3483
  function stringify(value) {
2946
3484
  if (value === null || value === void 0) return "-";
@@ -2949,13 +3487,13 @@ function stringify(value) {
2949
3487
  return String(value);
2950
3488
  }
2951
3489
  function loadComputedFromIndex(root, taskId) {
2952
- const indexPath = path14.join(root, ".coop", ".index", "tasks.json");
2953
- if (!fs10.existsSync(indexPath)) {
3490
+ const indexPath = path15.join(ensureCoopInitialized(root), ".index", "tasks.json");
3491
+ if (!fs12.existsSync(indexPath)) {
2954
3492
  return null;
2955
3493
  }
2956
3494
  let parsed;
2957
3495
  try {
2958
- parsed = JSON.parse(fs10.readFileSync(indexPath, "utf8"));
3496
+ parsed = JSON.parse(fs12.readFileSync(indexPath, "utf8"));
2959
3497
  } catch {
2960
3498
  return null;
2961
3499
  }
@@ -2994,7 +3532,7 @@ function showTask(taskId) {
2994
3532
  const root = resolveRepoRoot();
2995
3533
  const coop = ensureCoopInitialized(root);
2996
3534
  const target = resolveReference(root, taskId, "task");
2997
- const taskFile = path14.join(root, ...target.file.split("/"));
3535
+ const taskFile = path15.join(root, ...target.file.split("/"));
2998
3536
  const parsed = parseTaskFile9(taskFile);
2999
3537
  const task = parsed.task;
3000
3538
  const body = parsed.body.trim();
@@ -3013,7 +3551,7 @@ function showTask(taskId) {
3013
3551
  `Tags: ${stringify(task.tags)}`,
3014
3552
  `Created: ${task.created}`,
3015
3553
  `Updated: ${task.updated}`,
3016
- `File: ${path14.relative(root, taskFile)}`,
3554
+ `File: ${path15.relative(root, taskFile)}`,
3017
3555
  "",
3018
3556
  "Body:",
3019
3557
  body || "-",
@@ -3021,7 +3559,7 @@ function showTask(taskId) {
3021
3559
  "Computed:"
3022
3560
  ];
3023
3561
  if (!computed) {
3024
- lines.push(`index not built (${path14.relative(root, path14.join(coop, ".index", "tasks.json"))} missing)`);
3562
+ lines.push(`index not built (${path15.relative(root, path15.join(coop, ".index", "tasks.json"))} missing)`);
3025
3563
  } else {
3026
3564
  for (const [key, value] of Object.entries(computed)) {
3027
3565
  lines.push(`- ${key}: ${stringify(value)}`);
@@ -3033,7 +3571,7 @@ function showIdea(ideaId) {
3033
3571
  const root = resolveRepoRoot();
3034
3572
  ensureCoopInitialized(root);
3035
3573
  const target = resolveReference(root, ideaId, "idea");
3036
- const ideaFile = path14.join(root, ...target.file.split("/"));
3574
+ const ideaFile = path15.join(root, ...target.file.split("/"));
3037
3575
  const parsed = parseIdeaFile4(ideaFile);
3038
3576
  const idea = parsed.idea;
3039
3577
  const body = parsed.body.trim();
@@ -3047,7 +3585,7 @@ function showIdea(ideaId) {
3047
3585
  `Tags: ${stringify(idea.tags)}`,
3048
3586
  `Linked Tasks: ${stringify(idea.linked_tasks)}`,
3049
3587
  `Created: ${idea.created}`,
3050
- `File: ${path14.relative(root, ideaFile)}`,
3588
+ `File: ${path15.relative(root, ideaFile)}`,
3051
3589
  "",
3052
3590
  "Body:",
3053
3591
  body || "-"
@@ -3154,7 +3692,7 @@ function registerStatusCommand(program) {
3154
3692
  }
3155
3693
 
3156
3694
  // src/commands/transition.ts
3157
- import path15 from "path";
3695
+ import path16 from "path";
3158
3696
  import {
3159
3697
  TaskStatus as TaskStatus3,
3160
3698
  check_permission as check_permission3,
@@ -3183,8 +3721,8 @@ import { Octokit } from "octokit";
3183
3721
  function isObject(value) {
3184
3722
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
3185
3723
  }
3186
- function sanitizeBranchPart(input, fallback) {
3187
- const normalized = input.trim().toLowerCase().replace(/[^a-z0-9/_-]+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
3724
+ function sanitizeBranchPart(input3, fallback) {
3725
+ const normalized = input3.trim().toLowerCase().replace(/[^a-z0-9/_-]+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
3188
3726
  return normalized || fallback;
3189
3727
  }
3190
3728
  function readGitRemote(root) {
@@ -3317,15 +3855,15 @@ function toGitHubClient(config) {
3317
3855
  baseUrl: config.apiBaseUrl
3318
3856
  });
3319
3857
  return {
3320
- async createPullRequest(input) {
3858
+ async createPullRequest(input3) {
3321
3859
  const response = await octokit.rest.pulls.create({
3322
- owner: input.owner,
3323
- repo: input.repo,
3324
- title: input.title,
3325
- body: input.body,
3326
- head: input.head,
3327
- base: input.base,
3328
- draft: input.draft
3860
+ owner: input3.owner,
3861
+ repo: input3.repo,
3862
+ title: input3.title,
3863
+ body: input3.body,
3864
+ head: input3.head,
3865
+ base: input3.base,
3866
+ draft: input3.draft
3329
3867
  });
3330
3868
  return {
3331
3869
  number: response.data.number,
@@ -3335,14 +3873,14 @@ function toGitHubClient(config) {
3335
3873
  draft: response.data.draft
3336
3874
  };
3337
3875
  },
3338
- async updatePullRequest(input) {
3876
+ async updatePullRequest(input3) {
3339
3877
  const response = await octokit.rest.pulls.update({
3340
- owner: input.owner,
3341
- repo: input.repo,
3342
- pull_number: input.pull_number,
3343
- title: input.title,
3344
- body: input.body,
3345
- base: input.base
3878
+ owner: input3.owner,
3879
+ repo: input3.repo,
3880
+ pull_number: input3.pull_number,
3881
+ title: input3.title,
3882
+ body: input3.body,
3883
+ base: input3.base
3346
3884
  });
3347
3885
  return {
3348
3886
  number: response.data.number,
@@ -3352,11 +3890,11 @@ function toGitHubClient(config) {
3352
3890
  draft: response.data.draft
3353
3891
  };
3354
3892
  },
3355
- async getPullRequest(input) {
3893
+ async getPullRequest(input3) {
3356
3894
  const response = await octokit.rest.pulls.get({
3357
- owner: input.owner,
3358
- repo: input.repo,
3359
- pull_number: input.pull_number
3895
+ owner: input3.owner,
3896
+ repo: input3.repo,
3897
+ pull_number: input3.pull_number
3360
3898
  });
3361
3899
  return {
3362
3900
  number: response.data.number,
@@ -3366,12 +3904,12 @@ function toGitHubClient(config) {
3366
3904
  draft: response.data.draft
3367
3905
  };
3368
3906
  },
3369
- async mergePullRequest(input) {
3907
+ async mergePullRequest(input3) {
3370
3908
  const response = await octokit.rest.pulls.merge({
3371
- owner: input.owner,
3372
- repo: input.repo,
3373
- pull_number: input.pull_number,
3374
- merge_method: input.merge_method
3909
+ owner: input3.owner,
3910
+ repo: input3.repo,
3911
+ pull_number: input3.pull_number,
3912
+ merge_method: input3.merge_method
3375
3913
  });
3376
3914
  return {
3377
3915
  merged: response.data.merged,
@@ -3387,7 +3925,9 @@ function readGitHubConfig(root) {
3387
3925
  const owner = typeof githubRaw.owner === "string" && githubRaw.owner.trim() ? githubRaw.owner.trim() : inferred?.owner;
3388
3926
  const repo = typeof githubRaw.repo === "string" && githubRaw.repo.trim() ? githubRaw.repo.trim() : inferred?.repo;
3389
3927
  if (!owner || !repo) {
3390
- throw new Error("GitHub integration requires github.owner and github.repo in .coop/config.yml or a GitHub origin remote.");
3928
+ throw new Error(
3929
+ "GitHub integration requires github.owner and github.repo in the active COOP project config or a GitHub origin remote."
3930
+ );
3391
3931
  }
3392
3932
  const tokenEnv = typeof githubRaw.token_env === "string" && githubRaw.token_env.trim() ? githubRaw.token_env.trim() : "GITHUB_TOKEN";
3393
3933
  const token = process.env[tokenEnv];
@@ -3748,7 +4288,7 @@ function registerTransitionCommand(program) {
3748
4288
  }
3749
4289
  console.warn(`[COOP][auth] override: user '${user}' forced transition_task on '${reference.id}'.`);
3750
4290
  }
3751
- const filePath = path15.join(root, ...reference.file.split("/"));
4291
+ const filePath = path16.join(root, ...reference.file.split("/"));
3752
4292
  const parsed = parseTaskFile11(filePath);
3753
4293
  const result = transition2(parsed.task, target, {
3754
4294
  actor: options.actor ?? user,
@@ -3794,19 +4334,19 @@ ${errors}`);
3794
4334
  }
3795
4335
 
3796
4336
  // src/commands/ui.ts
3797
- import fs11 from "fs";
3798
- import path16 from "path";
4337
+ import fs13 from "fs";
4338
+ import path17 from "path";
3799
4339
  import { createRequire } from "module";
3800
4340
  import { fileURLToPath } from "url";
3801
4341
  import { spawn } from "child_process";
3802
- import { IndexManager as IndexManager2 } from "@kitsy/coop-core";
4342
+ import { IndexManager as IndexManager3 } from "@kitsy/coop-core";
3803
4343
  function findPackageRoot(entryPath) {
3804
- let current = path16.dirname(entryPath);
4344
+ let current = path17.dirname(entryPath);
3805
4345
  while (true) {
3806
- if (fs11.existsSync(path16.join(current, "package.json"))) {
4346
+ if (fs13.existsSync(path17.join(current, "package.json"))) {
3807
4347
  return current;
3808
4348
  }
3809
- const parent = path16.dirname(current);
4349
+ const parent = path17.dirname(current);
3810
4350
  if (parent === current) {
3811
4351
  throw new Error(`Unable to locate package root for ${entryPath}.`);
3812
4352
  }
@@ -3837,7 +4377,7 @@ function openBrowser(url) {
3837
4377
  }
3838
4378
  function ensureIndex(repoRoot) {
3839
4379
  const directory = ensureCoopInitialized(repoRoot);
3840
- const manager = new IndexManager2(directory);
4380
+ const manager = new IndexManager3(directory);
3841
4381
  if (manager.is_stale() || !manager.load_indexed_graph()) {
3842
4382
  manager.build_full_index();
3843
4383
  }
@@ -3852,11 +4392,12 @@ function attachSignalForwarding(child) {
3852
4392
  process.once("SIGTERM", () => forward("SIGTERM"));
3853
4393
  }
3854
4394
  async function startUiServer(repoRoot, host, port, shouldOpen) {
4395
+ const project = resolveProject(repoRoot);
3855
4396
  ensureIndex(repoRoot);
3856
4397
  const uiRoot = resolveUiPackageRoot();
3857
- const requireFromUi = createRequire(path16.join(uiRoot, "package.json"));
4398
+ const requireFromUi = createRequire(path17.join(uiRoot, "package.json"));
3858
4399
  const vitePackageJson = requireFromUi.resolve("vite/package.json");
3859
- const viteBin = path16.join(path16.dirname(vitePackageJson), "bin", "vite.js");
4400
+ const viteBin = path17.join(path17.dirname(vitePackageJson), "bin", "vite.js");
3860
4401
  const url = `http://${host}:${port}`;
3861
4402
  console.log(`COOP UI: ${url}`);
3862
4403
  const child = spawn(process.execPath, [viteBin, "--host", host, "--port", String(port)], {
@@ -3865,6 +4406,7 @@ async function startUiServer(repoRoot, host, port, shouldOpen) {
3865
4406
  env: {
3866
4407
  ...process.env,
3867
4408
  COOP_REPO_ROOT: repoRoot,
4409
+ COOP_PROJECT_ID: project.id,
3868
4410
  COOP_UI_HOST: host,
3869
4411
  COOP_UI_PORT: String(port)
3870
4412
  }
@@ -4212,9 +4754,9 @@ function registerWebhookCommand(program) {
4212
4754
  }
4213
4755
 
4214
4756
  // src/merge-driver/merge-driver.ts
4215
- import fs12 from "fs";
4757
+ import fs14 from "fs";
4216
4758
  import os2 from "os";
4217
- import path17 from "path";
4759
+ import path18 from "path";
4218
4760
  import { spawnSync as spawnSync5 } from "child_process";
4219
4761
  import { stringifyFrontmatter as stringifyFrontmatter3, parseFrontmatterContent, parseYamlContent, stringifyYamlContent } from "@kitsy/coop-core";
4220
4762
  var STATUS_RANK = {
@@ -4273,16 +4815,16 @@ function chooseFieldValue(key, baseValue, oursValue, theirsValue, oursUpdated, t
4273
4815
  }
4274
4816
  function mergeTaskFrontmatter(base, ours, theirs) {
4275
4817
  const keys = /* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(ours), ...Object.keys(theirs)]);
4276
- const output = {};
4818
+ const output3 = {};
4277
4819
  const oursUpdated = asTimestamp(ours.updated);
4278
4820
  const theirsUpdated = asTimestamp(theirs.updated);
4279
4821
  for (const key of keys) {
4280
4822
  const merged = chooseFieldValue(key, base[key], ours[key], theirs[key], oursUpdated, theirsUpdated);
4281
4823
  if (merged !== void 0) {
4282
- output[key] = merged;
4824
+ output3[key] = merged;
4283
4825
  }
4284
4826
  }
4285
- return output;
4827
+ return output3;
4286
4828
  }
4287
4829
  function mergeTextWithGit(ancestor, ours, theirs) {
4288
4830
  const result = spawnSync5("git", ["merge-file", "-p", ours, ancestor, theirs], {
@@ -4297,33 +4839,33 @@ function mergeTextWithGit(ancestor, ours, theirs) {
4297
4839
  return { ok: false, output: stdout };
4298
4840
  }
4299
4841
  function mergeTaskFile(ancestorPath, oursPath, theirsPath) {
4300
- const ancestorRaw = fs12.readFileSync(ancestorPath, "utf8");
4301
- const oursRaw = fs12.readFileSync(oursPath, "utf8");
4302
- const theirsRaw = fs12.readFileSync(theirsPath, "utf8");
4842
+ const ancestorRaw = fs14.readFileSync(ancestorPath, "utf8");
4843
+ const oursRaw = fs14.readFileSync(oursPath, "utf8");
4844
+ const theirsRaw = fs14.readFileSync(theirsPath, "utf8");
4303
4845
  const ancestor = parseTaskDocument(ancestorRaw, ancestorPath);
4304
4846
  const ours = parseTaskDocument(oursRaw, oursPath);
4305
4847
  const theirs = parseTaskDocument(theirsRaw, theirsPath);
4306
4848
  const mergedFrontmatter = mergeTaskFrontmatter(ancestor.frontmatter, ours.frontmatter, theirs.frontmatter);
4307
- const tempDir = fs12.mkdtempSync(path17.join(os2.tmpdir(), "coop-merge-body-"));
4849
+ const tempDir = fs14.mkdtempSync(path18.join(os2.tmpdir(), "coop-merge-body-"));
4308
4850
  try {
4309
- const ancestorBody = path17.join(tempDir, "ancestor.md");
4310
- const oursBody = path17.join(tempDir, "ours.md");
4311
- const theirsBody = path17.join(tempDir, "theirs.md");
4312
- fs12.writeFileSync(ancestorBody, ancestor.body, "utf8");
4313
- fs12.writeFileSync(oursBody, ours.body, "utf8");
4314
- fs12.writeFileSync(theirsBody, theirs.body, "utf8");
4851
+ const ancestorBody = path18.join(tempDir, "ancestor.md");
4852
+ const oursBody = path18.join(tempDir, "ours.md");
4853
+ const theirsBody = path18.join(tempDir, "theirs.md");
4854
+ fs14.writeFileSync(ancestorBody, ancestor.body, "utf8");
4855
+ fs14.writeFileSync(oursBody, ours.body, "utf8");
4856
+ fs14.writeFileSync(theirsBody, theirs.body, "utf8");
4315
4857
  const mergedBody = mergeTextWithGit(ancestorBody, oursBody, theirsBody);
4316
- const output = stringifyFrontmatter3(mergedFrontmatter, mergedBody.output);
4317
- fs12.writeFileSync(oursPath, output, "utf8");
4858
+ const output3 = stringifyFrontmatter3(mergedFrontmatter, mergedBody.output);
4859
+ fs14.writeFileSync(oursPath, output3, "utf8");
4318
4860
  return mergedBody.ok ? 0 : 1;
4319
4861
  } finally {
4320
- fs12.rmSync(tempDir, { recursive: true, force: true });
4862
+ fs14.rmSync(tempDir, { recursive: true, force: true });
4321
4863
  }
4322
4864
  }
4323
4865
  function mergeDeliveryFile(ancestorPath, oursPath, theirsPath) {
4324
- const ancestor = parseYamlContent(fs12.readFileSync(ancestorPath, "utf8"), ancestorPath);
4325
- const ours = parseYamlContent(fs12.readFileSync(oursPath, "utf8"), oursPath);
4326
- const theirs = parseYamlContent(fs12.readFileSync(theirsPath, "utf8"), theirsPath);
4866
+ const ancestor = parseYamlContent(fs14.readFileSync(ancestorPath, "utf8"), ancestorPath);
4867
+ const ours = parseYamlContent(fs14.readFileSync(oursPath, "utf8"), oursPath);
4868
+ const theirs = parseYamlContent(fs14.readFileSync(theirsPath, "utf8"), theirsPath);
4327
4869
  const oursUpdated = asTimestamp(ours.updated);
4328
4870
  const theirsUpdated = asTimestamp(theirs.updated);
4329
4871
  const base = ancestor;
@@ -4333,7 +4875,7 @@ function mergeDeliveryFile(ancestorPath, oursPath, theirsPath) {
4333
4875
  const value = chooseFieldValue(key, base[key], ours[key], theirs[key], oursUpdated, theirsUpdated);
4334
4876
  if (value !== void 0) merged[key] = value;
4335
4877
  }
4336
- fs12.writeFileSync(oursPath, stringifyYamlContent(merged), "utf8");
4878
+ fs14.writeFileSync(oursPath, stringifyYamlContent(merged), "utf8");
4337
4879
  return 0;
4338
4880
  }
4339
4881
  function runMergeDriver(kind, ancestorPath, oursPath, theirsPath) {
@@ -4346,9 +4888,9 @@ function runMergeDriver(kind, ancestorPath, oursPath, theirsPath) {
4346
4888
  // src/index.ts
4347
4889
  function readVersion() {
4348
4890
  const currentFile = fileURLToPath2(import.meta.url);
4349
- const packageJsonPath = path18.resolve(path18.dirname(currentFile), "..", "package.json");
4891
+ const packageJsonPath = path19.resolve(path19.dirname(currentFile), "..", "package.json");
4350
4892
  try {
4351
- const parsed = JSON.parse(fs13.readFileSync(packageJsonPath, "utf8"));
4893
+ const parsed = JSON.parse(fs15.readFileSync(packageJsonPath, "utf8"));
4352
4894
  return parsed.version ?? "0.0.0";
4353
4895
  } catch {
4354
4896
  return "0.0.0";
@@ -4365,6 +4907,7 @@ function createProgram() {
4365
4907
  program.version(readVersion());
4366
4908
  program.description("COOP CLI");
4367
4909
  program.option("--verbose", "Print stack traces for command errors");
4910
+ program.option("-p, --project <id>", "Select the active COOP project");
4368
4911
  registerInitCommand(program);
4369
4912
  registerCreateCommand(program);
4370
4913
  registerAssignCommand(program);
@@ -4378,6 +4921,7 @@ function createProgram() {
4378
4921
  registerLogCommand(program);
4379
4922
  registerMigrateCommand(program);
4380
4923
  registerPlanCommand(program);
4924
+ registerProjectCommand(program);
4381
4925
  registerRunCommand(program);
4382
4926
  registerServeCommand(program);
4383
4927
  registerStatusCommand(program);
@@ -4441,7 +4985,7 @@ async function runCli(argv = process.argv) {
4441
4985
  function isMainModule() {
4442
4986
  const entry = process.argv[1];
4443
4987
  if (!entry) return false;
4444
- return path18.resolve(entry) === fileURLToPath2(import.meta.url);
4988
+ return path19.resolve(entry) === fileURLToPath2(import.meta.url);
4445
4989
  }
4446
4990
  if (isMainModule()) {
4447
4991
  await runCli(process.argv);