@kitsy/coop 1.0.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.
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";
6
- import { fileURLToPath as fileURLToPath2, pathToFileURL } from "url";
4
+ import fs15 from "fs";
5
+ import path19 from "path";
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,12 +17,29 @@ 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";
32
+ function resolveCoopHome() {
33
+ const configured = process.env.COOP_HOME?.trim();
34
+ if (configured) {
35
+ return path.resolve(configured);
36
+ }
37
+ return path.join(os.homedir(), ".coop");
38
+ }
22
39
  function resolveRepoRoot(start = process.cwd()) {
23
40
  let current = path.resolve(start);
24
41
  while (true) {
25
- if (fs.existsSync(path.join(current, ".git"))) {
42
+ if (fs.existsSync(path.join(current, ".git")) || fs.existsSync(path.join(current, ".coop"))) {
26
43
  return current;
27
44
  }
28
45
  const parent = path.dirname(current);
@@ -32,21 +49,54 @@ function resolveRepoRoot(start = process.cwd()) {
32
49
  current = parent;
33
50
  }
34
51
  }
35
- 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) {
36
66
  return path.join(root, ".coop");
37
67
  }
38
- function ensureCoopInitialized(root) {
39
- const dir = coopDir(root);
40
- if (!fs.existsSync(dir)) {
41
- 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'.`);
42
79
  }
43
- return dir;
80
+ return project.root;
44
81
  }
45
- function coopConfigPath(root) {
46
- return path.join(coopDir(root), "config.yml");
82
+ function coopConfigPath(root, projectId = resolveRequestedProject()) {
83
+ return coop_project_config_path(ensureCoopInitialized(root, projectId));
47
84
  }
48
- function readCoopConfig(root) {
49
- 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);
50
100
  const repoName = path.basename(path.resolve(root));
51
101
  if (!fs.existsSync(configPath)) {
52
102
  return {
@@ -62,10 +112,10 @@ function readCoopConfig(root) {
62
112
  filePath: configPath
63
113
  };
64
114
  }
65
- const config = parseYamlFile(configPath);
115
+ const config = read_project_config(project.root);
66
116
  const projectRaw = typeof config.project === "object" && config.project !== null ? config.project : {};
67
117
  const projectName = typeof projectRaw.name === "string" && projectRaw.name.trim().length > 0 ? projectRaw.name.trim() : repoName;
68
- 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;
69
119
  const projectAliases = Array.isArray(projectRaw.aliases) ? projectRaw.aliases.filter((entry) => typeof entry === "string" && entry.trim().length > 0) : [];
70
120
  const idPrefixesRaw = config.id_prefixes;
71
121
  const idPrefixes = typeof idPrefixesRaw === "object" && idPrefixesRaw !== null ? idPrefixesRaw : {};
@@ -86,7 +136,7 @@ function readCoopConfig(root) {
86
136
  idNamingTemplate,
87
137
  idSeqPadding,
88
138
  projectName: projectName || "COOP Workspace",
89
- projectId: projectId || "workspace",
139
+ projectId: resolvedProjectId || "workspace",
90
140
  projectAliases,
91
141
  raw: config,
92
142
  filePath: configPath
@@ -151,15 +201,15 @@ function findTaskFileById(root, id) {
151
201
  const target = `${id}.md`.toLowerCase();
152
202
  const match = listTaskFiles(root).find((filePath) => path.basename(filePath).toLowerCase() === target);
153
203
  if (!match) {
154
- throw new Error(`Task '${id}' not found in .coop/tasks.`);
204
+ throw new Error(`Task '${id}' not found in the active COOP project.`);
155
205
  }
156
206
  return match;
157
207
  }
158
208
  function todayIsoDate() {
159
209
  return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
160
210
  }
161
- function normalizeIdPart(input, fallback, maxLength = 12) {
162
- 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, "");
163
213
  if (!normalized) return fallback;
164
214
  return normalized.slice(0, maxLength);
165
215
  }
@@ -195,8 +245,8 @@ function shortDateToken(now = /* @__PURE__ */ new Date()) {
195
245
  function randomToken() {
196
246
  return crypto.randomBytes(4).toString("hex").toUpperCase();
197
247
  }
198
- function sanitizeTemplateValue(input, fallback = "X") {
199
- 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, "-");
200
250
  return normalized || fallback;
201
251
  }
202
252
  function sequenceForPattern(existingIds, prefix, suffix) {
@@ -304,27 +354,27 @@ function generateConfiguredId(root, existingIds, context) {
304
354
 
305
355
  // src/utils/aliases.ts
306
356
  var ALIAS_PATTERN = /^[A-Z0-9]+(?:[.-][A-Z0-9]+)*$/;
307
- function toPosixPath(input) {
308
- return input.replace(/\\/g, "/");
357
+ function toPosixPath(input3) {
358
+ return input3.replace(/\\/g, "/");
309
359
  }
310
360
  function indexFilePath(root) {
311
361
  const { indexDataFormat } = readCoopConfig(root);
312
362
  const extension = indexDataFormat === "json" ? "json" : "yml";
313
- return path2.join(root, ".coop", ".index", `aliases.${extension}`);
363
+ return path2.join(ensureCoopInitialized(root), ".index", `aliases.${extension}`);
314
364
  }
315
- function normalizeAliasValue(input) {
316
- return input.trim().toUpperCase().replace(/_/g, ".").replace(/\.+/g, ".");
365
+ function normalizeAliasValue(input3) {
366
+ return input3.trim().toUpperCase().replace(/_/g, ".").replace(/\.+/g, ".");
317
367
  }
318
- function normalizePatternValue(input) {
319
- return input.trim().toUpperCase().replace(/_/g, ".");
368
+ function normalizePatternValue(input3) {
369
+ return input3.trim().toUpperCase().replace(/_/g, ".");
320
370
  }
321
- function normalizeAlias(input) {
322
- const normalized = normalizeAliasValue(input);
371
+ function normalizeAlias(input3) {
372
+ const normalized = normalizeAliasValue(input3);
323
373
  if (!normalized) {
324
374
  throw new Error("Alias cannot be empty.");
325
375
  }
326
376
  if (!ALIAS_PATTERN.test(normalized)) {
327
- throw new Error(`Invalid alias '${input}'. Use letters/numbers with '.' and '-'.`);
377
+ throw new Error(`Invalid alias '${input3}'. Use letters/numbers with '.' and '-'.`);
328
378
  }
329
379
  return normalized;
330
380
  }
@@ -376,7 +426,7 @@ function readIndexFile(root, aliasIndexPath) {
376
426
  const parsed = JSON.parse(fs2.readFileSync(aliasIndexPath, "utf8"));
377
427
  return parsed;
378
428
  }
379
- return parseYamlFile2(aliasIndexPath);
429
+ return parseYamlFile(aliasIndexPath);
380
430
  } catch {
381
431
  return null;
382
432
  }
@@ -387,14 +437,14 @@ function writeIndexFile(root, data) {
387
437
  if (aliasIndexPath.endsWith(".json")) {
388
438
  fs2.writeFileSync(aliasIndexPath, `${JSON.stringify(data, null, 2)}
389
439
  `, "utf8");
390
- const yamlPath = path2.join(root, ".coop", ".index", "aliases.yml");
440
+ const yamlPath = path2.join(ensureCoopInitialized(root), ".index", "aliases.yml");
391
441
  if (fs2.existsSync(yamlPath)) {
392
442
  fs2.rmSync(yamlPath, { force: true });
393
443
  }
394
444
  return;
395
445
  }
396
446
  writeYamlFile2(aliasIndexPath, data);
397
- const jsonPath = path2.join(root, ".coop", ".index", "aliases.json");
447
+ const jsonPath = path2.join(ensureCoopInitialized(root), ".index", "aliases.json");
398
448
  if (fs2.existsSync(jsonPath)) {
399
449
  fs2.rmSync(jsonPath, { force: true });
400
450
  }
@@ -511,8 +561,8 @@ function updateTaskAliases(filePath, aliases) {
511
561
  function updateIdeaAliases(filePath, aliases) {
512
562
  const parsed = parseIdeaFile(filePath);
513
563
  const nextRaw = { ...parsed.raw, aliases };
514
- const output = stringifyFrontmatter(nextRaw, parsed.body);
515
- fs2.writeFileSync(filePath, output, "utf8");
564
+ const output3 = stringifyFrontmatter(nextRaw, parsed.body);
565
+ fs2.writeFileSync(filePath, output3, "utf8");
516
566
  }
517
567
  function resolveFilePath(root, target) {
518
568
  return path2.join(root, ...target.file.split("/"));
@@ -1054,14 +1104,14 @@ function updateIdeaLinkedTasks(filePath, idea, raw, body, linked) {
1054
1104
  };
1055
1105
  fs3.writeFileSync(filePath, stringifyFrontmatter2(nextRaw, body), "utf8");
1056
1106
  }
1057
- function makeTaskDraft(input) {
1107
+ function makeTaskDraft(input3) {
1058
1108
  return {
1059
- title: input.title,
1060
- type: input.type,
1061
- status: input.status,
1062
- track: input.track,
1063
- priority: input.priority,
1064
- 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
1065
1115
  };
1066
1116
  }
1067
1117
  function registerCreateCommand(program) {
@@ -1412,10 +1462,10 @@ import {
1412
1462
  function normalize(value) {
1413
1463
  return value.trim().toLowerCase();
1414
1464
  }
1415
- function resolveDelivery(graph, input) {
1416
- const direct = graph.deliveries.get(input);
1465
+ function resolveDelivery(graph, input3) {
1466
+ const direct = graph.deliveries.get(input3);
1417
1467
  if (direct) return direct;
1418
- const target = normalize(input);
1468
+ const target = normalize(input3);
1419
1469
  const byId = Array.from(graph.deliveries.values()).find((delivery) => normalize(delivery.id) === target);
1420
1470
  if (byId) return byId;
1421
1471
  const byName = Array.from(graph.deliveries.values()).filter((delivery) => normalize(delivery.name) === target);
@@ -1423,9 +1473,9 @@ function resolveDelivery(graph, input) {
1423
1473
  return byName[0];
1424
1474
  }
1425
1475
  if (byName.length > 1) {
1426
- throw new Error(`Multiple deliveries match '${input}'. Use delivery id instead.`);
1476
+ throw new Error(`Multiple deliveries match '${input3}'. Use delivery id instead.`);
1427
1477
  }
1428
- throw new Error(`Delivery '${input}' not found.`);
1478
+ throw new Error(`Delivery '${input3}' not found.`);
1429
1479
  }
1430
1480
 
1431
1481
  // src/commands/graph.ts
@@ -1616,6 +1666,8 @@ function registerIndexCommand(program) {
1616
1666
  import fs6 from "fs";
1617
1667
  import path8 from "path";
1618
1668
  import { spawnSync as spawnSync3 } from "child_process";
1669
+ import { createInterface } from "readline/promises";
1670
+ import { stdin as input, stdout as output } from "process";
1619
1671
  import { CURRENT_SCHEMA_VERSION, write_schema_version } from "@kitsy/coop-core";
1620
1672
 
1621
1673
  // src/hooks/pre-commit.ts
@@ -1643,7 +1695,41 @@ function toPosixPath2(filePath) {
1643
1695
  }
1644
1696
  function stagedTaskFiles(repoRoot) {
1645
1697
  const { stdout } = runGit(repoRoot, ["diff", "--cached", "--name-only", "--diff-filter=ACMR"]);
1646
- 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));
1647
1733
  }
1648
1734
  function readGitBlob(repoRoot, spec) {
1649
1735
  const result = runGit(repoRoot, ["show", spec], true);
@@ -1658,6 +1744,7 @@ function parseStagedTasks(repoRoot, relativePaths) {
1658
1744
  const staged = [];
1659
1745
  for (const relativePath of relativePaths) {
1660
1746
  const absolutePath = path6.join(repoRoot, ...relativePath.split("/"));
1747
+ const projectRoot = projectRootFromRelativePath(repoRoot, relativePath);
1661
1748
  const stagedBlob = readGitBlob(repoRoot, `:${relativePath}`);
1662
1749
  if (!stagedBlob) {
1663
1750
  errors.push(`[COOP] Unable to read staged task '${relativePath}' from git index.`);
@@ -1678,6 +1765,7 @@ function parseStagedTasks(repoRoot, relativePaths) {
1678
1765
  staged.push({
1679
1766
  relativePath,
1680
1767
  absolutePath,
1768
+ projectRoot,
1681
1769
  task
1682
1770
  });
1683
1771
  }
@@ -1712,13 +1800,13 @@ function buildGraphForCycleCheck(tasks) {
1712
1800
  deliveries: /* @__PURE__ */ new Map()
1713
1801
  };
1714
1802
  }
1715
- function collectTasksForCycleCheck(repoRoot, stagedTasks) {
1803
+ function collectTasksForCycleCheck(projectRoot, stagedTasks) {
1716
1804
  const stagedByPath = /* @__PURE__ */ new Map();
1717
1805
  for (const staged of stagedTasks) {
1718
1806
  stagedByPath.set(toPosixPath2(staged.absolutePath), staged.task);
1719
1807
  }
1720
1808
  const tasks = [];
1721
- for (const filePath of listTaskFiles(repoRoot)) {
1809
+ for (const filePath of listTaskFilesForProject(projectRoot)) {
1722
1810
  const normalized = toPosixPath2(path6.resolve(filePath));
1723
1811
  const stagedTask = stagedByPath.get(normalized);
1724
1812
  if (stagedTask) {
@@ -1737,16 +1825,25 @@ function runPreCommitChecks(repoRoot) {
1737
1825
  const parsed = parseStagedTasks(repoRoot, relativeTaskFiles);
1738
1826
  const errors = [...parsed.errors];
1739
1827
  if (parsed.staged.length > 0 && errors.length === 0) {
1740
- try {
1741
- const tasks = collectTasksForCycleCheck(repoRoot, parsed.staged);
1742
- const graph = buildGraphForCycleCheck(tasks);
1743
- const cycle = detect_cycle(graph);
1744
- if (cycle) {
1745
- 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}`);
1746
1846
  }
1747
- } catch (error) {
1748
- const message = error instanceof Error ? error.message : String(error);
1749
- errors.push(`[COOP] Failed to run dependency cycle check: ${message}`);
1750
1847
  }
1751
1848
  }
1752
1849
  return {
@@ -1806,28 +1903,37 @@ function installPreCommitHook(repoRoot) {
1806
1903
  // src/hooks/post-merge-validate.ts
1807
1904
  import fs5 from "fs";
1808
1905
  import path7 from "path";
1906
+ import { list_projects } from "@kitsy/coop-core";
1809
1907
  import { load_graph as load_graph3, validate_graph as validate_graph2 } from "@kitsy/coop-core";
1810
1908
  var HOOK_BLOCK_START2 = "# COOP_POST_MERGE_START";
1811
1909
  var HOOK_BLOCK_END2 = "# COOP_POST_MERGE_END";
1812
1910
  function runPostMergeValidate(repoRoot) {
1813
- const coopDir2 = path7.join(repoRoot, ".coop");
1814
- if (!fs5.existsSync(coopDir2)) {
1911
+ const workspaceDir = path7.join(repoRoot, ".coop");
1912
+ if (!fs5.existsSync(workspaceDir)) {
1815
1913
  return {
1816
1914
  ok: true,
1817
1915
  warnings: ["[COOP] Skipped post-merge validation (.coop not found)."]
1818
1916
  };
1819
1917
  }
1820
1918
  try {
1821
- const graph = load_graph3(coopDir2);
1822
- const issues = validate_graph2(graph);
1823
- if (issues.length === 0) {
1824
- 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
+ }
1825
1933
  }
1826
1934
  return {
1827
1935
  ok: true,
1828
- warnings: issues.map(
1829
- (issue) => `[COOP] post-merge warning [${issue.invariant}] ${issue.message}`
1830
- )
1936
+ warnings
1831
1937
  };
1832
1938
  } catch (error) {
1833
1939
  return {
@@ -1886,14 +1992,32 @@ function installPostMergeHook(repoRoot) {
1886
1992
  }
1887
1993
 
1888
1994
  // src/commands/init.ts
1889
- function buildDefaultConfig(root) {
1890
- const repoName = repoDisplayName(root);
1891
- 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) {
1892
2016
  return `version: 2
1893
2017
  project:
1894
- name: "${repoName}"
1895
- id: "${repoId}"
1896
- aliases: []
2018
+ name: ${JSON.stringify(projectName)}
2019
+ id: "${projectId}"
2020
+ aliases:${aliasesYaml(projectAliases)}
1897
2021
 
1898
2022
  id_prefixes:
1899
2023
  idea: "IDEA"
@@ -1943,8 +2067,8 @@ api:
1943
2067
  remotes: {}
1944
2068
 
1945
2069
  hooks:
1946
- on_task_transition: .coop/hooks/on-task-transition.sh
1947
- 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
1948
2072
 
1949
2073
  index:
1950
2074
  data: yaml
@@ -2016,6 +2140,12 @@ triggers:
2016
2140
  type: github_pr
2017
2141
  operation: merge
2018
2142
  `;
2143
+ var COOP_IGNORE_TEMPLATE = `.index/
2144
+ logs/
2145
+ tmp/
2146
+ *.log
2147
+ *.tmp
2148
+ `;
2019
2149
  function ensureDir(dirPath) {
2020
2150
  fs6.mkdirSync(dirPath, { recursive: true });
2021
2151
  }
@@ -2064,8 +2194,8 @@ function setGitConfig(root, key, value) {
2064
2194
  }
2065
2195
  function installMergeDrivers(root) {
2066
2196
  const entries = [
2067
- ".coop/tasks/*.md merge=coop-task",
2068
- ".coop/deliveries/*.yml merge=coop-delivery"
2197
+ ".coop/projects/*/tasks/*.md merge=coop-task",
2198
+ ".coop/projects/*/deliveries/*.yml merge=coop-delivery"
2069
2199
  ];
2070
2200
  for (const entry of entries) {
2071
2201
  ensureGitattributesEntry(root, entry);
@@ -2081,10 +2211,58 @@ function installMergeDrivers(root) {
2081
2211
  }
2082
2212
  return "Updated .gitattributes but could not register merge drivers in git config.";
2083
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
+ }
2084
2259
  function registerInitCommand(program) {
2085
- 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) => {
2086
2261
  const root = resolveRepoRoot();
2087
- 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);
2088
2266
  const dirs = [
2089
2267
  "ideas",
2090
2268
  "tasks",
@@ -2100,23 +2278,36 @@ function registerInitCommand(program) {
2100
2278
  "history/deliveries",
2101
2279
  ".index"
2102
2280
  ];
2281
+ ensureDir(path8.join(workspaceDir, "projects"));
2103
2282
  for (const dir of dirs) {
2104
- ensureDir(path8.join(coop, dir));
2283
+ ensureDir(path8.join(projectRoot, dir));
2105
2284
  }
2106
- writeIfMissing(path8.join(coop, "config.yml"), buildDefaultConfig(root));
2107
- if (!fs6.existsSync(path8.join(coop, "schema-version"))) {
2108
- write_schema_version(coop, CURRENT_SCHEMA_VERSION);
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);
2109
2291
  }
2110
- writeIfMissing(path8.join(coop, "templates/task.md"), TASK_TEMPLATE);
2111
- writeIfMissing(path8.join(coop, "templates/idea.md"), IDEA_TEMPLATE);
2112
- writeIfMissing(path8.join(coop, "plugins/console-log.yml"), PLUGIN_CONSOLE_TEMPLATE);
2113
- writeIfMissing(path8.join(coop, "plugins/github-pr.yml"), PLUGIN_GITHUB_TEMPLATE);
2114
- ensureGitignoreEntry(root, ".coop/.index/");
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/");
2115
2301
  const preCommitHook = installPreCommitHook(root);
2116
2302
  const postMergeHook = installPostMergeHook(root);
2117
2303
  const mergeDrivers = installMergeDrivers(root);
2304
+ const project = resolveProject(root, projectId);
2118
2305
  console.log("Initialized COOP workspace.");
2119
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)"}`);
2120
2311
  console.log(`- ${preCommitHook.message}`);
2121
2312
  if (preCommitHook.installed) {
2122
2313
  console.log(`- Hook: ${path8.relative(root, preCommitHook.hookPath)}`);
@@ -2275,11 +2466,11 @@ function resolveRepoSafe(start = process.cwd()) {
2275
2466
  }
2276
2467
  function resolveCliLogFile(start = process.cwd()) {
2277
2468
  const root = resolveRepoSafe(start);
2278
- const coop = coopDir(root);
2279
- if (fs7.existsSync(coop)) {
2280
- 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");
2281
2472
  }
2282
- return path10.join(root, ".coop-cli.log");
2473
+ return path10.join(resolveCoopHome(), "logs", "cli.log");
2283
2474
  }
2284
2475
  function appendLogEntry(entry, logFile) {
2285
2476
  fs7.mkdirSync(path10.dirname(logFile), { recursive: true });
@@ -2358,8 +2549,11 @@ function registerLogCommand(program) {
2358
2549
  }
2359
2550
 
2360
2551
  // src/commands/migrate.ts
2552
+ import fs8 from "fs";
2361
2553
  import path11 from "path";
2362
- 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";
2363
2557
  function parseTargetVersion(raw) {
2364
2558
  if (!raw) return CURRENT_SCHEMA_VERSION2;
2365
2559
  const parsed = Number(raw);
@@ -2368,8 +2562,160 @@ function parseTargetVersion(raw) {
2368
2562
  }
2369
2563
  return parsed;
2370
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
+ }
2371
2717
  function registerMigrateCommand(program) {
2372
- 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) => {
2373
2719
  const root = resolveRepoRoot();
2374
2720
  ensureCoopInitialized(root);
2375
2721
  const target = parseTargetVersion(options.to);
@@ -2394,6 +2740,15 @@ function registerMigrateCommand(program) {
2394
2740
  console.log(`- schema-version updated to v${report.to_version}`);
2395
2741
  }
2396
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
+ });
2397
2752
  }
2398
2753
 
2399
2754
  // src/commands/plan.ts
@@ -2629,9 +2984,206 @@ function registerPlanCommand(program) {
2629
2984
  });
2630
2985
  }
2631
2986
 
2632
- // src/commands/run.ts
2633
- import fs8 from "fs";
2987
+ // src/commands/project.ts
2988
+ import fs9 from "fs";
2634
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";
2635
3187
  import { load_graph as load_graph5, parseTaskFile as parseTaskFile7 } from "@kitsy/coop-core";
2636
3188
  import {
2637
3189
  build_contract,
@@ -2641,8 +3193,8 @@ import {
2641
3193
  } from "@kitsy/coop-ai";
2642
3194
  function loadTask(root, idOrAlias) {
2643
3195
  const target = resolveReference(root, idOrAlias, "task");
2644
- const taskFile = path12.join(root, ...target.file.split("/"));
2645
- if (!fs8.existsSync(taskFile)) {
3196
+ const taskFile = path13.join(root, ...target.file.split("/"));
3197
+ if (!fs10.existsSync(taskFile)) {
2646
3198
  throw new Error(`Task file not found: ${target.file}`);
2647
3199
  }
2648
3200
  return parseTaskFile7(taskFile).task;
@@ -2681,22 +3233,22 @@ function registerRunCommand(program) {
2681
3233
  on_progress: (message) => console.log(`[COOP] ${message}`)
2682
3234
  });
2683
3235
  if (result.status === "failed") {
2684
- 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)}`);
2685
3237
  }
2686
3238
  if (result.status === "paused") {
2687
3239
  console.log(`[COOP] run paused: ${result.run.id}`);
2688
- console.log(`[COOP] log: ${path12.relative(root, result.log_path)}`);
3240
+ console.log(`[COOP] log: ${path13.relative(root, result.log_path)}`);
2689
3241
  return;
2690
3242
  }
2691
3243
  console.log(`[COOP] run completed: ${result.run.id}`);
2692
- console.log(`[COOP] log: ${path12.relative(root, result.log_path)}`);
3244
+ console.log(`[COOP] log: ${path13.relative(root, result.log_path)}`);
2693
3245
  });
2694
3246
  }
2695
3247
 
2696
3248
  // src/server/api.ts
2697
- import fs9 from "fs";
3249
+ import fs11 from "fs";
2698
3250
  import http from "http";
2699
- import path13 from "path";
3251
+ import path14 from "path";
2700
3252
  import {
2701
3253
  analyze_feasibility as analyze_feasibility2,
2702
3254
  load_graph as load_graph6,
@@ -2742,12 +3294,12 @@ function taskSummary(graph, task, external = []) {
2742
3294
  };
2743
3295
  }
2744
3296
  function taskFileById(root, id) {
2745
- const tasksDir = path13.join(root, ".coop", "tasks");
2746
- if (!fs9.existsSync(tasksDir)) return null;
2747
- 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 });
2748
3300
  const target = `${id}.md`.toLowerCase();
2749
3301
  const match = entries.find((entry) => entry.isFile() && entry.name.toLowerCase() === target);
2750
- return match ? path13.join(tasksDir, match.name) : null;
3302
+ return match ? path14.join(tasksDir, match.name) : null;
2751
3303
  }
2752
3304
  function loadRemoteConfig(root) {
2753
3305
  const raw = readCoopConfig(root).raw;
@@ -2811,7 +3363,8 @@ function createApiServer(root, options = {}) {
2811
3363
  json(res, 200, workspaceMeta(repoRoot));
2812
3364
  return;
2813
3365
  }
2814
- const coopPath = coopDir(repoRoot);
3366
+ const project = resolveProject(repoRoot);
3367
+ const coopPath = project.root;
2815
3368
  const graph = load_graph6(coopPath);
2816
3369
  const resolutions = await externalResolutions(repoRoot, graph, options);
2817
3370
  if (pathname === "/api/tasks") {
@@ -2836,7 +3389,7 @@ function createApiServer(root, options = {}) {
2836
3389
  created: task.created,
2837
3390
  updated: task.updated,
2838
3391
  body: parsed.body,
2839
- file_path: path13.relative(repoRoot, filePath).replace(/\\/g, "/")
3392
+ file_path: path14.relative(repoRoot, filePath).replace(/\\/g, "/")
2840
3393
  }
2841
3394
  });
2842
3395
  return;
@@ -2924,8 +3477,8 @@ function registerServeCommand(program) {
2924
3477
  }
2925
3478
 
2926
3479
  // src/commands/show.ts
2927
- import fs10 from "fs";
2928
- import path14 from "path";
3480
+ import fs12 from "fs";
3481
+ import path15 from "path";
2929
3482
  import { parseIdeaFile as parseIdeaFile4, parseTaskFile as parseTaskFile9 } from "@kitsy/coop-core";
2930
3483
  function stringify(value) {
2931
3484
  if (value === null || value === void 0) return "-";
@@ -2934,13 +3487,13 @@ function stringify(value) {
2934
3487
  return String(value);
2935
3488
  }
2936
3489
  function loadComputedFromIndex(root, taskId) {
2937
- const indexPath = path14.join(root, ".coop", ".index", "tasks.json");
2938
- if (!fs10.existsSync(indexPath)) {
3490
+ const indexPath = path15.join(ensureCoopInitialized(root), ".index", "tasks.json");
3491
+ if (!fs12.existsSync(indexPath)) {
2939
3492
  return null;
2940
3493
  }
2941
3494
  let parsed;
2942
3495
  try {
2943
- parsed = JSON.parse(fs10.readFileSync(indexPath, "utf8"));
3496
+ parsed = JSON.parse(fs12.readFileSync(indexPath, "utf8"));
2944
3497
  } catch {
2945
3498
  return null;
2946
3499
  }
@@ -2979,7 +3532,7 @@ function showTask(taskId) {
2979
3532
  const root = resolveRepoRoot();
2980
3533
  const coop = ensureCoopInitialized(root);
2981
3534
  const target = resolveReference(root, taskId, "task");
2982
- const taskFile = path14.join(root, ...target.file.split("/"));
3535
+ const taskFile = path15.join(root, ...target.file.split("/"));
2983
3536
  const parsed = parseTaskFile9(taskFile);
2984
3537
  const task = parsed.task;
2985
3538
  const body = parsed.body.trim();
@@ -2998,7 +3551,7 @@ function showTask(taskId) {
2998
3551
  `Tags: ${stringify(task.tags)}`,
2999
3552
  `Created: ${task.created}`,
3000
3553
  `Updated: ${task.updated}`,
3001
- `File: ${path14.relative(root, taskFile)}`,
3554
+ `File: ${path15.relative(root, taskFile)}`,
3002
3555
  "",
3003
3556
  "Body:",
3004
3557
  body || "-",
@@ -3006,7 +3559,7 @@ function showTask(taskId) {
3006
3559
  "Computed:"
3007
3560
  ];
3008
3561
  if (!computed) {
3009
- 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)`);
3010
3563
  } else {
3011
3564
  for (const [key, value] of Object.entries(computed)) {
3012
3565
  lines.push(`- ${key}: ${stringify(value)}`);
@@ -3018,7 +3571,7 @@ function showIdea(ideaId) {
3018
3571
  const root = resolveRepoRoot();
3019
3572
  ensureCoopInitialized(root);
3020
3573
  const target = resolveReference(root, ideaId, "idea");
3021
- const ideaFile = path14.join(root, ...target.file.split("/"));
3574
+ const ideaFile = path15.join(root, ...target.file.split("/"));
3022
3575
  const parsed = parseIdeaFile4(ideaFile);
3023
3576
  const idea = parsed.idea;
3024
3577
  const body = parsed.body.trim();
@@ -3032,7 +3585,7 @@ function showIdea(ideaId) {
3032
3585
  `Tags: ${stringify(idea.tags)}`,
3033
3586
  `Linked Tasks: ${stringify(idea.linked_tasks)}`,
3034
3587
  `Created: ${idea.created}`,
3035
- `File: ${path14.relative(root, ideaFile)}`,
3588
+ `File: ${path15.relative(root, ideaFile)}`,
3036
3589
  "",
3037
3590
  "Body:",
3038
3591
  body || "-"
@@ -3139,7 +3692,7 @@ function registerStatusCommand(program) {
3139
3692
  }
3140
3693
 
3141
3694
  // src/commands/transition.ts
3142
- import path15 from "path";
3695
+ import path16 from "path";
3143
3696
  import {
3144
3697
  TaskStatus as TaskStatus3,
3145
3698
  check_permission as check_permission3,
@@ -3168,8 +3721,8 @@ import { Octokit } from "octokit";
3168
3721
  function isObject(value) {
3169
3722
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
3170
3723
  }
3171
- function sanitizeBranchPart(input, fallback) {
3172
- 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, "");
3173
3726
  return normalized || fallback;
3174
3727
  }
3175
3728
  function readGitRemote(root) {
@@ -3302,15 +3855,15 @@ function toGitHubClient(config) {
3302
3855
  baseUrl: config.apiBaseUrl
3303
3856
  });
3304
3857
  return {
3305
- async createPullRequest(input) {
3858
+ async createPullRequest(input3) {
3306
3859
  const response = await octokit.rest.pulls.create({
3307
- owner: input.owner,
3308
- repo: input.repo,
3309
- title: input.title,
3310
- body: input.body,
3311
- head: input.head,
3312
- base: input.base,
3313
- 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
3314
3867
  });
3315
3868
  return {
3316
3869
  number: response.data.number,
@@ -3320,14 +3873,14 @@ function toGitHubClient(config) {
3320
3873
  draft: response.data.draft
3321
3874
  };
3322
3875
  },
3323
- async updatePullRequest(input) {
3876
+ async updatePullRequest(input3) {
3324
3877
  const response = await octokit.rest.pulls.update({
3325
- owner: input.owner,
3326
- repo: input.repo,
3327
- pull_number: input.pull_number,
3328
- title: input.title,
3329
- body: input.body,
3330
- 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
3331
3884
  });
3332
3885
  return {
3333
3886
  number: response.data.number,
@@ -3337,11 +3890,11 @@ function toGitHubClient(config) {
3337
3890
  draft: response.data.draft
3338
3891
  };
3339
3892
  },
3340
- async getPullRequest(input) {
3893
+ async getPullRequest(input3) {
3341
3894
  const response = await octokit.rest.pulls.get({
3342
- owner: input.owner,
3343
- repo: input.repo,
3344
- pull_number: input.pull_number
3895
+ owner: input3.owner,
3896
+ repo: input3.repo,
3897
+ pull_number: input3.pull_number
3345
3898
  });
3346
3899
  return {
3347
3900
  number: response.data.number,
@@ -3351,12 +3904,12 @@ function toGitHubClient(config) {
3351
3904
  draft: response.data.draft
3352
3905
  };
3353
3906
  },
3354
- async mergePullRequest(input) {
3907
+ async mergePullRequest(input3) {
3355
3908
  const response = await octokit.rest.pulls.merge({
3356
- owner: input.owner,
3357
- repo: input.repo,
3358
- pull_number: input.pull_number,
3359
- 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
3360
3913
  });
3361
3914
  return {
3362
3915
  merged: response.data.merged,
@@ -3372,7 +3925,9 @@ function readGitHubConfig(root) {
3372
3925
  const owner = typeof githubRaw.owner === "string" && githubRaw.owner.trim() ? githubRaw.owner.trim() : inferred?.owner;
3373
3926
  const repo = typeof githubRaw.repo === "string" && githubRaw.repo.trim() ? githubRaw.repo.trim() : inferred?.repo;
3374
3927
  if (!owner || !repo) {
3375
- 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
+ );
3376
3931
  }
3377
3932
  const tokenEnv = typeof githubRaw.token_env === "string" && githubRaw.token_env.trim() ? githubRaw.token_env.trim() : "GITHUB_TOKEN";
3378
3933
  const token = process.env[tokenEnv];
@@ -3733,7 +4288,7 @@ function registerTransitionCommand(program) {
3733
4288
  }
3734
4289
  console.warn(`[COOP][auth] override: user '${user}' forced transition_task on '${reference.id}'.`);
3735
4290
  }
3736
- const filePath = path15.join(root, ...reference.file.split("/"));
4291
+ const filePath = path16.join(root, ...reference.file.split("/"));
3737
4292
  const parsed = parseTaskFile11(filePath);
3738
4293
  const result = transition2(parsed.task, target, {
3739
4294
  actor: options.actor ?? user,
@@ -3779,19 +4334,19 @@ ${errors}`);
3779
4334
  }
3780
4335
 
3781
4336
  // src/commands/ui.ts
3782
- import fs11 from "fs";
3783
- import path16 from "path";
4337
+ import fs13 from "fs";
4338
+ import path17 from "path";
3784
4339
  import { createRequire } from "module";
3785
4340
  import { fileURLToPath } from "url";
3786
4341
  import { spawn } from "child_process";
3787
- import { IndexManager as IndexManager2 } from "@kitsy/coop-core";
4342
+ import { IndexManager as IndexManager3 } from "@kitsy/coop-core";
3788
4343
  function findPackageRoot(entryPath) {
3789
- let current = path16.dirname(entryPath);
4344
+ let current = path17.dirname(entryPath);
3790
4345
  while (true) {
3791
- if (fs11.existsSync(path16.join(current, "package.json"))) {
4346
+ if (fs13.existsSync(path17.join(current, "package.json"))) {
3792
4347
  return current;
3793
4348
  }
3794
- const parent = path16.dirname(current);
4349
+ const parent = path17.dirname(current);
3795
4350
  if (parent === current) {
3796
4351
  throw new Error(`Unable to locate package root for ${entryPath}.`);
3797
4352
  }
@@ -3822,7 +4377,7 @@ function openBrowser(url) {
3822
4377
  }
3823
4378
  function ensureIndex(repoRoot) {
3824
4379
  const directory = ensureCoopInitialized(repoRoot);
3825
- const manager = new IndexManager2(directory);
4380
+ const manager = new IndexManager3(directory);
3826
4381
  if (manager.is_stale() || !manager.load_indexed_graph()) {
3827
4382
  manager.build_full_index();
3828
4383
  }
@@ -3837,11 +4392,12 @@ function attachSignalForwarding(child) {
3837
4392
  process.once("SIGTERM", () => forward("SIGTERM"));
3838
4393
  }
3839
4394
  async function startUiServer(repoRoot, host, port, shouldOpen) {
4395
+ const project = resolveProject(repoRoot);
3840
4396
  ensureIndex(repoRoot);
3841
4397
  const uiRoot = resolveUiPackageRoot();
3842
- const requireFromUi = createRequire(path16.join(uiRoot, "package.json"));
4398
+ const requireFromUi = createRequire(path17.join(uiRoot, "package.json"));
3843
4399
  const vitePackageJson = requireFromUi.resolve("vite/package.json");
3844
- const viteBin = path16.join(path16.dirname(vitePackageJson), "bin", "vite.js");
4400
+ const viteBin = path17.join(path17.dirname(vitePackageJson), "bin", "vite.js");
3845
4401
  const url = `http://${host}:${port}`;
3846
4402
  console.log(`COOP UI: ${url}`);
3847
4403
  const child = spawn(process.execPath, [viteBin, "--host", host, "--port", String(port)], {
@@ -3850,6 +4406,7 @@ async function startUiServer(repoRoot, host, port, shouldOpen) {
3850
4406
  env: {
3851
4407
  ...process.env,
3852
4408
  COOP_REPO_ROOT: repoRoot,
4409
+ COOP_PROJECT_ID: project.id,
3853
4410
  COOP_UI_HOST: host,
3854
4411
  COOP_UI_PORT: String(port)
3855
4412
  }
@@ -4197,9 +4754,9 @@ function registerWebhookCommand(program) {
4197
4754
  }
4198
4755
 
4199
4756
  // src/merge-driver/merge-driver.ts
4200
- import fs12 from "fs";
4757
+ import fs14 from "fs";
4201
4758
  import os2 from "os";
4202
- import path17 from "path";
4759
+ import path18 from "path";
4203
4760
  import { spawnSync as spawnSync5 } from "child_process";
4204
4761
  import { stringifyFrontmatter as stringifyFrontmatter3, parseFrontmatterContent, parseYamlContent, stringifyYamlContent } from "@kitsy/coop-core";
4205
4762
  var STATUS_RANK = {
@@ -4258,16 +4815,16 @@ function chooseFieldValue(key, baseValue, oursValue, theirsValue, oursUpdated, t
4258
4815
  }
4259
4816
  function mergeTaskFrontmatter(base, ours, theirs) {
4260
4817
  const keys = /* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(ours), ...Object.keys(theirs)]);
4261
- const output = {};
4818
+ const output3 = {};
4262
4819
  const oursUpdated = asTimestamp(ours.updated);
4263
4820
  const theirsUpdated = asTimestamp(theirs.updated);
4264
4821
  for (const key of keys) {
4265
4822
  const merged = chooseFieldValue(key, base[key], ours[key], theirs[key], oursUpdated, theirsUpdated);
4266
4823
  if (merged !== void 0) {
4267
- output[key] = merged;
4824
+ output3[key] = merged;
4268
4825
  }
4269
4826
  }
4270
- return output;
4827
+ return output3;
4271
4828
  }
4272
4829
  function mergeTextWithGit(ancestor, ours, theirs) {
4273
4830
  const result = spawnSync5("git", ["merge-file", "-p", ours, ancestor, theirs], {
@@ -4282,33 +4839,33 @@ function mergeTextWithGit(ancestor, ours, theirs) {
4282
4839
  return { ok: false, output: stdout };
4283
4840
  }
4284
4841
  function mergeTaskFile(ancestorPath, oursPath, theirsPath) {
4285
- const ancestorRaw = fs12.readFileSync(ancestorPath, "utf8");
4286
- const oursRaw = fs12.readFileSync(oursPath, "utf8");
4287
- 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");
4288
4845
  const ancestor = parseTaskDocument(ancestorRaw, ancestorPath);
4289
4846
  const ours = parseTaskDocument(oursRaw, oursPath);
4290
4847
  const theirs = parseTaskDocument(theirsRaw, theirsPath);
4291
4848
  const mergedFrontmatter = mergeTaskFrontmatter(ancestor.frontmatter, ours.frontmatter, theirs.frontmatter);
4292
- const tempDir = fs12.mkdtempSync(path17.join(os2.tmpdir(), "coop-merge-body-"));
4849
+ const tempDir = fs14.mkdtempSync(path18.join(os2.tmpdir(), "coop-merge-body-"));
4293
4850
  try {
4294
- const ancestorBody = path17.join(tempDir, "ancestor.md");
4295
- const oursBody = path17.join(tempDir, "ours.md");
4296
- const theirsBody = path17.join(tempDir, "theirs.md");
4297
- fs12.writeFileSync(ancestorBody, ancestor.body, "utf8");
4298
- fs12.writeFileSync(oursBody, ours.body, "utf8");
4299
- 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");
4300
4857
  const mergedBody = mergeTextWithGit(ancestorBody, oursBody, theirsBody);
4301
- const output = stringifyFrontmatter3(mergedFrontmatter, mergedBody.output);
4302
- fs12.writeFileSync(oursPath, output, "utf8");
4858
+ const output3 = stringifyFrontmatter3(mergedFrontmatter, mergedBody.output);
4859
+ fs14.writeFileSync(oursPath, output3, "utf8");
4303
4860
  return mergedBody.ok ? 0 : 1;
4304
4861
  } finally {
4305
- fs12.rmSync(tempDir, { recursive: true, force: true });
4862
+ fs14.rmSync(tempDir, { recursive: true, force: true });
4306
4863
  }
4307
4864
  }
4308
4865
  function mergeDeliveryFile(ancestorPath, oursPath, theirsPath) {
4309
- const ancestor = parseYamlContent(fs12.readFileSync(ancestorPath, "utf8"), ancestorPath);
4310
- const ours = parseYamlContent(fs12.readFileSync(oursPath, "utf8"), oursPath);
4311
- 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);
4312
4869
  const oursUpdated = asTimestamp(ours.updated);
4313
4870
  const theirsUpdated = asTimestamp(theirs.updated);
4314
4871
  const base = ancestor;
@@ -4318,7 +4875,7 @@ function mergeDeliveryFile(ancestorPath, oursPath, theirsPath) {
4318
4875
  const value = chooseFieldValue(key, base[key], ours[key], theirs[key], oursUpdated, theirsUpdated);
4319
4876
  if (value !== void 0) merged[key] = value;
4320
4877
  }
4321
- fs12.writeFileSync(oursPath, stringifyYamlContent(merged), "utf8");
4878
+ fs14.writeFileSync(oursPath, stringifyYamlContent(merged), "utf8");
4322
4879
  return 0;
4323
4880
  }
4324
4881
  function runMergeDriver(kind, ancestorPath, oursPath, theirsPath) {
@@ -4331,9 +4888,9 @@ function runMergeDriver(kind, ancestorPath, oursPath, theirsPath) {
4331
4888
  // src/index.ts
4332
4889
  function readVersion() {
4333
4890
  const currentFile = fileURLToPath2(import.meta.url);
4334
- const packageJsonPath = path18.resolve(path18.dirname(currentFile), "..", "package.json");
4891
+ const packageJsonPath = path19.resolve(path19.dirname(currentFile), "..", "package.json");
4335
4892
  try {
4336
- const parsed = JSON.parse(fs13.readFileSync(packageJsonPath, "utf8"));
4893
+ const parsed = JSON.parse(fs15.readFileSync(packageJsonPath, "utf8"));
4337
4894
  return parsed.version ?? "0.0.0";
4338
4895
  } catch {
4339
4896
  return "0.0.0";
@@ -4350,6 +4907,7 @@ function createProgram() {
4350
4907
  program.version(readVersion());
4351
4908
  program.description("COOP CLI");
4352
4909
  program.option("--verbose", "Print stack traces for command errors");
4910
+ program.option("-p, --project <id>", "Select the active COOP project");
4353
4911
  registerInitCommand(program);
4354
4912
  registerCreateCommand(program);
4355
4913
  registerAssignCommand(program);
@@ -4363,6 +4921,7 @@ function createProgram() {
4363
4921
  registerLogCommand(program);
4364
4922
  registerMigrateCommand(program);
4365
4923
  registerPlanCommand(program);
4924
+ registerProjectCommand(program);
4366
4925
  registerRunCommand(program);
4367
4926
  registerServeCommand(program);
4368
4927
  registerStatusCommand(program);
@@ -4426,7 +4985,7 @@ async function runCli(argv = process.argv) {
4426
4985
  function isMainModule() {
4427
4986
  const entry = process.argv[1];
4428
4987
  if (!entry) return false;
4429
- return import.meta.url === pathToFileURL(path18.resolve(entry)).href;
4988
+ return path19.resolve(entry) === fileURLToPath2(import.meta.url);
4430
4989
  }
4431
4990
  if (isMainModule()) {
4432
4991
  await runCli(process.argv);