@kitsy/coop 2.1.0 → 2.1.1

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 +12 -2
  2. package/dist/index.js +550 -142
  3. package/package.json +4 -4
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/index.ts
4
4
  import fs18 from "fs";
5
- import path22 from "path";
5
+ import path24 from "path";
6
6
  import { fileURLToPath as fileURLToPath2 } from "url";
7
7
  import { Command } from "commander";
8
8
 
@@ -29,6 +29,28 @@ import {
29
29
  writeYamlFile
30
30
  } from "@kitsy/coop-core";
31
31
  var SEQ_MARKER = "COOPSEQTOKEN";
32
+ var DEFAULT_ID_NAMING_TEMPLATE = "<TYPE>-<TITLE16>-<SEQ>";
33
+ var DEFAULT_ARTIFACTS_DIR = "docs";
34
+ var DEFAULT_TITLE_TOKEN_LENGTH = 16;
35
+ var SEMANTIC_WORD_MAX = 4;
36
+ var SEMANTIC_STOP_WORDS = /* @__PURE__ */ new Set([
37
+ "A",
38
+ "AN",
39
+ "AND",
40
+ "AS",
41
+ "AT",
42
+ "BY",
43
+ "FOR",
44
+ "FROM",
45
+ "IN",
46
+ "INTO",
47
+ "OF",
48
+ "ON",
49
+ "OR",
50
+ "THE",
51
+ "TO",
52
+ "WITH"
53
+ ]);
32
54
  function resolveCoopHome() {
33
55
  const configured = process.env.COOP_HOME?.trim();
34
56
  if (configured) {
@@ -103,8 +125,9 @@ function readCoopConfig(root, projectId = resolveRequestedProject()) {
103
125
  ideaPrefix: "IDEA",
104
126
  taskPrefix: "PM",
105
127
  indexDataFormat: "yaml",
106
- idNamingTemplate: "<TYPE>-<USER>-<YYMMDD>-<RAND>",
128
+ idNamingTemplate: DEFAULT_ID_NAMING_TEMPLATE,
107
129
  idSeqPadding: 0,
130
+ artifactsDir: DEFAULT_ARTIFACTS_DIR,
108
131
  projectName: repoName || "COOP Workspace",
109
132
  projectId: repoName || "workspace",
110
133
  projectAliases: [],
@@ -126,15 +149,18 @@ function readCoopConfig(root, projectId = resolveRequestedProject()) {
126
149
  const indexDataFormat = indexDataRaw === "json" ? "json" : "yaml";
127
150
  const idRaw = typeof config.id === "object" && config.id !== null ? config.id : {};
128
151
  const idNamingTemplateRaw = idRaw.naming;
129
- const idNamingTemplate = typeof idNamingTemplateRaw === "string" && idNamingTemplateRaw.trim().length > 0 ? idNamingTemplateRaw.trim() : "<TYPE>-<USER>-<YYMMDD>-<RAND>";
152
+ const idNamingTemplate = typeof idNamingTemplateRaw === "string" && idNamingTemplateRaw.trim().length > 0 ? idNamingTemplateRaw.trim() : DEFAULT_ID_NAMING_TEMPLATE;
130
153
  const idSeqPaddingRaw = idRaw.seq_padding;
131
154
  const idSeqPadding = Number.isInteger(idSeqPaddingRaw) && Number(idSeqPaddingRaw) >= 0 ? Number(idSeqPaddingRaw) : 0;
155
+ const artifactsRaw = typeof config.artifacts === "object" && config.artifacts !== null ? config.artifacts : {};
156
+ const artifactsDir = typeof artifactsRaw.dir === "string" && artifactsRaw.dir.trim().length > 0 ? artifactsRaw.dir.trim() : DEFAULT_ARTIFACTS_DIR;
132
157
  return {
133
158
  ideaPrefix,
134
159
  taskPrefix,
135
160
  indexDataFormat,
136
161
  idNamingTemplate,
137
162
  idSeqPadding,
163
+ artifactsDir,
138
164
  projectName: projectName || "COOP Workspace",
139
165
  projectId: resolvedProjectId || "workspace",
140
166
  projectAliases,
@@ -249,6 +275,59 @@ function sanitizeTemplateValue(input3, fallback = "X") {
249
275
  const normalized = input3.toUpperCase().replace(/[^A-Z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-");
250
276
  return normalized || fallback;
251
277
  }
278
+ function sanitizeSemanticWord(input3) {
279
+ return input3.toUpperCase().replace(/[^A-Z0-9]+/g, "").trim();
280
+ }
281
+ function semanticWords(input3) {
282
+ const rawWords = input3.split(/[^a-zA-Z0-9]+/g).map(sanitizeSemanticWord).filter(Boolean);
283
+ if (rawWords.length <= 1) {
284
+ return rawWords;
285
+ }
286
+ const filtered = rawWords.filter((word) => !SEMANTIC_STOP_WORDS.has(word));
287
+ return filtered.length > 0 ? filtered : rawWords;
288
+ }
289
+ function semanticTitleToken(input3, maxLength = DEFAULT_TITLE_TOKEN_LENGTH) {
290
+ const safeMaxLength = Number.isFinite(maxLength) ? Math.max(4, Math.floor(maxLength)) : DEFAULT_TITLE_TOKEN_LENGTH;
291
+ const words = semanticWords(input3);
292
+ if (words.length === 0) {
293
+ return "UNTITLED";
294
+ }
295
+ if (words.length === 1) {
296
+ return words[0].slice(0, safeMaxLength) || "UNTITLED";
297
+ }
298
+ const segments = [];
299
+ let used = 0;
300
+ for (const word of words) {
301
+ const remaining = safeMaxLength - used - (segments.length > 0 ? 1 : 0);
302
+ if (remaining < 2) {
303
+ break;
304
+ }
305
+ const segment = word.slice(0, Math.min(SEMANTIC_WORD_MAX, remaining));
306
+ if (!segment) {
307
+ continue;
308
+ }
309
+ segments.push(segment);
310
+ used += segment.length + (segments.length > 1 ? 1 : 0);
311
+ }
312
+ if (segments.length > 0) {
313
+ return segments.join("-");
314
+ }
315
+ return words[0].slice(0, safeMaxLength) || "UNTITLED";
316
+ }
317
+ function namingTokenExamples(title = "Natural-language COOP command recommender") {
318
+ return {
319
+ "<TYPE>": "IDEA",
320
+ "<TITLE>": semanticTitleToken(title, DEFAULT_TITLE_TOKEN_LENGTH),
321
+ "<TITLE16>": semanticTitleToken(title, 16),
322
+ "<TITLE24>": semanticTitleToken(title, 24),
323
+ "<TRACK>": "MVP",
324
+ "<SEQ>": "1",
325
+ "<USER>": "PKVSI",
326
+ "<YYMMDD>": "260320",
327
+ "<RAND>": "AB12CD34",
328
+ "<PREFIX>": "PM"
329
+ };
330
+ }
252
331
  function sequenceForPattern(existingIds, prefix, suffix) {
253
332
  const escapedPrefix = prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
254
333
  const escapedSuffix = suffix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -284,7 +363,9 @@ function buildIdContext(root, config, context) {
284
363
  ENTITY: sanitizeTemplateValue(entityType, entityType),
285
364
  USER: normalizeIdPart(actor, "USER", 16),
286
365
  YYMMDD: shortDateToken(now),
287
- TITLE: sanitizeTemplateValue(title, "UNTITLED"),
366
+ TITLE: semanticTitleToken(title, DEFAULT_TITLE_TOKEN_LENGTH),
367
+ TITLE16: semanticTitleToken(title, 16),
368
+ TITLE24: semanticTitleToken(title, 24),
288
369
  TRACK: sanitizeTemplateValue(track || "UNASSIGNED", "UNASSIGNED"),
289
370
  STATUS: sanitizeTemplateValue(status || "TODO", "TODO"),
290
371
  TASK_TYPE: sanitizeTemplateValue(taskType || "FEATURE", "FEATURE"),
@@ -307,6 +388,9 @@ function replaceTemplateToken(token, contextMap) {
307
388
  if (upper === "USER") return contextMap.USER;
308
389
  if (upper === "TYPE" || upper === "ENTITY") return contextMap.TYPE;
309
390
  if (upper === "TITLE") return contextMap.TITLE;
391
+ if (/^TITLE\d+$/.test(upper)) {
392
+ return contextMap[upper] || contextMap.TITLE;
393
+ }
310
394
  if (upper === "TRACK") return contextMap.TRACK;
311
395
  if (upper === "TASK_TYPE") return contextMap.TASK_TYPE;
312
396
  if (upper === "STATUS") return contextMap.STATUS;
@@ -316,7 +400,7 @@ function replaceTemplateToken(token, contextMap) {
316
400
  return sanitizeTemplateValue(upper);
317
401
  }
318
402
  function renderNamingTemplate(template, contextMap) {
319
- const normalizedTemplate = template.trim().length > 0 ? template : "<TYPE>-<USER>-<YYMMDD>-<RAND>";
403
+ const normalizedTemplate = template.trim().length > 0 ? template : DEFAULT_ID_NAMING_TEMPLATE;
320
404
  const replaced = normalizedTemplate.replace(/<([^>]+)>/g, (_, token) => replaceTemplateToken(token, contextMap));
321
405
  return sanitizeTemplateValue(replaced, "COOP-ID");
322
406
  }
@@ -324,6 +408,17 @@ function defaultCoopAuthor(root) {
324
408
  const actor = inferActor(root);
325
409
  return actor.trim() || "unknown";
326
410
  }
411
+ function previewNamingTemplate(template, context, root = process.cwd()) {
412
+ const config = readCoopConfig(root);
413
+ const contextMap = buildIdContext(root, config, context);
414
+ const rendered = renderNamingTemplate(template, {
415
+ ...contextMap,
416
+ RAND: "AB12CD34",
417
+ YYMMDD: "260320",
418
+ USER: "PKVSI"
419
+ });
420
+ return rendered.replace(SEQ_MARKER, "1");
421
+ }
327
422
  function generateConfiguredId(root, existingIds, context) {
328
423
  const config = readCoopConfig(root);
329
424
  const contextMap = buildIdContext(root, config, context);
@@ -1550,6 +1645,7 @@ var INDEX_DATA_KEY = "index.data";
1550
1645
  var ID_NAMING_KEY = "id.naming";
1551
1646
  var AI_PROVIDER_KEY = "ai.provider";
1552
1647
  var AI_MODEL_KEY = "ai.model";
1648
+ var ARTIFACTS_DIR_KEY = "artifacts.dir";
1553
1649
  var PROJECT_NAME_KEY = "project.name";
1554
1650
  var PROJECT_ID_KEY = "project.id";
1555
1651
  var PROJECT_ALIASES_KEY = "project.aliases";
@@ -1558,6 +1654,7 @@ var SUPPORTED_KEYS = [
1558
1654
  ID_NAMING_KEY,
1559
1655
  AI_PROVIDER_KEY,
1560
1656
  AI_MODEL_KEY,
1657
+ ARTIFACTS_DIR_KEY,
1561
1658
  PROJECT_NAME_KEY,
1562
1659
  PROJECT_ID_KEY,
1563
1660
  PROJECT_ALIASES_KEY
@@ -1585,6 +1682,9 @@ function readAiModelValue(root) {
1585
1682
  function readProjectNameValue(root) {
1586
1683
  return readCoopConfig(root).projectName;
1587
1684
  }
1685
+ function readArtifactsDirValue(root) {
1686
+ return readCoopConfig(root).artifactsDir;
1687
+ }
1588
1688
  function readProjectIdValue(root) {
1589
1689
  return readCoopConfig(root).projectId;
1590
1690
  }
@@ -1653,6 +1753,18 @@ function writeProjectNameValue(root, value) {
1653
1753
  next.project = projectRaw;
1654
1754
  writeCoopConfig(root, next);
1655
1755
  }
1756
+ function writeArtifactsDirValue(root, value) {
1757
+ const nextValue = value.trim().replace(/\\/g, "/").replace(/\/+/g, "/").replace(/^\/+|\/+$/g, "");
1758
+ if (!nextValue) {
1759
+ throw new Error("Invalid value for artifacts.dir. Provide a non-empty relative path.");
1760
+ }
1761
+ const config = readCoopConfig(root).raw;
1762
+ const next = { ...config };
1763
+ const artifactsRaw = typeof next.artifacts === "object" && next.artifacts !== null ? { ...next.artifacts } : {};
1764
+ artifactsRaw.dir = nextValue;
1765
+ next.artifacts = artifactsRaw;
1766
+ writeCoopConfig(root, next);
1767
+ }
1656
1768
  function writeProjectIdValue(root, value) {
1657
1769
  const nextValue = value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-");
1658
1770
  if (!nextValue) {
@@ -1702,6 +1814,10 @@ function registerConfigCommand(program) {
1702
1814
  console.log(`${AI_PROVIDER_KEY}=${readAiProviderValue(root)}`);
1703
1815
  return;
1704
1816
  }
1817
+ if (keyPath === ARTIFACTS_DIR_KEY) {
1818
+ console.log(`${ARTIFACTS_DIR_KEY}=${readArtifactsDirValue(root)}`);
1819
+ return;
1820
+ }
1705
1821
  if (keyPath === PROJECT_NAME_KEY) {
1706
1822
  console.log(`${PROJECT_NAME_KEY}=${readProjectNameValue(root)}`);
1707
1823
  return;
@@ -1732,6 +1848,11 @@ function registerConfigCommand(program) {
1732
1848
  console.log(`${AI_PROVIDER_KEY}=${readAiProviderValue(root)}`);
1733
1849
  return;
1734
1850
  }
1851
+ if (keyPath === ARTIFACTS_DIR_KEY) {
1852
+ writeArtifactsDirValue(root, value);
1853
+ console.log(`${ARTIFACTS_DIR_KEY}=${readArtifactsDirValue(root)}`);
1854
+ return;
1855
+ }
1735
1856
  if (keyPath === PROJECT_NAME_KEY) {
1736
1857
  writeProjectNameValue(root, value);
1737
1858
  console.log(`${PROJECT_NAME_KEY}=${readProjectNameValue(root)}`);
@@ -2778,6 +2899,7 @@ function registerGraphCommand(program) {
2778
2899
  }
2779
2900
 
2780
2901
  // src/utils/ai-help.ts
2902
+ import path7 from "path";
2781
2903
  var catalog = {
2782
2904
  purpose: "COOP is a Git-native planning, backlog, execution, and orchestration CLI. It stores canonical data in .coop/projects/<project.id>/ and should be treated as the source of truth for work selection and lifecycle state.",
2783
2905
  selection_rules: [
@@ -2792,15 +2914,43 @@ var catalog = {
2792
2914
  "Multi-project workspaces require `--project <id>` or `coop project use <id>`.",
2793
2915
  "Canonical storage is under `.coop/projects/<project.id>/...`."
2794
2916
  ],
2917
+ allowed_lifecycle_commands: [
2918
+ "coop start task <id>",
2919
+ "coop review task <id>",
2920
+ "coop complete task <id>",
2921
+ "coop block task <id>",
2922
+ "coop unblock task <id>",
2923
+ "coop cancel task <id>",
2924
+ "coop reopen task <id>",
2925
+ "coop transition task <id> <status>"
2926
+ ],
2927
+ unsupported_command_warnings: [
2928
+ "Do not invent COOP commands such as `coop complete` when they are not present in `help-ai` output.",
2929
+ "If a desired lifecycle action is not represented by an allowed command, state that explicitly instead of paraphrasing.",
2930
+ "Quote exact COOP command names from `coop help-ai --format json` before proposing them."
2931
+ ],
2932
+ artifact_policy: {
2933
+ config_key: "artifacts.dir",
2934
+ default_dir: "docs",
2935
+ required_for: ["contract reviews", "spike findings", "audit notes", "planning artifacts"],
2936
+ guidance: [
2937
+ "Write agent-produced artifacts under the configured artifact directory.",
2938
+ "If `artifacts.dir` is unset, use `docs`.",
2939
+ "Do not invent ad hoc artifact paths outside the configured directory unless the user explicitly asks for a different location."
2940
+ ]
2941
+ },
2795
2942
  command_groups: [
2796
2943
  {
2797
2944
  name: "Workspace",
2798
2945
  description: "Initialize and select the active COOP project.",
2799
2946
  commands: [
2800
2947
  { usage: "coop init --yes", purpose: "Initialize a COOP workspace with repo-name defaults." },
2948
+ { usage: "coop init --naming <TYPE>-<TITLE16>-<SEQ>", purpose: "Initialize a workspace with an explicit ID naming template." },
2801
2949
  { usage: "coop project list", purpose: "List projects in the current workspace." },
2802
2950
  { usage: "coop project show", purpose: "Show the active project id, name, path, and layout." },
2803
- { usage: "coop project use <id>", purpose: "Switch the active project in a multi-project workspace." }
2951
+ { usage: "coop project use <id>", purpose: "Switch the active project in a multi-project workspace." },
2952
+ { usage: "coop naming", purpose: "Explain the current naming template, tokens, and examples." },
2953
+ { usage: 'coop naming preview "Natural-language COOP command recommender"', purpose: "Preview a semantic ID before creating an item." }
2804
2954
  ]
2805
2955
  },
2806
2956
  {
@@ -2838,7 +2988,13 @@ var catalog = {
2838
2988
  { usage: "coop next task", purpose: "Show the top ready task using the default track or full workspace context." },
2839
2989
  { usage: "coop graph next --delivery MVP", purpose: "Show the ready queue for a delivery with scores and blockers." },
2840
2990
  { usage: "coop pick task --delivery MVP --claim --actor dev1 --user lead-user", purpose: "Select the top ready task, optionally assign it, and move it to in_progress." },
2841
- { usage: "coop start task PM-101 --claim --actor dev1 --user lead-user", purpose: "Start a specific task or the top ready task if no id is provided." }
2991
+ { usage: "coop start task PM-101 --claim --actor dev1 --user lead-user", purpose: "Start a specific task or the top ready task if no id is provided." },
2992
+ { usage: "coop review task PM-101", purpose: "Move an in-progress task into in_review using a DX-friendly verb." },
2993
+ { usage: "coop complete task PM-101", purpose: "Move a task in review into done using a DX-friendly verb." },
2994
+ { usage: "coop block task PM-101", purpose: "Mark a task as blocked." },
2995
+ { usage: "coop unblock task PM-101", purpose: "Move a blocked task back to todo." },
2996
+ { usage: "coop cancel task PM-101", purpose: "Cancel a task." },
2997
+ { usage: "coop reopen task PM-101", purpose: "Move a canceled task back to todo." }
2842
2998
  ]
2843
2999
  },
2844
3000
  {
@@ -2858,6 +3014,7 @@ var catalog = {
2858
3014
  name: "Execute And Integrate",
2859
3015
  description: "Hand execution to an AI provider or code-agent CLI and expose COOP through MCP/API.",
2860
3016
  commands: [
3017
+ { usage: "coop help-ai --initial-prompt --strict --repo C:/path/to/repo --delivery MVP --command coop.cmd", purpose: "Generate a strict agent bootstrap prompt for a repository and delivery." },
2861
3018
  { usage: "coop config ai.provider codex_cli", purpose: "Use the installed Codex CLI as the execution/refinement backend." },
2862
3019
  { usage: "coop config ai.provider claude_cli", purpose: "Use the installed Claude CLI as the execution/refinement backend." },
2863
3020
  { usage: "coop config ai.provider gemini_cli", purpose: "Use the installed Gemini CLI as the execution/refinement backend." },
@@ -2883,9 +3040,82 @@ var catalog = {
2883
3040
  execution_model: [
2884
3041
  "Agents or services may send drafts through files or stdin, but COOP owns canonical writes.",
2885
3042
  "Use `coop create ... --from-file|--stdin` and `coop apply draft` instead of editing `.coop` task or idea files directly.",
2886
- "Use `coop log --last --verbose` when command execution fails and the concise error points to a stack trace."
3043
+ "Use `coop log --last --verbose` when command execution fails and the concise error points to a stack trace.",
3044
+ "If a workflow depends on stable human-readable IDs, inspect `coop naming` or `coop config id.naming` before creating items."
2887
3045
  ]
2888
3046
  };
3047
+ function normalizeRepoPath(repoPath) {
3048
+ return repoPath ? path7.resolve(repoPath) : process.cwd();
3049
+ }
3050
+ function formatSelectionCommand(commandName, delivery, track) {
3051
+ if (delivery) {
3052
+ return `${commandName} graph next --delivery ${delivery}`;
3053
+ }
3054
+ if (track) {
3055
+ return `${commandName} next task --track ${track}`;
3056
+ }
3057
+ return `${commandName} next task`;
3058
+ }
3059
+ function renderInitialPrompt(options = {}) {
3060
+ const rigour = options.rigour ?? "balanced";
3061
+ const repoPath = normalizeRepoPath(options.repoPath);
3062
+ const commandName = options.commandName?.trim() || "coop";
3063
+ const artifactsDir = options.artifactsDir?.trim() || "docs";
3064
+ const selectionCommand = formatSelectionCommand(commandName, options.delivery, options.track);
3065
+ const planningCommand = options.delivery ? `${commandName} plan delivery ${options.delivery}` : `${commandName} status`;
3066
+ const scopeLine = options.delivery ? `- Active delivery: ${options.delivery}` : options.track ? `- Active track: ${options.track}` : "- Work scope: use the workspace default track or delivery context from COOP";
3067
+ const lines = [
3068
+ `You are working in:`,
3069
+ repoPath,
3070
+ "",
3071
+ "COOP is the source of truth for backlog, planning, and execution in this repository.",
3072
+ "",
3073
+ "Before doing anything else:",
3074
+ `1. Run \`${commandName} help-ai --format markdown\``,
3075
+ `2. Run \`${commandName} project show\``,
3076
+ `3. Run \`${selectionCommand}\``,
3077
+ `4. Run \`${planningCommand}\``,
3078
+ "",
3079
+ "Context:",
3080
+ scopeLine,
3081
+ `- Artifact directory: ${artifactsDir}`,
3082
+ "",
3083
+ "Rules:",
3084
+ "- Learn COOP capabilities from `coop help-ai` before proposing commands.",
3085
+ "- Use only commands that actually exist in COOP. Do not invent command names.",
3086
+ "- Do not reprioritize work outside COOP unless the user explicitly overrides it.",
3087
+ "- Select only the first ready task from the COOP readiness output.",
3088
+ "- Inspect the selected task with `coop show task <id>` before implementation.",
3089
+ `- Write any contract-review, audit, or planning artifact under \`${artifactsDir}\` unless the user explicitly chooses another location.`
3090
+ ];
3091
+ if (rigour === "strict") {
3092
+ lines.push(
3093
+ "- Before mentioning a COOP command, verify the exact command name from `coop help-ai --format json`.",
3094
+ "- If a needed workflow is not supported by an exact COOP command, say that explicitly instead of inventing one.",
3095
+ "- Do not switch to another task unless COOP state changes or the user explicitly redirects you."
3096
+ );
3097
+ } else if (rigour === "balanced") {
3098
+ lines.push(
3099
+ "- Prefer exact COOP commands from `coop help-ai --format json` when describing next actions.",
3100
+ "- If COOP lacks a named command for a desired action, state the limitation plainly."
3101
+ );
3102
+ } else {
3103
+ lines.push("- Keep COOP as the authority for task selection and lifecycle state.");
3104
+ }
3105
+ lines.push(
3106
+ "",
3107
+ "Output format:",
3108
+ "- Active project",
3109
+ "- Selected task",
3110
+ "- Why selected",
3111
+ "- Task acceptance/tests/refs summary",
3112
+ "- Immediate implementation steps",
3113
+ "",
3114
+ "Do not start code changes until you have reported the selected task and plan."
3115
+ );
3116
+ return `${lines.join("\n")}
3117
+ `;
3118
+ }
2889
3119
  function renderAiHelp(format) {
2890
3120
  if (format === "json") {
2891
3121
  return `${JSON.stringify(catalog, null, 2)}
@@ -2906,6 +3136,24 @@ function renderAiHelp(format) {
2906
3136
  lines.push(bullet(rule));
2907
3137
  }
2908
3138
  lines.push("");
3139
+ lines.push(format === "markdown" ? "## Allowed Lifecycle Commands" : "Allowed Lifecycle Commands");
3140
+ for (const item of catalog.allowed_lifecycle_commands) {
3141
+ lines.push(bullet(`\`${item}\``));
3142
+ }
3143
+ lines.push("");
3144
+ lines.push(format === "markdown" ? "## Unsupported / Invented Command Warnings" : "Unsupported / Invented Command Warnings");
3145
+ for (const item of catalog.unsupported_command_warnings) {
3146
+ lines.push(bullet(item));
3147
+ }
3148
+ lines.push("");
3149
+ lines.push(format === "markdown" ? "## Artifact Policy" : "Artifact Policy");
3150
+ lines.push(`${format === "markdown" ? "- " : "- "}Config key: \`${catalog.artifact_policy.config_key}\``);
3151
+ lines.push(`${format === "markdown" ? "- " : "- "}Default dir: \`${catalog.artifact_policy.default_dir}\``);
3152
+ lines.push(`${format === "markdown" ? "- " : "- "}Required for: ${catalog.artifact_policy.required_for.join(", ")}`);
3153
+ for (const item of catalog.artifact_policy.guidance) {
3154
+ lines.push(bullet(item));
3155
+ }
3156
+ lines.push("");
2909
3157
  for (const group of catalog.command_groups) {
2910
3158
  lines.push(format === "markdown" ? `## ${group.name}` : group.name);
2911
3159
  lines.push(group.description);
@@ -2928,20 +3176,43 @@ function renderAiHelp(format) {
2928
3176
  return `${lines.join("\n")}
2929
3177
  `;
2930
3178
  }
3179
+ function renderAiInitialPrompt(options = {}) {
3180
+ return renderInitialPrompt(options);
3181
+ }
2931
3182
 
2932
3183
  // src/commands/help-ai.ts
2933
3184
  function registerHelpAiCommand(program) {
2934
- program.command("help-ai").description("Print AI-oriented COOP capability and usage help").option("--format <format>", "Output format: text | json | markdown", "text").action((options) => {
3185
+ program.command("help-ai").description("Print AI-oriented COOP capability and usage help").option("--format <format>", "Output format: text | json | markdown", "text").option("--initial-prompt", "Print an agent bootstrap prompt instead of the capability catalog").option("--repo <path>", "Repository path to embed in the initial prompt").option("--delivery <delivery>", "Delivery to embed in the initial prompt").option("--track <track>", "Track to embed in the initial prompt").option("--command <name>", "Command name to embed, e.g. coop or coop.cmd", "coop").option("--rigour <level>", "Prompt rigour: strict | balanced | light", "balanced").option("--strict", "Shortcut for --rigour strict").option("--balanced", "Shortcut for --rigour balanced").option("--light", "Shortcut for --rigour light").action((options) => {
2935
3186
  const format = options.format ?? "text";
2936
3187
  if (format !== "text" && format !== "json" && format !== "markdown") {
2937
3188
  throw new Error(`Unsupported help-ai format '${format}'. Expected text|json|markdown.`);
2938
3189
  }
2939
- console.log(renderAiHelp(format).trimEnd());
3190
+ const rigour = options.strict ? "strict" : options.light ? "light" : options.balanced ? "balanced" : options.rigour ?? "balanced";
3191
+ if (rigour !== "strict" && rigour !== "balanced" && rigour !== "light") {
3192
+ throw new Error(`Unsupported help-ai rigour '${rigour}'. Expected strict|balanced|light.`);
3193
+ }
3194
+ let artifactsDir = "docs";
3195
+ const repoRoot = options.repo ? resolveRepoRoot(options.repo) : resolveRepoRoot();
3196
+ try {
3197
+ ensureCoopInitialized(repoRoot);
3198
+ artifactsDir = readCoopConfig(repoRoot).artifactsDir;
3199
+ } catch {
3200
+ artifactsDir = "docs";
3201
+ }
3202
+ const output3 = options.initialPrompt ? renderAiInitialPrompt({
3203
+ repoPath: options.repo,
3204
+ delivery: options.delivery,
3205
+ track: options.track,
3206
+ commandName: options.command,
3207
+ rigour,
3208
+ artifactsDir
3209
+ }) : renderAiHelp(format);
3210
+ console.log(output3.trimEnd());
2940
3211
  });
2941
3212
  }
2942
3213
 
2943
3214
  // src/commands/index.ts
2944
- import path7 from "path";
3215
+ import path8 from "path";
2945
3216
  import { IndexManager as IndexManager2 } from "@kitsy/coop-core";
2946
3217
  function runStatus(options) {
2947
3218
  const root = resolveRepoRoot();
@@ -2951,7 +3222,7 @@ function runStatus(options) {
2951
3222
  const freshness = status.stale ? "stale" : "fresh";
2952
3223
  const existsText = status.exists ? "present" : "missing";
2953
3224
  console.log(`[COOP] index ${existsText}, ${freshness}`);
2954
- console.log(`[COOP] graph: ${path7.relative(root, status.graph_path)}`);
3225
+ console.log(`[COOP] graph: ${path8.relative(root, status.graph_path)}`);
2955
3226
  if (status.generated_at) {
2956
3227
  console.log(`[COOP] generated_at: ${status.generated_at}`);
2957
3228
  }
@@ -2974,7 +3245,7 @@ function runRebuild() {
2974
3245
  const graph = manager.build_full_index();
2975
3246
  const elapsed = Date.now() - start;
2976
3247
  console.log(`[COOP] index rebuilt: ${graph.nodes.size} tasks (${elapsed} ms)`);
2977
- console.log(`[COOP] graph: ${path7.relative(root, manager.graphPath)}`);
3248
+ console.log(`[COOP] graph: ${path8.relative(root, manager.graphPath)}`);
2978
3249
  }
2979
3250
  function registerIndexCommand(program) {
2980
3251
  const index = program.command("index").description("Index management commands");
@@ -2991,7 +3262,7 @@ function registerIndexCommand(program) {
2991
3262
 
2992
3263
  // src/commands/init.ts
2993
3264
  import fs8 from "fs";
2994
- import path10 from "path";
3265
+ import path11 from "path";
2995
3266
  import { spawnSync as spawnSync4 } from "child_process";
2996
3267
  import { createInterface } from "readline/promises";
2997
3268
  import { stdin as input, stdout as output } from "process";
@@ -2999,7 +3270,7 @@ import { CURRENT_SCHEMA_VERSION, write_schema_version } from "@kitsy/coop-core";
2999
3270
 
3000
3271
  // src/hooks/pre-commit.ts
3001
3272
  import fs6 from "fs";
3002
- import path8 from "path";
3273
+ import path9 from "path";
3003
3274
  import { spawnSync as spawnSync3 } from "child_process";
3004
3275
  import { detect_cycle, parseTaskContent, parseTaskFile as parseTaskFile7, validateStructural as validateStructural5 } from "@kitsy/coop-core";
3005
3276
  var HOOK_BLOCK_START = "# COOP_PRE_COMMIT_START";
@@ -3030,15 +3301,15 @@ function projectRootFromRelativePath(repoRoot, relativePath) {
3030
3301
  const normalized = toPosixPath2(relativePath);
3031
3302
  const projectMatch = /^\.coop\/projects\/([^/]+)\/tasks\/.+\.md$/i.exec(normalized);
3032
3303
  if (projectMatch?.[1]) {
3033
- return path8.join(repoRoot, ".coop", "projects", projectMatch[1]);
3304
+ return path9.join(repoRoot, ".coop", "projects", projectMatch[1]);
3034
3305
  }
3035
3306
  if (normalized.startsWith(".coop/tasks/")) {
3036
- return path8.join(repoRoot, ".coop");
3307
+ return path9.join(repoRoot, ".coop");
3037
3308
  }
3038
3309
  throw new Error(`Unsupported staged COOP task path '${relativePath}'.`);
3039
3310
  }
3040
3311
  function listTaskFilesForProject(projectRoot) {
3041
- const tasksDir = path8.join(projectRoot, "tasks");
3312
+ const tasksDir = path9.join(projectRoot, "tasks");
3042
3313
  if (!fs6.existsSync(tasksDir)) return [];
3043
3314
  const out = [];
3044
3315
  const stack = [tasksDir];
@@ -3046,12 +3317,12 @@ function listTaskFilesForProject(projectRoot) {
3046
3317
  const current = stack.pop();
3047
3318
  const entries = fs6.readdirSync(current, { withFileTypes: true });
3048
3319
  for (const entry of entries) {
3049
- const fullPath = path8.join(current, entry.name);
3320
+ const fullPath = path9.join(current, entry.name);
3050
3321
  if (entry.isDirectory()) {
3051
3322
  stack.push(fullPath);
3052
3323
  continue;
3053
3324
  }
3054
- if (entry.isFile() && path8.extname(entry.name).toLowerCase() === ".md") {
3325
+ if (entry.isFile() && path9.extname(entry.name).toLowerCase() === ".md") {
3055
3326
  out.push(fullPath);
3056
3327
  }
3057
3328
  }
@@ -3070,7 +3341,7 @@ function parseStagedTasks(repoRoot, relativePaths) {
3070
3341
  const errors = [];
3071
3342
  const staged = [];
3072
3343
  for (const relativePath of relativePaths) {
3073
- const absolutePath = path8.join(repoRoot, ...relativePath.split("/"));
3344
+ const absolutePath = path9.join(repoRoot, ...relativePath.split("/"));
3074
3345
  const projectRoot = projectRootFromRelativePath(repoRoot, relativePath);
3075
3346
  const stagedBlob = readGitBlob(repoRoot, `:${relativePath}`);
3076
3347
  if (!stagedBlob) {
@@ -3134,7 +3405,7 @@ function collectTasksForCycleCheck(projectRoot, stagedTasks) {
3134
3405
  }
3135
3406
  const tasks = [];
3136
3407
  for (const filePath of listTaskFilesForProject(projectRoot)) {
3137
- const normalized = toPosixPath2(path8.resolve(filePath));
3408
+ const normalized = toPosixPath2(path9.resolve(filePath));
3138
3409
  const stagedTask = stagedByPath.get(normalized);
3139
3410
  if (stagedTask) {
3140
3411
  tasks.push(stagedTask);
@@ -3164,7 +3435,7 @@ function runPreCommitChecks(repoRoot) {
3164
3435
  const graph = buildGraphForCycleCheck(tasks);
3165
3436
  const cycle = detect_cycle(graph);
3166
3437
  if (cycle) {
3167
- const projectLabel = toPosixPath2(path8.relative(repoRoot, projectRoot));
3438
+ const projectLabel = toPosixPath2(path9.relative(repoRoot, projectRoot));
3168
3439
  errors.push(`[COOP] Dependency cycle detected in ${projectLabel}: ${cycle.join(" -> ")}.`);
3169
3440
  }
3170
3441
  } catch (error) {
@@ -3196,8 +3467,8 @@ function hookScriptBlock() {
3196
3467
  ].join("\n");
3197
3468
  }
3198
3469
  function installPreCommitHook(repoRoot) {
3199
- const hookPath = path8.join(repoRoot, ".git", "hooks", "pre-commit");
3200
- const hookDir = path8.dirname(hookPath);
3470
+ const hookPath = path9.join(repoRoot, ".git", "hooks", "pre-commit");
3471
+ const hookDir = path9.dirname(hookPath);
3201
3472
  if (!fs6.existsSync(hookDir)) {
3202
3473
  return {
3203
3474
  installed: false,
@@ -3229,13 +3500,13 @@ function installPreCommitHook(repoRoot) {
3229
3500
 
3230
3501
  // src/hooks/post-merge-validate.ts
3231
3502
  import fs7 from "fs";
3232
- import path9 from "path";
3503
+ import path10 from "path";
3233
3504
  import { list_projects } from "@kitsy/coop-core";
3234
3505
  import { load_graph as load_graph4, validate_graph as validate_graph2 } from "@kitsy/coop-core";
3235
3506
  var HOOK_BLOCK_START2 = "# COOP_POST_MERGE_START";
3236
3507
  var HOOK_BLOCK_END2 = "# COOP_POST_MERGE_END";
3237
3508
  function runPostMergeValidate(repoRoot) {
3238
- const workspaceDir = path9.join(repoRoot, ".coop");
3509
+ const workspaceDir = path10.join(repoRoot, ".coop");
3239
3510
  if (!fs7.existsSync(workspaceDir)) {
3240
3511
  return {
3241
3512
  ok: true,
@@ -3288,8 +3559,8 @@ function postMergeHookBlock() {
3288
3559
  ].join("\n");
3289
3560
  }
3290
3561
  function installPostMergeHook(repoRoot) {
3291
- const hookPath = path9.join(repoRoot, ".git", "hooks", "post-merge");
3292
- const hookDir = path9.dirname(hookPath);
3562
+ const hookPath = path10.join(repoRoot, ".git", "hooks", "post-merge");
3563
+ const hookDir = path10.dirname(hookPath);
3293
3564
  if (!fs7.existsSync(hookDir)) {
3294
3565
  return {
3295
3566
  installed: false,
@@ -3339,7 +3610,7 @@ function aliasesYaml(aliases) {
3339
3610
  return `
3340
3611
  ${aliases.map((alias) => ` - "${alias}"`).join("\n")}`;
3341
3612
  }
3342
- function buildProjectConfig(projectId, projectName, projectAliases) {
3613
+ function buildProjectConfig(projectId, projectName, projectAliases, namingTemplate) {
3343
3614
  return `version: 2
3344
3615
  project:
3345
3616
  name: ${JSON.stringify(projectName)}
@@ -3353,7 +3624,7 @@ id_prefixes:
3353
3624
  run: "RUN"
3354
3625
 
3355
3626
  id:
3356
- naming: "<TYPE>-<USER>-<YYMMDD>-<RAND>"
3627
+ naming: ${JSON.stringify(namingTemplate)}
3357
3628
  seq_padding: 0
3358
3629
 
3359
3630
  defaults:
@@ -3482,7 +3753,7 @@ function writeIfMissing(filePath, content) {
3482
3753
  }
3483
3754
  }
3484
3755
  function ensureGitignoreEntry(root, entry) {
3485
- const gitignorePath = path10.join(root, ".gitignore");
3756
+ const gitignorePath = path11.join(root, ".gitignore");
3486
3757
  if (!fs8.existsSync(gitignorePath)) {
3487
3758
  fs8.writeFileSync(gitignorePath, `${entry}
3488
3759
  `, "utf8");
@@ -3497,7 +3768,7 @@ function ensureGitignoreEntry(root, entry) {
3497
3768
  }
3498
3769
  }
3499
3770
  function ensureGitattributesEntry(root, entry) {
3500
- const attrsPath = path10.join(root, ".gitattributes");
3771
+ const attrsPath = path11.join(root, ".gitattributes");
3501
3772
  if (!fs8.existsSync(attrsPath)) {
3502
3773
  fs8.writeFileSync(attrsPath, `${entry}
3503
3774
  `, "utf8");
@@ -3559,10 +3830,12 @@ async function promptInitIdentity(root, options) {
3559
3830
  throw new Error("Invalid project id. Use letters, numbers, and hyphens.");
3560
3831
  }
3561
3832
  const aliasesInput = options.aliases !== void 0 ? options.aliases : await ask2("Aliases (csv, optional)", "");
3833
+ const namingTemplate = options.naming?.trim() || await ask2("ID naming template", DEFAULT_ID_NAMING_TEMPLATE);
3562
3834
  return {
3563
3835
  projectName,
3564
3836
  projectId,
3565
- projectAliases: parseAliases2(aliasesInput)
3837
+ projectAliases: parseAliases2(aliasesInput),
3838
+ namingTemplate
3566
3839
  };
3567
3840
  } finally {
3568
3841
  rl.close();
@@ -3573,6 +3846,7 @@ async function resolveInitIdentity(root, options) {
3573
3846
  const fallbackName = options.name?.trim() || defaultName;
3574
3847
  const fallbackId = normalizeProjectId(options.id?.trim() || fallbackName) || repoIdentityId(root);
3575
3848
  const fallbackAliases = parseAliases2(options.aliases);
3849
+ const fallbackNaming = options.naming?.trim() || DEFAULT_ID_NAMING_TEMPLATE;
3576
3850
  const interactive = Boolean(options.interactive || !options.yes && input.isTTY && output.isTTY);
3577
3851
  if (interactive) {
3578
3852
  return promptInitIdentity(root, options);
@@ -3580,16 +3854,17 @@ async function resolveInitIdentity(root, options) {
3580
3854
  return {
3581
3855
  projectName: fallbackName,
3582
3856
  projectId: fallbackId,
3583
- projectAliases: fallbackAliases
3857
+ projectAliases: fallbackAliases,
3858
+ namingTemplate: fallbackNaming
3584
3859
  };
3585
3860
  }
3586
3861
  function registerInitCommand(program) {
3587
- 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) => {
3862
+ 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("--naming <template>", "ID naming template (default: <TYPE>-<TITLE16>-<SEQ>)").option("-y, --yes", "Use defaults without prompting").option("--interactive", "Prompt for missing project identity values").action(async (options) => {
3588
3863
  const root = resolveRepoRoot();
3589
3864
  const workspaceDir = coopWorkspaceDir(root);
3590
3865
  const identity = await resolveInitIdentity(root, options);
3591
3866
  const projectId = identity.projectId;
3592
- const projectRoot = path10.join(workspaceDir, "projects", projectId);
3867
+ const projectRoot = path11.join(workspaceDir, "projects", projectId);
3593
3868
  const dirs = [
3594
3869
  "ideas",
3595
3870
  "tasks",
@@ -3605,23 +3880,23 @@ function registerInitCommand(program) {
3605
3880
  "history/deliveries",
3606
3881
  ".index"
3607
3882
  ];
3608
- ensureDir(path10.join(workspaceDir, "projects"));
3883
+ ensureDir(path11.join(workspaceDir, "projects"));
3609
3884
  for (const dir of dirs) {
3610
- ensureDir(path10.join(projectRoot, dir));
3885
+ ensureDir(path11.join(projectRoot, dir));
3611
3886
  }
3612
3887
  writeIfMissing(
3613
- path10.join(projectRoot, "config.yml"),
3614
- buildProjectConfig(projectId, identity.projectName, identity.projectAliases)
3888
+ path11.join(projectRoot, "config.yml"),
3889
+ buildProjectConfig(projectId, identity.projectName, identity.projectAliases, identity.namingTemplate)
3615
3890
  );
3616
- if (!fs8.existsSync(path10.join(projectRoot, "schema-version"))) {
3891
+ if (!fs8.existsSync(path11.join(projectRoot, "schema-version"))) {
3617
3892
  write_schema_version(projectRoot, CURRENT_SCHEMA_VERSION);
3618
3893
  }
3619
- writeIfMissing(path10.join(projectRoot, "templates/task.md"), TASK_TEMPLATE);
3620
- writeIfMissing(path10.join(projectRoot, "templates/idea.md"), IDEA_TEMPLATE);
3621
- writeIfMissing(path10.join(projectRoot, "plugins/console-log.yml"), PLUGIN_CONSOLE_TEMPLATE);
3622
- writeIfMissing(path10.join(projectRoot, "plugins/github-pr.yml"), PLUGIN_GITHUB_TEMPLATE);
3623
- writeIfMissing(path10.join(workspaceDir, ".ignore"), COOP_IGNORE_TEMPLATE);
3624
- writeIfMissing(path10.join(workspaceDir, ".gitignore"), COOP_IGNORE_TEMPLATE);
3894
+ writeIfMissing(path11.join(projectRoot, "templates/task.md"), TASK_TEMPLATE);
3895
+ writeIfMissing(path11.join(projectRoot, "templates/idea.md"), IDEA_TEMPLATE);
3896
+ writeIfMissing(path11.join(projectRoot, "plugins/console-log.yml"), PLUGIN_CONSOLE_TEMPLATE);
3897
+ writeIfMissing(path11.join(projectRoot, "plugins/github-pr.yml"), PLUGIN_GITHUB_TEMPLATE);
3898
+ writeIfMissing(path11.join(workspaceDir, ".ignore"), COOP_IGNORE_TEMPLATE);
3899
+ writeIfMissing(path11.join(workspaceDir, ".gitignore"), COOP_IGNORE_TEMPLATE);
3625
3900
  writeWorkspaceConfig(root, { version: 2, current_project: projectId });
3626
3901
  ensureGitignoreEntry(root, ".coop/logs/");
3627
3902
  ensureGitignoreEntry(root, ".coop/tmp/");
@@ -3631,17 +3906,18 @@ function registerInitCommand(program) {
3631
3906
  const project = resolveProject(root, projectId);
3632
3907
  console.log("Initialized COOP workspace.");
3633
3908
  console.log(`- Root: ${root}`);
3634
- console.log(`- Workspace: ${path10.relative(root, workspaceDir)}`);
3635
- console.log(`- Project: ${project.id} (${path10.relative(root, project.root)})`);
3909
+ console.log(`- Workspace: ${path11.relative(root, workspaceDir)}`);
3910
+ console.log(`- Project: ${project.id} (${path11.relative(root, project.root)})`);
3636
3911
  console.log(`- Name: ${identity.projectName}`);
3637
3912
  console.log(`- Aliases: ${identity.projectAliases.length > 0 ? identity.projectAliases.join(", ") : "(none)"}`);
3913
+ console.log(`- ID naming: ${identity.namingTemplate}`);
3638
3914
  console.log(`- ${preCommitHook.message}`);
3639
3915
  if (preCommitHook.installed) {
3640
- console.log(`- Hook: ${path10.relative(root, preCommitHook.hookPath)}`);
3916
+ console.log(`- Hook: ${path11.relative(root, preCommitHook.hookPath)}`);
3641
3917
  }
3642
3918
  console.log(`- ${postMergeHook.message}`);
3643
3919
  if (postMergeHook.installed) {
3644
- console.log(`- Hook: ${path10.relative(root, postMergeHook.hookPath)}`);
3920
+ console.log(`- Hook: ${path11.relative(root, postMergeHook.hookPath)}`);
3645
3921
  }
3646
3922
  console.log(`- ${mergeDrivers}`);
3647
3923
  console.log("- Next steps:");
@@ -3651,9 +3927,70 @@ function registerInitCommand(program) {
3651
3927
  });
3652
3928
  }
3653
3929
 
3930
+ // src/commands/lifecycle.ts
3931
+ import path12 from "path";
3932
+ import { parseTaskFile as parseTaskFile8 } from "@kitsy/coop-core";
3933
+ var lifecycleVerbs = [
3934
+ {
3935
+ name: "review",
3936
+ description: "Move a task into review",
3937
+ targetStatus: "in_review"
3938
+ },
3939
+ {
3940
+ name: "complete",
3941
+ description: "Complete a task",
3942
+ targetStatus: "done"
3943
+ },
3944
+ {
3945
+ name: "block",
3946
+ description: "Mark a task as blocked",
3947
+ targetStatus: "blocked"
3948
+ },
3949
+ {
3950
+ name: "unblock",
3951
+ description: "Move a blocked task back to todo",
3952
+ targetStatus: "todo"
3953
+ },
3954
+ {
3955
+ name: "reopen",
3956
+ description: "Move a task back to todo",
3957
+ targetStatus: "todo"
3958
+ },
3959
+ {
3960
+ name: "cancel",
3961
+ description: "Cancel a task",
3962
+ targetStatus: "canceled"
3963
+ }
3964
+ ];
3965
+ function currentTaskSelection(root, id) {
3966
+ const reference = resolveReference(root, id, "task");
3967
+ const filePath = path12.join(root, ...reference.file.split("/"));
3968
+ const parsed = parseTaskFile8(filePath);
3969
+ return formatSelectedTask(
3970
+ {
3971
+ task: parsed.task,
3972
+ score: 0,
3973
+ readiness: parsed.task.status === "blocked" ? "blocked" : parsed.task.status === "in_review" ? "waiting_review" : parsed.task.status === "done" || parsed.task.status === "canceled" ? "done" : parsed.task.status === "in_progress" ? "in_progress" : "ready",
3974
+ fits_capacity: true,
3975
+ fits_wip: true
3976
+ },
3977
+ {}
3978
+ );
3979
+ }
3980
+ function registerLifecycleCommands(program) {
3981
+ for (const verb of lifecycleVerbs) {
3982
+ program.command(verb.name).description(verb.description).command("task").description(`${verb.description} by task id or alias`).argument("<id>", "Task ID or alias").option("--actor <actor>", "Actor performing the transition").option("--user <user>", "Current user for advisory authorization checks").option("--force", "Override advisory authorization checks").action(async (id, options) => {
3983
+ const root = resolveRepoRoot();
3984
+ console.log(currentTaskSelection(root, id));
3985
+ const result = await transitionTaskByReference(root, id, verb.targetStatus, options);
3986
+ console.log(`Updated ${result.task.id}: ${result.from} -> ${result.to}`);
3987
+ });
3988
+ }
3989
+ }
3990
+
3654
3991
  // src/commands/list.ts
3655
- import path11 from "path";
3656
- import { parseIdeaFile as parseIdeaFile3, parseTaskFile as parseTaskFile8 } from "@kitsy/coop-core";
3992
+ import path13 from "path";
3993
+ import { parseIdeaFile as parseIdeaFile3, parseTaskFile as parseTaskFile9 } from "@kitsy/coop-core";
3657
3994
  import chalk2 from "chalk";
3658
3995
 
3659
3996
  // src/utils/not-implemented.ts
@@ -3683,7 +4020,7 @@ function sortByIdAsc(items) {
3683
4020
  }
3684
4021
  function loadTasks(root) {
3685
4022
  return listTaskFiles(root).map((filePath) => ({
3686
- task: parseTaskFile8(filePath).task,
4023
+ task: parseTaskFile9(filePath).task,
3687
4024
  filePath
3688
4025
  }));
3689
4026
  }
@@ -3723,7 +4060,7 @@ function listTasks(options) {
3723
4060
  statusColor(entry.status),
3724
4061
  entry.priority,
3725
4062
  entry.track,
3726
- path11.relative(root, entry.filePath)
4063
+ path13.relative(root, entry.filePath)
3727
4064
  ])
3728
4065
  )
3729
4066
  );
@@ -3758,7 +4095,7 @@ function listIdeas(options) {
3758
4095
  statusColor(entry.status),
3759
4096
  entry.priority,
3760
4097
  entry.track,
3761
- path11.relative(root, entry.filePath)
4098
+ path13.relative(root, entry.filePath)
3762
4099
  ])
3763
4100
  )
3764
4101
  );
@@ -3783,24 +4120,24 @@ function registerListCommand(program) {
3783
4120
 
3784
4121
  // src/utils/logger.ts
3785
4122
  import fs9 from "fs";
3786
- import path12 from "path";
4123
+ import path14 from "path";
3787
4124
  function resolveRepoSafe(start = process.cwd()) {
3788
4125
  try {
3789
4126
  return resolveRepoRoot(start);
3790
4127
  } catch {
3791
- return path12.resolve(start);
4128
+ return path14.resolve(start);
3792
4129
  }
3793
4130
  }
3794
4131
  function resolveCliLogFile(start = process.cwd()) {
3795
4132
  const root = resolveRepoSafe(start);
3796
4133
  const workspace = coopWorkspaceDir(root);
3797
4134
  if (fs9.existsSync(workspace)) {
3798
- return path12.join(workspace, "logs", "cli.log");
4135
+ return path14.join(workspace, "logs", "cli.log");
3799
4136
  }
3800
- return path12.join(resolveCoopHome(), "logs", "cli.log");
4137
+ return path14.join(resolveCoopHome(), "logs", "cli.log");
3801
4138
  }
3802
4139
  function appendLogEntry(entry, logFile) {
3803
- fs9.mkdirSync(path12.dirname(logFile), { recursive: true });
4140
+ fs9.mkdirSync(path14.dirname(logFile), { recursive: true });
3804
4141
  fs9.appendFileSync(logFile, `${JSON.stringify(entry)}
3805
4142
  `, "utf8");
3806
4143
  }
@@ -3877,7 +4214,7 @@ function registerLogCommand(program) {
3877
4214
 
3878
4215
  // src/commands/migrate.ts
3879
4216
  import fs10 from "fs";
3880
- import path13 from "path";
4217
+ import path15 from "path";
3881
4218
  import { createInterface as createInterface2 } from "readline/promises";
3882
4219
  import { stdin as input2, stdout as output2 } from "process";
3883
4220
  import { CURRENT_SCHEMA_VERSION as CURRENT_SCHEMA_VERSION2, IndexManager as IndexManager3, migrate_repository, parseYamlFile as parseYamlFile2, writeYamlFile as writeYamlFile4 } from "@kitsy/coop-core";
@@ -3901,7 +4238,7 @@ function writeIfMissing2(filePath, content) {
3901
4238
  }
3902
4239
  }
3903
4240
  function ensureGitignoreEntry2(root, entry) {
3904
- const gitignorePath = path13.join(root, ".gitignore");
4241
+ const gitignorePath = path15.join(root, ".gitignore");
3905
4242
  if (!fs10.existsSync(gitignorePath)) {
3906
4243
  fs10.writeFileSync(gitignorePath, `${entry}
3907
4244
  `, "utf8");
@@ -3936,7 +4273,7 @@ function legacyWorkspaceProjectEntries(root) {
3936
4273
  "backlog",
3937
4274
  "plans",
3938
4275
  "releases"
3939
- ].filter((entry) => fs10.existsSync(path13.join(workspaceDir, entry)));
4276
+ ].filter((entry) => fs10.existsSync(path15.join(workspaceDir, entry)));
3940
4277
  }
3941
4278
  function normalizeProjectId2(value) {
3942
4279
  return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-");
@@ -3981,7 +4318,7 @@ async function promptProjectIdentity(defaults, options) {
3981
4318
  async function resolveMigrationIdentity(root, options) {
3982
4319
  const existing = readCoopConfig(root);
3983
4320
  const defaults = {
3984
- projectName: existing.projectName || path13.basename(root),
4321
+ projectName: existing.projectName || path15.basename(root),
3985
4322
  projectId: normalizeProjectId2(existing.projectId || repoIdentityId(root)) || repoIdentityId(root),
3986
4323
  projectAliases: options.aliases !== void 0 ? parseAliases3(options.aliases) : existing.projectAliases
3987
4324
  };
@@ -4004,7 +4341,7 @@ async function migrateWorkspaceLayout(root, options) {
4004
4341
  if (!fs10.existsSync(workspaceDir)) {
4005
4342
  throw new Error("Missing .coop directory. Run 'coop init' first.");
4006
4343
  }
4007
- const projectsDir = path13.join(workspaceDir, "projects");
4344
+ const projectsDir = path15.join(workspaceDir, "projects");
4008
4345
  const legacyEntries = legacyWorkspaceProjectEntries(root);
4009
4346
  if (legacyEntries.length === 0 && fs10.existsSync(projectsDir)) {
4010
4347
  console.log("[COOP] workspace layout already uses v2.");
@@ -4012,11 +4349,11 @@ async function migrateWorkspaceLayout(root, options) {
4012
4349
  }
4013
4350
  const identity = await resolveMigrationIdentity(root, options);
4014
4351
  const projectId = identity.projectId;
4015
- const projectRoot = path13.join(projectsDir, projectId);
4352
+ const projectRoot = path15.join(projectsDir, projectId);
4016
4353
  if (fs10.existsSync(projectRoot) && !options.force) {
4017
- throw new Error(`Project destination '${path13.relative(root, projectRoot)}' already exists. Re-run with --force.`);
4354
+ throw new Error(`Project destination '${path15.relative(root, projectRoot)}' already exists. Re-run with --force.`);
4018
4355
  }
4019
- const changed = legacyEntries.map((entry) => `${path13.join(".coop", entry)} -> ${path13.join(".coop", "projects", projectId, entry)}`);
4356
+ const changed = legacyEntries.map((entry) => `${path15.join(".coop", entry)} -> ${path15.join(".coop", "projects", projectId, entry)}`);
4020
4357
  changed.push(`.coop/config.yml -> workspace current_project=${projectId}`);
4021
4358
  console.log(`Workspace layout migration (${options.dryRun ? "DRY RUN" : "APPLY"})`);
4022
4359
  console.log(`- from: v1 flat layout`);
@@ -4035,17 +4372,17 @@ async function migrateWorkspaceLayout(root, options) {
4035
4372
  fs10.mkdirSync(projectsDir, { recursive: true });
4036
4373
  fs10.mkdirSync(projectRoot, { recursive: true });
4037
4374
  for (const entry of legacyEntries) {
4038
- const source = path13.join(workspaceDir, entry);
4039
- const destination = path13.join(projectRoot, entry);
4375
+ const source = path15.join(workspaceDir, entry);
4376
+ const destination = path15.join(projectRoot, entry);
4040
4377
  if (fs10.existsSync(destination)) {
4041
4378
  if (!options.force) {
4042
- throw new Error(`Destination '${path13.relative(root, destination)}' already exists.`);
4379
+ throw new Error(`Destination '${path15.relative(root, destination)}' already exists.`);
4043
4380
  }
4044
4381
  fs10.rmSync(destination, { recursive: true, force: true });
4045
4382
  }
4046
4383
  fs10.renameSync(source, destination);
4047
4384
  }
4048
- const movedConfigPath = path13.join(projectRoot, "config.yml");
4385
+ const movedConfigPath = path15.join(projectRoot, "config.yml");
4049
4386
  if (fs10.existsSync(movedConfigPath)) {
4050
4387
  const movedConfig = parseYamlFile2(movedConfigPath);
4051
4388
  const nextProject = typeof movedConfig.project === "object" && movedConfig.project !== null ? { ...movedConfig.project } : {};
@@ -4063,13 +4400,13 @@ async function migrateWorkspaceLayout(root, options) {
4063
4400
  }
4064
4401
  const workspace = readWorkspaceConfig(root);
4065
4402
  writeWorkspaceConfig(root, { ...workspace, version: 2, current_project: projectId });
4066
- writeIfMissing2(path13.join(workspaceDir, ".ignore"), COOP_IGNORE_TEMPLATE2);
4067
- writeIfMissing2(path13.join(workspaceDir, ".gitignore"), COOP_IGNORE_TEMPLATE2);
4403
+ writeIfMissing2(path15.join(workspaceDir, ".ignore"), COOP_IGNORE_TEMPLATE2);
4404
+ writeIfMissing2(path15.join(workspaceDir, ".gitignore"), COOP_IGNORE_TEMPLATE2);
4068
4405
  ensureGitignoreEntry2(root, ".coop/logs/");
4069
4406
  ensureGitignoreEntry2(root, ".coop/tmp/");
4070
4407
  const manager = new IndexManager3(projectRoot);
4071
4408
  manager.build_full_index();
4072
- console.log(`[COOP] migrated workspace to v2 at ${path13.relative(root, projectRoot)}`);
4409
+ console.log(`[COOP] migrated workspace to v2 at ${path15.relative(root, projectRoot)}`);
4073
4410
  }
4074
4411
  function registerMigrateCommand(program) {
4075
4412
  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) => {
@@ -4088,7 +4425,7 @@ function registerMigrateCommand(program) {
4088
4425
  if (report.changed_files.length > 0) {
4089
4426
  console.log("- changed files:");
4090
4427
  for (const filePath of report.changed_files) {
4091
- console.log(` - ${path13.relative(root, filePath)}`);
4428
+ console.log(` - ${path15.relative(root, filePath)}`);
4092
4429
  }
4093
4430
  }
4094
4431
  if (report.dry_run) {
@@ -4108,6 +4445,75 @@ function registerMigrateCommand(program) {
4108
4445
  });
4109
4446
  }
4110
4447
 
4448
+ // src/commands/naming.ts
4449
+ function printNamingOverview() {
4450
+ const root = resolveRepoRoot();
4451
+ const config = readCoopConfig(root);
4452
+ const sampleTitle = "Natural-language COOP command recommender";
4453
+ const sampleTaskTitle = "Implement billing payment contract review";
4454
+ console.log("COOP Naming");
4455
+ console.log(`Current template: ${config.idNamingTemplate}`);
4456
+ console.log(`Default template: ${DEFAULT_ID_NAMING_TEMPLATE}`);
4457
+ console.log("Tokens:");
4458
+ console.log(" <TYPE> entity type such as IDEA, TASK, DELIVERY");
4459
+ console.log(" <TITLE> semantic title token (defaults to TITLE16)");
4460
+ console.log(" <TITLE16> semantic title token capped to 16 chars");
4461
+ console.log(" <TITLE24> semantic title token capped to 24 chars");
4462
+ console.log(" <TRACK> task track");
4463
+ console.log(" <SEQ> sequential number within the rendered pattern");
4464
+ console.log(" <USER> actor/user namespace");
4465
+ console.log(" <YYMMDD> short date token");
4466
+ console.log(" <RAND> random uniqueness token");
4467
+ console.log(" <PREFIX> entity prefix override");
4468
+ console.log("Examples:");
4469
+ const tokenExamples = namingTokenExamples(sampleTitle);
4470
+ for (const [token, value] of Object.entries(tokenExamples)) {
4471
+ console.log(` ${token.padEnd(10)} ${value}`);
4472
+ }
4473
+ console.log(`Preview (${DEFAULT_ID_NAMING_TEMPLATE}): ${previewNamingTemplate(DEFAULT_ID_NAMING_TEMPLATE, {
4474
+ entityType: "idea",
4475
+ title: sampleTitle
4476
+ }, root)}`);
4477
+ console.log(`Preview (${config.idNamingTemplate}): ${previewNamingTemplate(config.idNamingTemplate, {
4478
+ entityType: "task",
4479
+ title: sampleTaskTitle,
4480
+ track: "mvp",
4481
+ status: "todo",
4482
+ taskType: "spike"
4483
+ }, root)}`);
4484
+ console.log("Try:");
4485
+ console.log(` coop naming preview "${sampleTitle}"`);
4486
+ console.log(' coop config id.naming "<TYPE>-<TITLE24>"');
4487
+ console.log(' coop config id.naming "<TRACK>-<SEQ>"');
4488
+ }
4489
+ function registerNamingCommand(program) {
4490
+ const naming = program.command("naming").description("Explain COOP ID naming templates and preview examples");
4491
+ naming.action(() => {
4492
+ printNamingOverview();
4493
+ });
4494
+ naming.command("preview").description("Preview the current or supplied naming template for a sample title").argument("<title>", "Sample title to render").option("--template <template>", "Override naming template").option("--entity <entity>", "Entity type: idea|task|track|delivery|run", "task").option("--track <track>", "Track token value").option("--status <status>", "Status token value").option("--task-type <taskType>", "Task type token value").action(
4495
+ (title, options) => {
4496
+ const root = resolveRepoRoot();
4497
+ const config = readCoopConfig(root);
4498
+ const entity = options.entity?.trim().toLowerCase() || "task";
4499
+ const template = options.template?.trim() || config.idNamingTemplate;
4500
+ console.log(
4501
+ previewNamingTemplate(
4502
+ template,
4503
+ {
4504
+ entityType: entity,
4505
+ title,
4506
+ track: options.track,
4507
+ status: options.status,
4508
+ taskType: options.taskType
4509
+ },
4510
+ root
4511
+ )
4512
+ );
4513
+ }
4514
+ );
4515
+ }
4516
+
4111
4517
  // src/commands/plan.ts
4112
4518
  import chalk3 from "chalk";
4113
4519
  import {
@@ -4343,7 +4749,7 @@ function registerPlanCommand(program) {
4343
4749
 
4344
4750
  // src/commands/project.ts
4345
4751
  import fs11 from "fs";
4346
- import path14 from "path";
4752
+ import path16 from "path";
4347
4753
  import { CURRENT_SCHEMA_VERSION as CURRENT_SCHEMA_VERSION3, write_schema_version as write_schema_version2 } from "@kitsy/coop-core";
4348
4754
  var TASK_TEMPLATE2 = `---
4349
4755
  id: TASK-001
@@ -4376,7 +4782,7 @@ linked_tasks: []
4376
4782
  ## Problem
4377
4783
  What problem are you solving?
4378
4784
  `;
4379
- var PROJECT_CONFIG_TEMPLATE = (projectId, projectName) => `version: 2
4785
+ var PROJECT_CONFIG_TEMPLATE = (projectId, projectName, namingTemplate) => `version: 2
4380
4786
  project:
4381
4787
  name: "${projectName}"
4382
4788
  id: "${projectId}"
@@ -4389,7 +4795,7 @@ id_prefixes:
4389
4795
  run: "RUN"
4390
4796
 
4391
4797
  id:
4392
- naming: "<TYPE>-<USER>-<YYMMDD>-<RAND>"
4798
+ naming: ${JSON.stringify(namingTemplate)}
4393
4799
  seq_padding: 0
4394
4800
 
4395
4801
  defaults:
@@ -4462,19 +4868,19 @@ function writeIfMissing3(filePath, content) {
4462
4868
  function normalizeProjectId3(value) {
4463
4869
  return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-");
4464
4870
  }
4465
- function createProject(root, projectId, projectName) {
4871
+ function createProject(root, projectId, projectName, namingTemplate = DEFAULT_ID_NAMING_TEMPLATE) {
4466
4872
  const workspaceDir = coopWorkspaceDir(root);
4467
- const projectRoot = path14.join(workspaceDir, "projects", projectId);
4468
- ensureDir2(path14.join(workspaceDir, "projects"));
4873
+ const projectRoot = path16.join(workspaceDir, "projects", projectId);
4874
+ ensureDir2(path16.join(workspaceDir, "projects"));
4469
4875
  for (const dir of PROJECT_DIRS) {
4470
- ensureDir2(path14.join(projectRoot, dir));
4876
+ ensureDir2(path16.join(projectRoot, dir));
4471
4877
  }
4472
- writeIfMissing3(path14.join(projectRoot, "config.yml"), PROJECT_CONFIG_TEMPLATE(projectId, projectName));
4473
- if (!fs11.existsSync(path14.join(projectRoot, "schema-version"))) {
4878
+ writeIfMissing3(path16.join(projectRoot, "config.yml"), PROJECT_CONFIG_TEMPLATE(projectId, projectName, namingTemplate));
4879
+ if (!fs11.existsSync(path16.join(projectRoot, "schema-version"))) {
4474
4880
  write_schema_version2(projectRoot, CURRENT_SCHEMA_VERSION3);
4475
4881
  }
4476
- writeIfMissing3(path14.join(projectRoot, "templates/task.md"), TASK_TEMPLATE2);
4477
- writeIfMissing3(path14.join(projectRoot, "templates/idea.md"), IDEA_TEMPLATE2);
4882
+ writeIfMissing3(path16.join(projectRoot, "templates/task.md"), TASK_TEMPLATE2);
4883
+ writeIfMissing3(path16.join(projectRoot, "templates/idea.md"), IDEA_TEMPLATE2);
4478
4884
  return projectRoot;
4479
4885
  }
4480
4886
  function registerProjectCommand(program) {
@@ -4502,7 +4908,7 @@ function registerProjectCommand(program) {
4502
4908
  }
4503
4909
  console.log(`id=${active.id}`);
4504
4910
  console.log(`name=${active.name}`);
4505
- console.log(`path=${path14.relative(root, active.root)}`);
4911
+ console.log(`path=${path16.relative(root, active.root)}`);
4506
4912
  console.log(`layout=${active.layout}`);
4507
4913
  });
4508
4914
  project.command("use").description("Set the active COOP project").argument("<id>", "Project id").action((id) => {
@@ -4516,7 +4922,7 @@ function registerProjectCommand(program) {
4516
4922
  writeWorkspaceConfig(root, { ...workspace, version: 2, current_project: match.id });
4517
4923
  console.log(`current_project=${match.id}`);
4518
4924
  });
4519
- 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) => {
4925
+ project.command("create").description("Create a new COOP project in the current workspace").argument("<id>", "Project id").option("--name <name>", "Project display name").option("--naming <template>", "ID naming template (default: <TYPE>-<TITLE16>-<SEQ>)").action((id, options) => {
4520
4926
  const root = resolveRepoRoot();
4521
4927
  const projectId = normalizeProjectId3(id);
4522
4928
  if (!projectId) {
@@ -4527,33 +4933,33 @@ function registerProjectCommand(program) {
4527
4933
  throw new Error(`Project '${projectId}' already exists.`);
4528
4934
  }
4529
4935
  const projectName = options.name?.trim() || repoDisplayName(root);
4530
- const projectRoot = createProject(root, projectId, projectName);
4936
+ const projectRoot = createProject(root, projectId, projectName, options.naming?.trim() || DEFAULT_ID_NAMING_TEMPLATE);
4531
4937
  const workspace = readWorkspaceConfig(root);
4532
4938
  writeWorkspaceConfig(root, {
4533
4939
  ...workspace,
4534
4940
  version: 2,
4535
4941
  current_project: workspace.current_project || projectId
4536
4942
  });
4537
- console.log(`Created project '${projectId}' at ${path14.relative(root, projectRoot)}`);
4943
+ console.log(`Created project '${projectId}' at ${path16.relative(root, projectRoot)}`);
4538
4944
  });
4539
4945
  }
4540
4946
 
4541
4947
  // src/commands/refine.ts
4542
4948
  import fs12 from "fs";
4543
- import path15 from "path";
4544
- import { parseIdeaFile as parseIdeaFile4, parseTaskFile as parseTaskFile9 } from "@kitsy/coop-core";
4949
+ import path17 from "path";
4950
+ import { parseIdeaFile as parseIdeaFile4, parseTaskFile as parseTaskFile10 } from "@kitsy/coop-core";
4545
4951
  import { create_provider_refinement_client, refine_idea_to_draft, refine_task_to_draft } from "@kitsy/coop-ai";
4546
4952
  function resolveTaskFile(root, idOrAlias) {
4547
4953
  const target = resolveReference(root, idOrAlias, "task");
4548
- return path15.join(root, ...target.file.split("/"));
4954
+ return path17.join(root, ...target.file.split("/"));
4549
4955
  }
4550
4956
  function resolveIdeaFile2(root, idOrAlias) {
4551
4957
  const target = resolveReference(root, idOrAlias, "idea");
4552
- return path15.join(root, ...target.file.split("/"));
4958
+ return path17.join(root, ...target.file.split("/"));
4553
4959
  }
4554
4960
  async function readSupplementalInput(root, options) {
4555
4961
  if (options.inputFile?.trim()) {
4556
- return fs12.readFileSync(path15.resolve(root, options.inputFile.trim()), "utf8");
4962
+ return fs12.readFileSync(path17.resolve(root, options.inputFile.trim()), "utf8");
4557
4963
  }
4558
4964
  if (options.stdin) {
4559
4965
  return readStdinText();
@@ -4571,7 +4977,7 @@ function loadAuthorityContext(root, refs) {
4571
4977
  for (const ref of refs ?? []) {
4572
4978
  const filePart = extractRefFile(ref);
4573
4979
  if (!filePart) continue;
4574
- const fullPath = path15.resolve(root, filePart);
4980
+ const fullPath = path17.resolve(root, filePart);
4575
4981
  if (!fs12.existsSync(fullPath) || !fs12.statSync(fullPath).isFile()) continue;
4576
4982
  out.push({
4577
4983
  ref,
@@ -4610,7 +5016,7 @@ function registerRefineCommand(program) {
4610
5016
  const root = resolveRepoRoot();
4611
5017
  const projectDir = ensureCoopInitialized(root);
4612
5018
  const taskFile = resolveTaskFile(root, id);
4613
- const parsed = parseTaskFile9(taskFile);
5019
+ const parsed = parseTaskFile10(taskFile);
4614
5020
  const supplemental = await readSupplementalInput(root, options);
4615
5021
  const client = create_provider_refinement_client(readCoopConfig(root).raw);
4616
5022
  const draft = await refine_task_to_draft(
@@ -4638,15 +5044,15 @@ function registerRefineCommand(program) {
4638
5044
  const written = applyRefinementDraft(root, projectDir, draft);
4639
5045
  console.log(`[COOP] applied draft from ${draftInput.source}: ${written.length} task file(s) updated`);
4640
5046
  for (const filePath of written) {
4641
- console.log(`- ${path15.relative(root, filePath)}`);
5047
+ console.log(`- ${path17.relative(root, filePath)}`);
4642
5048
  }
4643
5049
  });
4644
5050
  }
4645
5051
 
4646
5052
  // src/commands/run.ts
4647
5053
  import fs13 from "fs";
4648
- import path16 from "path";
4649
- import { load_graph as load_graph6, parseTaskFile as parseTaskFile10 } from "@kitsy/coop-core";
5054
+ import path18 from "path";
5055
+ import { load_graph as load_graph6, parseTaskFile as parseTaskFile11 } from "@kitsy/coop-core";
4650
5056
  import {
4651
5057
  build_contract,
4652
5058
  create_provider_agent_client,
@@ -4655,11 +5061,11 @@ import {
4655
5061
  } from "@kitsy/coop-ai";
4656
5062
  function loadTask(root, idOrAlias) {
4657
5063
  const target = resolveReference(root, idOrAlias, "task");
4658
- const taskFile = path16.join(root, ...target.file.split("/"));
5064
+ const taskFile = path18.join(root, ...target.file.split("/"));
4659
5065
  if (!fs13.existsSync(taskFile)) {
4660
5066
  throw new Error(`Task file not found: ${target.file}`);
4661
5067
  }
4662
- return parseTaskFile10(taskFile).task;
5068
+ return parseTaskFile11(taskFile).task;
4663
5069
  }
4664
5070
  function printContract(contract) {
4665
5071
  console.log(JSON.stringify(contract, null, 2));
@@ -4695,26 +5101,26 @@ function registerRunCommand(program) {
4695
5101
  on_progress: (message) => console.log(`[COOP] ${message}`)
4696
5102
  });
4697
5103
  if (result.status === "failed") {
4698
- throw new Error(`Run failed: ${result.run.id}. Log: ${path16.relative(root, result.log_path)}`);
5104
+ throw new Error(`Run failed: ${result.run.id}. Log: ${path18.relative(root, result.log_path)}`);
4699
5105
  }
4700
5106
  if (result.status === "paused") {
4701
5107
  console.log(`[COOP] run paused: ${result.run.id}`);
4702
- console.log(`[COOP] log: ${path16.relative(root, result.log_path)}`);
5108
+ console.log(`[COOP] log: ${path18.relative(root, result.log_path)}`);
4703
5109
  return;
4704
5110
  }
4705
5111
  console.log(`[COOP] run completed: ${result.run.id}`);
4706
- console.log(`[COOP] log: ${path16.relative(root, result.log_path)}`);
5112
+ console.log(`[COOP] log: ${path18.relative(root, result.log_path)}`);
4707
5113
  });
4708
5114
  }
4709
5115
 
4710
5116
  // src/server/api.ts
4711
5117
  import fs14 from "fs";
4712
5118
  import http2 from "http";
4713
- import path17 from "path";
5119
+ import path19 from "path";
4714
5120
  import {
4715
5121
  analyze_feasibility as analyze_feasibility2,
4716
5122
  load_graph as load_graph7,
4717
- parseTaskFile as parseTaskFile11,
5123
+ parseTaskFile as parseTaskFile12,
4718
5124
  resolve_external_dependencies
4719
5125
  } from "@kitsy/coop-core";
4720
5126
  function json(res, statusCode, payload) {
@@ -4756,12 +5162,12 @@ function taskSummary(graph, task, external = []) {
4756
5162
  };
4757
5163
  }
4758
5164
  function taskFileById(root, id) {
4759
- const tasksDir = path17.join(resolveProject(root).root, "tasks");
5165
+ const tasksDir = path19.join(resolveProject(root).root, "tasks");
4760
5166
  if (!fs14.existsSync(tasksDir)) return null;
4761
5167
  const entries = fs14.readdirSync(tasksDir, { withFileTypes: true });
4762
5168
  const target = `${id}.md`.toLowerCase();
4763
5169
  const match = entries.find((entry) => entry.isFile() && entry.name.toLowerCase() === target);
4764
- return match ? path17.join(tasksDir, match.name) : null;
5170
+ return match ? path19.join(tasksDir, match.name) : null;
4765
5171
  }
4766
5172
  function loadRemoteConfig(root) {
4767
5173
  const raw = readCoopConfig(root).raw;
@@ -4842,7 +5248,7 @@ function createApiServer(root, options = {}) {
4842
5248
  notFound(res, `Task '${taskId}' not found.`);
4843
5249
  return;
4844
5250
  }
4845
- const parsed = parseTaskFile11(filePath);
5251
+ const parsed = parseTaskFile12(filePath);
4846
5252
  const task = graph.nodes.get(parsed.task.id) ?? parsed.task;
4847
5253
  json(res, 200, {
4848
5254
  ...workspaceMeta(repoRoot),
@@ -4851,7 +5257,7 @@ function createApiServer(root, options = {}) {
4851
5257
  created: task.created,
4852
5258
  updated: task.updated,
4853
5259
  body: parsed.body,
4854
- file_path: path17.relative(repoRoot, filePath).replace(/\\/g, "/")
5260
+ file_path: path19.relative(repoRoot, filePath).replace(/\\/g, "/")
4855
5261
  }
4856
5262
  });
4857
5263
  return;
@@ -4940,8 +5346,8 @@ function registerServeCommand(program) {
4940
5346
 
4941
5347
  // src/commands/show.ts
4942
5348
  import fs15 from "fs";
4943
- import path18 from "path";
4944
- import { parseIdeaFile as parseIdeaFile5, parseTaskFile as parseTaskFile12 } from "@kitsy/coop-core";
5349
+ import path20 from "path";
5350
+ import { parseIdeaFile as parseIdeaFile5, parseTaskFile as parseTaskFile13 } from "@kitsy/coop-core";
4945
5351
  function stringify(value) {
4946
5352
  if (value === null || value === void 0) return "-";
4947
5353
  if (Array.isArray(value)) return value.length > 0 ? value.join(", ") : "-";
@@ -4959,7 +5365,7 @@ function pushListSection(lines, title, values) {
4959
5365
  }
4960
5366
  }
4961
5367
  function loadComputedFromIndex(root, taskId) {
4962
- const indexPath = path18.join(ensureCoopInitialized(root), ".index", "tasks.json");
5368
+ const indexPath = path20.join(ensureCoopInitialized(root), ".index", "tasks.json");
4963
5369
  if (!fs15.existsSync(indexPath)) {
4964
5370
  return null;
4965
5371
  }
@@ -5004,8 +5410,8 @@ function showTask(taskId) {
5004
5410
  const root = resolveRepoRoot();
5005
5411
  const coop = ensureCoopInitialized(root);
5006
5412
  const target = resolveReference(root, taskId, "task");
5007
- const taskFile = path18.join(root, ...target.file.split("/"));
5008
- const parsed = parseTaskFile12(taskFile);
5413
+ const taskFile = path20.join(root, ...target.file.split("/"));
5414
+ const parsed = parseTaskFile13(taskFile);
5009
5415
  const task = parsed.task;
5010
5416
  const body = parsed.body.trim();
5011
5417
  const computed = loadComputedFromIndex(root, task.id);
@@ -5023,7 +5429,7 @@ function showTask(taskId) {
5023
5429
  `Tags: ${stringify(task.tags)}`,
5024
5430
  `Created: ${task.created}`,
5025
5431
  `Updated: ${task.updated}`,
5026
- `File: ${path18.relative(root, taskFile)}`,
5432
+ `File: ${path20.relative(root, taskFile)}`,
5027
5433
  ""
5028
5434
  ];
5029
5435
  pushListSection(lines, "Acceptance", task.acceptance);
@@ -5048,7 +5454,7 @@ function showTask(taskId) {
5048
5454
  "Computed:"
5049
5455
  );
5050
5456
  if (!computed) {
5051
- lines.push(`index not built (${path18.relative(root, path18.join(coop, ".index", "tasks.json"))} missing)`);
5457
+ lines.push(`index not built (${path20.relative(root, path20.join(coop, ".index", "tasks.json"))} missing)`);
5052
5458
  } else {
5053
5459
  for (const [key, value] of Object.entries(computed)) {
5054
5460
  lines.push(`- ${key}: ${stringify(value)}`);
@@ -5060,7 +5466,7 @@ function showIdea(ideaId) {
5060
5466
  const root = resolveRepoRoot();
5061
5467
  ensureCoopInitialized(root);
5062
5468
  const target = resolveReference(root, ideaId, "idea");
5063
- const ideaFile = path18.join(root, ...target.file.split("/"));
5469
+ const ideaFile = path20.join(root, ...target.file.split("/"));
5064
5470
  const parsed = parseIdeaFile5(ideaFile);
5065
5471
  const idea = parsed.idea;
5066
5472
  const body = parsed.body.trim();
@@ -5074,7 +5480,7 @@ function showIdea(ideaId) {
5074
5480
  `Tags: ${stringify(idea.tags)}`,
5075
5481
  `Linked Tasks: ${stringify(idea.linked_tasks)}`,
5076
5482
  `Created: ${idea.created}`,
5077
- `File: ${path18.relative(root, ideaFile)}`,
5483
+ `File: ${path20.relative(root, ideaFile)}`,
5078
5484
  "",
5079
5485
  "Body:",
5080
5486
  body || "-"
@@ -5191,8 +5597,8 @@ function registerTransitionCommand(program) {
5191
5597
  }
5192
5598
 
5193
5599
  // src/commands/taskflow.ts
5194
- import path19 from "path";
5195
- import { parseTaskFile as parseTaskFile13 } from "@kitsy/coop-core";
5600
+ import path21 from "path";
5601
+ import { parseTaskFile as parseTaskFile14 } from "@kitsy/coop-core";
5196
5602
  function normalizeExecutor(value) {
5197
5603
  if (!value?.trim()) return void 0;
5198
5604
  const normalized = value.trim().toLowerCase();
@@ -5229,8 +5635,8 @@ async function claimAndStart(root, taskId, options) {
5229
5635
  }
5230
5636
  }
5231
5637
  const reference = resolveReference(root, taskId, "task");
5232
- const filePath = path19.join(root, ...reference.file.split("/"));
5233
- const parsed = parseTaskFile13(filePath);
5638
+ const filePath = path21.join(root, ...reference.file.split("/"));
5639
+ const parsed = parseTaskFile14(filePath);
5234
5640
  if (parsed.task.status === "in_progress") {
5235
5641
  console.log(`Task ${parsed.task.id} is already in_progress.`);
5236
5642
  return;
@@ -5276,8 +5682,8 @@ function registerTaskFlowCommands(program) {
5276
5682
  today: options.today
5277
5683
  }).entry.task.id;
5278
5684
  const reference = resolveReference(root, taskId, "task");
5279
- const filePath = path19.join(root, ...reference.file.split("/"));
5280
- const parsed = parseTaskFile13(filePath);
5685
+ const filePath = path21.join(root, ...reference.file.split("/"));
5686
+ const parsed = parseTaskFile14(filePath);
5281
5687
  console.log(
5282
5688
  formatSelectedTask(
5283
5689
  {
@@ -5301,18 +5707,18 @@ function registerTaskFlowCommands(program) {
5301
5707
 
5302
5708
  // src/commands/ui.ts
5303
5709
  import fs16 from "fs";
5304
- import path20 from "path";
5710
+ import path22 from "path";
5305
5711
  import { createRequire } from "module";
5306
5712
  import { fileURLToPath } from "url";
5307
5713
  import { spawn } from "child_process";
5308
5714
  import { IndexManager as IndexManager4 } from "@kitsy/coop-core";
5309
5715
  function findPackageRoot(entryPath) {
5310
- let current = path20.dirname(entryPath);
5716
+ let current = path22.dirname(entryPath);
5311
5717
  while (true) {
5312
- if (fs16.existsSync(path20.join(current, "package.json"))) {
5718
+ if (fs16.existsSync(path22.join(current, "package.json"))) {
5313
5719
  return current;
5314
5720
  }
5315
- const parent = path20.dirname(current);
5721
+ const parent = path22.dirname(current);
5316
5722
  if (parent === current) {
5317
5723
  throw new Error(`Unable to locate package root for ${entryPath}.`);
5318
5724
  }
@@ -5361,9 +5767,9 @@ async function startUiServer(repoRoot, host, port, shouldOpen) {
5361
5767
  const project = resolveProject(repoRoot);
5362
5768
  ensureIndex(repoRoot);
5363
5769
  const uiRoot = resolveUiPackageRoot();
5364
- const requireFromUi = createRequire(path20.join(uiRoot, "package.json"));
5770
+ const requireFromUi = createRequire(path22.join(uiRoot, "package.json"));
5365
5771
  const vitePackageJson = requireFromUi.resolve("vite/package.json");
5366
- const viteBin = path20.join(path20.dirname(vitePackageJson), "bin", "vite.js");
5772
+ const viteBin = path22.join(path22.dirname(vitePackageJson), "bin", "vite.js");
5367
5773
  const url = `http://${host}:${port}`;
5368
5774
  console.log(`COOP UI: ${url}`);
5369
5775
  const child = spawn(process.execPath, [viteBin, "--host", host, "--port", String(port)], {
@@ -5722,7 +6128,7 @@ function registerWebhookCommand(program) {
5722
6128
  // src/merge-driver/merge-driver.ts
5723
6129
  import fs17 from "fs";
5724
6130
  import os2 from "os";
5725
- import path21 from "path";
6131
+ import path23 from "path";
5726
6132
  import { spawnSync as spawnSync5 } from "child_process";
5727
6133
  import { stringifyFrontmatter as stringifyFrontmatter4, parseFrontmatterContent as parseFrontmatterContent3, parseYamlContent as parseYamlContent3, stringifyYamlContent as stringifyYamlContent2 } from "@kitsy/coop-core";
5728
6134
  var STATUS_RANK = {
@@ -5812,11 +6218,11 @@ function mergeTaskFile(ancestorPath, oursPath, theirsPath) {
5812
6218
  const ours = parseTaskDocument(oursRaw, oursPath);
5813
6219
  const theirs = parseTaskDocument(theirsRaw, theirsPath);
5814
6220
  const mergedFrontmatter = mergeTaskFrontmatter(ancestor.frontmatter, ours.frontmatter, theirs.frontmatter);
5815
- const tempDir = fs17.mkdtempSync(path21.join(os2.tmpdir(), "coop-merge-body-"));
6221
+ const tempDir = fs17.mkdtempSync(path23.join(os2.tmpdir(), "coop-merge-body-"));
5816
6222
  try {
5817
- const ancestorBody = path21.join(tempDir, "ancestor.md");
5818
- const oursBody = path21.join(tempDir, "ours.md");
5819
- const theirsBody = path21.join(tempDir, "theirs.md");
6223
+ const ancestorBody = path23.join(tempDir, "ancestor.md");
6224
+ const oursBody = path23.join(tempDir, "ours.md");
6225
+ const theirsBody = path23.join(tempDir, "theirs.md");
5820
6226
  fs17.writeFileSync(ancestorBody, ancestor.body, "utf8");
5821
6227
  fs17.writeFileSync(oursBody, ours.body, "utf8");
5822
6228
  fs17.writeFileSync(theirsBody, theirs.body, "utf8");
@@ -5854,7 +6260,7 @@ function runMergeDriver(kind, ancestorPath, oursPath, theirsPath) {
5854
6260
  // src/index.ts
5855
6261
  function readVersion() {
5856
6262
  const currentFile = fileURLToPath2(import.meta.url);
5857
- const packageJsonPath = path22.resolve(path22.dirname(currentFile), "..", "package.json");
6263
+ const packageJsonPath = path24.resolve(path24.dirname(currentFile), "..", "package.json");
5858
6264
  try {
5859
6265
  const parsed = JSON.parse(fs18.readFileSync(packageJsonPath, "utf8"));
5860
6266
  return parsed.version ?? "0.0.0";
@@ -5887,7 +6293,9 @@ function createProgram() {
5887
6293
  registerHelpAiCommand(program);
5888
6294
  registerIndexCommand(program);
5889
6295
  registerLogCommand(program);
6296
+ registerLifecycleCommands(program);
5890
6297
  registerMigrateCommand(program);
6298
+ registerNamingCommand(program);
5891
6299
  registerPlanCommand(program);
5892
6300
  registerProjectCommand(program);
5893
6301
  registerRefineCommand(program);
@@ -5954,7 +6362,7 @@ async function runCli(argv = process.argv) {
5954
6362
  function isMainModule() {
5955
6363
  const entry = process.argv[1];
5956
6364
  if (!entry) return false;
5957
- return path22.resolve(entry) === fileURLToPath2(import.meta.url);
6365
+ return path24.resolve(entry) === fileURLToPath2(import.meta.url);
5958
6366
  }
5959
6367
  if (isMainModule()) {
5960
6368
  await runCli(process.argv);