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