@kitsy/coop 2.2.5 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +17 -5
  2. package/dist/index.js +667 -185
  3. package/package.json +4 -4
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import fs22 from "fs";
4
+ import fs23 from "fs";
5
5
  import path26 from "path";
6
6
  import { fileURLToPath as fileURLToPath2 } from "url";
7
7
  import { Command } from "commander";
@@ -260,7 +260,11 @@ function readNamingTokens(rawConfig) {
260
260
  valuesRecord.values.filter((entry) => typeof entry === "string" && entry.trim().length > 0).map((entry) => normalizeNamingTokenValue(entry))
261
261
  )
262
262
  ).sort((a, b) => a.localeCompare(b)) : [];
263
- tokens[name] = { values };
263
+ const defaultValue = typeof valuesRecord.default === "string" && valuesRecord.default.trim().length > 0 ? normalizeNamingTokenValue(valuesRecord.default) : void 0;
264
+ if (defaultValue && values.length > 0 && !values.includes(defaultValue)) {
265
+ throw new Error(`Naming token '${name}' default '${defaultValue}' is not present in its allowed values.`);
266
+ }
267
+ tokens[name] = { values, default: defaultValue };
264
268
  }
265
269
  return tokens;
266
270
  }
@@ -480,7 +484,13 @@ function buildIdContext(root, config, context) {
480
484
  NAME_SLUG: slugifyLower(name || "item"),
481
485
  RAND: randomToken()
482
486
  };
