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