@kitsy/coop 2.2.4 → 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 +894 -265
  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.`);
@@ -1888,6 +1904,8 @@ function promoteTaskForContext(task, context) {
1888
1904
  const next = {
1889
1905
  ...task,
1890
1906
  updated: todayIsoDate(),
1907
+ promoted_at: (/* @__PURE__ */ new Date()).toISOString(),
1908
+ promoted_track: context.track?.trim() || null,
1891
1909
  fix_versions: [...task.fix_versions ?? []],
1892
1910
  delivery_tracks: [...task.delivery_tracks ?? []],
1893
1911
  priority_context: { ...task.priority_context ?? {} }
@@ -2023,6 +2041,7 @@ function formatResolvedContextMessage(values) {
2023
2041
  }
2024
2042
 
2025
2043
  // src/utils/taskflow.ts
2044
+ var UNIVERSAL_RECOVERY_STATES = /* @__PURE__ */ new Set(["todo", "blocked", "canceled"]);
2026
2045
  function configDefaultTrack(root) {
2027
2046
  return sharedDefault(root, "track");
2028
2047
  }
@@ -2135,7 +2154,8 @@ async function assignTaskByReference(root, id, options) {
2135
2154
  throw new Error(`Task '${reference.id}' not found.`);
2136
2155
  }
2137
2156
  const user = options.user?.trim() || defaultCoopAuthor(root);
2138
- const auth = load_auth_config(readCoopConfig(root).raw);
2157
+ const configView = readCoopConfig(root);
2158
+ const auth = load_auth_config(configView.raw);
2139
2159
  const allowed = check_permission(user, "assign_task", { config: auth });
2140
2160
  if (!allowed) {
2141
2161
  if (!options.force) {
@@ -2208,6 +2228,96 @@ function dependencyStatusMapForTask(taskId, graph) {
2208
2228
  }
2209
2229
  return statuses;
2210
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
+ }
2211
2321
  async function transitionTaskByReference(root, id, status, options) {
2212
2322
  const target = status.toLowerCase();
2213
2323
  if (!Object.values(TaskStatus).includes(target)) {
@@ -2221,7 +2331,8 @@ async function transitionTaskByReference(root, id, status, options) {
2221
2331
  throw new Error(`Task '${reference.id}' not found.`);
2222
2332
  }
2223
2333
  const user = options.user?.trim() || defaultCoopAuthor(root);
2224
- const auth = load_auth_config(readCoopConfig(root).raw);
2334
+ const configView = readCoopConfig(root);
2335
+ const auth = load_auth_config(configView.raw);
2225
2336
  const allowed = check_permission(user, "transition_task", {
2226
2337
  config: auth,
2227
2338
  taskOwner: existing.assignee ?? null
@@ -2236,7 +2347,8 @@ async function transitionTaskByReference(root, id, status, options) {
2236
2347
  const parsed = parseTaskFile5(filePath);
2237
2348
  const result = transition2(parsed.task, target, {
2238
2349
  actor: options.actor ?? user,
2239
- dependencyStatuses: dependencyStatusMapForTask(reference.id, graph)
2350
+ dependencyStatuses: dependencyStatusMapForTask(reference.id, graph),
2351
+ config: configView.raw
2240
2352
  });
2241
2353
  if (!result.success) {
2242
2354
  throw new Error(result.error ?? "Transition failed.");
@@ -2252,28 +2364,87 @@ ${errors}`);
2252
2364
  raw: parsed.raw,
2253
2365
  filePath
2254
2366
  });
2255
- const event = {
2256
- type: "task.transitioned",
2257
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2258
- payload: {
2259
- task_id: result.task.id,
2260
- from: parsed.task.status,
2261
- to: result.task.status,
2262
- actor: options.actor ?? user
2263
- }
2264
- };
2265
- const pluginResults = await run_plugins_for_event2(coopPath, event, {
2266
- graph: load_graph2(coopPath),
2267
- action_handlers: {
2268
- github_pr: async (plugin, trigger, action) => executeGitHubPluginAction(root, plugin, trigger, action, result.task.id)
2269
- }
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
2270
2385
  });