483
- for (const [key, value] of Object.entries(fields)) {
487
+ const resolvedFields = { ...fields };
488
+ for (const [tokenName, tokenConfig] of Object.entries(config.idTokens)) {
489
+ if (!resolvedFields[tokenName] && tokenConfig.default) {
490
+ resolvedFields[tokenName] = tokenConfig.default;
491
+ }
492
+ }
493
+ for (const [key, value] of Object.entries(resolvedFields)) {
484
494
  if (!value || !value.trim()) continue;
485
495
  const tokenName = normalizeNamingTokenName(key);
486
496
  const upper = tokenName.toUpperCase();
@@ -549,7 +559,7 @@ function namingTokensForRoot(root) {
549
559
  function requiredCustomNamingTokens(root, entityType) {
550
560
  const config = readCoopConfig(root);
551
561
  const template = config.idNamingTemplates[entityType];
552
- return extractTemplateTokens(template).filter((token) => !isBuiltInNamingToken(token)).map((token) => token.toLowerCase());
562
+ return extractTemplateTokens(template).filter((token) => !isBuiltInNamingToken(token)).map((token) => token.toLowerCase()).filter((token) => !config.idTokens[token]?.default);
553
563
  }
554
564
  function generateStableShortId(root, entityType, primaryId, existingShortIds = []) {
555
565
  const config = readCoopConfig(root);
@@ -630,7 +640,9 @@ function generateConfiguredId(root, existingIds, context) {
630
640
  if (!config.idTokens[token.toLowerCase()]) {
631
641
  throw new Error(`Naming template references unknown token <${token}>.`);
632
642
  }
633
- if (!context.fields || !Object.keys(context.fields).some((key) => normalizeNamingTokenName(key) === token.toLowerCase())) {
643
+ const normalizedToken = token.toLowerCase();
644
+ const hasField = context.fields && Object.keys(context.fields).some((key) => normalizeNamingTokenName(key) === normalizedToken);
645
+ if (!hasField && !config.idTokens[normalizedToken]?.default) {
634
646
  throw new Error(`Naming template requires token <${token}>. Pass --${token.toLowerCase()} <value>.`);
635
647
  }
636
648
  }
@@ -1071,10 +1083,12 @@ function registerAliasCommand(program) {
1071
1083
  }
1072
1084
 
1073
1085
  // src/utils/taskflow.ts
1086
+ import fs5 from "fs";
1074
1087
  import path5 from "path";
1075
1088
  import {
1076
1089
  TaskStatus,
1077
1090
  check_permission,
1091
+ resolveTaskTransitions,
1078
1092
  load_auth_config,
1079
1093
  load_graph as load_graph2,
1080
1094
  parseTaskFile as parseTaskFile5,
@@ -1082,6 +1096,7 @@ import {
1082
1096
  schedule_next,
1083
1097
  transition as transition2,
1084
1098
  validateStructural as validateStructural3,
1099
+ writeYamlFile as writeYamlFile4,
1085
1100
  writeTask as writeTask4
1086
1101
  } from "@kitsy/coop-core";
1087
1102
 
@@ -1534,7 +1549,8 @@ async function transitionTaskFromGithub(root, parsed, actor, options = {}) {
1534
1549
  }
1535
1550
  const result = transition(parsed.task, "done", {
1536
1551
  actor,
1537
- dependencyStatuses: dependencyStatuses(root, parsed.task.id)
1552
+ dependencyStatuses: dependencyStatuses(root, parsed.task.id),
1553
+ config: readCoopConfig(root).raw
1538
1554
  });
1539
1555
  if (!result.success) {
1540
1556
  throw new Error(result.error ?? `Failed to transition task '${parsed.task.id}' to done.`);
@@ -2025,6 +2041,7 @@ function formatResolvedContextMessage(values) {
2025
2041
  }
2026
2042
 
2027
2043
  // src/utils/taskflow.ts
2044
+ var UNIVERSAL_RECOVERY_STATES = /* @__PURE__ */ new Set(["todo", "blocked", "canceled"]);
2028
2045
  function configDefaultTrack(root) {
2029
2046
  return sharedDefault(root, "track");
2030
2047
  }
@@ -2137,7 +2154,8 @@ async function assignTaskByReference(root, id, options) {
2137
2154
  throw new Error(`Task '${reference.id}' not found.`);
2138
2155
  }
2139
2156
  const user = options.user?.trim() || defaultCoopAuthor(root);
2140
- const auth = load_auth_config(readCoopConfig(root).raw);
2157
+ const configView = readCoopConfig(root);
2158
+ const auth = load_auth_config(configView.raw);
2141
2159
  const allowed = check_permission(user, "assign_task", { config: auth });
2142
2160
  if (!allowed) {
2143
2161
  if (!options.force) {
@@ -2210,6 +2228,96 @@ function dependencyStatusMapForTask(taskId, graph) {
2210
2228
  }
2211
2229
  return statuses;
2212
2230
  }
2231
+ function isDirectlyAllowed(from, to, transitions) {
2232
+ return transitions.get(from)?.has(to) ?? false;
2233
+ }
2234
+ function findRecoveryPaths(from, to, transitions) {
2235
+ const queue = [[from]];
2236
+ const paths = [];
2237
+ const bestDepth = /* @__PURE__ */ new Map([[from, 0]]);
2238
+ let shortest = null;
2239
+ while (queue.length > 0) {
2240
+ const currentPath = queue.shift();
2241
+ const current = currentPath[currentPath.length - 1];
2242
+ const depth = currentPath.length - 1;
2243
+ if (shortest !== null && depth >= shortest) {
2244
+ continue;
2245
+ }
2246
+ for (const next of transitions.get(current) ?? []) {
2247
+ if (next !== to && !UNIVERSAL_RECOVERY_STATES.has(next)) {
2248
+ continue;
2249
+ }
2250
+ if (currentPath.includes(next)) {
2251
+ continue;
2252
+ }
2253
+ const nextPath = [...currentPath, next];
2254
+ if (next === to) {
2255
+ shortest = nextPath.length - 1;
2256
+ paths.push(nextPath);
2257
+ continue;
2258
+ }
2259
+ const best = bestDepth.get(next);
2260
+ if (best !== void 0 && best <= depth + 1) {
2261
+ continue;
2262
+ }
2263
+ bestDepth.set(next, depth + 1);
2264
+ queue.push(nextPath);
2265
+ }
2266
+ }
2267
+ return shortest === null ? [] : paths.filter((path27) => path27.length - 1 === shortest);
2268
+ }
2269
+ function findUniqueRecoveryPath(from, to, transitions) {
2270
+ const paths = findRecoveryPaths(from, to, transitions);
2271
+ if (paths.length === 0) {
2272
+ return null;
2273
+ }
2274
+ if (paths.length > 1) {
2275
+ throw new Error(
2276
+ `No unique safe recovery path for ${from} -> ${to}. Manual sequence candidates: ${paths.map((path27) => path27.join(" -> ")).join(" | ")}.`
2277
+ );
2278
+ }
2279
+ return paths[0];
2280
+ }
2281
+ function writeTransitionAuditRecord(root, task, command, requestedTarget, pathTaken, actor) {
2282
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
2283
+ const fileSafeTimestamp = timestamp.replace(/[:.]/g, "-");
2284
+ const auditDir = path5.join(coopDir(root), "audits", "transitions", task.id);
2285
+ fs5.mkdirSync(auditDir, { recursive: true });
2286
+ const filePath = path5.join(auditDir, `${fileSafeTimestamp}.yml`);
2287
+ writeYamlFile4(filePath, {
2288
+ task_id: task.id,
2289
+ actor,
2290
+ requested_command: command,
2291
+ requested_target_status: requestedTarget,
2292
+ actual_transition_path: pathTaken,
2293
+ timestamp,
2294
+ source: "auto_hop_recovery"
2295
+ });
2296
+ return filePath;
2297
+ }
2298
+ async function emitTransitionPluginEvent(root, coopPath, task, fromStatus, toStatus, actor) {
2299
+ const event = {
2300
+ type: "task.transitioned",
2301
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2302
+ payload: {
2303
+ task_id: task.id,
2304
+ from: fromStatus,
2305
+ to: toStatus,
2306
+ actor
2307
+ }
2308
+ };
2309
+ const pluginResults = await run_plugins_for_event2(coopPath, event, {
2310
+ graph: load_graph2(coopPath),
2311
+ action_handlers: {
2312
+ github_pr: async (plugin, trigger, action) => executeGitHubPluginAction(root, plugin, trigger, action, task.id)
2313
+ }
2314
+ });
2315
+ for (const pluginResult of pluginResults) {
2316
+ if (!pluginResult.success) {
2317
+ console.warn(`[COOP][plugin:${pluginResult.plugin_id}] ${pluginResult.error ?? "trigger execution failed."}`);
2318
+ }
2319
+ }
2320
+ }
2213
2321
  async function transitionTaskByReference(root, id, status, options) {
2214
2322
  const target = status.toLowerCase();
2215
2323
  if (!Object.values(TaskStatus).includes(target)) {
@@ -2223,7 +2331,8 @@ async function transitionTaskByReference(root, id, status, options) {
2223
2331
  throw new Error(`Task '${reference.id}' not found.`);
2224
2332
  }
2225
2333
  const user = options.user?.trim() || defaultCoopAuthor(root);
2226
- const auth = load_auth_config(readCoopConfig(root).raw);
2334
+ const configView = readCoopConfig(root);
2335
+ const auth = load_auth_config(configView.raw);
2227
2336
  const allowed = check_permission(user, "transition_task", {
2228
2337
  config: auth,
2229
2338
  taskOwner: existing.assignee ?? null
@@ -2238,7 +2347,8 @@ async function transitionTaskByReference(root, id, status, options) {
2238
2347
  const parsed = parseTaskFile5(filePath);
2239
2348
  const result = transition2(parsed.task, target, {
2240
2349
  actor: options.actor ?? user,
2241
- dependencyStatuses: dependencyStatusMapForTask(reference.id, graph)
2350
+ dependencyStatuses: dependencyStatusMapForTask(reference.id, graph),
2351
+ config: configView.raw
2242
2352
  });
2243
2353
  if (!result.success) {
2244
2354
  throw new Error(result.error ?? "Transition failed.");
@@ -2254,28 +2364,87 @@ ${errors}`);
2254
2364
  raw: parsed.raw,
2255
2365
  filePath
2256
2366
  });
2257
- const event = {
2258
- type: "task.transitioned",
2259
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2260
- payload: {
2261
- task_id: result.task.id,
2262
- from: parsed.task.status,
2263
- to: result.task.status,
2264
- actor: options.actor ?? user
2265
- }
2266
- };
2267
- const pluginResults = await run_plugins_for_event2(coopPath, event, {
2268
- graph: load_graph2(coopPath),
2269
- action_handlers: {
2270
- github_pr: async (plugin, trigger, action) => executeGitHubPluginAction(root, plugin, trigger, action, result.task.id)
2271
- }
2367
+ await emitTransitionPluginEvent(root, coopPath, result.task, parsed.task.status, result.task.status, options.actor ?? user);
2368
+ return { task: result.task, from: parsed.task.status, to: result.task.status };
2369
+ }
2370
+ async function lifecycleTransitionTaskByReference(root, id, command, targetStatus, options) {
2371
+ const coopPath = coopDir(root);
2372
+ const graph = load_graph2(coopPath);
2373
+ const reference = resolveReference(root, id, "task");
2374
+ const existing = graph.nodes.get(reference.id);
2375
+ if (!existing) {
2376
+ throw new Error(`Task '${reference.id}' not found.`);
2377
+ }
2378
+ const user = options.user?.trim() || defaultCoopAuthor(root);
2379
+ const actor = options.actor?.trim() || user;
2380
+ const configView = readCoopConfig(root);
2381
+ const auth = load_auth_config(configView.raw);
2382
+ const allowed = check_permission(user, "transition_task", {
2383
+ config: auth,
2384
+ taskOwner: existing.assignee ?? null
2272
2385
  });
2273
- for (const pluginResult of pluginResults) {
2274
- if (!pluginResult.success) {
2275
- console.warn(`[COOP][plugin:${pluginResult.plugin_id}] ${pluginResult.error ?? "trigger execution failed."}`);
2386
+ if (!allowed) {
2387
+ if (!options.force) {
2388
+ throw new Error(`User '${user}' is not permitted to transition task '${reference.id}'. Re-run with --force to override.`);
2276
2389
  }
2390
+ console.warn(`[COOP][auth] override: user '${user}' forced transition_task on '${reference.id}'.`);
2277
2391
  }
2278
- return { task: result.task, from: parsed.task.status, to: result.task.status };
2392
+ const filePath = path5.join(root, ...reference.file.split("/"));
2393
+ const parsed = parseTaskFile5(filePath);
2394
+ const transitions = resolveTaskTransitions(configView.raw);
2395
+ const fromStatus = parsed.task.status;
2396
+ if (isDirectlyAllowed(fromStatus, targetStatus, transitions)) {
2397
+ const direct = await transitionTaskByReference(root, id, targetStatus, options);
2398
+ return { ...direct };
2399
+ }
2400
+ if (!UNIVERSAL_RECOVERY_STATES.has(targetStatus)) {
2401
+ const direct = await transitionTaskByReference(root, id, targetStatus, options);
2402
+ return { ...direct };
2403
+ }
2404
+ const recoveryPath = findUniqueRecoveryPath(fromStatus, targetStatus, transitions);
2405
+ if (!recoveryPath || recoveryPath.length < 2) {
2406
+ const allowedTargets = Array.from(transitions.get(fromStatus) ?? []);
2407
+ throw new Error(
2408
+ `Invalid transition ${fromStatus} -> ${targetStatus}. Allowed: ${allowedTargets.join(", ") || "none"}.`
2409
+ );
2410
+ }
2411
+ if (recoveryPath.length === 2) {
2412
+ const direct = await transitionTaskByReference(root, id, targetStatus, options);
2413
+ return { ...direct };
2414
+ }
2415
+ let currentTask = parsed.task;
2416
+ const dependencyStatuses2 = dependencyStatusMapForTask(reference.id, graph);
2417
+ for (const nextStatus of recoveryPath.slice(1)) {
2418
+ const result = transition2(currentTask, nextStatus, {
2419
+ actor,
2420
+ dependencyStatuses: dependencyStatuses2,
2421
+ config: configView.raw
2422
+ });
2423
+ if (!result.success) {
2424
+ throw new Error(result.error ?? "Transition failed.");
2425
+ }
2426
+ currentTask = result.task;
2427
+ }
2428
+ const structuralIssues = validateStructural3(currentTask, { filePath });
2429
+ if (structuralIssues.length > 0) {
2430
+ const errors = structuralIssues.map((issue) => `- ${issue.message}`).join("\n");
2431
+ throw new Error(`Updated task is structurally invalid:
2432
+ ${errors}`);
2433
+ }
2434
+ writeTask4(currentTask, {
2435
+ body: parsed.body,
2436
+ raw: parsed.raw,
2437
+ filePath
2438
+ });
2439
+ const auditPath = writeTransitionAuditRecord(root, currentTask, command, targetStatus, recoveryPath, actor);
2440
+ await emitTransitionPluginEvent(root, coopPath, currentTask, parsed.task.status, currentTask.status, actor);
2441
+ return {
2442
+ task: currentTask,
2443
+ from: parsed.task.status,
2444
+ to: currentTask.status,
2445
+ path: recoveryPath,
2446
+ auditPath
2447
+ };
2279
2448
  }
2280
2449
 
2281
2450
  // src/commands/assign.ts
@@ -2316,6 +2485,124 @@ function registerCommentCommand(program) {
2316
2485
  });
2317
2486
  }
2318
2487
 
2488
+ // src/utils/workflow.ts
2489
+ import { TaskStatus as TaskStatus2, resolveTaskTransitions as resolveTaskTransitions2, validateResolvedTaskTransitions } from "@kitsy/coop-core";
2490
+ function allTaskStatuses() {
2491
+ return Object.values(TaskStatus2);
2492
+ }
2493
+ function normalizeTaskStatusesCsv(value) {
2494
+ const entries = value.split(",").map((entry) => entry.trim().toLowerCase()).filter(Boolean);
2495
+ return normalizeTaskStatuses(entries);
2496
+ }
2497
+ function normalizeTaskStatuses(values) {
2498
+ const unique4 = Array.from(new Set(values.map((value) => value.trim().toLowerCase()).filter(Boolean)));
2499
+ for (const entry of unique4) {
2500
+ if (!allTaskStatuses().includes(entry)) {
2501
+ throw new Error(`Invalid task status '${entry}'. Expected one of ${allTaskStatuses().join(", ")}.`);
2502
+ }
2503
+ }
2504
+ return unique4;
2505
+ }
2506
+ function assertTaskStatus(value) {
2507
+ const normalized = value.trim().toLowerCase();
2508
+ if (!allTaskStatuses().includes(normalized)) {
2509
+ throw new Error(`Invalid task status '${value}'. Expected one of ${allTaskStatuses().join(", ")}.`);
2510
+ }
2511
+ return normalized;
2512
+ }
2513
+ function currentWorkflowConfig(root) {
2514
+ const rawConfig = readCoopConfig(root).raw;
2515
+ const workflow = typeof rawConfig.workflow === "object" && rawConfig.workflow !== null ? { ...rawConfig.workflow } : {};
2516
+ return { rawConfig, workflow };
2517
+ }
2518
+ function validateWorkflowConfigOrThrow(config) {
2519
+ const resolved = resolveTaskTransitions2(config);
2520
+ const errors = validateResolvedTaskTransitions(resolved);
2521
+ if (errors.length > 0) {
2522
+ throw new Error(errors.join("\n"));
2523
+ }
2524
+ }
2525
+ function readEffectiveWorkflowTransitions(root) {
2526
+ return resolveTaskTransitions2(readCoopConfig(root).raw);
2527
+ }
2528
+ function readWorkflowTransitionOverrides(root) {
2529
+ const workflow = typeof readCoopConfig(root).raw.workflow === "object" && readCoopConfig(root).raw.workflow !== null ? readCoopConfig(root).raw.workflow : {};
2530
+ return { ...workflow.task_transitions ?? {} };
2531
+ }
2532
+ function setWorkflowTransitionTargets(root, fromStatus, targets) {
2533
+ const from = assertTaskStatus(fromStatus);
2534
+ const normalizedTargets = normalizeTaskStatuses(targets);
2535
+ const { rawConfig, workflow } = currentWorkflowConfig(root);
2536
+ const nextTransitions = { ...workflow.task_transitions ?? {} };
2537
+ nextTransitions[from] = normalizedTargets;
2538
+ const nextWorkflow = {
2539
+ ...workflow,
2540
+ task_transitions: nextTransitions,
2541
+ track_overrides: workflow.track_overrides ?? {}
2542
+ };
2543
+ const nextConfig = {
2544
+ ...rawConfig,
2545
+ workflow: nextWorkflow
2546
+ };
2547
+ validateWorkflowConfigOrThrow(nextConfig);
2548
+ writeCoopConfig(root, nextConfig);
2549
+ return normalizedTargets;
2550
+ }
2551
+ function addWorkflowTransitionTarget(root, fromStatus, toStatus) {
2552
+ const from = assertTaskStatus(fromStatus);
2553
+ const target = assertTaskStatus(toStatus);
2554
+ const effective = readEffectiveWorkflowTransitions(root);
2555
+ const next = Array.from(/* @__PURE__ */ new Set([...effective.get(from) ?? [], target]));
2556
+ return setWorkflowTransitionTargets(root, from, next);
2557
+ }
2558
+ function removeWorkflowTransitionTarget(root, fromStatus, toStatus) {
2559
+ const from = assertTaskStatus(fromStatus);
2560
+ const target = assertTaskStatus(toStatus);
2561
+ const effective = readEffectiveWorkflowTransitions(root);
2562
+ const next = Array.from(effective.get(from) ?? []).filter((status) => status !== target);
2563
+ return setWorkflowTransitionTargets(root, from, next);
2564
+ }
2565
+ function resetWorkflowTransitionStatus(root, fromStatus) {
2566
+ const from = assertTaskStatus(fromStatus);
2567
+ const { rawConfig, workflow } = currentWorkflowConfig(root);
2568
+ const nextTransitions = { ...workflow.task_transitions ?? {} };
2569
+ delete nextTransitions[from];
2570
+ const nextWorkflow = {
2571
+ ...workflow,
2572
+ task_transitions: nextTransitions,
2573
+ track_overrides: workflow.track_overrides ?? {}
2574
+ };
2575
+ const nextConfig = {
2576
+ ...rawConfig,
2577
+ workflow: nextWorkflow
2578
+ };
2579
+ validateWorkflowConfigOrThrow(nextConfig);
2580
+ writeCoopConfig(root, nextConfig);
2581
+ }
2582
+ function resetAllWorkflowTransitions(root) {
2583
+ const { rawConfig, workflow } = currentWorkflowConfig(root);
2584
+ const nextWorkflow = {
2585
+ ...workflow,
2586
+ task_transitions: {},
2587
+ track_overrides: workflow.track_overrides ?? {}
2588
+ };
2589
+ const nextConfig = {
2590
+ ...rawConfig,
2591
+ workflow: nextWorkflow
2592
+ };
2593
+ validateWorkflowConfigOrThrow(nextConfig);
2594
+ writeCoopConfig(root, nextConfig);
2595
+ }
2596
+ function formatWorkflowTransitions(transitions, fromStatus) {
2597
+ const lines = [];
2598
+ const statuses = fromStatus ? [assertTaskStatus(fromStatus)] : allTaskStatuses();
2599
+ for (const status of statuses) {
2600
+ const targets = Array.from(transitions.get(status) ?? []);
2601
+ lines.push(`${status}: ${targets.join(", ") || "(none)"}`);
2602
+ }
2603
+ return lines.join("\n");
2604
+ }
2605
+
2319
2606
  // src/commands/config.ts
2320
2607
  var INDEX_DATA_KEY = "index.data";
2321
2608
  var ID_NAMING_KEY = "id.naming";
@@ -2325,6 +2612,7 @@ var ARTIFACTS_DIR_KEY = "artifacts.dir";
2325
2612
  var PROJECT_NAME_KEY = "project.name";
2326
2613
  var PROJECT_ID_KEY = "project.id";
2327
2614
  var PROJECT_ALIASES_KEY = "project.aliases";
2615
+ var WORKFLOW_TASK_TRANSITIONS_KEY = "workflow.task-transitions";
2328
2616
  var SUPPORTED_KEYS = [
2329
2617
  INDEX_DATA_KEY,
2330
2618
  ID_NAMING_KEY,
@@ -2333,8 +2621,12 @@ var SUPPORTED_KEYS = [
2333
2621
  ARTIFACTS_DIR_KEY,
2334
2622
  PROJECT_NAME_KEY,
2335
2623
  PROJECT_ID_KEY,
2336
- PROJECT_ALIASES_KEY
2624
+ PROJECT_ALIASES_KEY,
2625
+ WORKFLOW_TASK_TRANSITIONS_KEY
2337
2626
  ];
2627
+ function isWorkflowTaskTransitionsKey(keyPath) {
2628
+ return keyPath === WORKFLOW_TASK_TRANSITIONS_KEY || keyPath.startsWith(`${WORKFLOW_TASK_TRANSITIONS_KEY}.`);
2629
+ }
2338
2630
  function readIndexDataValue(root) {
2339
2631
  const { indexDataFormat } = readCoopConfig(root);
2340
2632
  return indexDataFormat;
@@ -2368,6 +2660,15 @@ function readProjectAliasesValue(root) {
2368
2660
  const aliases = readCoopConfig(root).projectAliases;
2369
2661
  return aliases.length > 0 ? aliases.join(",") : "(unset)";
2370
2662
  }
2663
+ function readWorkflowTaskTransitionsValue(root, keyPath) {
2664
+ const transitions = readWorkflowTransitionOverrides(root);
2665
+ if (keyPath === WORKFLOW_TASK_TRANSITIONS_KEY) {
2666
+ return JSON.stringify(transitions, null, 2);
2667
+ }
2668
+ const fromStatus = keyPath.slice(`${WORKFLOW_TASK_TRANSITIONS_KEY}.`.length);
2669
+ const allowed = Array.isArray(transitions[fromStatus]) ? transitions[fromStatus] : [];
2670
+ return allowed.filter((entry) => typeof entry === "string").join(",");
2671
+ }
2371
2672
  function writeIndexDataValue(root, value) {
2372
2673
  const nextValue = value.trim().toLowerCase();
2373
2674
  if (nextValue !== "yaml" && nextValue !== "json") {
@@ -2471,12 +2772,17 @@ function writeProjectAliasesValue(root, value) {
2471
2772
  next.project = projectRaw;
2472
2773
  writeCoopConfig(root, next);
2473
2774
  }
2775
+ function writeWorkflowTaskTransitionsValue(root, keyPath, value) {
2776
+ const fromStatus = keyPath.slice(`${WORKFLOW_TASK_TRANSITIONS_KEY}.`.length).trim().toLowerCase();
2777
+ const transitions = normalizeTaskStatusesCsv(value);
2778
+ setWorkflowTransitionTargets(root, fromStatus, transitions);
2779
+ }
2474
2780
  function registerConfigCommand(program) {
2475
2781
  program.command("config").description("Read or update COOP config values").argument("<key>", "Config key path").argument("[value]", "Optional value to set").action((key, value) => {
2476
2782
  const root = resolveRepoRoot();
2477
2783
  ensureCoopInitialized(root);
2478
2784
  const keyPath = key.trim();
2479
- if (!SUPPORTED_KEYS.includes(keyPath)) {
2785
+ if (!SUPPORTED_KEYS.includes(keyPath) && !isWorkflowTaskTransitionsKey(keyPath)) {
2480
2786
  throw new Error(`Unsupported config key '${keyPath}'. Supported keys: ${SUPPORTED_KEYS.join(", ")}`);
2481
2787
  }
2482
2788
  if (value === void 0) {
@@ -2508,6 +2814,10 @@ function registerConfigCommand(program) {
2508
2814
  console.log(`${PROJECT_ALIASES_KEY}=${readProjectAliasesValue(root)}`);
2509
2815
  return;
2510
2816
  }
2817
+ if (isWorkflowTaskTransitionsKey(keyPath)) {
2818
+ console.log(`${keyPath}=${readWorkflowTaskTransitionsValue(root, keyPath)}`);
2819
+ return;
2820
+ }
2511
2821
  console.log(`${AI_MODEL_KEY}=${readAiModelValue(root)}`);
2512
2822
  return;
2513
2823
  }
@@ -2546,26 +2856,31 @@ function registerConfigCommand(program) {
2546
2856
  console.log(`${PROJECT_ALIASES_KEY}=${readProjectAliasesValue(root)}`);
2547
2857
  return;
2548
2858
  }
2859
+ if (isWorkflowTaskTransitionsKey(keyPath)) {
2860
+ writeWorkflowTaskTransitionsValue(root, keyPath, value);
2861
+ console.log(`${keyPath}=${readWorkflowTaskTransitionsValue(root, keyPath)}`);
2862
+ return;
2863
+ }
2549
2864
  writeAiModelValue(root, value);
2550
2865
  console.log(`${AI_MODEL_KEY}=${readAiModelValue(root)}`);
2551
2866
  });
2552
2867
  }
2553
2868
 
2554
2869
  // src/commands/create.ts
2555
- import fs7 from "fs";
2870
+ import fs8 from "fs";
2556
2871
  import path8 from "path";
2557
2872
  import {
2558
2873
  DeliveryStatus,
2559
2874
  IdeaStatus as IdeaStatus2,
2560
2875
  parseIdeaFile as parseIdeaFile3,
2561
- TaskStatus as TaskStatus3,
2876
+ TaskStatus as TaskStatus4,
2562
2877
  TaskType as TaskType2,
2563
2878
  check_permission as check_permission2,
2564
2879
  load_auth_config as load_auth_config2,
2565
2880
  parseDeliveryFile as parseDeliveryFile2,
2566
2881
  parseTaskFile as parseTaskFile7,
2567
2882
  parseYamlFile as parseYamlFile3,
2568
- writeYamlFile as writeYamlFile4,
2883
+ writeYamlFile as writeYamlFile5,
2569
2884
  stringifyFrontmatter as stringifyFrontmatter4,
2570
2885
  writeTask as writeTask6
2571
2886
  } from "@kitsy/coop-core";
@@ -2657,7 +2972,7 @@ async function select(question, choices, defaultIndex = 0) {
2657
2972
  }
2658
2973
 
2659
2974
  // src/utils/idea-drafts.ts
2660
- import fs5 from "fs";
2975
+ import fs6 from "fs";
2661
2976
  import path6 from "path";
2662
2977
  import {
2663
2978
  IdeaStatus,
@@ -2744,15 +3059,15 @@ function writeIdeaFromDraft(root, projectDir, draft) {
2744
3059
  linked_tasks: draft.linked_tasks ?? []
2745
3060
  };
2746
3061
  const filePath = path6.join(projectDir, "ideas", `${id}.md`);
2747
- if (fs5.existsSync(filePath)) {
3062
+ if (fs6.existsSync(filePath)) {
2748
3063
  throw new Error(`Idea '${id}' already exists.`);
2749
3064
  }
2750
- fs5.writeFileSync(filePath, stringifyFrontmatter3(frontmatter, draft.body ?? ""), "utf8");
3065
+ fs6.writeFileSync(filePath, stringifyFrontmatter3(frontmatter, draft.body ?? ""), "utf8");
2751
3066
  return filePath;
2752
3067
  }
2753
3068
 
2754
3069
  // src/utils/refinement-drafts.ts
2755
- import fs6 from "fs";
3070
+ import fs7 from "fs";
2756
3071
  import path7 from "path";
2757
3072
  import {
2758
3073
  IndexManager,
@@ -2795,7 +3110,7 @@ function formatIgnoredFieldWarning2(source, context, keys) {
2795
3110
  }
2796
3111
  function refinementDir(projectDir) {
2797
3112
  const dir = path7.join(projectDir, "tmp", "refinements");
2798
- fs6.mkdirSync(dir, { recursive: true });
3113
+ fs7.mkdirSync(dir, { recursive: true });
2799
3114
  return dir;
2800
3115
  }
2801
3116
  function draftPath(projectDir, mode, sourceId) {
@@ -2833,8 +3148,8 @@ function assignCreateProposalIds(root, draft) {
2833
3148
  }
2834
3149
  function writeDraftFile(root, projectDir, draft, outputFile) {
2835
3150
  const filePath = outputFile?.trim() ? path7.resolve(root, outputFile.trim()) : draftPath(projectDir, draft.mode, draft.source.id);
2836
- fs6.mkdirSync(path7.dirname(filePath), { recursive: true });
2837
- fs6.writeFileSync(filePath, stringifyYamlContent(draft), "utf8");
3151
+ fs7.mkdirSync(path7.dirname(filePath), { recursive: true });
3152
+ fs7.writeFileSync(filePath, stringifyYamlContent(draft), "utf8");
2838
3153
  return filePath;
2839
3154
  }
2840
3155
  function printDraftSummary(root, draft, filePath) {
@@ -2966,7 +3281,7 @@ function applyCreateProposal(projectDir, proposal) {
2966
3281
  throw new Error(`Create proposal '${proposal.title}' is missing id.`);
2967
3282
  }
2968
3283
  const filePath = path7.join(projectDir, "tasks", `${id}.md`);
2969
- if (fs6.existsSync(filePath)) {
3284
+ if (fs7.existsSync(filePath)) {
2970
3285
  throw new Error(`Task '${id}' already exists.`);
2971
3286
  }
2972
3287
  const task = taskFromProposal({ ...proposal, id }, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10));
@@ -3028,7 +3343,7 @@ function applyRefinementDraft(root, projectDir, draft) {
3028
3343
  async function readDraftContent(root, options) {
3029
3344
  if (options.fromFile?.trim()) {
3030
3345
  const filePath = path7.resolve(root, options.fromFile.trim());
3031
- return { content: fs6.readFileSync(filePath, "utf8"), source: filePath };
3346
+ return { content: fs7.readFileSync(filePath, "utf8"), source: filePath };
3032
3347
  }
3033
3348
  if (options.stdin) {
3034
3349
  return { content: await readStdinText(), source: "<stdin>" };
@@ -3229,7 +3544,7 @@ function updateIdeaLinkedTasks(filePath, idea, raw, body, linked) {
3229
3544
  ...raw,
3230
3545
  linked_tasks: next
3231
3546
  };
3232
- fs7.writeFileSync(filePath, stringifyFrontmatter4(nextRaw, body), "utf8");
3547
+ fs8.writeFileSync(filePath, stringifyFrontmatter4(nextRaw, body), "utf8");
3233
3548
  }
3234
3549
  function makeTaskDraft(input2) {
3235
3550
  return {
@@ -3247,7 +3562,7 @@ function makeTaskDraft(input2) {
3247
3562
  }
3248
3563
  function registerCreateCommand(program) {
3249
3564
  const create = program.command("create").description("Create COOP entities");
3250
- create.command("task").description("Create a task").allowUnknownOption().allowExcessArguments().argument("[title]", "Task title").option("--id <id>", "Task id").option("--from <idea>", "Create task(s) from an idea id/alias").option("--ai", "Use AI-assisted decomposition for --from").option("--title <title>", "Task title").option("--type <type>", `Task type (${Object.values(TaskType2).join(", ")})`).option("--status <status>", `Task status (${Object.values(TaskStatus3).join(", ")})`).option("--track <track>", "Home/origin track id").option("--delivery <delivery>", "Primary delivery id").option("--priority <priority>", "Task priority").option("--body <body>", "Markdown body").option("--acceptance <text>", "Acceptance criterion; repeat the flag to add multiple entries", collectRepeatedValue, []).option("--tests-required <text>", "Required test; repeat the flag to add multiple entries", collectRepeatedValue, []).option("--authority-ref <ref>", "Authority document reference; repeat the flag to add multiple entries", collectRepeatedValue, []).option("--derived-ref <ref>", "Derived planning document reference; repeat the flag to add multiple entries", collectRepeatedValue, []).option("--from-file <path>", "Create task(s) from task draft/refinement draft file").option("--stdin", "Read task draft/refinement draft from stdin").option("--interactive", "Prompt for optional fields").action(async (titleArg, options) => {
3565
+ create.command("task").description("Create a task").allowUnknownOption().allowExcessArguments().argument("[title]", "Task title").option("--id <id>", "Task id").option("--from <idea>", "Create task(s) from an idea id/alias").option("--ai", "Use AI-assisted decomposition for --from").option("--title <title>", "Task title").option("--type <type>", `Task type (${Object.values(TaskType2).join(", ")})`).option("--status <status>", `Task status (${Object.values(TaskStatus4).join(", ")})`).option("--track <track>", "Home/origin track id").option("--delivery <delivery>", "Primary delivery id").option("--priority <priority>", "Task priority").option("--body <body>", "Markdown body").option("--acceptance <text>", "Acceptance criterion; repeat the flag to add multiple entries", collectRepeatedValue, []).option("--tests-required <text>", "Required test; repeat the flag to add multiple entries", collectRepeatedValue, []).option("--authority-ref <ref>", "Authority document reference; repeat the flag to add multiple entries", collectRepeatedValue, []).option("--derived-ref <ref>", "Derived planning document reference; repeat the flag to add multiple entries", collectRepeatedValue, []).option("--from-file <path>", "Create task(s) from task draft/refinement draft file").option("--stdin", "Read task draft/refinement draft from stdin").option("--interactive", "Prompt for optional fields").action(async (titleArg, options) => {
3251
3566
  const root = resolveRepoRoot();
3252
3567
  const coop = ensureCoopInitialized(root);
3253
3568
  const interactive = Boolean(options.interactive);
@@ -3509,7 +3824,7 @@ function registerCreateCommand(program) {
3509
3824
  throw new Error(`Invalid idea status '${status}'.`);
3510
3825
  }
3511
3826
  const filePath = path8.join(coop, "ideas", `${id}.md`);
3512
- fs7.writeFileSync(filePath, stringifyFrontmatter4(frontmatter, body), "utf8");
3827
+ fs8.writeFileSync(filePath, stringifyFrontmatter4(frontmatter, body), "utf8");
3513
3828
  console.log(`Created idea: ${path8.relative(root, filePath)}`);
3514
3829
  });
3515
3830
  create.command("track").description("Create a track").allowUnknownOption().allowExcessArguments().argument("[name]", "Track name").option("--id <id>", "Track id").option("--name <name>", "Track name").option("--profiles <profiles>", "Comma-separated capacity profile ids").option("--max-wip <number>", "Max concurrent tasks").option("--allowed-types <types>", "Comma-separated allowed task types").option("--interactive", "Prompt for optional fields").action(async (nameArg, options) => {
@@ -3566,7 +3881,7 @@ function registerCreateCommand(program) {
3566
3881
  }
3567
3882
  };
3568
3883
  const filePath = path8.join(coop, "tracks", `${id}.yml`);
3569
- writeYamlFile4(filePath, payload);
3884
+ writeYamlFile5(filePath, payload);
3570
3885
  console.log(`Created track: ${path8.relative(root, filePath)}`);
3571
3886
  });
3572
3887
  create.command("delivery").description("Create a delivery").allowUnknownOption().allowExcessArguments().argument("[name]", "Delivery name").option("--id <id>", "Delivery id").option("--name <name>", "Delivery name").option("--status <status>", `Delivery status (${Object.values(DeliveryStatus).join(", ")})`).option("--commit", "Create delivery directly in committed state").option("--target-date <date>", "Target date (YYYY-MM-DD)").option("--budget-hours <hours>", "Engineering budget hours").option("--budget-cost <usd>", "Cost budget in USD").option("--profiles <profiles>", "Comma-separated capacity profile ids").option("--scope <ids>", "Comma-separated task ids to include in scope").option("--exclude <ids>", "Comma-separated task ids to exclude from scope").option("--user <user>", "Current user for advisory authorization checks").option("--force", "Override advisory authorization checks").option("--interactive", "Prompt for optional fields").action(async (nameArg, options) => {
@@ -3700,7 +4015,7 @@ function registerCreateCommand(program) {
3700
4015
  }
3701
4016
  };
3702
4017
  const filePath = path8.join(coop, "deliveries", `${id}.yml`);
3703
- writeYamlFile4(filePath, payload);
4018
+ writeYamlFile5(filePath, payload);
3704
4019
  console.log(`Created delivery: ${path8.relative(root, filePath)}`);
3705
4020
  });
3706
4021
  }
@@ -3991,10 +4306,12 @@ var catalog = {
3991
4306
  selection_rules: [
3992
4307
  "Use `coop project show` first to confirm the active workspace and project.",
3993
4308
  "Use `coop use show` to inspect the current user-local working defaults for track, delivery, and version.",
4309
+ "When a command accepts an entity reference, the entity noun is optional: `coop show PM-100` and `coop show task PM-100` are equivalent. Keep the explicit noun only when it clarifies intent or when no shorthand exists yet.",
3994
4310
  "Before assigning `track` or `delivery` values to tasks, inspect or create named entities with `coop list tracks`, `coop list deliveries`, `coop create track`, and `coop create delivery`.",
3995
4311
  "Track and delivery references accept exact ids, stable short ids, and unique case-insensitive names.",
3996
- "Use `coop graph next --delivery <delivery>` or `coop next task` to choose work. Do not reprioritize outside COOP unless the user explicitly overrides it.",
4312
+ "Use `coop graph next --delivery <delivery>` or `coop next` to choose work. Do not reprioritize outside COOP unless the user explicitly overrides it.",
3997
4313
  "Commands resolve selection scope from: explicit CLI arg, then `coop use` working context, then shared project defaults.",
4314
+ "Use `coop list tasks --all` to ignore current `coop use` task-scope defaults temporarily while browsing the full backlog.",
3998
4315
  "Use `--track` for the workstream lens (home track or delivery_tracks). Use `--delivery` for release/scope membership.",
3999
4316
  "Use `coop show <id>` or `coop show task <id>` before implementation to read acceptance, tests_required, dependencies, origin refs, and task metadata.",
4000
4317
  "Use `coop refine idea` or `coop refine task` when the task lacks planning detail. COOP owns canonical writes; agents should not edit `.coop` files directly."
@@ -4006,20 +4323,33 @@ var catalog = {
4006
4323
  "Canonical storage is under `.coop/projects/<project.id>/...`."
4007
4324
  ],
4008
4325
  allowed_lifecycle_commands: [
4326
+ "coop start <id>",
4009
4327
  "coop start task <id>",
4328
+ "coop review <id>",
4010
4329
  "coop review task <id>",
4330
+ "coop complete <id>",
4011
4331
  "coop complete task <id>",
4332
+ "coop block <id>",
4012
4333
  "coop block task <id>",
4334
+ "coop unblock <id>",
4013
4335
  "coop unblock task <id>",
4336
+ "coop cancel <id>",
4014
4337
  "coop cancel task <id>",
4338
+ "coop reopen <id>",
4015
4339
  "coop reopen task <id>",
4016
4340
  "coop transition task <id> <status>"
4017
4341
  ],
4018
4342
  lifecycle_requirements: [
4019
- "`coop start task <id>` moves a ready task into `in_progress`.",
4020
- "`coop review task <id>` requires the task to already be `in_progress` and moves it to `in_review`.",
4021
- "`coop complete task <id>` requires the task to already be `in_review` and moves it to `done`.",
4022
- "Do not call `coop complete task <id>` directly from `in_progress` unless COOP explicitly adds that transition later."
4343
+ "`coop start <id>` and `coop start task <id>` move a ready task into `in_progress`.",
4344
+ "`coop start <id>` can also resume a blocked task directly into `in_progress` when `blocked -> in_progress` is allowed; that is a direct transition, not an auto-hop.",
4345
+ "`coop review <id>` / `coop review task <id>` require the task to already be `in_progress` and move it to `in_review`.",
4346
+ "`coop complete <id>` / `coop complete task <id>` requires the task to already be `in_review` and moves it to `done`.",
4347
+ "`coop block <id>`, `coop unblock <id>`, `coop cancel <id>`, and `coop reopen <id>` target universal recovery states by default.",
4348
+ "`coop unblock <id>` always means `blocked -> todo`; it does not restore a previous status.",
4349
+ "Actual multi-step recovery auto-hops write a transition audit record; ordinary direct transitions do not.",
4350
+ "Task status transitions can be customized per workspace through `coop workflow transitions ...` or `workflow.task_transitions` in COOP config.",
4351
+ "Do not call `coop complete task <id>` directly from `in_progress` unless COOP explicitly adds that transition later.",
4352
+ "Commands without an entity reference, such as `coop next`, remain task-first workflow surfaces rather than generic entity actions."
4023
4353
  ],
4024
4354
  unsupported_command_warnings: [
4025
4355
  "Do not invent COOP commands such as `coop complete` when they are not present in `help-ai` output.",
@@ -4061,7 +4391,13 @@ var catalog = {
4061
4391
  { usage: "coop naming token create proj", purpose: "Create a custom naming token." },
4062
4392
  { usage: "coop naming token remove proj", purpose: "Delete a custom naming token." },
4063
4393
  { usage: "coop naming token value add proj UX", purpose: "Register an allowed value for a naming token." },
4064
- { usage: "coop naming token value remove proj UX", purpose: "Remove an allowed value from a naming token." }
4394
+ { usage: "coop naming token value remove proj UX", purpose: "Remove an allowed value from a naming token." },
4395
+ { usage: "coop naming token default set proj UX", purpose: "Set a default value for a custom naming token so create/preview can omit the CLI flag." },
4396
+ { usage: "coop naming token default clear proj", purpose: "Clear a default custom-token value." },
4397
+ { usage: "coop config workflow.task-transitions.in_review done,todo,blocked", purpose: "Customize allowed task transitions for one source status." },
4398
+ { usage: "coop workflow transitions show", purpose: "Show the effective task transition map for the workspace." },
4399
+ { usage: "coop workflow transitions add blocked in_progress", purpose: "Add a task transition edge without editing config by hand." },
4400
+ { usage: "coop workflow transitions reset --all", purpose: "Reset all transition overrides back to the built-in defaults." }
4065
4401
  ]
4066
4402
  },
4067
4403
  {
@@ -4100,18 +4436,19 @@ var catalog = {
4100
4436
  name: "Select And Start",
4101
4437
  description: "Pick the next ready task and move it into execution.",
4102
4438
  commands: [
4103
- { usage: "coop next task", purpose: "Show the top ready task using the default track or full workspace context." },
4439
+ { usage: "coop next", purpose: "Show the top ready task using the default track or full workspace context." },
4104
4440
  { usage: "coop graph next --delivery MVP", purpose: "Show the ready queue for a delivery with scores and blockers." },
4105
- { usage: "coop pick task PM-101 --promote --claim --actor dev1 --user lead-user", purpose: "Select a specific task, optionally promote it in the current context, assign it, and move it to in_progress." },
4106
- { 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." },
4107
- { usage: "coop start task PM-101 --promote --claim --actor dev1 --user lead-user", purpose: "Start a specific task or the top ready task if no id is provided." },
4441
+ { usage: "coop pick PM-101 --promote --claim --actor dev1 --user lead-user", purpose: "Select a specific task, optionally promote it in the current context, assign it, and move it to in_progress." },
4442
+ { usage: "coop pick --delivery MVP --claim --actor dev1 --user lead-user", purpose: "Select the top ready task, optionally assign it, and move it to in_progress." },
4443
+ { usage: "coop start PM-101 --promote --claim --actor dev1 --user lead-user", purpose: "Start a specific task or the top ready task if no id is provided." },
4108
4444
  { usage: "coop promote task PM-101", purpose: "Promote a task using the current working track/version context; the promoted task moves to the top of that selection lens." },
4109
- { usage: "coop review task PM-101", purpose: "Move an in-progress task into in_review using a DX-friendly verb." },
4110
- { usage: "coop complete task PM-101", purpose: "Move a task in review into done using a DX-friendly verb." },
4111
- { usage: "coop block task PM-101", purpose: "Mark a task as blocked." },
4112
- { usage: "coop unblock task PM-101", purpose: "Move a blocked task back to todo." },
4113
- { usage: "coop cancel task PM-101", purpose: "Cancel a task." },
4114
- { usage: "coop reopen task PM-101", purpose: "Move a canceled task back to todo." }
4445
+ { usage: "coop review PM-101", purpose: "Move an in-progress task into in_review using a DX-friendly verb." },
4446
+ { usage: "coop complete PM-101", purpose: "Move a task in review into done using a DX-friendly verb." },
4447
+ { usage: "coop block PM-101", purpose: "Mark a task as blocked." },
4448
+ { usage: "coop unblock PM-101", purpose: "Move a blocked task back to todo." },
4449
+ { usage: "coop cancel PM-101", purpose: "Cancel a task." },
4450
+ { usage: "coop reopen PM-101", purpose: "Move a canceled task back to todo." },
4451
+ { usage: "coop workflow transitions show blocked", purpose: "Inspect the currently effective allowed targets for a source status." }
4115
4452
  ]
4116
4453
  },
4117
4454
  {
@@ -4119,6 +4456,8 @@ var catalog = {
4119
4456
  description: "Read backlog state, task details, and planning output.",
4120
4457
  commands: [
4121
4458
  { usage: "coop list tasks --status todo", purpose: "List tasks with filters." },
4459
+ { usage: "coop list tasks --completed", purpose: "List done tasks with a convenience flag instead of `--status done`." },
4460
+ { usage: "coop list tasks --all", purpose: "List tasks across all tracks/deliveries/versions without applying current `coop use` defaults." },
4122
4461
  { usage: "coop list tracks", purpose: "List valid named tracks." },
4123
4462
  { usage: "coop list deliveries", purpose: "List valid named deliveries." },
4124
4463
  { usage: "coop list tasks --track MVP --delivery MVP --ready --columns id,title,p,assignee,score", purpose: "List ready tasks with lean columns and score visible." },
@@ -4139,6 +4478,7 @@ var catalog = {
4139
4478
  { usage: "coop update PM-101 --authority-ref-set docs/spec.md#auth --derived-ref-set docs/plan.md#scope", purpose: "Replace the full origin reference lists without editing task files directly." },
4140
4479
  { usage: "coop update PM-101 --deps-set PM-201 --deps-set PM-202 --delivery-tracks-set MVP", purpose: "Replace identifier-based list fields such as depends_on and delivery_tracks." },
4141
4480
  { usage: "coop update IDEA-101 --linked-tasks-set PM-101 --linked-tasks-set PM-102", purpose: "Replace the full linked_tasks list on an idea." },
4481
+ { usage: "coop rename PM-101 APPS-TRUTH-TABLE", purpose: "Add a stable human-facing alias without changing the canonical id or breaking existing references." },
4142
4482
  { usage: 'coop comment PM-101 --message "Needs API review"', purpose: "Append a comment to a task." },
4143
4483
  { usage: 'coop log-time PM-101 --hours 2 --kind worked --note "pairing"', purpose: "Append a planned or worked time log to a task." },
4144
4484
  { usage: "coop alias remove PM-101 PAY.UPI", purpose: "Remove an alias from a task or idea." },
@@ -4314,9 +4654,9 @@ function formatSelectionCommand(commandName, delivery, track) {
4314
4654
  return `${commandName} graph next --delivery ${delivery}`;
4315
4655
  }
4316
4656
  if (track) {
4317
- return `${commandName} next task --track ${track}`;
4657
+ return `${commandName} next --track ${track}`;
4318
4658
  }
4319
- return `${commandName} next task`;
4659
+ return `${commandName} next`;
4320
4660
  }
4321
4661
  function renderInitialPrompt(options = {}) {
4322
4662
  const rigour = options.rigour ?? "balanced";
@@ -4578,13 +4918,13 @@ function registerIndexCommand(program) {
4578
4918
  }
4579
4919
 
4580
4920
  // src/commands/init.ts
4581
- import fs10 from "fs";
4921
+ import fs11 from "fs";
4582
4922
  import path13 from "path";
4583
4923
  import { spawnSync as spawnSync4 } from "child_process";
4584
4924
  import { CURRENT_SCHEMA_VERSION, write_schema_version } from "@kitsy/coop-core";
4585
4925
 
4586
4926
  // src/hooks/pre-commit.ts
4587
- import fs8 from "fs";
4927
+ import fs9 from "fs";
4588
4928
  import path11 from "path";
4589
4929
  import { spawnSync as spawnSync3 } from "child_process";
4590
4930
  import { detect_cycle, parseTaskContent, parseTaskFile as parseTaskFile8, validateStructural as validateStructural5 } from "@kitsy/coop-core";
@@ -4625,12 +4965,12 @@ function projectRootFromRelativePath(repoRoot, relativePath) {
4625
4965
  }
4626
4966
  function listTaskFilesForProject(projectRoot) {
4627
4967
  const tasksDir = path11.join(projectRoot, "tasks");
4628
- if (!fs8.existsSync(tasksDir)) return [];
4968
+ if (!fs9.existsSync(tasksDir)) return [];
4629
4969
  const out = [];
4630
4970
  const stack = [tasksDir];
4631
4971
  while (stack.length > 0) {
4632
4972
  const current = stack.pop();
4633
- const entries = fs8.readdirSync(current, { withFileTypes: true });
4973
+ const entries = fs9.readdirSync(current, { withFileTypes: true });
4634
4974
  for (const entry of entries) {
4635
4975
  const fullPath = path11.join(current, entry.name);
4636
4976
  if (entry.isDirectory()) {
@@ -4784,7 +5124,7 @@ function hookScriptBlock() {
4784
5124
  function installPreCommitHook(repoRoot) {
4785
5125
  const hookPath = path11.join(repoRoot, ".git", "hooks", "pre-commit");
4786
5126
  const hookDir = path11.dirname(hookPath);
4787
- if (!fs8.existsSync(hookDir)) {
5127
+ if (!fs9.existsSync(hookDir)) {
4788
5128
  return {
4789
5129
  installed: false,
4790
5130
  hookPath,
@@ -4792,18 +5132,18 @@ function installPreCommitHook(repoRoot) {
4792
5132
  };
4793
5133
  }
4794
5134
  const block = hookScriptBlock();
4795
- if (!fs8.existsSync(hookPath)) {
5135
+ if (!fs9.existsSync(hookPath)) {
4796
5136
  const content = ["#!/bin/sh", "", block].join("\n");
4797
- fs8.writeFileSync(hookPath, content, "utf8");
5137
+ fs9.writeFileSync(hookPath, content, "utf8");
4798
5138
  } else {
4799
- const existing = fs8.readFileSync(hookPath, "utf8");
5139
+ const existing = fs9.readFileSync(hookPath, "utf8");
4800
5140
  if (!existing.includes(HOOK_BLOCK_START)) {
4801
5141
  const suffix = existing.endsWith("\n") ? "" : "\n";
4802
- fs8.writeFileSync(hookPath, `${existing}${suffix}${block}`, "utf8");
5142
+ fs9.writeFileSync(hookPath, `${existing}${suffix}${block}`, "utf8");
4803
5143
  }
4804
5144
  }
4805
5145
  try {
4806
- fs8.chmodSync(hookPath, 493);
5146
+ fs9.chmodSync(hookPath, 493);
4807
5147
  } catch {
4808
5148
  }
4809
5149
  return {
@@ -4814,7 +5154,7 @@ function installPreCommitHook(repoRoot) {
4814
5154
  }
4815
5155
 
4816
5156
  // src/hooks/post-merge-validate.ts
4817
- import fs9 from "fs";
5157
+ import fs10 from "fs";
4818
5158
  import path12 from "path";
4819
5159
  import { list_projects } from "@kitsy/coop-core";
4820
5160
  import { load_graph as load_graph5, validate_graph as validate_graph2 } from "@kitsy/coop-core";
@@ -4822,7 +5162,7 @@ var HOOK_BLOCK_START2 = "# COOP_POST_MERGE_START";
4822
5162
  var HOOK_BLOCK_END2 = "# COOP_POST_MERGE_END";
4823
5163
  function runPostMergeValidate(repoRoot) {
4824
5164
  const workspaceDir = path12.join(repoRoot, ".coop");
4825
- if (!fs9.existsSync(workspaceDir)) {
5165
+ if (!fs10.existsSync(workspaceDir)) {
4826
5166
  return {
4827
5167
  ok: true,
4828
5168
  warnings: ["[COOP] Skipped post-merge validation (.coop not found)."]
@@ -4876,7 +5216,7 @@ function postMergeHookBlock() {
4876
5216
  function installPostMergeHook(repoRoot) {
4877
5217
  const hookPath = path12.join(repoRoot, ".git", "hooks", "post-merge");
4878
5218
  const hookDir = path12.dirname(hookPath);
4879
- if (!fs9.existsSync(hookDir)) {
5219
+ if (!fs10.existsSync(hookDir)) {
4880
5220
  return {
4881
5221
  installed: false,
4882
5222
  hookPath,
@@ -4884,17 +5224,17 @@ function installPostMergeHook(repoRoot) {
4884
5224
  };
4885
5225
  }
4886
5226
  const block = postMergeHookBlock();
4887
- if (!fs9.existsSync(hookPath)) {
4888
- fs9.writeFileSync(hookPath, ["#!/bin/sh", "", block].join("\n"), "utf8");
5227
+ if (!fs10.existsSync(hookPath)) {
5228
+ fs10.writeFileSync(hookPath, ["#!/bin/sh", "", block].join("\n"), "utf8");
4889
5229
  } else {
4890
- const existing = fs9.readFileSync(hookPath, "utf8");
5230
+ const existing = fs10.readFileSync(hookPath, "utf8");
4891
5231
  if (!existing.includes(HOOK_BLOCK_START2)) {
4892
5232
  const suffix = existing.endsWith("\n") ? "" : "\n";
4893
- fs9.writeFileSync(hookPath, `${existing}${suffix}${block}`, "utf8");
5233
+ fs10.writeFileSync(hookPath, `${existing}${suffix}${block}`, "utf8");
4894
5234
  }
4895
5235
  }
4896
5236
  try {
4897
- fs9.chmodSync(hookPath, 493);
5237
+ fs10.chmodSync(hookPath, 493);
4898
5238
  } catch {
4899
5239
  }
4900
5240
  return {
@@ -5088,40 +5428,40 @@ tmp/
5088
5428
  *.tmp
5089
5429
  `;
5090
5430
  function ensureDir(dirPath) {
5091
- fs10.mkdirSync(dirPath, { recursive: true });
5431
+ fs11.mkdirSync(dirPath, { recursive: true });
5092
5432
  }
5093
5433
  function writeIfMissing(filePath, content) {
5094
- if (!fs10.existsSync(filePath)) {
5095
- fs10.writeFileSync(filePath, content, "utf8");
5434
+ if (!fs11.existsSync(filePath)) {
5435
+ fs11.writeFileSync(filePath, content, "utf8");
5096
5436
  }
5097
5437
  }
5098
5438
  function ensureGitignoreEntry(root, entry) {
5099
5439
  const gitignorePath = path13.join(root, ".gitignore");
5100
- if (!fs10.existsSync(gitignorePath)) {
5101
- fs10.writeFileSync(gitignorePath, `${entry}
5440
+ if (!fs11.existsSync(gitignorePath)) {
5441
+ fs11.writeFileSync(gitignorePath, `${entry}
5102
5442
  `, "utf8");
5103
5443
  return;
5104
5444
  }
5105
- const content = fs10.readFileSync(gitignorePath, "utf8");
5445
+ const content = fs11.readFileSync(gitignorePath, "utf8");
5106
5446
  const lines = content.split(/\r?\n/).map((line) => line.trim());
5107
5447
  if (!lines.includes(entry)) {
5108
5448
  const suffix = content.endsWith("\n") ? "" : "\n";
5109
- fs10.writeFileSync(gitignorePath, `${content}${suffix}${entry}
5449
+ fs11.writeFileSync(gitignorePath, `${content}${suffix}${entry}
5110
5450
  `, "utf8");
5111
5451
  }
5112
5452
  }
5113
5453
  function ensureGitattributesEntry(root, entry) {
5114
5454
  const attrsPath = path13.join(root, ".gitattributes");
5115
- if (!fs10.existsSync(attrsPath)) {
5116
- fs10.writeFileSync(attrsPath, `${entry}
5455
+ if (!fs11.existsSync(attrsPath)) {
5456
+ fs11.writeFileSync(attrsPath, `${entry}
5117
5457
  `, "utf8");
5118
5458
  return;
5119
5459
  }
5120
- const content = fs10.readFileSync(attrsPath, "utf8");
5460
+ const content = fs11.readFileSync(attrsPath, "utf8");
5121
5461
  const lines = content.split(/\r?\n/).map((line) => line.trim());
5122
5462
  if (!lines.includes(entry)) {
5123
5463
  const suffix = content.endsWith("\n") ? "" : "\n";
5124
- fs10.writeFileSync(attrsPath, `${content}${suffix}${entry}
5464
+ fs11.writeFileSync(attrsPath, `${content}${suffix}${entry}
5125
5465
  `, "utf8");
5126
5466
  }
5127
5467
  }
@@ -5222,7 +5562,7 @@ function registerInitCommand(program) {
5222
5562
  path13.join(projectRoot, "config.yml"),
5223
5563
  buildProjectConfig(projectId, identity.projectName, identity.projectAliases, identity.namingTemplate)
5224
5564
  );
5225
- if (!fs10.existsSync(path13.join(projectRoot, "schema-version"))) {
5565
+ if (!fs11.existsSync(path13.join(projectRoot, "schema-version"))) {
5226
5566
  write_schema_version(projectRoot, CURRENT_SCHEMA_VERSION);
5227
5567
  }
5228
5568
  writeIfMissing(path13.join(projectRoot, "templates/task.md"), TASK_TEMPLATE);
@@ -5315,12 +5655,22 @@ function currentTaskSelection(root, id) {
5315
5655
  }
5316
5656
  function registerLifecycleCommands(program) {
5317
5657
  for (const verb of lifecycleVerbs) {
5318
- 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) => {
5658
+ const runTransition = async (id, options) => {
5319
5659
  const root = resolveRepoRoot();
5320
5660
  console.log(currentTaskSelection(root, id));
5321
- const result = await transitionTaskByReference(root, id, verb.targetStatus, options);
5661
+ const result = verb.name === "block" || verb.name === "unblock" || verb.name === "reopen" || verb.name === "cancel" ? await lifecycleTransitionTaskByReference(root, id, verb.name, verb.targetStatus, options) : await transitionTaskByReference(root, id, verb.targetStatus, options);
5322
5662
  console.log(`Updated ${result.task.id}: ${result.from} -> ${result.to}`);
5323
- });
5663
+ const maybePath = "path" in result ? result.path : void 0;
5664
+ if (Array.isArray(maybePath) && maybePath.length > 2) {
5665
+ console.log(`Auto-hop path: ${maybePath.join(" -> ")}`);
5666
+ }
5667
+ const maybeAuditPath = "auditPath" in result ? result.auditPath : void 0;
5668
+ if (typeof maybeAuditPath === "string" && maybeAuditPath.length > 0) {
5669
+ console.log(`Transition audit: ${maybeAuditPath}`);
5670
+ }
5671
+ };
5672
+ const command = program.command(verb.name).description(verb.description).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(runTransition);
5673
+ command.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(runTransition);
5324
5674
  }
5325
5675
  }
5326
5676
 
@@ -5616,8 +5966,8 @@ function listTasks(options) {
5616
5966
  ensureCoopInitialized(root);
5617
5967
  const context = readWorkingContext(root, resolveCoopHome());
5618
5968
  const graph = load_graph6(coopDir(root));
5619
- const rawResolvedTrack = resolveContextValueWithSource(options.track, context.track);
5620
- const rawResolvedDelivery = resolveContextValueWithSource(options.delivery, context.delivery);
5969
+ const rawResolvedTrack = options.all ? {} : resolveContextValueWithSource(options.track, context.track);
5970
+ const rawResolvedDelivery = options.all ? {} : resolveContextValueWithSource(options.delivery, context.delivery);
5621
5971
  const resolvedTrack = {
5622
5972
  ...rawResolvedTrack,
5623
5973
  value: rawResolvedTrack.value ? resolveExistingTrackId(root, rawResolvedTrack.value) ?? rawResolvedTrack.value : void 0
@@ -5626,7 +5976,12 @@ function listTasks(options) {
5626
5976
  ...rawResolvedDelivery,
5627
5977
  value: rawResolvedDelivery.value ? resolveExistingDeliveryId(root, rawResolvedDelivery.value) ?? rawResolvedDelivery.value : void 0
5628
5978
  };
5629
- const resolvedVersion = resolveContextValueWithSource(options.version, context.version);
5979
+ const resolvedVersion = options.all ? {} : resolveContextValueWithSource(options.version, context.version);
5980
+ const statusFilters = /* @__PURE__ */ new Set();
5981
+ if (options.status?.trim()) statusFilters.add(options.status.trim());
5982
+ if (options.completed) statusFilters.add("done");
5983
+ if (options.todo) statusFilters.add("todo");
5984
+ if (options.blocked) statusFilters.add("blocked");
5630
5985
  const assignee = options.mine ? defaultCoopAuthor(root) : options.assignee?.trim();
5631
5986
  const deliveryScope = resolvedDelivery.value ? new Set(graph.deliveries.get(resolvedDelivery.value)?.scope.include ?? []) : null;
5632
5987
  const defaultSort = options.ready || resolvedTrack.value || resolvedDelivery.value ? "score" : "id";
@@ -5650,7 +6005,7 @@ function listTasks(options) {
5650
6005
  const scoreMap = new Map(scoredEntries.map((entry) => [entry.task.id, entry.score]));
5651
6006
  const rows = loadTasks2(root).filter(({ task }) => {
5652
6007
  if (readyIds && !readyIds.has(task.id)) return false;
5653
- if (options.status && task.status !== options.status) return false;
6008
+ if (statusFilters.size > 0 && !statusFilters.has(task.status)) return false;
5654
6009
  if (resolvedTrack.value && task.track !== resolvedTrack.value && !(task.delivery_tracks ?? []).includes(resolvedTrack.value)) {
5655
6010
  return false;
5656
6011
  }
@@ -5804,7 +6159,7 @@ function listTracks() {
5804
6159
  }
5805
6160
  function registerListCommand(program) {
5806
6161
  const list = program.command("list").description("List COOP entities");
5807
- list.command("tasks").alias("task").description("List tasks").option("--status <status>", "Filter by status").option("--track <track>", "Filter by home/contributing track lens, using `coop use track` if omitted").option("--delivery <delivery>", "Filter by delivery membership, using `coop use delivery` if omitted").option("--priority <priority>", "Filter by effective priority").option("--assignee <assignee>", "Filter by assignee").option("--version <version>", "Filter by fix/released version, using `coop use version` if omitted").option("--mine", "Filter to the current default COOP author").option("--ready", "Only list ready tasks in scored order").option("--sort <sort>", "Sort by id|priority|status|title|updated|created|score").option("--columns <columns>", "Columns: id,short,title,status,priority,assignee,track,delivery,score,file or all").action((options) => {
6162
+ list.command("tasks").alias("task").description("List tasks").option("--status <status>", "Filter by status").option("--track <track>", "Filter by home/contributing track lens, using `coop use track` if omitted").option("--delivery <delivery>", "Filter by delivery membership, using `coop use delivery` if omitted").option("--priority <priority>", "Filter by effective priority").option("--assignee <assignee>", "Filter by assignee").option("--version <version>", "Filter by fix/released version, using `coop use version` if omitted").option("--mine", "Filter to the current default COOP author").option("--ready", "Only list ready tasks in scored order").option("--all", "Ignore current `coop use` task-scope filters and list across all tracks/deliveries/versions").option("--completed", "Filter to done tasks").option("--todo", "Filter to todo tasks").option("--blocked", "Filter to blocked tasks").option("--sort <sort>", "Sort by id|priority|status|title|updated|created|score").option("--columns <columns>", "Columns: id,short,title,status,priority,assignee,track,delivery,score,file or all").action((options) => {
5808
6163
  listTasks(options);
5809
6164
  });
5810
6165
  list.command("ideas").alias("idea").description("List ideas").option("--status <status>", "Filter by status").option("--sort <sort>", "Sort by id|status|title|updated|created").option("--columns <columns>", "Columns: id,short,title,status,file or all").action((options) => {
@@ -5822,7 +6177,7 @@ function registerListCommand(program) {
5822
6177
  }
5823
6178
 
5824
6179
  // src/utils/logger.ts
5825
- import fs11 from "fs";
6180
+ import fs12 from "fs";
5826
6181
  import os2 from "os";
5827
6182
  import path16 from "path";
5828
6183
  function resolveWorkspaceRoot(start = process.cwd()) {
@@ -5833,11 +6188,11 @@ function resolveWorkspaceRoot(start = process.cwd()) {
5833
6188
  const gitDir = path16.join(current, ".git");
5834
6189
  const coopDir2 = coopWorkspaceDir(current);
5835
6190
  const workspaceConfig = path16.join(coopDir2, "config.yml");
5836
- if (fs11.existsSync(gitDir)) {
6191
+ if (fs12.existsSync(gitDir)) {
5837
6192
  return current;
5838
6193
  }
5839
6194
  const resolvedCoopDir = path16.resolve(coopDir2);
5840
- if (resolvedCoopDir !== configuredCoopHome && resolvedCoopDir !== defaultCoopHome && fs11.existsSync(workspaceConfig)) {
6195
+ if (resolvedCoopDir !== configuredCoopHome && resolvedCoopDir !== defaultCoopHome && fs12.existsSync(workspaceConfig)) {
5841
6196
  return current;
5842
6197
  }
5843
6198
  const parent = path16.dirname(current);
@@ -5856,8 +6211,8 @@ function resolveCliLogFile(start = process.cwd()) {
5856
6211
  return path16.join(resolveCoopHome(), "logs", "cli.log");
5857
6212
  }
5858
6213
  function appendLogEntry(entry, logFile) {
5859
- fs11.mkdirSync(path16.dirname(logFile), { recursive: true });
5860
- fs11.appendFileSync(logFile, `${JSON.stringify(entry)}
6214
+ fs12.mkdirSync(path16.dirname(logFile), { recursive: true });
6215
+ fs12.appendFileSync(logFile, `${JSON.stringify(entry)}
5861
6216
  `, "utf8");
5862
6217
  }
5863
6218
  function logCliError(error, start = process.cwd()) {
@@ -5896,8 +6251,8 @@ function parseLogLine(line) {
5896
6251
  }
5897
6252
  function readLastCliLog(start = process.cwd()) {
5898
6253
  const logFile = resolveCliLogFile(start);
5899
- if (!fs11.existsSync(logFile)) return null;
5900
- const content = fs11.readFileSync(logFile, "utf8");
6254
+ if (!fs12.existsSync(logFile)) return null;
6255
+ const content = fs12.readFileSync(logFile, "utf8");
5901
6256
  const lines = content.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
5902
6257
  for (let i = lines.length - 1; i >= 0; i -= 1) {
5903
6258
  const entry = parseLogLine(lines[i] ?? "");
@@ -5960,11 +6315,11 @@ function registerLogTimeCommand(program) {
5960
6315
  }
5961
6316
 
5962
6317
  // src/commands/migrate.ts
5963
- import fs12 from "fs";
6318
+ import fs13 from "fs";
5964
6319
  import path17 from "path";
5965
6320
  import { createInterface } from "readline/promises";
5966
6321
  import { stdin as input, stdout as output } from "process";
5967
- import { CURRENT_SCHEMA_VERSION as CURRENT_SCHEMA_VERSION2, IndexManager as IndexManager3, migrate_repository, parseYamlFile as parseYamlFile5, writeYamlFile as writeYamlFile5 } from "@kitsy/coop-core";
6322
+ import { CURRENT_SCHEMA_VERSION as CURRENT_SCHEMA_VERSION2, IndexManager as IndexManager3, migrate_repository, parseYamlFile as parseYamlFile5, writeYamlFile as writeYamlFile6 } from "@kitsy/coop-core";
5968
6323
  var COOP_IGNORE_TEMPLATE2 = `.index/
5969
6324
  logs/
5970
6325
  tmp/
@@ -5980,22 +6335,22 @@ function parseTargetVersion(raw) {
5980
6335
  return parsed;
5981
6336
  }
5982
6337
  function writeIfMissing2(filePath, content) {
5983
- if (!fs12.existsSync(filePath)) {
5984
- fs12.writeFileSync(filePath, content, "utf8");
6338
+ if (!fs13.existsSync(filePath)) {
6339
+ fs13.writeFileSync(filePath, content, "utf8");
5985
6340
  }
5986
6341
  }
5987
6342
  function ensureGitignoreEntry2(root, entry) {
5988
6343
  const gitignorePath = path17.join(root, ".gitignore");
5989
- if (!fs12.existsSync(gitignorePath)) {
5990
- fs12.writeFileSync(gitignorePath, `${entry}
6344
+ if (!fs13.existsSync(gitignorePath)) {
6345
+ fs13.writeFileSync(gitignorePath, `${entry}
5991
6346
  `, "utf8");
5992
6347
  return;
5993
6348
  }
5994
- const content = fs12.readFileSync(gitignorePath, "utf8");
6349
+ const content = fs13.readFileSync(gitignorePath, "utf8");
5995
6350
  const lines = content.split(/\r?\n/).map((line) => line.trim());
5996
6351
  if (!lines.includes(entry)) {
5997
6352
  const suffix = content.endsWith("\n") ? "" : "\n";
5998
- fs12.writeFileSync(gitignorePath, `${content}${suffix}${entry}
6353
+ fs13.writeFileSync(gitignorePath, `${content}${suffix}${entry}
5999
6354
  `, "utf8");
6000
6355
  }
6001
6356
  }
@@ -6020,7 +6375,7 @@ function legacyWorkspaceProjectEntries(root) {
6020
6375
  "backlog",
6021
6376
  "plans",
6022
6377
  "releases"
6023
- ].filter((entry) => fs12.existsSync(path17.join(workspaceDir, entry)));
6378
+ ].filter((entry) => fs13.existsSync(path17.join(workspaceDir, entry)));
6024
6379
  }
6025
6380
  function normalizeProjectId2(value) {
6026
6381
  return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-");
@@ -6085,19 +6440,19 @@ async function migrateWorkspaceLayout(root, options) {
6085
6440
  throw new Error(`Unsupported workspace-layout target '${options.to ?? ""}'. Expected 'v2'.`);
6086
6441
  }
6087
6442
  const workspaceDir = coopWorkspaceDir(root);
6088
- if (!fs12.existsSync(workspaceDir)) {
6443
+ if (!fs13.existsSync(workspaceDir)) {
6089
6444
  throw new Error("Missing .coop directory. Run 'coop init' first.");
6090
6445
  }
6091
6446
  const projectsDir = path17.join(workspaceDir, "projects");
6092
6447
  const legacyEntries = legacyWorkspaceProjectEntries(root);
6093
- if (legacyEntries.length === 0 && fs12.existsSync(projectsDir)) {
6448
+ if (legacyEntries.length === 0 && fs13.existsSync(projectsDir)) {
6094
6449
  console.log("[COOP] workspace layout already uses v2.");
6095
6450
  return;
6096
6451
  }
6097
6452
  const identity = await resolveMigrationIdentity(root, options);
6098
6453
  const projectId = identity.projectId;
6099
6454
  const projectRoot = path17.join(projectsDir, projectId);
6100
- if (fs12.existsSync(projectRoot) && !options.force) {
6455
+ if (fs13.existsSync(projectRoot) && !options.force) {
6101
6456
  throw new Error(`Project destination '${path17.relative(root, projectRoot)}' already exists. Re-run with --force.`);
6102
6457
  }
6103
6458
  const changed = legacyEntries.map((entry) => `${path17.join(".coop", entry)} -> ${path17.join(".coop", "projects", projectId, entry)}`);
@@ -6116,21 +6471,21 @@ async function migrateWorkspaceLayout(root, options) {
6116
6471
  console.log("- no files were modified.");
6117
6472
  return;
6118
6473
  }
6119
- fs12.mkdirSync(projectsDir, { recursive: true });
6120
- fs12.mkdirSync(projectRoot, { recursive: true });
6474
+ fs13.mkdirSync(projectsDir, { recursive: true });
6475
+ fs13.mkdirSync(projectRoot, { recursive: true });
6121
6476
  for (const entry of legacyEntries) {
6122
6477
  const source = path17.join(workspaceDir, entry);
6123
6478
  const destination = path17.join(projectRoot, entry);
6124
- if (fs12.existsSync(destination)) {
6479
+ if (fs13.existsSync(destination)) {
6125
6480
  if (!options.force) {
6126
6481
  throw new Error(`Destination '${path17.relative(root, destination)}' already exists.`);
6127
6482
  }
6128
- fs12.rmSync(destination, { recursive: true, force: true });
6483
+ fs13.rmSync(destination, { recursive: true, force: true });
6129
6484
  }
6130
- fs12.renameSync(source, destination);
6485
+ fs13.renameSync(source, destination);
6131
6486
  }
6132
6487
  const movedConfigPath = path17.join(projectRoot, "config.yml");
6133
- if (fs12.existsSync(movedConfigPath)) {
6488
+ if (fs13.existsSync(movedConfigPath)) {
6134
6489
  const movedConfig = parseYamlFile5(movedConfigPath);
6135
6490
  const nextProject = typeof movedConfig.project === "object" && movedConfig.project !== null ? { ...movedConfig.project } : {};
6136
6491
  nextProject.name = identity.projectName;
@@ -6139,7 +6494,7 @@ async function migrateWorkspaceLayout(root, options) {
6139
6494
  const nextHooks = typeof movedConfig.hooks === "object" && movedConfig.hooks !== null ? { ...movedConfig.hooks } : {};
6140
6495
  nextHooks.on_task_transition = `.coop/projects/${projectId}/hooks/on-task-transition.sh`;
6141
6496
  nextHooks.on_delivery_complete = `.coop/projects/${projectId}/hooks/on-delivery-complete.sh`;
6142
- writeYamlFile5(movedConfigPath, {
6497
+ writeYamlFile6(movedConfigPath, {
6143
6498
  ...movedConfig,
6144
6499
  project: nextProject,
6145
6500
  hooks: nextHooks
@@ -6238,7 +6593,7 @@ function printNamingOverview() {
6238
6593
  console.log("- none");
6239
6594
  } else {
6240
6595
  for (const [token, definition] of Object.entries(tokens)) {
6241
- console.log(`- <${token.toUpperCase()}>: ${definition.values.length > 0 ? definition.values.join(", ") : "(no values)"}`);
6596
+ console.log(`- <${token.toUpperCase()}>: ${definition.values.length > 0 ? definition.values.join(", ") : "(no values)"}${definition.default ? ` | default=${definition.default}` : ""}`);
6242
6597
  }
6243
6598
  }
6244
6599
  console.log("Examples:");
@@ -6305,7 +6660,7 @@ function listTokens() {
6305
6660
  return;
6306
6661
  }
6307
6662
  for (const [token, definition] of Object.entries(tokens)) {
6308
- console.log(`${token}: ${definition.values.length > 0 ? definition.values.join(", ") : "(no values)"}`);
6663
+ console.log(`${token}: ${definition.values.length > 0 ? definition.values.join(", ") : "(no values)"}${definition.default ? ` | default=${definition.default}` : ""}`);
6309
6664
  }
6310
6665
  }
6311
6666
  function registerNamingCommand(program) {
@@ -6403,6 +6758,53 @@ function registerNamingCommand(program) {
6403
6758
  console.log(`Deleted naming token: ${normalizedToken}`);
6404
6759
  });
6405
6760
  const tokenValue = token.command("value").description("Manage allowed values for a naming token");
6761
+ const tokenDefault = token.command("default").description("Manage default values for a naming token");
6762
+ tokenDefault.command("show").description("Show the default value for a naming token").argument("<token>", "Token name").action((tokenName) => {
6763
+ const normalizedToken = normalizeNamingTokenName(tokenName);
6764
+ const tokens = namingTokensForRoot(resolveRepoRoot());
6765
+ const tokenConfig = tokens[normalizedToken];
6766
+ if (!tokenConfig) {
6767
+ throw new Error(`Naming token '${normalizedToken}' does not exist.`);
6768
+ }
6769
+ console.log(tokenConfig.default ?? "(unset)");
6770
+ });
6771
+ tokenDefault.command("set").description("Set a default value for a naming token").argument("<token>", "Token name").argument("<value>", "Default token value").action((tokenName, value) => {
6772
+ const root = resolveRepoRoot();
6773
+ const normalizedToken = normalizeNamingTokenName(tokenName);
6774
+ const normalizedValue = normalizeNamingTokenValue(value);
6775
+ const tokens = namingTokensForRoot(root);
6776
+ const tokenConfig = tokens[normalizedToken];
6777
+ if (!tokenConfig) {
6778
+ throw new Error(`Naming token '${normalizedToken}' does not exist.`);
6779
+ }
6780
+ if (tokenConfig.values.length > 0 && !tokenConfig.values.includes(normalizedValue)) {
6781
+ throw new Error(`Invalid value '${value}' for naming token '${normalizedToken}'. Allowed values: ${tokenConfig.values.join(", ")}.`);
6782
+ }
6783
+ writeNamingConfig(root, (config) => {
6784
+ const { next, tokens: tokensRaw } = ensureTokenRecord(config);
6785
+ const current = typeof tokensRaw[normalizedToken] === "object" && tokensRaw[normalizedToken] !== null ? { ...tokensRaw[normalizedToken] } : { values: [] };
6786
+ current.default = normalizedValue;
6787
+ tokensRaw[normalizedToken] = current;
6788
+ return next;
6789
+ });
6790
+ console.log(`Default naming token value: ${normalizedToken}=${normalizedValue}`);
6791
+ });
6792
+ tokenDefault.command("clear").description("Clear the default value for a naming token").argument("<token>", "Token name").action((tokenName) => {
6793
+ const root = resolveRepoRoot();
6794
+ const normalizedToken = normalizeNamingTokenName(tokenName);
6795
+ const tokens = namingTokensForRoot(root);
6796
+ if (!tokens[normalizedToken]) {
6797
+ throw new Error(`Naming token '${normalizedToken}' does not exist.`);
6798
+ }
6799
+ writeNamingConfig(root, (config) => {
6800
+ const { next, tokens: tokensRaw } = ensureTokenRecord(config);
6801
+ const current = typeof tokensRaw[normalizedToken] === "object" && tokensRaw[normalizedToken] !== null ? { ...tokensRaw[normalizedToken] } : { values: [] };
6802
+ delete current.default;
6803
+ tokensRaw[normalizedToken] = current;
6804
+ return next;
6805
+ });
6806
+ console.log(`Cleared naming token default: ${normalizedToken}`);
6807
+ });
6406
6808
  tokenValue.command("list").description("List allowed values for a naming token").argument("<token>", "Token name").action((tokenName) => {
6407
6809
  const normalizedToken = normalizeNamingTokenName(tokenName);
6408
6810
  const tokens = namingTokensForRoot(resolveRepoRoot());
@@ -6452,6 +6854,9 @@ function registerNamingCommand(program) {
6452
6854
  }
6453
6855
  const values = Array.isArray(tokenRecord.values) ? tokenRecord.values.filter((entry) => typeof entry === "string" && entry.trim().length > 0).map((entry) => normalizeNamingTokenValue(entry)).filter((entry) => entry !== normalizedValue) : [];
6454
6856
  tokenRecord.values = Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));
6857
+ if (tokenRecord.default === normalizedValue) {
6858
+ delete tokenRecord.default;
6859
+ }
6455
6860
  tokens[normalizedToken] = tokenRecord;
6456
6861
  return next;
6457
6862
  });
@@ -6730,7 +7135,7 @@ function registerPromoteCommand(program) {
6730
7135
  }
6731
7136
 
6732
7137
  // src/commands/project.ts
6733
- import fs13 from "fs";
7138
+ import fs14 from "fs";
6734
7139
  import path18 from "path";
6735
7140
  import { CURRENT_SCHEMA_VERSION as CURRENT_SCHEMA_VERSION3, write_schema_version as write_schema_version2 } from "@kitsy/coop-core";
6736
7141
  var TASK_TEMPLATE2 = `---
@@ -6846,11 +7251,11 @@ var PROJECT_DIRS = [
6846
7251
  ".index"
6847
7252
  ];
6848
7253
  function ensureDir2(dirPath) {
6849
- fs13.mkdirSync(dirPath, { recursive: true });
7254
+ fs14.mkdirSync(dirPath, { recursive: true });
6850
7255
  }
6851
7256
  function writeIfMissing3(filePath, content) {
6852
- if (!fs13.existsSync(filePath)) {
6853
- fs13.writeFileSync(filePath, content, "utf8");
7257
+ if (!fs14.existsSync(filePath)) {
7258
+ fs14.writeFileSync(filePath, content, "utf8");
6854
7259
  }
6855
7260
  }
6856
7261
  function normalizeProjectId3(value) {
@@ -6864,7 +7269,7 @@ function createProject(root, projectId, projectName, namingTemplate = DEFAULT_ID
6864
7269
  ensureDir2(path18.join(projectRoot, dir));
6865
7270
  }
6866
7271
  writeIfMissing3(path18.join(projectRoot, "config.yml"), PROJECT_CONFIG_TEMPLATE(projectId, projectName, namingTemplate));
6867
- if (!fs13.existsSync(path18.join(projectRoot, "schema-version"))) {
7272
+ if (!fs14.existsSync(path18.join(projectRoot, "schema-version"))) {
6868
7273
  write_schema_version2(projectRoot, CURRENT_SCHEMA_VERSION3);
6869
7274
  }
6870
7275
  writeIfMissing3(path18.join(projectRoot, "templates/task.md"), TASK_TEMPLATE2);
@@ -6933,7 +7338,7 @@ function registerProjectCommand(program) {
6933
7338
  }
6934
7339
 
6935
7340
  // src/commands/prompt.ts
6936
- import fs14 from "fs";
7341
+ import fs15 from "fs";
6937
7342
  function buildPayload(root, id) {
6938
7343
  const { parsed } = loadTaskEntry(root, id);
6939
7344
  const context = readWorkingContext(root, resolveCoopHome());
@@ -7018,7 +7423,7 @@ function registerPromptCommand(program) {
7018
7423
  output2 = renderMarkdown(payload);
7019
7424
  }
7020
7425
  if (options.save) {
7021
- fs14.writeFileSync(options.save, output2, "utf8");
7426
+ fs15.writeFileSync(options.save, output2, "utf8");
7022
7427
  }
7023
7428
  if (isVerboseRequested()) {
7024
7429
  for (const line of formatResolvedContextMessage({
@@ -7033,8 +7438,23 @@ function registerPromptCommand(program) {
7033
7438
  });
7034
7439
  }
7035
7440
 
7441
+ // src/commands/rename.ts
7442
+ function registerRenameCommand(program) {
7443
+ program.command("rename").description("Add a stable alias without changing the canonical COOP id").argument("<id-or-type>", "Task or idea id/alias, or an explicit entity type").argument("<id-or-alias>", "Entity id when an explicit type is provided, else the new alias").argument("[next-alias]", "New alias when an explicit type is provided").action((first, second, third) => {
7444
+ const resolved = resolveOptionalEntityArg(first, third ? second : void 0, ["task", "idea"], "task");
7445
+ const nextAlias = (third ?? second).trim();
7446
+ if (!nextAlias) {
7447
+ throw new Error("Provide the new alias. Example: coop rename TASK-123 APPS-TRUTH-TABLE");
7448
+ }
7449
+ const root = resolveRepoRoot();
7450
+ const result = addAliases(root, resolved.id, [nextAlias]);
7451
+ const alias = result.added[0] ?? nextAlias;
7452
+ console.log(`Added alias ${alias} to ${result.target.type} ${result.target.id}. Canonical id unchanged.`);
7453
+ });
7454
+ }
7455
+
7036
7456
  // src/commands/refine.ts
7037
- import fs15 from "fs";
7457
+ import fs16 from "fs";
7038
7458
  import path19 from "path";
7039
7459
  import { parseIdeaFile as parseIdeaFile5, parseTaskFile as parseTaskFile11 } from "@kitsy/coop-core";
7040
7460
  import { create_provider_refinement_client, refine_idea_to_draft, refine_task_to_draft } from "@kitsy/coop-ai";
@@ -7048,7 +7468,7 @@ function resolveIdeaFile3(root, idOrAlias) {
7048
7468
  }
7049
7469
  async function readSupplementalInput(root, options) {
7050
7470
  if (options.inputFile?.trim()) {
7051
- return fs15.readFileSync(path19.resolve(root, options.inputFile.trim()), "utf8");
7471
+ return fs16.readFileSync(path19.resolve(root, options.inputFile.trim()), "utf8");
7052
7472
  }
7053
7473
  if (options.stdin) {
7054
7474
  return readStdinText();
@@ -7067,10 +7487,10 @@ function loadAuthorityContext(root, refs) {
7067
7487
  const filePart = extractRefFile(ref);
7068
7488
  if (!filePart) continue;
7069
7489
  const fullPath = path19.resolve(root, filePart);
7070
- if (!fs15.existsSync(fullPath) || !fs15.statSync(fullPath).isFile()) continue;
7490
+ if (!fs16.existsSync(fullPath) || !fs16.statSync(fullPath).isFile()) continue;
7071
7491
  out.push({
7072
7492
  ref,
7073
- content: fs15.readFileSync(fullPath, "utf8")
7493
+ content: fs16.readFileSync(fullPath, "utf8")
7074
7494
  });
7075
7495
  }
7076
7496
  return out;
@@ -7143,7 +7563,7 @@ function registerRefineCommand(program) {
7143
7563
  }
7144
7564
 
7145
7565
  // src/commands/run.ts
7146
- import fs16 from "fs";
7566
+ import fs17 from "fs";
7147
7567
  import path20 from "path";
7148
7568
  import { load_graph as load_graph8, parseTaskFile as parseTaskFile12 } from "@kitsy/coop-core";
7149
7569
  import {
@@ -7155,7 +7575,7 @@ import {
7155
7575
  function loadTask(root, idOrAlias) {
7156
7576
  const target = resolveReference(root, idOrAlias, "task");
7157
7577
  const taskFile = path20.join(root, ...target.file.split("/"));
7158
- if (!fs16.existsSync(taskFile)) {
7578
+ if (!fs17.existsSync(taskFile)) {
7159
7579
  throw new Error(`Task file not found: ${target.file}`);
7160
7580
  }
7161
7581
  return parseTaskFile12(taskFile).task;
@@ -7322,7 +7742,7 @@ ${parsed.body}`, query)) continue;
7322
7742
  }
7323
7743
 
7324
7744
  // src/server/api.ts
7325
- import fs17 from "fs";
7745
+ import fs18 from "fs";
7326
7746
  import http2 from "http";
7327
7747
  import path21 from "path";
7328
7748
  import {
@@ -7371,8 +7791,8 @@ function taskSummary(graph, task, external = []) {
7371
7791
  }
7372
7792
  function taskFileById(root, id) {
7373
7793
  const tasksDir = path21.join(resolveProject(root).root, "tasks");
7374
- if (!fs17.existsSync(tasksDir)) return null;
7375
- const entries = fs17.readdirSync(tasksDir, { withFileTypes: true });
7794
+ if (!fs18.existsSync(tasksDir)) return null;
7795
+ const entries = fs18.readdirSync(tasksDir, { withFileTypes: true });
7376
7796
  const target = `${id}.md`.toLowerCase();
7377
7797
  const match = entries.find((entry) => entry.isFile() && entry.name.toLowerCase() === target);
7378
7798
  return match ? path21.join(tasksDir, match.name) : null;
@@ -7553,7 +7973,7 @@ function registerServeCommand(program) {
7553
7973
  }
7554
7974
 
7555
7975
  // src/commands/show.ts
7556
- import fs18 from "fs";
7976
+ import fs19 from "fs";
7557
7977
  import path22 from "path";
7558
7978
  import { parseIdeaFile as parseIdeaFile7 } from "@kitsy/coop-core";
7559
7979
  function stringify(value) {
@@ -7574,12 +7994,12 @@ function pushListSection(lines, title, values) {
7574
7994
  }
7575
7995
  function loadComputedFromIndex(root, taskId) {
7576
7996
  const indexPath = path22.join(ensureCoopInitialized(root), ".index", "tasks.json");
7577
- if (!fs18.existsSync(indexPath)) {
7997
+ if (!fs19.existsSync(indexPath)) {
7578
7998
  return null;
7579
7999
  }
7580
8000
  let parsed;
7581
8001
  try {
7582
- parsed = JSON.parse(fs18.readFileSync(indexPath, "utf8"));
8002
+ parsed = JSON.parse(fs19.readFileSync(indexPath, "utf8"));
7583
8003
  } catch {
7584
8004
  return null;
7585
8005
  }
@@ -7848,7 +8268,7 @@ function registerShowCommand(program) {
7848
8268
 
7849
8269
  // src/commands/status.ts
7850
8270
  import chalk4 from "chalk";
7851
- import { TaskStatus as TaskStatus4, analyze_feasibility as analyze_feasibility3, load_graph as load_graph11 } from "@kitsy/coop-core";
8271
+ import { TaskStatus as TaskStatus5, analyze_feasibility as analyze_feasibility3, load_graph as load_graph11 } from "@kitsy/coop-core";
7852
8272
  function countBy(values, keyFn) {
7853
8273
  const out = /* @__PURE__ */ new Map();
7854
8274
  for (const value of values) {
@@ -7887,7 +8307,7 @@ function registerStatusCommand(program) {
7887
8307
  lines.push(chalk4.bold("COOP Status Dashboard"));
7888
8308
  lines.push("");
7889
8309
  lines.push(chalk4.bold("Tasks By Status"));
7890
- const statusRows = Object.values(TaskStatus4).map((status) => [
8310
+ const statusRows = Object.values(TaskStatus5).map((status) => [
7891
8311
  status,
7892
8312
  String(tasksByStatus.get(status) ?? 0)
7893
8313
  ]);
@@ -8036,7 +8456,7 @@ function printResolvedSelectionContext(root, options) {
8036
8456
  }
8037
8457
  function registerTaskFlowCommands(program) {
8038
8458
  const next = program.command("next").description("Select the next COOP work item");
8039
- next.command("task").description("Show the top ready task").option("--track <track>", "Filter by the home/contributing track lens, else config.defaults.track if set").option("--delivery <delivery>", "Filter by delivery membership").option("--executor <executor>", "Filter by executor (human|ai|ci|hybrid)").option("--today <date>", "Evaluation date (YYYY-MM-DD)").action((options) => {
8459
+ const nextAction = (options) => {
8040
8460
  const root = resolveRepoRoot();
8041
8461
  printResolvedSelectionContext(root, options);
8042
8462
  const selected = selectTopReadyTask(root, {
@@ -8046,9 +8466,11 @@ function registerTaskFlowCommands(program) {
8046
8466
  today: options.today
8047
8467
  });
8048
8468
  console.log(formatSelectedTask(selected.entry, selected.selection));
8049
- });
8469
+ };
8470
+ next.description("Show the top ready task").option("--track <track>", "Filter by the home/contributing track lens, else config.defaults.track if set").option("--delivery <delivery>", "Filter by delivery membership").option("--executor <executor>", "Filter by executor (human|ai|ci|hybrid)").option("--today <date>", "Evaluation date (YYYY-MM-DD)").action(nextAction);
8471
+ next.command("task").description("Show the top ready task").option("--track <track>", "Filter by the home/contributing track lens, else config.defaults.track if set").option("--delivery <delivery>", "Filter by delivery membership").option("--executor <executor>", "Filter by executor (human|ai|ci|hybrid)").option("--today <date>", "Evaluation date (YYYY-MM-DD)").action(nextAction);
8050
8472
  const pick = program.command("pick").description("Pick the next COOP work item");
8051
- pick.command("task").description("Select the top ready task and move it into active work").argument("[id]", "Task ID or alias").option("--track <track>", "Filter by the home/contributing track lens, else config.defaults.track if set").option("--delivery <delivery>", "Filter by delivery membership").option("--executor <executor>", "Filter by executor (human|ai|ci|hybrid)").option("--today <date>", "Evaluation date (YYYY-MM-DD)").option("--promote", "Promote the task in the current track/version context before starting it").option("--to <assignee>", "Assign the selected task before starting it").option("--claim", "Assign the selected task to the actor/user/default author before starting it").option("--actor <actor>", "Actor performing assignment/transition").option("--user <user>", "Current user for advisory authorization checks").option("--force", "Override advisory authorization checks").action(async (id, options) => {
8473
+ const pickAction = async (id, options) => {
8052
8474
  const root = resolveRepoRoot();
8053
8475
  printResolvedSelectionContext(root, options);
8054
8476
  const selected = id?.trim() ? {
@@ -8076,9 +8498,11 @@ function registerTaskFlowCommands(program) {
8076
8498
  console.log(formatSelectedTask(selected.entry, selected.selection));
8077
8499
  maybePromote(root, selected.entry.task.id, options);
8078
8500
  await claimAndStart(root, selected.entry.task.id, options);
8079
- });
8501
+ };
8502
+ pick.argument("[id]", "Task ID or alias").description("Select the top ready task and move it into active work").option("--track <track>", "Filter by the home/contributing track lens, else config.defaults.track if set").option("--delivery <delivery>", "Filter by delivery membership").option("--executor <executor>", "Filter by executor (human|ai|ci|hybrid)").option("--today <date>", "Evaluation date (YYYY-MM-DD)").option("--promote", "Promote the task in the current track/version context before starting it").option("--to <assignee>", "Assign the selected task before starting it").option("--claim", "Assign the selected task to the actor/user/default author before starting it").option("--actor <actor>", "Actor performing assignment/transition").option("--user <user>", "Current user for advisory authorization checks").option("--force", "Override advisory authorization checks").action(pickAction);
8503
+ pick.command("task").description("Select the top ready task and move it into active work").argument("[id]", "Task ID or alias").option("--track <track>", "Filter by the home/contributing track lens, else config.defaults.track if set").option("--delivery <delivery>", "Filter by delivery membership").option("--executor <executor>", "Filter by executor (human|ai|ci|hybrid)").option("--today <date>", "Evaluation date (YYYY-MM-DD)").option("--promote", "Promote the task in the current track/version context before starting it").option("--to <assignee>", "Assign the selected task before starting it").option("--claim", "Assign the selected task to the actor/user/default author before starting it").option("--actor <actor>", "Actor performing assignment/transition").option("--user <user>", "Current user for advisory authorization checks").option("--force", "Override advisory authorization checks").action(pickAction);
8080
8504
  const start = program.command("start").description("Start COOP work on a task");
8081
- start.command("task").description("Start a specific task, or the top ready task if no id is provided").argument("[id]", "Task ID or alias").option("--track <track>", "Filter by the home/contributing track lens when no id is provided, else config.defaults.track if set").option("--delivery <delivery>", "Filter by delivery membership when no id is provided").option("--executor <executor>", "Filter by executor (human|ai|ci|hybrid) when no id is provided").option("--today <date>", "Evaluation date (YYYY-MM-DD) when no id is provided").option("--promote", "Promote the task in the current track/version context before starting it").option("--to <assignee>", "Assign the task before starting it").option("--claim", "Assign the task to the actor/user/default author before starting it").option("--actor <actor>", "Actor performing assignment/transition").option("--user <user>", "Current user for advisory authorization checks").option("--force", "Override advisory authorization checks").action(async (id, options) => {
8505
+ const startAction = async (id, options) => {
8082
8506
  const root = resolveRepoRoot();
8083
8507
  printResolvedSelectionContext(root, options);
8084
8508
  const taskId = id?.trim() || selectTopReadyTask(root, {
@@ -8111,11 +8535,13 @@ function registerTaskFlowCommands(program) {
8111
8535
  );
8112
8536
  maybePromote(root, reference.id, options);
8113
8537
  await claimAndStart(root, reference.id, options);
8114
- });
8538
+ };
8539
+ start.argument("[id]", "Task ID or alias").description("Start a specific task, or the top ready task if no id is provided").option("--track <track>", "Filter by the home/contributing track lens when no id is provided, else config.defaults.track if set").option("--delivery <delivery>", "Filter by delivery membership when no id is provided").option("--executor <executor>", "Filter by executor (human|ai|ci|hybrid) when no id is provided").option("--today <date>", "Evaluation date (YYYY-MM-DD) when no id is provided").option("--promote", "Promote the task in the current track/version context before starting it").option("--to <assignee>", "Assign the task before starting it").option("--claim", "Assign the task to the actor/user/default author before starting it").option("--actor <actor>", "Actor performing assignment/transition").option("--user <user>", "Current user for advisory authorization checks").option("--force", "Override advisory authorization checks").action(startAction);
8540
+ start.command("task").description("Start a specific task, or the top ready task if no id is provided").argument("[id]", "Task ID or alias").option("--track <track>", "Filter by the home/contributing track lens when no id is provided, else config.defaults.track if set").option("--delivery <delivery>", "Filter by delivery membership when no id is provided").option("--executor <executor>", "Filter by executor (human|ai|ci|hybrid) when no id is provided").option("--today <date>", "Evaluation date (YYYY-MM-DD) when no id is provided").option("--promote", "Promote the task in the current track/version context before starting it").option("--to <assignee>", "Assign the task before starting it").option("--claim", "Assign the task to the actor/user/default author before starting it").option("--actor <actor>", "Actor performing assignment/transition").option("--user <user>", "Current user for advisory authorization checks").option("--force", "Override advisory authorization checks").action(startAction);
8115
8541
  }
8116
8542
 
8117
8543
  // src/commands/ui.ts
8118
- import fs19 from "fs";
8544
+ import fs20 from "fs";
8119
8545
  import path24 from "path";
8120
8546
  import { createRequire } from "module";
8121
8547
  import { fileURLToPath } from "url";
@@ -8124,7 +8550,7 @@ import { IndexManager as IndexManager4 } from "@kitsy/coop-core";
8124
8550
  function findPackageRoot(entryPath) {
8125
8551
  let current = path24.dirname(entryPath);
8126
8552
  while (true) {
8127
- if (fs19.existsSync(path24.join(current, "package.json"))) {
8553
+ if (fs20.existsSync(path24.join(current, "package.json"))) {
8128
8554
  return current;
8129
8555
  }
8130
8556
  const parent = path24.dirname(current);
@@ -8225,8 +8651,8 @@ function registerUiCommand(program) {
8225
8651
  }
8226
8652
 
8227
8653
  // src/commands/update.ts
8228
- import fs20 from "fs";
8229
- import { IdeaStatus as IdeaStatus3, TaskPriority as TaskPriority3, TaskStatus as TaskStatus5, stringifyFrontmatter as stringifyFrontmatter5 } from "@kitsy/coop-core";
8654
+ import fs21 from "fs";
8655
+ import { IdeaStatus as IdeaStatus3, TaskPriority as TaskPriority3, TaskStatus as TaskStatus6, stringifyFrontmatter as stringifyFrontmatter5 } from "@kitsy/coop-core";
8230
8656
  function collect(value, previous = []) {
8231
8657
  return [...previous, value];
8232
8658
  }
@@ -8252,10 +8678,10 @@ function setValues(values) {
8252
8678
  }
8253
8679
  function loadBody(options) {
8254
8680
  if (options.bodyFile) {
8255
- return fs20.readFileSync(options.bodyFile, "utf8");
8681
+ return fs21.readFileSync(options.bodyFile, "utf8");
8256
8682
  }
8257
8683
  if (options.bodyStdin) {
8258
- return fs20.readFileSync(0, "utf8");
8684
+ return fs21.readFileSync(0, "utf8");
8259
8685
  }
8260
8686
  return void 0;
8261
8687
  }
@@ -8288,8 +8714,8 @@ function clearTrackPriorityOverrides(task, tracks) {
8288
8714
  }
8289
8715
  function normalizeTaskStatus(status) {
8290
8716
  const value = status.trim().toLowerCase();
8291
- if (!Object.values(TaskStatus5).includes(value)) {
8292
- throw new Error(`Invalid status '${status}'. Expected one of ${Object.values(TaskStatus5).join(", ")}.`);
8717
+ if (!Object.values(TaskStatus6).includes(value)) {
8718
+ throw new Error(`Invalid status '${status}'. Expected one of ${Object.values(TaskStatus6).join(", ")}.`);
8293
8719
  }
8294
8720
  return value;
8295
8721
  }
@@ -8798,8 +9224,51 @@ function registerWebhookCommand(program) {
8798
9224
  });
8799
9225
  }
8800
9226
 
9227
+ // src/commands/workflow.ts
9228
+ function registerWorkflowCommand(program) {
9229
+ const workflow = program.command("workflow").description("Configure workflow rules and lifecycle behavior");
9230
+ const transitions = workflow.command("transitions").description("Inspect or customize task transition rules");
9231
+ transitions.command("show").description("Show the effective task transition map").argument("[from]", "Optional source status").action((from) => {
9232
+ const root = resolveRepoRoot();
9233
+ ensureCoopInitialized(root);
9234
+ console.log(formatWorkflowTransitions(readEffectiveWorkflowTransitions(root), from));
9235
+ });
9236
+ transitions.command("set").description("Replace the allowed targets for one source status").argument("<from>", "Source task status").argument("<to_csv>", "Comma-separated target statuses").action((from, toCsv) => {
9237
+ const root = resolveRepoRoot();
9238
+ ensureCoopInitialized(root);
9239
+ const targets = setWorkflowTransitionTargets(root, from, toCsv.split(","));
9240
+ console.log(`${from.trim().toLowerCase()}: ${targets.join(", ")}`);
9241
+ });
9242
+ transitions.command("add").description("Add one allowed target status for a source status").argument("<from>", "Source task status").argument("<to>", "Target task status").action((from, to) => {
9243
+ const root = resolveRepoRoot();
9244
+ ensureCoopInitialized(root);
9245
+ const targets = addWorkflowTransitionTarget(root, from, to);
9246
+ console.log(`${from.trim().toLowerCase()}: ${targets.join(", ")}`);
9247
+ });
9248
+ transitions.command("remove").description("Remove one allowed target status for a source status").argument("<from>", "Source task status").argument("<to>", "Target task status").action((from, to) => {
9249
+ const root = resolveRepoRoot();
9250
+ ensureCoopInitialized(root);
9251
+ const targets = removeWorkflowTransitionTarget(root, from, to);
9252
+ console.log(`${from.trim().toLowerCase()}: ${targets.join(", ") || "(none)"}`);
9253
+ });
9254
+ transitions.command("reset").description("Reset transition overrides for one status or the whole workspace").argument("[from]", "Optional source task status").option("--all", "Reset all task transition overrides").action((from, options) => {
9255
+ const root = resolveRepoRoot();
9256
+ ensureCoopInitialized(root);
9257
+ if (options.all) {
9258
+ resetAllWorkflowTransitions(root);
9259
+ console.log("Reset all workflow transition overrides.");
9260
+ return;
9261
+ }
9262
+ if (!from?.trim()) {
9263
+ throw new Error("Provide <from> or pass --all.");
9264
+ }
9265
+ resetWorkflowTransitionStatus(root, from);
9266
+ console.log(`Reset workflow transition override for ${from.trim().toLowerCase()}.`);
9267
+ });
9268
+ }
9269
+
8801
9270
  // src/merge-driver/merge-driver.ts
8802
- import fs21 from "fs";
9271
+ import fs22 from "fs";
8803
9272
  import os3 from "os";
8804
9273
  import path25 from "path";
8805
9274
  import { spawnSync as spawnSync5 } from "child_process";
@@ -8884,33 +9353,33 @@ function mergeTextWithGit(ancestor, ours, theirs) {
8884
9353
  return { ok: false, output: stdout };
8885
9354
  }
8886
9355
  function mergeTaskFile(ancestorPath, oursPath, theirsPath) {
8887
- const ancestorRaw = fs21.readFileSync(ancestorPath, "utf8");
8888
- const oursRaw = fs21.readFileSync(oursPath, "utf8");
8889
- const theirsRaw = fs21.readFileSync(theirsPath, "utf8");
9356
+ const ancestorRaw = fs22.readFileSync(ancestorPath, "utf8");
9357
+ const oursRaw = fs22.readFileSync(oursPath, "utf8");
9358
+ const theirsRaw = fs22.readFileSync(theirsPath, "utf8");
8890
9359
  const ancestor = parseTaskDocument(ancestorRaw, ancestorPath);
8891
9360
  const ours = parseTaskDocument(oursRaw, oursPath);
8892
9361
  const theirs = parseTaskDocument(theirsRaw, theirsPath);
8893
9362
  const mergedFrontmatter = mergeTaskFrontmatter(ancestor.frontmatter, ours.frontmatter, theirs.frontmatter);
8894
- const tempDir = fs21.mkdtempSync(path25.join(os3.tmpdir(), "coop-merge-body-"));
9363
+ const tempDir = fs22.mkdtempSync(path25.join(os3.tmpdir(), "coop-merge-body-"));
8895
9364
  try {
8896
9365
  const ancestorBody = path25.join(tempDir, "ancestor.md");
8897
9366
  const oursBody = path25.join(tempDir, "ours.md");
8898
9367
  const theirsBody = path25.join(tempDir, "theirs.md");
8899
- fs21.writeFileSync(ancestorBody, ancestor.body, "utf8");
8900
- fs21.writeFileSync(oursBody, ours.body, "utf8");
8901
- fs21.writeFileSync(theirsBody, theirs.body, "utf8");
9368
+ fs22.writeFileSync(ancestorBody, ancestor.body, "utf8");
9369
+ fs22.writeFileSync(oursBody, ours.body, "utf8");
9370
+ fs22.writeFileSync(theirsBody, theirs.body, "utf8");
8902
9371
  const mergedBody = mergeTextWithGit(ancestorBody, oursBody, theirsBody);
8903
9372
  const output2 = stringifyFrontmatter6(mergedFrontmatter, mergedBody.output);
8904
- fs21.writeFileSync(oursPath, output2, "utf8");
9373
+ fs22.writeFileSync(oursPath, output2, "utf8");
8905
9374
  return mergedBody.ok ? 0 : 1;
8906
9375
  } finally {
8907
- fs21.rmSync(tempDir, { recursive: true, force: true });
9376
+ fs22.rmSync(tempDir, { recursive: true, force: true });
8908
9377
  }
8909
9378
  }
8910
9379
  function mergeDeliveryFile(ancestorPath, oursPath, theirsPath) {
8911
- const ancestor = parseYamlContent3(fs21.readFileSync(ancestorPath, "utf8"), ancestorPath);
8912
- const ours = parseYamlContent3(fs21.readFileSync(oursPath, "utf8"), oursPath);
8913
- const theirs = parseYamlContent3(fs21.readFileSync(theirsPath, "utf8"), theirsPath);
9380
+ const ancestor = parseYamlContent3(fs22.readFileSync(ancestorPath, "utf8"), ancestorPath);
9381
+ const ours = parseYamlContent3(fs22.readFileSync(oursPath, "utf8"), oursPath);
9382
+ const theirs = parseYamlContent3(fs22.readFileSync(theirsPath, "utf8"), theirsPath);
8914
9383
  const oursUpdated = asTimestamp(ours.updated);
8915
9384
  const theirsUpdated = asTimestamp(theirs.updated);
8916
9385
  const base = ancestor;
@@ -8920,7 +9389,7 @@ function mergeDeliveryFile(ancestorPath, oursPath, theirsPath) {
8920
9389
  const value = chooseFieldValue(key, base[key], ours[key], theirs[key], oursUpdated, theirsUpdated);
8921
9390
  if (value !== void 0) merged[key] = value;
8922
9391
  }
8923
- fs21.writeFileSync(oursPath, stringifyYamlContent2(merged), "utf8");
9392
+ fs22.writeFileSync(oursPath, stringifyYamlContent2(merged), "utf8");
8924
9393
  return 0;
8925
9394
  }
8926
9395
  function runMergeDriver(kind, ancestorPath, oursPath, theirsPath) {
@@ -8935,25 +9404,34 @@ function renderBasicHelp() {
8935
9404
  return [
8936
9405
  "COOP Basics",
8937
9406
  "",
9407
+ "Command shape rule:",
9408
+ "- If a command accepts an entity reference, the entity noun is optional: `coop show PM-100` and `coop show task PM-100` are equivalent.",
9409
+ "- Commands without an entity reference, such as `coop next`, remain task-first workflow surfaces.",
9410
+ "",
8938
9411
  "Day-to-day commands:",
8939
9412
  "- `coop current`: show working context, active work, and the next ready task",
8940
9413
  "- `coop use track <id>` / `coop use delivery <id>` / `coop use reset`: manage working scope defaults",
8941
9414
  "- `coop list tracks` / `coop list deliveries`: inspect valid named values before assigning them",
8942
9415
  "- `coop naming`: inspect per-entity ID rules and naming tokens",
9416
+ "- `coop naming token default set <token> <value>` / `clear`: store default custom-token values used during auto-naming when CLI args are omitted",
8943
9417
  '- `coop naming preview "Title" --entity task`: preview the generated ID before creating an item; templates support `TITLE##` like `TITLE18`, `TITLE8`, or `TITLE08`',
8944
9418
  "- `coop naming reset task`: reset one entity's naming template to the default",
8945
9419
  "- `coop create task --from-file draft.yml` / `coop create idea --stdin`: ingest structured drafts; COOP keeps only recognized fields, fills metadata, and warns on ignored fields",
8946
- "- `coop next task` or `coop pick task`: choose work from COOP",
9420
+ "- `coop next` or `coop pick [id]`: choose work from COOP (`next` shorthand is task-only)",
8947
9421
  "- `coop promote <id>`: move a task to the top of the current working track/version selection lens",
8948
9422
  "- `coop show <id>`: inspect a task, idea, or delivery",
8949
- "- `coop list tasks --track <id>`: browse scoped work",
9423
+ "- `coop rename <id> <alias>`: add a stable human-facing name without changing the canonical id",
9424
+ "- `coop list tasks --track <id>` / `--completed` / `--all`: browse scoped work, done work, or ignore current `coop use` filters",
8950
9425
  "- `coop update <id> --track <id> --delivery <id>`: update task metadata",
8951
9426
  '- `coop update <id> --acceptance-set "..." --acceptance-set "..."`: replace the whole acceptance list cleanly when an old task was created with the wrong parsing',
8952
9427
  "- `coop update <id> --tests-set ... --authority-ref-set ... --deps-set ...`: the same full-replace pattern works for other mutable list fields too",
8953
9428
  '- `coop comment <id> --message "..."`: append a task comment',
8954
9429
  "- `coop log-time <id> --hours 2 --kind worked`: append time spent",
8955
9430
  "- `coop alias remove <id> <alias>`: remove a shorthand alias from a task or idea",
8956
- "- `coop review task <id>` / `coop complete task <id>`: move work through lifecycle",
9431
+ "- `coop review <id>` / `coop complete <id>`: move work through lifecycle (`... task <id>` still works too)",
9432
+ "- `coop block <id>` / `coop unblock <id>`: use the universal recovery states; `unblock` always lands on `todo`",
9433
+ "- `coop workflow transitions show`: inspect the effective lifecycle map",
9434
+ "- `coop workflow transitions add <from> <to>` / `remove` / `reset`: customize transition rules without hand-editing config",
8957
9435
  "- `coop help-ai --initial-prompt --strict --repo C:/path/to/repo --delivery MVP --command coop.cmd`: hand off COOP context to an agent",
8958
9436
  "",
8959
9437
  "Use `coop <command> --help` for detailed flags."
@@ -8970,7 +9448,7 @@ function readVersion() {
8970
9448
  const currentFile = fileURLToPath2(import.meta.url);
8971
9449
  const packageJsonPath = path26.resolve(path26.dirname(currentFile), "..", "package.json");
8972
9450
  try {
8973
- const parsed = JSON.parse(fs22.readFileSync(packageJsonPath, "utf8"));
9451
+ const parsed = JSON.parse(fs23.readFileSync(packageJsonPath, "utf8"));
8974
9452
  return parsed.version ?? "0.0.0";
8975
9453
  } catch {
8976
9454
  return "0.0.0";
@@ -8992,9 +9470,11 @@ function createProgram() {
8992
9470
  Common day-to-day commands:
8993
9471
  coop basics
8994
9472
  coop current
8995
- coop next task
9473
+ coop next
8996
9474
  coop show <id>
9475
+ coop rename <id> <alias>
8997
9476
  coop naming
9477
+ coop workflow transitions show
8998
9478
  `);
8999
9479
  registerInitCommand(program);
9000
9480
  registerCreateCommand(program);
@@ -9020,6 +9500,7 @@ Common day-to-day commands:
9020
9500
  registerPromoteCommand(program);
9021
9501
  registerProjectCommand(program);
9022
9502
  registerPromptCommand(program);
9503
+ registerRenameCommand(program);
9023
9504
  registerRefineCommand(program);
9024
9505
  registerRunCommand(program);
9025
9506
  registerSearchCommand(program);
@@ -9030,6 +9511,7 @@ Common day-to-day commands:
9030
9511
  registerUseCommand(program);
9031
9512
  registerViewCommand(program);
9032
9513
  registerWebhookCommand(program);
9514
+ registerWorkflowCommand(program);
9033
9515
  registerPhasePlaceholder(program, "ext", 3, "Plugin extension commands");
9034
9516
  program.command("basics").alias("help-basic").description("Show the small set of COOP commands that cover most day-to-day work").action(() => {
9035
9517
  console.log(renderBasicHelp());