2271
- for (const pluginResult of pluginResults) {
2272
- if (!pluginResult.success) {
2273
- 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.`);
2274
2389
  }
2390
+ console.warn(`[COOP][auth] override: user '${user}' forced transition_task on '${reference.id}'.`);
2275
2391
  }
2276
- 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
+ };
2277
2448
  }
2278
2449
 
2279
2450
  // src/commands/assign.ts
@@ -2314,6 +2485,124 @@ function registerCommentCommand(program) {
2314
2485
  });
2315
2486
  }
2316
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
+
2317
2606
  // src/commands/config.ts
2318
2607
  var INDEX_DATA_KEY = "index.data";
2319
2608
  var ID_NAMING_KEY = "id.naming";
@@ -2323,6 +2612,7 @@ var ARTIFACTS_DIR_KEY = "artifacts.dir";
2323
2612
  var PROJECT_NAME_KEY = "project.name";
2324
2613
  var PROJECT_ID_KEY = "project.id";
2325
2614
  var PROJECT_ALIASES_KEY = "project.aliases";
2615
+ var WORKFLOW_TASK_TRANSITIONS_KEY = "workflow.task-transitions";
2326
2616
  var SUPPORTED_KEYS = [
2327
2617
  INDEX_DATA_KEY,
2328
2618
  ID_NAMING_KEY,
@@ -2331,8 +2621,12 @@ var SUPPORTED_KEYS = [
2331
2621
  ARTIFACTS_DIR_KEY,
2332
2622
  PROJECT_NAME_KEY,
2333
2623
  PROJECT_ID_KEY,
2334
- PROJECT_ALIASES_KEY
2624
+ PROJECT_ALIASES_KEY,
2625
+ WORKFLOW_TASK_TRANSITIONS_KEY
2335
2626
  ];
2627
+ function isWorkflowTaskTransitionsKey(keyPath) {
2628
+ return keyPath === WORKFLOW_TASK_TRANSITIONS_KEY || keyPath.startsWith(`${WORKFLOW_TASK_TRANSITIONS_KEY}.`);
2629
+ }
2336
2630
  function readIndexDataValue(root) {
2337
2631
  const { indexDataFormat } = readCoopConfig(root);
2338
2632
  return indexDataFormat;
@@ -2366,6 +2660,15 @@ function readProjectAliasesValue(root) {
2366
2660
  const aliases = readCoopConfig(root).projectAliases;
2367
2661
  return aliases.length > 0 ? aliases.join(",") : "(unset)";
2368
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
+ }
2369
2672
  function writeIndexDataValue(root, value) {
2370
2673
  const nextValue = value.trim().toLowerCase();
2371
2674
  if (nextValue !== "yaml" && nextValue !== "json") {
@@ -2469,12 +2772,17 @@ function writeProjectAliasesValue(root, value) {
2469
2772
  next.project = projectRaw;
2470
2773
  writeCoopConfig(root, next);
2471
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
+ }
2472
2780
  function registerConfigCommand(program) {
2473
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) => {
2474
2782
  const root = resolveRepoRoot();
2475
2783
  ensureCoopInitialized(root);
2476
2784
  const keyPath = key.trim();
2477
- if (!SUPPORTED_KEYS.includes(keyPath)) {
2785
+ if (!SUPPORTED_KEYS.includes(keyPath) && !isWorkflowTaskTransitionsKey(keyPath)) {
2478
2786
  throw new Error(`Unsupported config key '${keyPath}'. Supported keys: ${SUPPORTED_KEYS.join(", ")}`);
2479
2787
  }
2480
2788
  if (value === void 0) {
@@ -2506,6 +2814,10 @@ function registerConfigCommand(program) {
2506
2814
  console.log(`${PROJECT_ALIASES_KEY}=${readProjectAliasesValue(root)}`);
2507
2815
  return;
2508
2816
  }
2817
+ if (isWorkflowTaskTransitionsKey(keyPath)) {
2818
+ console.log(`${keyPath}=${readWorkflowTaskTransitionsValue(root, keyPath)}`);
2819
+ return;
2820
+ }
2509
2821
  console.log(`${AI_MODEL_KEY}=${readAiModelValue(root)}`);
2510
2822
  return;
2511
2823
  }
@@ -2544,26 +2856,31 @@ function registerConfigCommand(program) {
2544
2856
  console.log(`${PROJECT_ALIASES_KEY}=${readProjectAliasesValue(root)}`);
2545
2857
  return;
2546
2858
  }
2859
+ if (isWorkflowTaskTransitionsKey(keyPath)) {
2860
+ writeWorkflowTaskTransitionsValue(root, keyPath, value);
2861
+ console.log(`${keyPath}=${readWorkflowTaskTransitionsValue(root, keyPath)}`);
2862
+ return;
2863
+ }
2547
2864
  writeAiModelValue(root, value);
2548
2865
  console.log(`${AI_MODEL_KEY}=${readAiModelValue(root)}`);
2549
2866
  });
2550
2867
  }
2551
2868
 
2552
2869
  // src/commands/create.ts
2553
- import fs7 from "fs";
2870
+ import fs8 from "fs";
2554
2871
  import path8 from "path";
2555
2872
  import {
2556
2873
  DeliveryStatus,
2557
2874
  IdeaStatus as IdeaStatus2,
2558
2875
  parseIdeaFile as parseIdeaFile3,
2559
- TaskStatus as TaskStatus3,
2876
+ TaskStatus as TaskStatus4,
2560
2877
  TaskType as TaskType2,
2561
2878
  check_permission as check_permission2,
2562
2879
  load_auth_config as load_auth_config2,
2563
2880
  parseDeliveryFile as parseDeliveryFile2,
2564
2881
  parseTaskFile as parseTaskFile7,
2565
2882
  parseYamlFile as parseYamlFile3,
2566
- writeYamlFile as writeYamlFile4,
2883
+ writeYamlFile as writeYamlFile5,
2567
2884
  stringifyFrontmatter as stringifyFrontmatter4,
2568
2885
  writeTask as writeTask6
2569
2886
  } from "@kitsy/coop-core";
@@ -2655,7 +2972,7 @@ async function select(question, choices, defaultIndex = 0) {
2655
2972
  }
2656
2973
 
2657
2974
  // src/utils/idea-drafts.ts
2658
- import fs5 from "fs";
2975
+ import fs6 from "fs";
2659
2976
  import path6 from "path";
2660
2977
  import {
2661
2978
  IdeaStatus,
@@ -2672,6 +2989,12 @@ function asUniqueStrings(value) {
2672
2989
  );
2673
2990
  return entries.length > 0 ? entries : void 0;
2674
2991
  }
2992
+ function formatIgnoredFieldWarning(source, context, keys) {
2993
+ if (keys.length === 0) {
2994
+ return "";
2995
+ }
2996
+ return `${source}: ignored unknown ${context} field(s): ${keys.sort((a, b) => a.localeCompare(b)).join(", ")}`;
2997
+ }
2675
2998
  function parseIdeaDraftObject(record, source) {
2676
2999
  const title = typeof record.title === "string" ? record.title.trim() : "";
2677
3000
  if (!title) {
@@ -2681,25 +3004,34 @@ function parseIdeaDraftObject(record, source) {
2681
3004
  if (status && !Object.values(IdeaStatus).includes(status)) {
2682
3005
  throw new Error(`${source}: invalid idea status '${status}'.`);
2683
3006
  }
3007
+ const allowedKeys = /* @__PURE__ */ new Set(["id", "title", "author", "source", "status", "tags", "aliases", "linked_tasks", "body"]);
3008
+ const ignoredKeys = Object.keys(record).filter((key) => !allowedKeys.has(key));
2684
3009
  return {
2685
- id: typeof record.id === "string" && record.id.trim() ? record.id.trim().toUpperCase() : void 0,
2686
- title,
2687
- author: typeof record.author === "string" && record.author.trim() ? record.author.trim() : void 0,
2688
- source: typeof record.source === "string" && record.source.trim() ? record.source.trim() : void 0,
2689
- status: status ? status : void 0,
2690
- tags: asUniqueStrings(record.tags),
2691
- aliases: asUniqueStrings(record.aliases),
2692
- linked_tasks: asUniqueStrings(record.linked_tasks),
2693
- body: typeof record.body === "string" ? record.body : void 0
3010
+ value: {
3011
+ id: typeof record.id === "string" && record.id.trim() ? record.id.trim().toUpperCase() : void 0,
3012
+ title,
3013
+ author: typeof record.author === "string" && record.author.trim() ? record.author.trim() : void 0,
3014
+ source: typeof record.source === "string" && record.source.trim() ? record.source.trim() : void 0,
3015
+ status: status ? status : void 0,
3016
+ tags: asUniqueStrings(record.tags),
3017
+ aliases: asUniqueStrings(record.aliases),
3018
+ linked_tasks: asUniqueStrings(record.linked_tasks),
3019
+ body: typeof record.body === "string" ? record.body : void 0
3020
+ },
3021
+ warnings: ignoredKeys.length > 0 ? [formatIgnoredFieldWarning(source, "idea draft", ignoredKeys)] : []
2694
3022
  };
2695
3023
  }
2696
- function parseIdeaDraftInput(content, source) {
3024
+ function parseIdeaDraftInputWithWarnings(content, source) {
2697
3025
  const trimmed = content.trimStart();
2698
3026
  if (trimmed.startsWith("---")) {
2699
3027
  const { frontmatter, body } = parseFrontmatterContent(content, source);
3028
+ const parsed = parseIdeaDraftObject(frontmatter, source);
2700
3029
  return {
2701
- ...parseIdeaDraftObject(frontmatter, source),
2702
- body: body || (typeof frontmatter.body === "string" ? frontmatter.body : void 0)
3030
+ value: {
3031
+ ...parsed.value,
3032
+ body: body || (typeof frontmatter.body === "string" ? frontmatter.body : void 0)
3033
+ },
3034
+ warnings: parsed.warnings
2703
3035
  };
2704
3036
  }
2705
3037
  return parseIdeaDraftObject(parseYamlContent(content, source), source);
@@ -2727,15 +3059,15 @@ function writeIdeaFromDraft(root, projectDir, draft) {
2727
3059
  linked_tasks: draft.linked_tasks ?? []
2728
3060
  };
2729
3061
  const filePath = path6.join(projectDir, "ideas", `${id}.md`);
2730
- if (fs5.existsSync(filePath)) {
3062
+ if (fs6.existsSync(filePath)) {
2731
3063
  throw new Error(`Idea '${id}' already exists.`);
2732
3064
  }
2733
- fs5.writeFileSync(filePath, stringifyFrontmatter3(frontmatter, draft.body ?? ""), "utf8");
3065
+ fs6.writeFileSync(filePath, stringifyFrontmatter3(frontmatter, draft.body ?? ""), "utf8");
2734
3066
  return filePath;
2735
3067
  }
2736
3068
 
2737
3069
  // src/utils/refinement-drafts.ts
2738
- import fs6 from "fs";
3070
+ import fs7 from "fs";
2739
3071
  import path7 from "path";
2740
3072
  import {
2741
3073
  IndexManager,
@@ -2770,9 +3102,15 @@ function nonEmptyStrings(value) {
2770
3102
  const entries = value.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean);
2771
3103
  return entries.length > 0 ? entries : void 0;
2772
3104
  }
3105
+ function formatIgnoredFieldWarning2(source, context, keys) {
3106
+ if (keys.length === 0) {
3107
+ return "";
3108
+ }
3109
+ return `${source}: ignored unknown ${context} field(s): ${keys.sort((a, b) => a.localeCompare(b)).join(", ")}`;
3110
+ }
2773
3111
  function refinementDir(projectDir) {
2774
3112
  const dir = path7.join(projectDir, "tmp", "refinements");
2775
- fs6.mkdirSync(dir, { recursive: true });
3113
+ fs7.mkdirSync(dir, { recursive: true });
2776
3114
  return dir;
2777
3115
  }
2778
3116
  function draftPath(projectDir, mode, sourceId) {
@@ -2810,8 +3148,8 @@ function assignCreateProposalIds(root, draft) {
2810
3148
  }
2811
3149
  function writeDraftFile(root, projectDir, draft, outputFile) {
2812
3150
  const filePath = outputFile?.trim() ? path7.resolve(root, outputFile.trim()) : draftPath(projectDir, draft.mode, draft.source.id);
2813
- fs6.mkdirSync(path7.dirname(filePath), { recursive: true });
2814
- fs6.writeFileSync(filePath, stringifyYamlContent(draft), "utf8");
3151
+ fs7.mkdirSync(path7.dirname(filePath), { recursive: true });
3152
+ fs7.writeFileSync(filePath, stringifyYamlContent(draft), "utf8");
2815
3153
  return filePath;
2816
3154
  }
2817
3155
  function printDraftSummary(root, draft, filePath) {
@@ -2826,7 +3164,7 @@ function printDraftSummary(root, draft, filePath) {
2826
3164
  }
2827
3165
  console.log(`[COOP] apply with: coop apply draft --from-file ${path7.relative(root, filePath)}`);
2828
3166
  }
2829
- function parseRefinementDraftInput(content, source) {
3167
+ function parseRefinementDraftInputWithWarnings(content, source) {
2830
3168
  const parsed = parseYamlContent2(content, source);
2831
3169
  if (parsed.kind !== "refinement_draft" || parsed.version !== 1) {
2832
3170
  throw new Error(`${source}: not a supported COOP refinement draft.`);
@@ -2838,7 +3176,34 @@ function parseRefinementDraftInput(content, source) {
2838
3176
  const sourceId = typeof sourceRecord.id === "string" ? sourceRecord.id : "";
2839
3177
  const sourceTitle = typeof sourceRecord.title === "string" ? sourceRecord.title : sourceId;
2840
3178
  const proposalsRaw = Array.isArray(parsed.proposals) ? parsed.proposals : [];
2841
- const proposals = proposalsRaw.map((entry) => {
3179
+ const warnings = [];
3180
+ const draftAllowedKeys = /* @__PURE__ */ new Set(["kind", "version", "mode", "source", "summary", "generated_at", "proposals"]);
3181
+ const ignoredDraftKeys = Object.keys(parsed).filter((key) => !draftAllowedKeys.has(key));
3182
+ if (ignoredDraftKeys.length > 0) {
3183
+ warnings.push(formatIgnoredFieldWarning2(source, "refinement draft", ignoredDraftKeys));
3184
+ }
3185
+ const sourceAllowedKeys = /* @__PURE__ */ new Set(["id", "title"]);
3186
+ const ignoredSourceKeys = Object.keys(sourceRecord).filter((key) => !sourceAllowedKeys.has(key));
3187
+ if (ignoredSourceKeys.length > 0) {
3188
+ warnings.push(formatIgnoredFieldWarning2(source, "refinement draft source", ignoredSourceKeys));
3189
+ }
3190
+ const proposalAllowedKeys = /* @__PURE__ */ new Set([
3191
+ "action",
3192
+ "id",
3193
+ "target_id",
3194
+ "title",
3195
+ "type",
3196
+ "status",
3197
+ "track",
3198
+ "priority",
3199
+ "depends_on",
3200
+ "acceptance",
3201
+ "tests_required",
3202
+ "authority_refs",
3203
+ "derived_refs",
3204
+ "body"
3205
+ ]);
3206
+ const proposals = proposalsRaw.map((entry, index) => {
2842
3207
  if (!isObject2(entry)) {
2843
3208
  throw new Error(`${source}: refinement draft proposal must be an object.`);
2844
3209
  }
@@ -2850,6 +3215,10 @@ function parseRefinementDraftInput(content, source) {
2850
3215
  if (!title) {
2851
3216
  throw new Error(`${source}: refinement draft proposal title is required.`);
2852
3217
  }
3218
+ const ignoredProposalKeys = Object.keys(entry).filter((key) => !proposalAllowedKeys.has(key));
3219
+ if (ignoredProposalKeys.length > 0) {
3220
+ warnings.push(formatIgnoredFieldWarning2(source, `refinement draft proposal ${index + 1}`, ignoredProposalKeys));
3221
+ }
2853
3222
  return {
2854
3223
  action,
2855
3224
  id: typeof entry.id === "string" ? entry.id.trim() || void 0 : void 0,
@@ -2871,17 +3240,20 @@ function parseRefinementDraftInput(content, source) {
2871
3240
  throw new Error(`${source}: refinement draft has no proposals.`);
2872
3241
  }
2873
3242
  return {
2874
- kind: "refinement_draft",
2875
- version: 1,
2876
- mode: parsed.mode,
2877
- source: {
2878
- entity_type: parsed.mode,
2879
- id: sourceId,
2880
- title: sourceTitle
3243
+ value: {
3244
+ kind: "refinement_draft",
3245
+ version: 1,
3246
+ mode: parsed.mode,
3247
+ source: {
3248
+ entity_type: parsed.mode,
3249
+ id: sourceId,
3250
+ title: sourceTitle
3251
+ },
3252
+ summary: typeof parsed.summary === "string" && parsed.summary.trim() ? parsed.summary.trim() : `Refinement draft for ${sourceId}`,
3253
+ generated_at: typeof parsed.generated_at === "string" && parsed.generated_at.trim() ? parsed.generated_at.trim() : (/* @__PURE__ */ new Date()).toISOString(),
3254
+ proposals
2881
3255
  },
2882
- summary: typeof parsed.summary === "string" && parsed.summary.trim() ? parsed.summary.trim() : `Refinement draft for ${sourceId}`,
2883
- generated_at: typeof parsed.generated_at === "string" && parsed.generated_at.trim() ? parsed.generated_at.trim() : (/* @__PURE__ */ new Date()).toISOString(),
2884
- proposals
3256
+ warnings
2885
3257
  };
2886
3258
  }
2887
3259
  function taskFromProposal(proposal, fallbackDate) {
@@ -2909,7 +3281,7 @@ function applyCreateProposal(projectDir, proposal) {
2909
3281
  throw new Error(`Create proposal '${proposal.title}' is missing id.`);
2910
3282
  }
2911
3283
  const filePath = path7.join(projectDir, "tasks", `${id}.md`);
2912
- if (fs6.existsSync(filePath)) {
3284
+ if (fs7.existsSync(filePath)) {
2913
3285
  throw new Error(`Task '${id}' already exists.`);
2914
3286
  }
2915
3287
  const task = taskFromProposal({ ...proposal, id }, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10));
@@ -2971,7 +3343,7 @@ function applyRefinementDraft(root, projectDir, draft) {
2971
3343
  async function readDraftContent(root, options) {
2972
3344
  if (options.fromFile?.trim()) {
2973
3345
  const filePath = path7.resolve(root, options.fromFile.trim());
2974
- return { content: fs6.readFileSync(filePath, "utf8"), source: filePath };
3346
+ return { content: fs7.readFileSync(filePath, "utf8"), source: filePath };
2975
3347
  }
2976
3348
  if (options.stdin) {
2977
3349
  return { content: await readStdinText(), source: "<stdin>" };
@@ -2988,50 +3360,78 @@ function parseTaskDraftObject(record, source) {
2988
3360
  const status = record.status === "todo" || record.status === "blocked" || record.status === "in_progress" || record.status === "in_review" || record.status === "done" || record.status === "canceled" ? record.status : void 0;
2989
3361
  const priority = record.priority === "p0" || record.priority === "p1" || record.priority === "p2" || record.priority === "p3" ? record.priority : void 0;
2990
3362
  const origin = isObject2(record.origin) ? record.origin : {};
3363
+ const taskAllowedKeys = /* @__PURE__ */ new Set([
3364
+ "id",
3365
+ "title",
3366
+ "type",
3367
+ "status",
3368
+ "track",
3369
+ "priority",
3370
+ "depends_on",
3371
+ "acceptance",
3372
+ "tests_required",
3373
+ "authority_refs",
3374
+ "derived_refs",
3375
+ "origin",
3376
+ "body"
3377
+ ]);
3378
+ const originAllowedKeys = /* @__PURE__ */ new Set(["authority_refs", "derived_refs"]);
3379
+ const ignoredTaskKeys = Object.keys(record).filter((key) => !taskAllowedKeys.has(key));
3380
+ const ignoredOriginKeys = Object.keys(origin).filter((key) => !originAllowedKeys.has(key));
3381
+ const warnings = [];
3382
+ if (ignoredTaskKeys.length > 0) {
3383
+ warnings.push(formatIgnoredFieldWarning2(source, "task draft", ignoredTaskKeys));
3384
+ }
3385
+ if (ignoredOriginKeys.length > 0) {
3386
+ warnings.push(formatIgnoredFieldWarning2(source, "task draft origin", ignoredOriginKeys));
3387
+ }
2991
3388
  return {
2992
- kind: "refinement_draft",
2993
- version: 1,
2994
- mode: "task",
2995
- source: {
2996
- entity_type: "task",
2997
- id: id ?? "draft-task",
2998
- title
3389
+ value: {
3390
+ kind: "refinement_draft",
3391
+ version: 1,
3392
+ mode: "task",
3393
+ source: {
3394
+ entity_type: "task",
3395
+ id: id ?? "draft-task",
3396
+ title
3397
+ },
3398
+ summary: `Imported task draft for ${title}`,
3399
+ generated_at: (/* @__PURE__ */ new Date()).toISOString(),
3400
+ proposals: [
3401
+ {
3402
+ action: "create",
3403
+ id,
3404
+ title,
3405
+ type,
3406
+ status,
3407
+ track: typeof record.track === "string" ? record.track.trim() || void 0 : void 0,
3408
+ priority,
3409
+ depends_on: nonEmptyStrings(record.depends_on),
3410
+ acceptance: nonEmptyStrings(record.acceptance),
3411
+ tests_required: nonEmptyStrings(record.tests_required),
3412
+ authority_refs: nonEmptyStrings(record.authority_refs) ?? nonEmptyStrings(origin.authority_refs),
3413
+ derived_refs: nonEmptyStrings(record.derived_refs) ?? nonEmptyStrings(origin.derived_refs),
3414
+ body: typeof record.body === "string" ? record.body : void 0
3415
+ }
3416
+ ]
2999
3417
  },
3000
- summary: `Imported task draft for ${title}`,
3001
- generated_at: (/* @__PURE__ */ new Date()).toISOString(),
3002
- proposals: [
3003
- {
3004
- action: "create",
3005
- id,
3006
- title,
3007
- type,
3008
- status,
3009
- track: typeof record.track === "string" ? record.track.trim() || void 0 : void 0,
3010
- priority,
3011
- depends_on: nonEmptyStrings(record.depends_on),
3012
- acceptance: nonEmptyStrings(record.acceptance),
3013
- tests_required: nonEmptyStrings(record.tests_required),
3014
- authority_refs: nonEmptyStrings(record.authority_refs) ?? nonEmptyStrings(origin.authority_refs),
3015
- derived_refs: nonEmptyStrings(record.derived_refs) ?? nonEmptyStrings(origin.derived_refs),
3016
- body: typeof record.body === "string" ? record.body : void 0
3017
- }
3018
- ]
3418
+ warnings
3019
3419
  };
3020
3420
  }
3021
- function parseTaskDraftOrRefinement(content, source) {
3421
+ function parseTaskDraftOrRefinementWithWarnings(content, source) {
3022
3422
  const trimmed = content.trimStart();
3023
3423
  if (trimmed.startsWith("---")) {
3024
3424
  const { frontmatter, body } = parseFrontmatterContent2(content, source);
3025
- const draft = parseTaskDraftObject(frontmatter, source);
3026
- draft.proposals[0] = {
3027
- ...draft.proposals[0],
3028
- body: body || draft.proposals[0]?.body
3425
+ const parsed2 = parseTaskDraftObject(frontmatter, source);
3426
+ parsed2.value.proposals[0] = {
3427
+ ...parsed2.value.proposals[0],
3428
+ body: body || parsed2.value.proposals[0]?.body
3029
3429
  };
3030
- return draft;
3430
+ return parsed2;
3031
3431
  }
3032
3432
  const parsed = parseYamlContent2(content, source);
3033
3433
  if (parsed.kind === "refinement_draft") {
3034
- return parseRefinementDraftInput(content, source);
3434
+ return parseRefinementDraftInputWithWarnings(content, source);
3035
3435
  }
3036
3436
  return parseTaskDraftObject(parsed, source);
3037
3437
  }
@@ -3047,8 +3447,12 @@ function parseCsv(value) {
3047
3447
  if (!value) return [];
3048
3448
  return value.split(",").map((entry) => entry.trim()).filter(Boolean);
3049
3449
  }
3050
- function collectMultiValue(value, previous = []) {
3051
- return [...previous, ...parseCsv(value)];
3450
+ function collectRepeatedValue(value, previous = []) {
3451
+ const next = value.trim();
3452
+ if (!next) {
3453
+ return previous;
3454
+ }
3455
+ return [...previous, next];
3052
3456
  }
3053
3457
  function toNumber(value, field) {
3054
3458
  if (value == null || value.trim().length === 0) return void 0;
@@ -3066,6 +3470,11 @@ function plusDaysIso(days) {
3066
3470
  function unique2(values) {
3067
3471
  return Array.from(new Set(values));
3068
3472
  }
3473
+ function printDraftWarnings(warnings) {
3474
+ for (const warning of warnings) {
3475
+ console.warn(`[COOP][warn] ${warning}`);
3476
+ }
3477
+ }
3069
3478
  function assertNoCaseInsensitiveNameConflict(kind, entries, candidateId, candidateName) {
3070
3479
  const normalizedName = candidateName.trim().toLowerCase();
3071
3480
  if (!normalizedName) {
@@ -3135,7 +3544,7 @@ function updateIdeaLinkedTasks(filePath, idea, raw, body, linked) {
3135
3544
  ...raw,
3136
3545
  linked_tasks: next
3137
3546
  };
3138
- fs7.writeFileSync(filePath, stringifyFrontmatter4(nextRaw, body), "utf8");
3547
+ fs8.writeFileSync(filePath, stringifyFrontmatter4(nextRaw, body), "utf8");
3139
3548
  }
3140
3549
  function makeTaskDraft(input2) {
3141
3550
  return {
@@ -3153,7 +3562,7 @@ function makeTaskDraft(input2) {
3153
3562
  }
3154
3563
  function registerCreateCommand(program) {
3155
3564
  const create = program.command("create").description("Create COOP entities");
3156
- 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 <items>", "Comma-separated acceptance criteria", collectMultiValue, []).option("--tests-required <items>", "Comma-separated required tests", collectMultiValue, []).option("--authority-ref <ref>", "Authority document reference", collectMultiValue, []).option("--derived-ref <ref>", "Derived planning document reference", collectMultiValue, []).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) => {
3157
3566
  const root = resolveRepoRoot();
3158
3567
  const coop = ensureCoopInitialized(root);
3159
3568
  const interactive = Boolean(options.interactive);
@@ -3189,8 +3598,10 @@ function registerCreateCommand(program) {
3189
3598
  fromFile: options.fromFile,
3190
3599
  stdin: options.stdin
3191
3600
  });
3601
+ const parsedDraftResult = parseTaskDraftOrRefinementWithWarnings(draftInput.content, draftInput.source);
3602
+ printDraftWarnings(parsedDraftResult.warnings);
3192
3603
  const parsedDraft = ensureValidCreateOnlyDraft(
3193
- assignCreateProposalIds(root, parseTaskDraftOrRefinement(draftInput.content, draftInput.source)),
3604
+ assignCreateProposalIds(root, parsedDraftResult.value),
3194
3605
  draftInput.source
3195
3606
  );
3196
3607
  const written = applyRefinementDraft(root, coop, parsedDraft);
@@ -3370,7 +3781,9 @@ function registerCreateCommand(program) {
3370
3781
  fromFile: options.fromFile,
3371
3782
  stdin: options.stdin
3372
3783
  });
3373
- const written = writeIdeaFromDraft(root, coop, parseIdeaDraftInput(draftInput.content, draftInput.source));
3784
+ const parsedIdeaDraft = parseIdeaDraftInputWithWarnings(draftInput.content, draftInput.source);
3785
+ printDraftWarnings(parsedIdeaDraft.warnings);
3786
+ const written = writeIdeaFromDraft(root, coop, parsedIdeaDraft.value);
3374
3787
  console.log(`[COOP] created 1 idea file from ${draftInput.source}`);
3375
3788
  console.log(`Created idea: ${path8.relative(root, written)}`);
3376
3789
  return;
@@ -3411,7 +3824,7 @@ function registerCreateCommand(program) {
3411
3824
  throw new Error(`Invalid idea status '${status}'.`);
3412
3825
  }
3413
3826
  const filePath = path8.join(coop, "ideas", `${id}.md`);
3414
- fs7.writeFileSync(filePath, stringifyFrontmatter4(frontmatter, body), "utf8");
3827
+ fs8.writeFileSync(filePath, stringifyFrontmatter4(frontmatter, body), "utf8");
3415
3828
  console.log(`Created idea: ${path8.relative(root, filePath)}`);
3416
3829
  });
3417
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) => {
@@ -3468,7 +3881,7 @@ function registerCreateCommand(program) {
3468
3881
  }
3469
3882
  };
3470
3883
  const filePath = path8.join(coop, "tracks", `${id}.yml`);
3471
- writeYamlFile4(filePath, payload);
3884
+ writeYamlFile5(filePath, payload);
3472
3885
  console.log(`Created track: ${path8.relative(root, filePath)}`);
3473
3886
  });
3474
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) => {
@@ -3602,7 +4015,7 @@ function registerCreateCommand(program) {
3602
4015
  }
3603
4016
  };
3604
4017
  const filePath = path8.join(coop, "deliveries", `${id}.yml`);
3605
- writeYamlFile4(filePath, payload);
4018
+ writeYamlFile5(filePath, payload);
3606
4019
  console.log(`Created delivery: ${path8.relative(root, filePath)}`);
3607
4020
  });
3608
4021
  }
@@ -3893,10 +4306,12 @@ var catalog = {
3893
4306
  selection_rules: [
3894
4307
  "Use `coop project show` first to confirm the active workspace and project.",
3895
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.",
3896
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`.",
3897
4311
  "Track and delivery references accept exact ids, stable short ids, and unique case-insensitive names.",
3898
- "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.",
3899
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.",
3900
4315
  "Use `--track` for the workstream lens (home track or delivery_tracks). Use `--delivery` for release/scope membership.",
3901
4316
  "Use `coop show <id>` or `coop show task <id>` before implementation to read acceptance, tests_required, dependencies, origin refs, and task metadata.",
3902
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."
@@ -3908,20 +4323,33 @@ var catalog = {
3908
4323
  "Canonical storage is under `.coop/projects/<project.id>/...`."
3909
4324
  ],
3910
4325
  allowed_lifecycle_commands: [
4326
+ "coop start <id>",
3911
4327
  "coop start task <id>",
4328
+ "coop review <id>",
3912
4329
  "coop review task <id>",
4330
+ "coop complete <id>",
3913
4331
  "coop complete task <id>",
4332
+ "coop block <id>",
3914
4333
  "coop block task <id>",
4334
+ "coop unblock <id>",
3915
4335
  "coop unblock task <id>",
4336
+ "coop cancel <id>",
3916
4337
  "coop cancel task <id>",
4338
+ "coop reopen <id>",
3917
4339
  "coop reopen task <id>",
3918
4340
  "coop transition task <id> <status>"
3919
4341
  ],
3920
4342
  lifecycle_requirements: [
3921
- "`coop start task <id>` moves a ready task into `in_progress`.",
3922
- "`coop review task <id>` requires the task to already be `in_progress` and moves it to `in_review`.",
3923
- "`coop complete task <id>` requires the task to already be `in_review` and moves it to `done`.",
3924
- "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."
3925
4353
  ],
3926
4354
  unsupported_command_warnings: [
3927
4355
  "Do not invent COOP commands such as `coop complete` when they are not present in `help-ai` output.",
@@ -3963,7 +4391,13 @@ var catalog = {
3963
4391
  { usage: "coop naming token create proj", purpose: "Create a custom naming token." },
3964
4392
  { usage: "coop naming token remove proj", purpose: "Delete a custom naming token." },
3965
4393
  { usage: "coop naming token value add proj UX", purpose: "Register an allowed value for a naming token." },
3966
- { 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." }
3967
4401
  ]
3968
4402
  },
3969
4403
  {
@@ -3971,19 +4405,19 @@ var catalog = {
3971
4405
  description: "Create ideas, tasks, tracks, and deliveries.",
3972
4406
  commands: [
3973
4407
  { usage: 'coop create idea "Subscription page"', purpose: "Create an idea with a positional title." },
3974
- { usage: "coop create idea --from-file idea-draft.yml", purpose: "Ingest a structured idea draft file." },
3975
- { usage: "cat idea.md | coop create idea --stdin", purpose: "Ingest an idea draft from stdin." },
4408
+ { usage: "coop create idea --from-file idea-draft.yml", purpose: "Ingest a structured idea draft file; COOP keeps only recognized fields, synthesizes canonical metadata, and warns on ignored fields." },
4409
+ { usage: "cat idea.md | coop create idea --stdin", purpose: "Ingest an idea draft from stdin with the same schema-filtered behavior." },
3976
4410
  { usage: 'coop create task "Implement webhook pipeline"', purpose: "Create a task with defaults." },
3977
4411
  { usage: 'coop create task "UX: Auth user journey" --id UX-AUTH-1', purpose: "Create a task with an explicit primary ID." },
3978
4412
  { usage: 'coop create task "UX: Auth user journey" --proj UX --feat AUTH', purpose: "Create a task using configured naming tokens." },
3979
4413
  { usage: 'coop create task --title "Lock auth contract" --track MVP --delivery MVP', purpose: "Create a task directly inside a track and delivery scope." },
3980
4414
  { usage: "coop create track MVP", purpose: "Create a named track with slug-style default ID." },
3981
4415
  {
3982
- usage: 'coop create task --title "Lock auth contract" --acceptance "Contract approved,Client mapping documented" --tests-required "Contract fixture test" --authority-ref docs/webapp-mvp-plan.md#auth',
3983
- purpose: "Create a planning-grade task with acceptance, tests, and origin refs."
4416
+ usage: 'coop create task --title "Lock auth contract" --acceptance "Contract approved, client mapping documented" --acceptance "Rollback path documented" --tests-required "Contract fixture test" --authority-ref docs/webapp-mvp-plan.md#auth',
4417
+ purpose: "Create a planning-grade task with repeatable free-text acceptance/tests fields and origin refs."
3984
4418
  },
3985
- { usage: "coop create task --from-file task-draft.yml", purpose: "Ingest a structured task draft file." },
3986
- { usage: "cat task.md | coop create task --stdin", purpose: "Ingest a task draft from stdin." },
4419
+ { usage: "coop create task --from-file task-draft.yml", purpose: "Ingest a structured task draft file; COOP keeps only recognized fields, synthesizes canonical metadata, and warns on ignored fields." },
4420
+ { usage: "cat task.md | coop create task --stdin", purpose: "Ingest a task draft from stdin with the same schema-filtered behavior." },
3987
4421
  { usage: "coop create delivery --name MVP --scope PM-100,PM-101", purpose: "Create a delivery from task scope." }
3988
4422
  ]
3989
4423
  },
@@ -3995,25 +4429,26 @@ var catalog = {
3995
4429
  { usage: "coop refine idea IDEA-101 --apply", purpose: "Refine an idea and apply the resulting draft." },
3996
4430
  { usage: "coop refine task PM-101", purpose: "Enrich a task with planning context and execution detail." },
3997
4431
  { usage: "coop refine task PM-101 --input-file docs/plan.md", purpose: "Use an additional planning brief during refinement." },
3998
- { usage: "cat refinement.yml | coop apply draft --stdin", purpose: "Apply a refinement draft streamed from another process." }
4432
+ { usage: "cat refinement.yml | coop apply draft --stdin", purpose: "Apply a refinement draft streamed from another process; unknown draft fields are ignored with warnings." }
3999
4433
  ]
4000
4434
  },
4001
4435
  {
4002
4436
  name: "Select And Start",
4003
4437
  description: "Pick the next ready task and move it into execution.",
4004
4438
  commands: [
4005
- { 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." },
4006
4440
  { usage: "coop graph next --delivery MVP", purpose: "Show the ready queue for a delivery with scores and blockers." },
4007
- { 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." },
4008
- { 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." },
4009
- { 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." },
4010
- { usage: "coop promote task PM-101", purpose: "Promote a task using the current working track/version context." },
4011
- { usage: "coop review task PM-101", purpose: "Move an in-progress task into in_review using a DX-friendly verb." },
4012
- { usage: "coop complete task PM-101", purpose: "Move a task in review into done using a DX-friendly verb." },
4013
- { usage: "coop block task PM-101", purpose: "Mark a task as blocked." },
4014
- { usage: "coop unblock task PM-101", purpose: "Move a blocked task back to todo." },
4015
- { usage: "coop cancel task PM-101", purpose: "Cancel a task." },
4016
- { usage: "coop reopen task PM-101", purpose: "Move a canceled task back to todo." }
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." },
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." },
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." }
4017
4452
  ]
4018
4453
  },
4019
4454
  {
@@ -4021,6 +4456,8 @@ var catalog = {
4021
4456
  description: "Read backlog state, task details, and planning output.",
4022
4457
  commands: [
4023
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." },
4024
4461
  { usage: "coop list tracks", purpose: "List valid named tracks." },
4025
4462
  { usage: "coop list deliveries", purpose: "List valid named deliveries." },
4026
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." },
@@ -4036,6 +4473,12 @@ var catalog = {
4036
4473
  { usage: "coop update PM-101 --track MVP --delivery MVP", purpose: "Update a task's home track or primary delivery without editing `.coop` files directly." },
4037
4474
  { usage: "coop update PM-101 --add-delivery-track MVP --priority-in MVP:p0", purpose: "Add a contributing track lens and scoped priority override." },
4038
4475
  { usage: "coop update PM-101 --priority p1 --add-fix-version v2", purpose: "Update task metadata without editing `.coop` files directly." },
4476
+ { usage: 'coop update PM-101 --acceptance-set "Contract approved, client mapping documented" --acceptance-set "Rollback path documented"', purpose: "Replace the full acceptance list with repeatable free-text entries; useful when an older task was created with the wrong parsing." },
4477
+ { usage: 'coop update PM-101 --tests-set "Contract fixture test" --tests-set "Integration smoke"', purpose: "Replace the full tests_required list with repeatable entries." },
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." },
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." },
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." },
4039
4482
  { usage: 'coop comment PM-101 --message "Needs API review"', purpose: "Append a comment to a task." },
4040
4483
  { usage: 'coop log-time PM-101 --hours 2 --kind worked --note "pairing"', purpose: "Append a planned or worked time log to a task." },
4041
4484
  { usage: "coop alias remove PM-101 PAY.UPI", purpose: "Remove an alias from a task or idea." },
@@ -4081,7 +4524,10 @@ var catalog = {
4081
4524
  execution_model: [
4082
4525
  "Agents or services may send drafts through files or stdin, but COOP owns canonical writes.",
4083
4526
  "Use `coop create ... --from-file|--stdin` and `coop apply draft` instead of editing `.coop` task or idea files directly.",
4527
+ "Draft files are input bundles, not raw frontmatter passthrough. COOP keeps only recognized fields, fills canonical metadata itself, and warns when it ignores unknown fields.",
4528
+ "For task creation, repeat `--acceptance`, `--tests-required`, `--authority-ref`, and `--derived-ref` to append multiple entries. Commas inside one value are preserved.",
4084
4529
  "Use `coop update`, `coop comment`, and `coop log-time` for task mutations instead of manually editing task files.",
4530
+ "When a mutable list field needs correction, prefer the `--...-set` variants on `coop update` over manual file edits. This applies to acceptance, tests_required, authority_refs, derived_refs, depends_on, delivery_tracks, tags, fix_versions, released_in, and idea linked_tasks.",
4085
4531
  "Use `coop log --last --verbose` when command execution fails and the concise error points to a stack trace.",
4086
4532
  "If a workflow depends on stable human-readable IDs, inspect `coop naming` or `coop config id.naming` before creating items."
4087
4533
  ],
@@ -4208,9 +4654,9 @@ function formatSelectionCommand(commandName, delivery, track) {
4208
4654
  return `${commandName} graph next --delivery ${delivery}`;
4209
4655
  }
4210
4656
  if (track) {
4211
- return `${commandName} next task --track ${track}`;
4657
+ return `${commandName} next --track ${track}`;
4212
4658
  }
4213
- return `${commandName} next task`;
4659
+ return `${commandName} next`;
4214
4660
  }
4215
4661
  function renderInitialPrompt(options = {}) {
4216
4662
  const rigour = options.rigour ?? "balanced";
@@ -4472,13 +4918,13 @@ function registerIndexCommand(program) {
4472
4918
  }
4473
4919
 
4474
4920
  // src/commands/init.ts
4475
- import fs10 from "fs";
4921
+ import fs11 from "fs";
4476
4922
  import path13 from "path";
4477
4923
  import { spawnSync as spawnSync4 } from "child_process";
4478
4924
  import { CURRENT_SCHEMA_VERSION, write_schema_version } from "@kitsy/coop-core";
4479
4925
 
4480
4926
  // src/hooks/pre-commit.ts
4481
- import fs8 from "fs";
4927
+ import fs9 from "fs";
4482
4928
  import path11 from "path";
4483
4929
  import { spawnSync as spawnSync3 } from "child_process";
4484
4930
  import { detect_cycle, parseTaskContent, parseTaskFile as parseTaskFile8, validateStructural as validateStructural5 } from "@kitsy/coop-core";
@@ -4519,12 +4965,12 @@ function projectRootFromRelativePath(repoRoot, relativePath) {
4519
4965
  }
4520
4966
  function listTaskFilesForProject(projectRoot) {
4521
4967
  const tasksDir = path11.join(projectRoot, "tasks");
4522
- if (!fs8.existsSync(tasksDir)) return [];
4968
+ if (!fs9.existsSync(tasksDir)) return [];
4523
4969
  const out = [];
4524
4970
  const stack = [tasksDir];
4525
4971
  while (stack.length > 0) {
4526
4972
  const current = stack.pop();
4527
- const entries = fs8.readdirSync(current, { withFileTypes: true });
4973
+ const entries = fs9.readdirSync(current, { withFileTypes: true });
4528
4974
  for (const entry of entries) {
4529
4975
  const fullPath = path11.join(current, entry.name);
4530
4976
  if (entry.isDirectory()) {
@@ -4678,7 +5124,7 @@ function hookScriptBlock() {
4678
5124
  function installPreCommitHook(repoRoot) {
4679
5125
  const hookPath = path11.join(repoRoot, ".git", "hooks", "pre-commit");
4680
5126
  const hookDir = path11.dirname(hookPath);
4681
- if (!fs8.existsSync(hookDir)) {
5127
+ if (!fs9.existsSync(hookDir)) {
4682
5128
  return {
4683
5129
  installed: false,
4684
5130
  hookPath,
@@ -4686,18 +5132,18 @@ function installPreCommitHook(repoRoot) {
4686
5132
  };
4687
5133
  }
4688
5134
  const block = hookScriptBlock();
4689
- if (!fs8.existsSync(hookPath)) {
5135
+ if (!fs9.existsSync(hookPath)) {
4690
5136
  const content = ["#!/bin/sh", "", block].join("\n");
4691
- fs8.writeFileSync(hookPath, content, "utf8");
5137
+ fs9.writeFileSync(hookPath, content, "utf8");
4692
5138
  } else {
4693
- const existing = fs8.readFileSync(hookPath, "utf8");
5139
+ const existing = fs9.readFileSync(hookPath, "utf8");
4694
5140
  if (!existing.includes(HOOK_BLOCK_START)) {
4695
5141
  const suffix = existing.endsWith("\n") ? "" : "\n";
4696
- fs8.writeFileSync(hookPath, `${existing}${suffix}${block}`, "utf8");
5142
+ fs9.writeFileSync(hookPath, `${existing}${suffix}${block}`, "utf8");
4697
5143
  }
4698
5144
  }
4699
5145
  try {
4700
- fs8.chmodSync(hookPath, 493);
5146
+ fs9.chmodSync(hookPath, 493);
4701
5147
  } catch {
4702
5148
  }
4703
5149
  return {
@@ -4708,7 +5154,7 @@ function installPreCommitHook(repoRoot) {
4708
5154
  }
4709
5155
 
4710
5156
  // src/hooks/post-merge-validate.ts
4711
- import fs9 from "fs";
5157
+ import fs10 from "fs";
4712
5158
  import path12 from "path";
4713
5159
  import { list_projects } from "@kitsy/coop-core";
4714
5160
  import { load_graph as load_graph5, validate_graph as validate_graph2 } from "@kitsy/coop-core";
@@ -4716,7 +5162,7 @@ var HOOK_BLOCK_START2 = "# COOP_POST_MERGE_START";
4716
5162
  var HOOK_BLOCK_END2 = "# COOP_POST_MERGE_END";
4717
5163
  function runPostMergeValidate(repoRoot) {
4718
5164
  const workspaceDir = path12.join(repoRoot, ".coop");
4719
- if (!fs9.existsSync(workspaceDir)) {
5165
+ if (!fs10.existsSync(workspaceDir)) {
4720
5166
  return {
4721
5167
  ok: true,
4722
5168
  warnings: ["[COOP] Skipped post-merge validation (.coop not found)."]
@@ -4770,7 +5216,7 @@ function postMergeHookBlock() {
4770
5216
  function installPostMergeHook(repoRoot) {
4771
5217
  const hookPath = path12.join(repoRoot, ".git", "hooks", "post-merge");
4772
5218
  const hookDir = path12.dirname(hookPath);
4773
- if (!fs9.existsSync(hookDir)) {
5219
+ if (!fs10.existsSync(hookDir)) {
4774
5220
  return {
4775
5221
  installed: false,
4776
5222
  hookPath,
@@ -4778,17 +5224,17 @@ function installPostMergeHook(repoRoot) {
4778
5224
  };
4779
5225
  }
4780
5226
  const block = postMergeHookBlock();
4781
- if (!fs9.existsSync(hookPath)) {
4782
- fs9.writeFileSync(hookPath, ["#!/bin/sh", "", block].join("\n"), "utf8");
5227
+ if (!fs10.existsSync(hookPath)) {
5228
+ fs10.writeFileSync(hookPath, ["#!/bin/sh", "", block].join("\n"), "utf8");
4783
5229
  } else {
4784
- const existing = fs9.readFileSync(hookPath, "utf8");
5230
+ const existing = fs10.readFileSync(hookPath, "utf8");
4785
5231
  if (!existing.includes(HOOK_BLOCK_START2)) {
4786
5232
  const suffix = existing.endsWith("\n") ? "" : "\n";
4787
- fs9.writeFileSync(hookPath, `${existing}${suffix}${block}`, "utf8");
5233
+ fs10.writeFileSync(hookPath, `${existing}${suffix}${block}`, "utf8");
4788
5234
  }
4789
5235
  }
4790
5236
  try {
4791
- fs9.chmodSync(hookPath, 493);
5237
+ fs10.chmodSync(hookPath, 493);
4792
5238
  } catch {
4793
5239
  }
4794
5240
  return {
@@ -4982,40 +5428,40 @@ tmp/
4982
5428
  *.tmp
4983
5429
  `;
4984
5430
  function ensureDir(dirPath) {
4985
- fs10.mkdirSync(dirPath, { recursive: true });
5431
+ fs11.mkdirSync(dirPath, { recursive: true });
4986
5432
  }
4987
5433
  function writeIfMissing(filePath, content) {
4988
- if (!fs10.existsSync(filePath)) {
4989
- fs10.writeFileSync(filePath, content, "utf8");
5434
+ if (!fs11.existsSync(filePath)) {
5435
+ fs11.writeFileSync(filePath, content, "utf8");
4990
5436
  }
4991
5437
  }
4992
5438
  function ensureGitignoreEntry(root, entry) {
4993
5439
  const gitignorePath = path13.join(root, ".gitignore");
4994
- if (!fs10.existsSync(gitignorePath)) {
4995
- fs10.writeFileSync(gitignorePath, `${entry}
5440
+ if (!fs11.existsSync(gitignorePath)) {
5441
+ fs11.writeFileSync(gitignorePath, `${entry}
4996
5442
  `, "utf8");
4997
5443
  return;
4998
5444
  }
4999
- const content = fs10.readFileSync(gitignorePath, "utf8");
5445
+ const content = fs11.readFileSync(gitignorePath, "utf8");
5000
5446
  const lines = content.split(/\r?\n/).map((line) => line.trim());
5001
5447
  if (!lines.includes(entry)) {
5002
5448
  const suffix = content.endsWith("\n") ? "" : "\n";
5003
- fs10.writeFileSync(gitignorePath, `${content}${suffix}${entry}
5449
+ fs11.writeFileSync(gitignorePath, `${content}${suffix}${entry}
5004
5450
  `, "utf8");
5005
5451
  }
5006
5452
  }
5007
5453
  function ensureGitattributesEntry(root, entry) {
5008
5454
  const attrsPath = path13.join(root, ".gitattributes");
5009
- if (!fs10.existsSync(attrsPath)) {
5010
- fs10.writeFileSync(attrsPath, `${entry}
5455
+ if (!fs11.existsSync(attrsPath)) {
5456
+ fs11.writeFileSync(attrsPath, `${entry}
5011
5457
  `, "utf8");
5012
5458
  return;
5013
5459
  }
5014
- const content = fs10.readFileSync(attrsPath, "utf8");
5460
+ const content = fs11.readFileSync(attrsPath, "utf8");
5015
5461
  const lines = content.split(/\r?\n/).map((line) => line.trim());
5016
5462
  if (!lines.includes(entry)) {
5017
5463
  const suffix = content.endsWith("\n") ? "" : "\n";
5018
- fs10.writeFileSync(attrsPath, `${content}${suffix}${entry}
5464
+ fs11.writeFileSync(attrsPath, `${content}${suffix}${entry}
5019
5465
  `, "utf8");
5020
5466
  }
5021
5467
  }
@@ -5116,7 +5562,7 @@ function registerInitCommand(program) {
5116
5562
  path13.join(projectRoot, "config.yml"),
5117
5563
  buildProjectConfig(projectId, identity.projectName, identity.projectAliases, identity.namingTemplate)
5118
5564
  );
5119
- if (!fs10.existsSync(path13.join(projectRoot, "schema-version"))) {
5565
+ if (!fs11.existsSync(path13.join(projectRoot, "schema-version"))) {
5120
5566
  write_schema_version(projectRoot, CURRENT_SCHEMA_VERSION);
5121
5567
  }
5122
5568
  writeIfMissing(path13.join(projectRoot, "templates/task.md"), TASK_TEMPLATE);
@@ -5209,12 +5655,22 @@ function currentTaskSelection(root, id) {
5209
5655
  }
5210
5656
  function registerLifecycleCommands(program) {
5211
5657
  for (const verb of lifecycleVerbs) {
5212
- 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) => {
5213
5659
  const root = resolveRepoRoot();
5214
5660
  console.log(currentTaskSelection(root, id));
5215
- 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);
5216
5662
  console.log(`Updated ${result.task.id}: ${result.from} -> ${result.to}`);
5217
- });
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);
5218
5674
  }
5219
5675
  }
5220
5676
 
@@ -5510,8 +5966,8 @@ function listTasks(options) {
5510
5966
  ensureCoopInitialized(root);
5511
5967
  const context = readWorkingContext(root, resolveCoopHome());
5512
5968
  const graph = load_graph6(coopDir(root));
5513
- const rawResolvedTrack = resolveContextValueWithSource(options.track, context.track);
5514
- 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);
5515
5971
  const resolvedTrack = {
5516
5972
  ...rawResolvedTrack,
5517
5973
  value: rawResolvedTrack.value ? resolveExistingTrackId(root, rawResolvedTrack.value) ?? rawResolvedTrack.value : void 0
@@ -5520,7 +5976,12 @@ function listTasks(options) {
5520
5976
  ...rawResolvedDelivery,
5521
5977
  value: rawResolvedDelivery.value ? resolveExistingDeliveryId(root, rawResolvedDelivery.value) ?? rawResolvedDelivery.value : void 0
5522
5978
  };
5523
- 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");
5524
5985
  const assignee = options.mine ? defaultCoopAuthor(root) : options.assignee?.trim();
5525
5986
  const deliveryScope = resolvedDelivery.value ? new Set(graph.deliveries.get(resolvedDelivery.value)?.scope.include ?? []) : null;
5526
5987
  const defaultSort = options.ready || resolvedTrack.value || resolvedDelivery.value ? "score" : "id";
@@ -5544,7 +6005,7 @@ function listTasks(options) {
5544
6005
  const scoreMap = new Map(scoredEntries.map((entry) => [entry.task.id, entry.score]));
5545
6006
  const rows = loadTasks2(root).filter(({ task }) => {
5546
6007
  if (readyIds && !readyIds.has(task.id)) return false;
5547
- if (options.status && task.status !== options.status) return false;
6008
+ if (statusFilters.size > 0 && !statusFilters.has(task.status)) return false;
5548
6009
  if (resolvedTrack.value && task.track !== resolvedTrack.value && !(task.delivery_tracks ?? []).includes(resolvedTrack.value)) {
5549
6010
  return false;
5550
6011
  }
@@ -5698,7 +6159,7 @@ function listTracks() {
5698
6159
  }
5699
6160
  function registerListCommand(program) {
5700
6161
  const list = program.command("list").description("List COOP entities");
5701
- 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) => {
5702
6163
  listTasks(options);
5703
6164
  });
5704
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) => {
@@ -5716,7 +6177,7 @@ function registerListCommand(program) {
5716
6177
  }
5717
6178
 
5718
6179
  // src/utils/logger.ts
5719
- import fs11 from "fs";
6180
+ import fs12 from "fs";
5720
6181
  import os2 from "os";
5721
6182
  import path16 from "path";
5722
6183
  function resolveWorkspaceRoot(start = process.cwd()) {
@@ -5727,11 +6188,11 @@ function resolveWorkspaceRoot(start = process.cwd()) {
5727
6188
  const gitDir = path16.join(current, ".git");
5728
6189
  const coopDir2 = coopWorkspaceDir(current);
5729
6190
  const workspaceConfig = path16.join(coopDir2, "config.yml");
5730
- if (fs11.existsSync(gitDir)) {
6191
+ if (fs12.existsSync(gitDir)) {
5731
6192
  return current;
5732
6193
  }
5733
6194
  const resolvedCoopDir = path16.resolve(coopDir2);
5734
- if (resolvedCoopDir !== configuredCoopHome && resolvedCoopDir !== defaultCoopHome && fs11.existsSync(workspaceConfig)) {
6195
+ if (resolvedCoopDir !== configuredCoopHome && resolvedCoopDir !== defaultCoopHome && fs12.existsSync(workspaceConfig)) {
5735
6196
  return current;
5736
6197
  }
5737
6198
  const parent = path16.dirname(current);
@@ -5750,8 +6211,8 @@ function resolveCliLogFile(start = process.cwd()) {
5750
6211
  return path16.join(resolveCoopHome(), "logs", "cli.log");
5751
6212
  }
5752
6213
  function appendLogEntry(entry, logFile) {
5753
- fs11.mkdirSync(path16.dirname(logFile), { recursive: true });
5754
- fs11.appendFileSync(logFile, `${JSON.stringify(entry)}
6214
+ fs12.mkdirSync(path16.dirname(logFile), { recursive: true });
6215
+ fs12.appendFileSync(logFile, `${JSON.stringify(entry)}
5755
6216
  `, "utf8");
5756
6217
  }
5757
6218
  function logCliError(error, start = process.cwd()) {
@@ -5790,8 +6251,8 @@ function parseLogLine(line) {
5790
6251
  }
5791
6252
  function readLastCliLog(start = process.cwd()) {
5792
6253
  const logFile = resolveCliLogFile(start);
5793
- if (!fs11.existsSync(logFile)) return null;
5794
- const content = fs11.readFileSync(logFile, "utf8");
6254
+ if (!fs12.existsSync(logFile)) return null;
6255
+ const content = fs12.readFileSync(logFile, "utf8");
5795
6256
  const lines = content.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
5796
6257
  for (let i = lines.length - 1; i >= 0; i -= 1) {
5797
6258
  const entry = parseLogLine(lines[i] ?? "");
@@ -5854,11 +6315,11 @@ function registerLogTimeCommand(program) {
5854
6315
  }
5855
6316
 
5856
6317
  // src/commands/migrate.ts
5857
- import fs12 from "fs";
6318
+ import fs13 from "fs";
5858
6319
  import path17 from "path";
5859
6320
  import { createInterface } from "readline/promises";
5860
6321
  import { stdin as input, stdout as output } from "process";
5861
- 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";
5862
6323
  var COOP_IGNORE_TEMPLATE2 = `.index/
5863
6324
  logs/
5864
6325
  tmp/
@@ -5874,22 +6335,22 @@ function parseTargetVersion(raw) {
5874
6335
  return parsed;
5875
6336
  }
5876
6337
  function writeIfMissing2(filePath, content) {
5877
- if (!fs12.existsSync(filePath)) {
5878
- fs12.writeFileSync(filePath, content, "utf8");
6338
+ if (!fs13.existsSync(filePath)) {
6339
+ fs13.writeFileSync(filePath, content, "utf8");
5879
6340
  }
5880
6341
  }
5881
6342
  function ensureGitignoreEntry2(root, entry) {
5882
6343
  const gitignorePath = path17.join(root, ".gitignore");
5883
- if (!fs12.existsSync(gitignorePath)) {
5884
- fs12.writeFileSync(gitignorePath, `${entry}
6344
+ if (!fs13.existsSync(gitignorePath)) {
6345
+ fs13.writeFileSync(gitignorePath, `${entry}
5885
6346
  `, "utf8");
5886
6347
  return;
5887
6348
  }
5888
- const content = fs12.readFileSync(gitignorePath, "utf8");
6349
+ const content = fs13.readFileSync(gitignorePath, "utf8");
5889
6350
  const lines = content.split(/\r?\n/).map((line) => line.trim());
5890
6351
  if (!lines.includes(entry)) {
5891
6352
  const suffix = content.endsWith("\n") ? "" : "\n";
5892
- fs12.writeFileSync(gitignorePath, `${content}${suffix}${entry}
6353
+ fs13.writeFileSync(gitignorePath, `${content}${suffix}${entry}
5893
6354
  `, "utf8");
5894
6355
  }
5895
6356
  }
@@ -5914,7 +6375,7 @@ function legacyWorkspaceProjectEntries(root) {
5914
6375
  "backlog",
5915
6376
  "plans",
5916
6377
  "releases"
5917
- ].filter((entry) => fs12.existsSync(path17.join(workspaceDir, entry)));
6378
+ ].filter((entry) => fs13.existsSync(path17.join(workspaceDir, entry)));
5918
6379
  }
5919
6380
  function normalizeProjectId2(value) {
5920
6381
  return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-");
@@ -5979,19 +6440,19 @@ async function migrateWorkspaceLayout(root, options) {
5979
6440
  throw new Error(`Unsupported workspace-layout target '${options.to ?? ""}'. Expected 'v2'.`);
5980
6441
  }
5981
6442
  const workspaceDir = coopWorkspaceDir(root);
5982
- if (!fs12.existsSync(workspaceDir)) {
6443
+ if (!fs13.existsSync(workspaceDir)) {
5983
6444
  throw new Error("Missing .coop directory. Run 'coop init' first.");
5984
6445
  }
5985
6446
  const projectsDir = path17.join(workspaceDir, "projects");
5986
6447
  const legacyEntries = legacyWorkspaceProjectEntries(root);
5987
- if (legacyEntries.length === 0 && fs12.existsSync(projectsDir)) {
6448
+ if (legacyEntries.length === 0 && fs13.existsSync(projectsDir)) {
5988
6449
  console.log("[COOP] workspace layout already uses v2.");
5989
6450
  return;
5990
6451
  }
5991
6452
  const identity = await resolveMigrationIdentity(root, options);
5992
6453
  const projectId = identity.projectId;
5993
6454
  const projectRoot = path17.join(projectsDir, projectId);
5994
- if (fs12.existsSync(projectRoot) && !options.force) {
6455
+ if (fs13.existsSync(projectRoot) && !options.force) {
5995
6456
  throw new Error(`Project destination '${path17.relative(root, projectRoot)}' already exists. Re-run with --force.`);
5996
6457
  }
5997
6458
  const changed = legacyEntries.map((entry) => `${path17.join(".coop", entry)} -> ${path17.join(".coop", "projects", projectId, entry)}`);
@@ -6010,21 +6471,21 @@ async function migrateWorkspaceLayout(root, options) {
6010
6471
  console.log("- no files were modified.");
6011
6472
  return;
6012
6473
  }
6013
- fs12.mkdirSync(projectsDir, { recursive: true });
6014
- fs12.mkdirSync(projectRoot, { recursive: true });
6474
+ fs13.mkdirSync(projectsDir, { recursive: true });
6475
+ fs13.mkdirSync(projectRoot, { recursive: true });
6015
6476
  for (const entry of legacyEntries) {
6016
6477
  const source = path17.join(workspaceDir, entry);
6017
6478
  const destination = path17.join(projectRoot, entry);
6018
- if (fs12.existsSync(destination)) {
6479
+ if (fs13.existsSync(destination)) {
6019
6480
  if (!options.force) {
6020
6481
  throw new Error(`Destination '${path17.relative(root, destination)}' already exists.`);
6021
6482
  }
6022
- fs12.rmSync(destination, { recursive: true, force: true });
6483
+ fs13.rmSync(destination, { recursive: true, force: true });
6023
6484
  }
6024
- fs12.renameSync(source, destination);
6485
+ fs13.renameSync(source, destination);
6025
6486
  }
6026
6487
  const movedConfigPath = path17.join(projectRoot, "config.yml");
6027
- if (fs12.existsSync(movedConfigPath)) {
6488
+ if (fs13.existsSync(movedConfigPath)) {
6028
6489
  const movedConfig = parseYamlFile5(movedConfigPath);
6029
6490
  const nextProject = typeof movedConfig.project === "object" && movedConfig.project !== null ? { ...movedConfig.project } : {};
6030
6491
  nextProject.name = identity.projectName;
@@ -6033,7 +6494,7 @@ async function migrateWorkspaceLayout(root, options) {
6033
6494
  const nextHooks = typeof movedConfig.hooks === "object" && movedConfig.hooks !== null ? { ...movedConfig.hooks } : {};
6034
6495
  nextHooks.on_task_transition = `.coop/projects/${projectId}/hooks/on-task-transition.sh`;
6035
6496
  nextHooks.on_delivery_complete = `.coop/projects/${projectId}/hooks/on-delivery-complete.sh`;
6036
- writeYamlFile5(movedConfigPath, {
6497
+ writeYamlFile6(movedConfigPath, {
6037
6498
  ...movedConfig,
6038
6499
  project: nextProject,
6039
6500
  hooks: nextHooks
@@ -6132,7 +6593,7 @@ function printNamingOverview() {
6132
6593
  console.log("- none");
6133
6594
  } else {
6134
6595
  for (const [token, definition] of Object.entries(tokens)) {
6135
- 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}` : ""}`);
6136
6597
  }
6137
6598
  }
6138
6599
  console.log("Examples:");
@@ -6199,7 +6660,7 @@ function listTokens() {
6199
6660
  return;
6200
6661
  }
6201
6662
  for (const [token, definition] of Object.entries(tokens)) {
6202
- 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}` : ""}`);
6203
6664
  }
6204
6665
  }
6205
6666
  function registerNamingCommand(program) {
@@ -6297,6 +6758,53 @@ function registerNamingCommand(program) {
6297
6758
  console.log(`Deleted naming token: ${normalizedToken}`);
6298
6759
  });
6299
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
+ });
6300
6808
  tokenValue.command("list").description("List allowed values for a naming token").argument("<token>", "Token name").action((tokenName) => {
6301
6809
  const normalizedToken = normalizeNamingTokenName(tokenName);
6302
6810
  const tokens = namingTokensForRoot(resolveRepoRoot());
@@ -6346,6 +6854,9 @@ function registerNamingCommand(program) {
6346
6854
  }
6347
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) : [];
6348
6856
  tokenRecord.values = Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));
6857
+ if (tokenRecord.default === normalizedValue) {
6858
+ delete tokenRecord.default;
6859
+ }
6349
6860
  tokens[normalizedToken] = tokenRecord;
6350
6861
  return next;
6351
6862
  });
@@ -6624,7 +7135,7 @@ function registerPromoteCommand(program) {
6624
7135
  }
6625
7136
 
6626
7137
  // src/commands/project.ts
6627
- import fs13 from "fs";
7138
+ import fs14 from "fs";
6628
7139
  import path18 from "path";
6629
7140
  import { CURRENT_SCHEMA_VERSION as CURRENT_SCHEMA_VERSION3, write_schema_version as write_schema_version2 } from "@kitsy/coop-core";
6630
7141
  var TASK_TEMPLATE2 = `---
@@ -6740,11 +7251,11 @@ var PROJECT_DIRS = [
6740
7251
  ".index"
6741
7252
  ];
6742
7253
  function ensureDir2(dirPath) {
6743
- fs13.mkdirSync(dirPath, { recursive: true });
7254
+ fs14.mkdirSync(dirPath, { recursive: true });
6744
7255
  }
6745
7256
  function writeIfMissing3(filePath, content) {
6746
- if (!fs13.existsSync(filePath)) {
6747
- fs13.writeFileSync(filePath, content, "utf8");
7257
+ if (!fs14.existsSync(filePath)) {
7258
+ fs14.writeFileSync(filePath, content, "utf8");
6748
7259
  }
6749
7260
  }
6750
7261
  function normalizeProjectId3(value) {
@@ -6758,7 +7269,7 @@ function createProject(root, projectId, projectName, namingTemplate = DEFAULT_ID
6758
7269
  ensureDir2(path18.join(projectRoot, dir));
6759
7270
  }
6760
7271
  writeIfMissing3(path18.join(projectRoot, "config.yml"), PROJECT_CONFIG_TEMPLATE(projectId, projectName, namingTemplate));
6761
- if (!fs13.existsSync(path18.join(projectRoot, "schema-version"))) {
7272
+ if (!fs14.existsSync(path18.join(projectRoot, "schema-version"))) {
6762
7273
  write_schema_version2(projectRoot, CURRENT_SCHEMA_VERSION3);
6763
7274
  }
6764
7275
  writeIfMissing3(path18.join(projectRoot, "templates/task.md"), TASK_TEMPLATE2);
@@ -6827,7 +7338,7 @@ function registerProjectCommand(program) {
6827
7338
  }
6828
7339
 
6829
7340
  // src/commands/prompt.ts
6830
- import fs14 from "fs";
7341
+ import fs15 from "fs";
6831
7342
  function buildPayload(root, id) {
6832
7343
  const { parsed } = loadTaskEntry(root, id);
6833
7344
  const context = readWorkingContext(root, resolveCoopHome());
@@ -6912,7 +7423,7 @@ function registerPromptCommand(program) {
6912
7423
  output2 = renderMarkdown(payload);
6913
7424
  }
6914
7425
  if (options.save) {
6915
- fs14.writeFileSync(options.save, output2, "utf8");
7426
+ fs15.writeFileSync(options.save, output2, "utf8");
6916
7427
  }
6917
7428
  if (isVerboseRequested()) {
6918
7429
  for (const line of formatResolvedContextMessage({
@@ -6927,8 +7438,23 @@ function registerPromptCommand(program) {
6927
7438
  });
6928
7439
  }
6929
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
+
6930
7456
  // src/commands/refine.ts
6931
- import fs15 from "fs";
7457
+ import fs16 from "fs";
6932
7458
  import path19 from "path";
6933
7459
  import { parseIdeaFile as parseIdeaFile5, parseTaskFile as parseTaskFile11 } from "@kitsy/coop-core";
6934
7460
  import { create_provider_refinement_client, refine_idea_to_draft, refine_task_to_draft } from "@kitsy/coop-ai";
@@ -6942,7 +7468,7 @@ function resolveIdeaFile3(root, idOrAlias) {
6942
7468
  }
6943
7469
  async function readSupplementalInput(root, options) {
6944
7470
  if (options.inputFile?.trim()) {
6945
- return fs15.readFileSync(path19.resolve(root, options.inputFile.trim()), "utf8");
7471
+ return fs16.readFileSync(path19.resolve(root, options.inputFile.trim()), "utf8");
6946
7472
  }
6947
7473
  if (options.stdin) {
6948
7474
  return readStdinText();
@@ -6961,10 +7487,10 @@ function loadAuthorityContext(root, refs) {
6961
7487
  const filePart = extractRefFile(ref);
6962
7488
  if (!filePart) continue;
6963
7489
  const fullPath = path19.resolve(root, filePart);
6964
- if (!fs15.existsSync(fullPath) || !fs15.statSync(fullPath).isFile()) continue;
7490
+ if (!fs16.existsSync(fullPath) || !fs16.statSync(fullPath).isFile()) continue;
6965
7491
  out.push({
6966
7492
  ref,
6967
- content: fs15.readFileSync(fullPath, "utf8")
7493
+ content: fs16.readFileSync(fullPath, "utf8")
6968
7494
  });
6969
7495
  }
6970
7496
  return out;
@@ -7023,7 +7549,11 @@ function registerRefineCommand(program) {
7023
7549
  const root = resolveRepoRoot();
7024
7550
  const projectDir = ensureCoopInitialized(root);
7025
7551
  const draftInput = await readDraftContent(root, options);
7026
- const draft = parseRefinementDraftInput(draftInput.content, draftInput.source);
7552
+ const parsedDraft = parseRefinementDraftInputWithWarnings(draftInput.content, draftInput.source);
7553
+ for (const warning of parsedDraft.warnings) {
7554
+ console.warn(`[COOP][warn] ${warning}`);
7555
+ }
7556
+ const draft = parsedDraft.value;
7027
7557
  const written = applyRefinementDraft(root, projectDir, draft);
7028
7558
  console.log(`[COOP] applied draft from ${draftInput.source}: ${written.length} task file(s) updated`);
7029
7559
  for (const filePath of written) {
@@ -7033,7 +7563,7 @@ function registerRefineCommand(program) {
7033
7563
  }
7034
7564
 
7035
7565
  // src/commands/run.ts
7036
- import fs16 from "fs";
7566
+ import fs17 from "fs";
7037
7567
  import path20 from "path";
7038
7568
  import { load_graph as load_graph8, parseTaskFile as parseTaskFile12 } from "@kitsy/coop-core";
7039
7569
  import {
@@ -7045,7 +7575,7 @@ import {
7045
7575
  function loadTask(root, idOrAlias) {
7046
7576
  const target = resolveReference(root, idOrAlias, "task");
7047
7577
  const taskFile = path20.join(root, ...target.file.split("/"));
7048
- if (!fs16.existsSync(taskFile)) {
7578
+ if (!fs17.existsSync(taskFile)) {
7049
7579
  throw new Error(`Task file not found: ${target.file}`);
7050
7580
  }
7051
7581
  return parseTaskFile12(taskFile).task;
@@ -7212,7 +7742,7 @@ ${parsed.body}`, query)) continue;
7212
7742
  }
7213
7743
 
7214
7744
  // src/server/api.ts
7215
- import fs17 from "fs";
7745
+ import fs18 from "fs";
7216
7746
  import http2 from "http";
7217
7747
  import path21 from "path";
7218
7748
  import {
@@ -7261,8 +7791,8 @@ function taskSummary(graph, task, external = []) {
7261
7791
  }
7262
7792
  function taskFileById(root, id) {
7263
7793
  const tasksDir = path21.join(resolveProject(root).root, "tasks");
7264
- if (!fs17.existsSync(tasksDir)) return null;
7265
- const entries = fs17.readdirSync(tasksDir, { withFileTypes: true });
7794
+ if (!fs18.existsSync(tasksDir)) return null;
7795
+ const entries = fs18.readdirSync(tasksDir, { withFileTypes: true });
7266
7796
  const target = `${id}.md`.toLowerCase();
7267
7797
  const match = entries.find((entry) => entry.isFile() && entry.name.toLowerCase() === target);
7268
7798
  return match ? path21.join(tasksDir, match.name) : null;
@@ -7443,7 +7973,7 @@ function registerServeCommand(program) {
7443
7973
  }
7444
7974
 
7445
7975
  // src/commands/show.ts
7446
- import fs18 from "fs";
7976
+ import fs19 from "fs";
7447
7977
  import path22 from "path";
7448
7978
  import { parseIdeaFile as parseIdeaFile7 } from "@kitsy/coop-core";
7449
7979
  function stringify(value) {
@@ -7464,12 +7994,12 @@ function pushListSection(lines, title, values) {
7464
7994
  }
7465
7995
  function loadComputedFromIndex(root, taskId) {
7466
7996
  const indexPath = path22.join(ensureCoopInitialized(root), ".index", "tasks.json");
7467
- if (!fs18.existsSync(indexPath)) {
7997
+ if (!fs19.existsSync(indexPath)) {
7468
7998
  return null;
7469
7999
  }
7470
8000
  let parsed;
7471
8001
  try {
7472
- parsed = JSON.parse(fs18.readFileSync(indexPath, "utf8"));
8002
+ parsed = JSON.parse(fs19.readFileSync(indexPath, "utf8"));
7473
8003
  } catch {
7474
8004
  return null;
7475
8005
  }
@@ -7738,7 +8268,7 @@ function registerShowCommand(program) {
7738
8268
 
7739
8269
  // src/commands/status.ts
7740
8270
  import chalk4 from "chalk";
7741
- 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";
7742
8272
  function countBy(values, keyFn) {
7743
8273
  const out = /* @__PURE__ */ new Map();
7744
8274
  for (const value of values) {
@@ -7777,7 +8307,7 @@ function registerStatusCommand(program) {
7777
8307
  lines.push(chalk4.bold("COOP Status Dashboard"));
7778
8308
  lines.push("");
7779
8309
  lines.push(chalk4.bold("Tasks By Status"));
7780
- const statusRows = Object.values(TaskStatus4).map((status) => [
8310
+ const statusRows = Object.values(TaskStatus5).map((status) => [
7781
8311
  status,
7782
8312
  String(tasksByStatus.get(status) ?? 0)
7783
8313
  ]);
@@ -7926,7 +8456,7 @@ function printResolvedSelectionContext(root, options) {
7926
8456
  }
7927
8457
  function registerTaskFlowCommands(program) {
7928
8458
  const next = program.command("next").description("Select the next COOP work item");
7929
- 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) => {
7930
8460
  const root = resolveRepoRoot();
7931
8461
  printResolvedSelectionContext(root, options);
7932
8462
  const selected = selectTopReadyTask(root, {
@@ -7936,9 +8466,11 @@ function registerTaskFlowCommands(program) {
7936
8466
  today: options.today
7937
8467
  });
7938
8468
  console.log(formatSelectedTask(selected.entry, selected.selection));
7939
- });
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);
7940
8472
  const pick = program.command("pick").description("Pick the next COOP work item");
7941
- 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) => {
7942
8474
  const root = resolveRepoRoot();
7943
8475
  printResolvedSelectionContext(root, options);
7944
8476
  const selected = id?.trim() ? {
@@ -7966,9 +8498,11 @@ function registerTaskFlowCommands(program) {
7966
8498
  console.log(formatSelectedTask(selected.entry, selected.selection));
7967
8499
  maybePromote(root, selected.entry.task.id, options);
7968
8500
  await claimAndStart(root, selected.entry.task.id, options);
7969
- });
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);
7970
8504
  const start = program.command("start").description("Start COOP work on a task");
7971
- 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) => {
7972
8506
  const root = resolveRepoRoot();
7973
8507
  printResolvedSelectionContext(root, options);
7974
8508
  const taskId = id?.trim() || selectTopReadyTask(root, {
@@ -8001,11 +8535,13 @@ function registerTaskFlowCommands(program) {
8001
8535
  );
8002
8536
  maybePromote(root, reference.id, options);
8003
8537
  await claimAndStart(root, reference.id, options);
8004
- });
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);
8005
8541
  }
8006
8542
 
8007
8543
  // src/commands/ui.ts
8008
- import fs19 from "fs";
8544
+ import fs20 from "fs";
8009
8545
  import path24 from "path";
8010
8546
  import { createRequire } from "module";
8011
8547
  import { fileURLToPath } from "url";
@@ -8014,7 +8550,7 @@ import { IndexManager as IndexManager4 } from "@kitsy/coop-core";
8014
8550
  function findPackageRoot(entryPath) {
8015
8551
  let current = path24.dirname(entryPath);
8016
8552
  while (true) {
8017
- if (fs19.existsSync(path24.join(current, "package.json"))) {
8553
+ if (fs20.existsSync(path24.join(current, "package.json"))) {
8018
8554
  return current;
8019
8555
  }
8020
8556
  const parent = path24.dirname(current);
@@ -8115,8 +8651,8 @@ function registerUiCommand(program) {
8115
8651
  }
8116
8652
 
8117
8653
  // src/commands/update.ts
8118
- import fs20 from "fs";
8119
- 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";
8120
8656
  function collect(value, previous = []) {
8121
8657
  return [...previous, value];
8122
8658
  }
@@ -8133,12 +8669,19 @@ function addValues(source, values) {
8133
8669
  const next = unique3([...source ?? [], ...values ?? []]);
8134
8670
  return next.length > 0 ? next : void 0;
8135
8671
  }
8672
+ function setValues(values) {
8673
+ if (!values || values.length === 0) {
8674
+ return void 0;
8675
+ }
8676
+ const next = unique3(values);
8677
+ return next.length > 0 ? next : void 0;
8678
+ }
8136
8679
  function loadBody(options) {
8137
8680
  if (options.bodyFile) {
8138
- return fs20.readFileSync(options.bodyFile, "utf8");
8681
+ return fs21.readFileSync(options.bodyFile, "utf8");
8139
8682
  }
8140
8683
  if (options.bodyStdin) {
8141
- return fs20.readFileSync(0, "utf8");
8684
+ return fs21.readFileSync(0, "utf8");
8142
8685
  }
8143
8686
  return void 0;
8144
8687
  }
@@ -8171,8 +8714,8 @@ function clearTrackPriorityOverrides(task, tracks) {
8171
8714
  }
8172
8715
  function normalizeTaskStatus(status) {
8173
8716
  const value = status.trim().toLowerCase();
8174
- if (!Object.values(TaskStatus5).includes(value)) {
8175
- 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(", ")}.`);
8176
8719
  }
8177
8720
  return value;
8178
8721
  }
@@ -8186,6 +8729,29 @@ function normalizeTaskPriority(priority) {
8186
8729
  function renderTaskPreview(task, body) {
8187
8730
  return stringifyFrontmatter5(task, body);
8188
8731
  }
8732
+ function normalizeOrigin(task, authorityRefs, derivedRefs) {
8733
+ const nextOrigin = {
8734
+ ...task.origin ?? {},
8735
+ authority_refs: authorityRefs,
8736
+ derived_refs: derivedRefs
8737
+ };
8738
+ const hasAuthority = Array.isArray(nextOrigin.authority_refs) && nextOrigin.authority_refs.length > 0;
8739
+ const hasDerived = Array.isArray(nextOrigin.derived_refs) && nextOrigin.derived_refs.length > 0;
8740
+ const hasPromoted = Array.isArray(nextOrigin.promoted_from) && nextOrigin.promoted_from.length > 0;
8741
+ if (!hasAuthority) {
8742
+ delete nextOrigin.authority_refs;
8743
+ }
8744
+ if (!hasDerived) {
8745
+ delete nextOrigin.derived_refs;
8746
+ }
8747
+ if (!hasPromoted) {
8748
+ delete nextOrigin.promoted_from;
8749
+ }
8750
+ return {
8751
+ ...task,
8752
+ origin: hasAuthority || hasDerived || hasPromoted ? nextOrigin : void 0
8753
+ };
8754
+ }
8189
8755
  function updateTask(id, options) {
8190
8756
  const root = resolveRepoRoot();
8191
8757
  const { filePath, parsed } = loadTaskEntry(root, id);
@@ -8201,16 +8767,19 @@ function updateTask(id, options) {
8201
8767
  if (options.delivery !== void 0) next.delivery = options.delivery.trim() || null;
8202
8768
  if (options.storyPoints !== void 0) next.story_points = Number(options.storyPoints);
8203
8769
  if (options.plannedHours !== void 0) next = setTaskPlannedHours(next, Number(options.plannedHours));
8770
+ const nextAuthorityRefs = options.authorityRefSet && options.authorityRefSet.length > 0 ? setValues(options.authorityRefSet) : addValues(removeValues(next.origin?.authority_refs, options.authorityRefRemove), options.authorityRefAdd);
8771
+ const nextDerivedRefs = options.derivedRefSet && options.derivedRefSet.length > 0 ? setValues(options.derivedRefSet) : addValues(removeValues(next.origin?.derived_refs, options.derivedRefRemove), options.derivedRefAdd);
8204
8772
  next = {
8205
8773
  ...next,
8206
- delivery_tracks: addValues(removeValues(next.delivery_tracks, options.removeDeliveryTrack), options.addDeliveryTrack),
8207
- depends_on: addValues(removeValues(next.depends_on, options.removeDep), options.addDep),
8208
- tags: addValues(removeValues(next.tags, options.removeTag), options.addTag),
8209
- fix_versions: addValues(removeValues(next.fix_versions, options.removeFixVersion), options.addFixVersion),
8210
- released_in: addValues(removeValues(next.released_in, options.removeReleasedIn), options.addReleasedIn),
8211
- acceptance: addValues(removeValues(next.acceptance, options.acceptanceRemove), options.acceptanceAdd),
8212
- tests_required: addValues(removeValues(next.tests_required, options.testsRemove), options.testsAdd)
8774
+ delivery_tracks: options.deliveryTracksSet && options.deliveryTracksSet.length > 0 ? setValues(options.deliveryTracksSet) : addValues(removeValues(next.delivery_tracks, options.removeDeliveryTrack), options.addDeliveryTrack),
8775
+ depends_on: options.depsSet && options.depsSet.length > 0 ? setValues(options.depsSet) : addValues(removeValues(next.depends_on, options.removeDep), options.addDep),
8776
+ tags: options.tagsSet && options.tagsSet.length > 0 ? setValues(options.tagsSet) : addValues(removeValues(next.tags, options.removeTag), options.addTag),
8777
+ fix_versions: options.fixVersionsSet && options.fixVersionsSet.length > 0 ? setValues(options.fixVersionsSet) : addValues(removeValues(next.fix_versions, options.removeFixVersion), options.addFixVersion),
8778
+ released_in: options.releasedInSet && options.releasedInSet.length > 0 ? setValues(options.releasedInSet) : addValues(removeValues(next.released_in, options.removeReleasedIn), options.addReleasedIn),
8779
+ acceptance: options.acceptanceSet && options.acceptanceSet.length > 0 ? setValues(options.acceptanceSet) : addValues(removeValues(next.acceptance, options.acceptanceRemove), options.acceptanceAdd),
8780
+ tests_required: options.testsSet && options.testsSet.length > 0 ? setValues(options.testsSet) : addValues(removeValues(next.tests_required, options.testsRemove), options.testsAdd)
8213
8781
  };
8782
+ next = normalizeOrigin(next, nextAuthorityRefs, nextDerivedRefs);
8214
8783
  next = clearTrackPriorityOverrides(applyTrackPriorityOverrides(next, options.priorityIn), options.clearPriorityIn);
8215
8784
  next = validateTaskForWrite(root, next, filePath);
8216
8785
  const nextBody = loadBody(options) ?? parsed.body;
@@ -8232,8 +8801,8 @@ function updateIdea(id, options) {
8232
8801
  ...parsed.idea,
8233
8802
  title: options.title?.trim() || parsed.idea.title,
8234
8803
  status: nextStatus || parsed.idea.status,
8235
- tags: addValues(removeValues(parsed.idea.tags, options.removeTag), options.addTag) ?? [],
8236
- linked_tasks: addValues(removeValues(parsed.idea.linked_tasks, options.removeLinkedTask), options.addLinkedTask) ?? []
8804
+ tags: (options.tagsSet && options.tagsSet.length > 0 ? setValues(options.tagsSet) : addValues(removeValues(parsed.idea.tags, options.removeTag), options.addTag)) ?? [],
8805
+ linked_tasks: (options.linkedTasksSet && options.linkedTasksSet.length > 0 ? setValues(options.linkedTasksSet) : addValues(removeValues(parsed.idea.linked_tasks, options.removeLinkedTask), options.addLinkedTask)) ?? []
8237
8806
  };
8238
8807
  const nextBody = loadBody(options) ?? parsed.body;
8239
8808
  if (options.dryRun) {
@@ -8244,7 +8813,7 @@ function updateIdea(id, options) {
8244
8813
  console.log(`Updated ${next.id}`);
8245
8814
  }
8246
8815
  function registerUpdateCommand(program) {
8247
- program.command("update").description("Update an existing COOP task or idea").argument("<id-or-type>", "Task or idea id/alias, or an explicit entity type").argument("[id]", "Entity id when an explicit type is provided").option("--title <title>").option("--priority <priority>").option("--status <status>").option("--assign <user>").option("--track <id>", "Set the task home/origin track").option("--delivery <id>", "Set the task primary delivery id").option("--story-points <n>").option("--planned-hours <n>").option("--add-delivery-track <id>", "", collect, []).option("--remove-delivery-track <id>", "", collect, []).option("--priority-in <track:priority>", "", collect, []).option("--clear-priority-in <track>", "", collect, []).option("--add-dep <id>", "", collect, []).option("--remove-dep <id>", "", collect, []).option("--add-tag <tag>", "", collect, []).option("--remove-tag <tag>", "", collect, []).option("--add-fix-version <v>", "", collect, []).option("--remove-fix-version <v>", "", collect, []).option("--add-released-in <v>", "", collect, []).option("--remove-released-in <v>", "", collect, []).option("--acceptance-add <text>", "", collect, []).option("--acceptance-remove <text>", "", collect, []).option("--tests-add <text>", "", collect, []).option("--tests-remove <text>", "", collect, []).option("--add-linked-task <id>", "", collect, []).option("--remove-linked-task <id>", "", collect, []).option("--body-file <path>").option("--body-stdin").option("--dry-run").action((first, second, options) => {
8816
+ program.command("update").description("Update an existing COOP task or idea").argument("<id-or-type>", "Task or idea id/alias, or an explicit entity type").argument("[id]", "Entity id when an explicit type is provided").option("--title <title>").option("--priority <priority>").option("--status <status>").option("--assign <user>").option("--track <id>", "Set the task home/origin track").option("--delivery <id>", "Set the task primary delivery id").option("--story-points <n>").option("--planned-hours <n>").option("--delivery-tracks-set <id>", "Replace the full delivery_tracks list", collect, []).option("--add-delivery-track <id>", "", collect, []).option("--remove-delivery-track <id>", "", collect, []).option("--priority-in <track:priority>", "", collect, []).option("--clear-priority-in <track>", "", collect, []).option("--deps-set <id>", "Replace the full depends_on list", collect, []).option("--add-dep <id>", "", collect, []).option("--remove-dep <id>", "", collect, []).option("--tags-set <tag>", "Replace the full tags list", collect, []).option("--add-tag <tag>", "", collect, []).option("--remove-tag <tag>", "", collect, []).option("--fix-versions-set <v>", "Replace the full fix_versions list", collect, []).option("--add-fix-version <v>", "", collect, []).option("--remove-fix-version <v>", "", collect, []).option("--released-in-set <v>", "Replace the full released_in list", collect, []).option("--add-released-in <v>", "", collect, []).option("--remove-released-in <v>", "", collect, []).option("--authority-ref-set <ref>", "Replace the full authority_refs list", collect, []).option("--authority-ref-add <ref>", "", collect, []).option("--authority-ref-remove <ref>", "", collect, []).option("--derived-ref-set <ref>", "Replace the full derived_refs list", collect, []).option("--derived-ref-add <ref>", "", collect, []).option("--derived-ref-remove <ref>", "", collect, []).option("--acceptance-set <text>", "Replace the full acceptance list; repeat the flag to set multiple entries", collect, []).option("--acceptance-add <text>", "", collect, []).option("--acceptance-remove <text>", "", collect, []).option("--tests-set <text>", "Replace the full tests_required list; repeat the flag to set multiple entries", collect, []).option("--tests-add <text>", "", collect, []).option("--tests-remove <text>", "", collect, []).option("--linked-tasks-set <id>", "Replace the full linked_tasks list on an idea", collect, []).option("--add-linked-task <id>", "", collect, []).option("--remove-linked-task <id>", "", collect, []).option("--body-file <path>").option("--body-stdin").option("--dry-run").action((first, second, options) => {
8248
8817
  let resolved = resolveOptionalEntityArg(first, second, ["task", "idea"], "task");
8249
8818
  if (!second && resolved.entity === "task") {
8250
8819
  try {
@@ -8655,8 +9224,51 @@ function registerWebhookCommand(program) {
8655
9224
  });
8656
9225
  }
8657
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
+
8658
9270
  // src/merge-driver/merge-driver.ts
8659
- import fs21 from "fs";
9271
+ import fs22 from "fs";
8660
9272
  import os3 from "os";
8661
9273
  import path25 from "path";
8662
9274
  import { spawnSync as spawnSync5 } from "child_process";
@@ -8741,33 +9353,33 @@ function mergeTextWithGit(ancestor, ours, theirs) {
8741
9353
  return { ok: false, output: stdout };
8742
9354
  }
8743
9355
  function mergeTaskFile(ancestorPath, oursPath, theirsPath) {
8744
- const ancestorRaw = fs21.readFileSync(ancestorPath, "utf8");
8745
- const oursRaw = fs21.readFileSync(oursPath, "utf8");
8746
- 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");
8747
9359
  const ancestor = parseTaskDocument(ancestorRaw, ancestorPath);
8748
9360
  const ours = parseTaskDocument(oursRaw, oursPath);
8749
9361
  const theirs = parseTaskDocument(theirsRaw, theirsPath);
8750
9362
  const mergedFrontmatter = mergeTaskFrontmatter(ancestor.frontmatter, ours.frontmatter, theirs.frontmatter);
8751
- const tempDir = fs21.mkdtempSync(path25.join(os3.tmpdir(), "coop-merge-body-"));
9363
+ const tempDir = fs22.mkdtempSync(path25.join(os3.tmpdir(), "coop-merge-body-"));
8752
9364
  try {
8753
9365
  const ancestorBody = path25.join(tempDir, "ancestor.md");
8754
9366
  const oursBody = path25.join(tempDir, "ours.md");
8755
9367
  const theirsBody = path25.join(tempDir, "theirs.md");
8756
- fs21.writeFileSync(ancestorBody, ancestor.body, "utf8");
8757
- fs21.writeFileSync(oursBody, ours.body, "utf8");
8758
- 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");
8759
9371
  const mergedBody = mergeTextWithGit(ancestorBody, oursBody, theirsBody);
8760
9372
  const output2 = stringifyFrontmatter6(mergedFrontmatter, mergedBody.output);
8761
- fs21.writeFileSync(oursPath, output2, "utf8");
9373
+ fs22.writeFileSync(oursPath, output2, "utf8");
8762
9374
  return mergedBody.ok ? 0 : 1;
8763
9375
  } finally {
8764
- fs21.rmSync(tempDir, { recursive: true, force: true });
9376
+ fs22.rmSync(tempDir, { recursive: true, force: true });
8765
9377
  }
8766
9378
  }
8767
9379
  function mergeDeliveryFile(ancestorPath, oursPath, theirsPath) {
8768
- const ancestor = parseYamlContent3(fs21.readFileSync(ancestorPath, "utf8"), ancestorPath);
8769
- const ours = parseYamlContent3(fs21.readFileSync(oursPath, "utf8"), oursPath);
8770
- 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);
8771
9383
  const oursUpdated = asTimestamp(ours.updated);
8772
9384
  const theirsUpdated = asTimestamp(theirs.updated);
8773
9385
  const base = ancestor;
@@ -8777,7 +9389,7 @@ function mergeDeliveryFile(ancestorPath, oursPath, theirsPath) {
8777
9389
  const value = chooseFieldValue(key, base[key], ours[key], theirs[key], oursUpdated, theirsUpdated);
8778
9390
  if (value !== void 0) merged[key] = value;
8779
9391
  }
8780
- fs21.writeFileSync(oursPath, stringifyYamlContent2(merged), "utf8");
9392
+ fs22.writeFileSync(oursPath, stringifyYamlContent2(merged), "utf8");
8781
9393
  return 0;
8782
9394
  }
8783
9395
  function runMergeDriver(kind, ancestorPath, oursPath, theirsPath) {
@@ -8792,21 +9404,34 @@ function renderBasicHelp() {
8792
9404
  return [
8793
9405
  "COOP Basics",
8794
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
+ "",
8795
9411
  "Day-to-day commands:",
8796
9412
  "- `coop current`: show working context, active work, and the next ready task",
8797
9413
  "- `coop use track <id>` / `coop use delivery <id>` / `coop use reset`: manage working scope defaults",
8798
9414
  "- `coop list tracks` / `coop list deliveries`: inspect valid named values before assigning them",
8799
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",
8800
9417
  '- `coop naming preview "Title" --entity task`: preview the generated ID before creating an item; templates support `TITLE##` like `TITLE18`, `TITLE8`, or `TITLE08`',
8801
9418
  "- `coop naming reset task`: reset one entity's naming template to the default",
8802
- "- `coop next task` or `coop pick task`: choose work from COOP",
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",
9420
+ "- `coop next` or `coop pick [id]`: choose work from COOP (`next` shorthand is task-only)",
9421
+ "- `coop promote <id>`: move a task to the top of the current working track/version selection lens",
8803
9422
  "- `coop show <id>`: inspect a task, idea, or delivery",
8804
- "- `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",
8805
9425
  "- `coop update <id> --track <id> --delivery <id>`: update task metadata",
9426
+ '- `coop update <id> --acceptance-set "..." --acceptance-set "..."`: replace the whole acceptance list cleanly when an old task was created with the wrong parsing',
9427
+ "- `coop update <id> --tests-set ... --authority-ref-set ... --deps-set ...`: the same full-replace pattern works for other mutable list fields too",
8806
9428
  '- `coop comment <id> --message "..."`: append a task comment',
8807
9429
  "- `coop log-time <id> --hours 2 --kind worked`: append time spent",
8808
9430
  "- `coop alias remove <id> <alias>`: remove a shorthand alias from a task or idea",
8809
- "- `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",
8810
9435
  "- `coop help-ai --initial-prompt --strict --repo C:/path/to/repo --delivery MVP --command coop.cmd`: hand off COOP context to an agent",
8811
9436
  "",
8812
9437
  "Use `coop <command> --help` for detailed flags."
@@ -8823,7 +9448,7 @@ function readVersion() {
8823
9448
  const currentFile = fileURLToPath2(import.meta.url);
8824
9449
  const packageJsonPath = path26.resolve(path26.dirname(currentFile), "..", "package.json");
8825
9450
  try {
8826
- const parsed = JSON.parse(fs22.readFileSync(packageJsonPath, "utf8"));
9451
+ const parsed = JSON.parse(fs23.readFileSync(packageJsonPath, "utf8"));
8827
9452
  return parsed.version ?? "0.0.0";
8828
9453
  } catch {
8829
9454
  return "0.0.0";
@@ -8845,9 +9470,11 @@ function createProgram() {
8845
9470
  Common day-to-day commands:
8846
9471
  coop basics
8847
9472
  coop current
8848
- coop next task
9473
+ coop next
8849
9474
  coop show <id>
9475
+ coop rename <id> <alias>
8850
9476
  coop naming
9477
+ coop workflow transitions show
8851
9478
  `);
8852
9479
  registerInitCommand(program);
8853
9480
  registerCreateCommand(program);
@@ -8873,6 +9500,7 @@ Common day-to-day commands:
8873
9500
  registerPromoteCommand(program);
8874
9501
  registerProjectCommand(program);
8875
9502
  registerPromptCommand(program);
9503
+ registerRenameCommand(program);
8876
9504
  registerRefineCommand(program);
8877
9505
  registerRunCommand(program);
8878
9506
  registerSearchCommand(program);
@@ -8883,6 +9511,7 @@ Common day-to-day commands:
8883
9511
  registerUseCommand(program);
8884
9512
  registerViewCommand(program);
8885
9513
  registerWebhookCommand(program);
9514
+ registerWorkflowCommand(program);
8886
9515
  registerPhasePlaceholder(program, "ext", 3, "Plugin extension commands");
8887
9516
  program.command("basics").alias("help-basic").description("Show the small set of COOP commands that cover most day-to-day work").action(() => {
8888
9517
  console.log(renderBasicHelp());