@kitsy/coop 2.1.0 → 2.1.2
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 +23 -2
- package/dist/index.js +722 -142
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import fs18 from "fs";
|
|
5
|
-
import
|
|
5
|
+
import path24 from "path";
|
|
6
6
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
7
7
|
import { Command } from "commander";
|
|
8
8
|
|
|
@@ -29,6 +29,28 @@ import {
|
|
|
29
29
|
writeYamlFile
|
|
30
30
|
} from "@kitsy/coop-core";
|
|
31
31
|
var SEQ_MARKER = "COOPSEQTOKEN";
|
|
32
|
+
var DEFAULT_ID_NAMING_TEMPLATE = "<TYPE>-<TITLE16>-<SEQ>";
|
|
33
|
+
var DEFAULT_ARTIFACTS_DIR = "docs";
|
|
34
|
+
var DEFAULT_TITLE_TOKEN_LENGTH = 16;
|
|
35
|
+
var SEMANTIC_WORD_MAX = 4;
|
|
36
|
+
var SEMANTIC_STOP_WORDS = /* @__PURE__ */ new Set([
|
|
37
|
+
"A",
|
|
38
|
+
"AN",
|
|
39
|
+
"AND",
|
|
40
|
+
"AS",
|
|
41
|
+
"AT",
|
|
42
|
+
"BY",
|
|
43
|
+
"FOR",
|
|
44
|
+
"FROM",
|
|
45
|
+
"IN",
|
|
46
|
+
"INTO",
|
|
47
|
+
"OF",
|
|
48
|
+
"ON",
|
|
49
|
+
"OR",
|
|
50
|
+
"THE",
|
|
51
|
+
"TO",
|
|
52
|
+
"WITH"
|
|
53
|
+
]);
|
|
32
54
|
function resolveCoopHome() {
|
|
33
55
|
const configured = process.env.COOP_HOME?.trim();
|
|
34
56
|
if (configured) {
|
|
@@ -103,8 +125,9 @@ function readCoopConfig(root, projectId = resolveRequestedProject()) {
|
|
|
103
125
|
ideaPrefix: "IDEA",
|
|
104
126
|
taskPrefix: "PM",
|
|
105
127
|
indexDataFormat: "yaml",
|
|
106
|
-
idNamingTemplate:
|
|
128
|
+
idNamingTemplate: DEFAULT_ID_NAMING_TEMPLATE,
|
|
107
129
|
idSeqPadding: 0,
|
|
130
|
+
artifactsDir: DEFAULT_ARTIFACTS_DIR,
|
|
108
131
|
projectName: repoName || "COOP Workspace",
|
|
109
132
|
projectId: repoName || "workspace",
|
|
110
133
|
projectAliases: [],
|
|
@@ -126,15 +149,18 @@ function readCoopConfig(root, projectId = resolveRequestedProject()) {
|
|
|
126
149
|
const indexDataFormat = indexDataRaw === "json" ? "json" : "yaml";
|
|
127
150
|
const idRaw = typeof config.id === "object" && config.id !== null ? config.id : {};
|
|
128
151
|
const idNamingTemplateRaw = idRaw.naming;
|
|
129
|
-
const idNamingTemplate = typeof idNamingTemplateRaw === "string" && idNamingTemplateRaw.trim().length > 0 ? idNamingTemplateRaw.trim() :
|
|
152
|
+
const idNamingTemplate = typeof idNamingTemplateRaw === "string" && idNamingTemplateRaw.trim().length > 0 ? idNamingTemplateRaw.trim() : DEFAULT_ID_NAMING_TEMPLATE;
|
|
130
153
|
const idSeqPaddingRaw = idRaw.seq_padding;
|
|
131
154
|
const idSeqPadding = Number.isInteger(idSeqPaddingRaw) && Number(idSeqPaddingRaw) >= 0 ? Number(idSeqPaddingRaw) : 0;
|
|
155
|
+
const artifactsRaw = typeof config.artifacts === "object" && config.artifacts !== null ? config.artifacts : {};
|
|
156
|
+
const artifactsDir = typeof artifactsRaw.dir === "string" && artifactsRaw.dir.trim().length > 0 ? artifactsRaw.dir.trim() : DEFAULT_ARTIFACTS_DIR;
|
|
132
157
|
return {
|
|
133
158
|
ideaPrefix,
|
|
134
159
|
taskPrefix,
|
|
135
160
|
indexDataFormat,
|
|
136
161
|
idNamingTemplate,
|
|
137
162
|
idSeqPadding,
|
|
163
|
+
artifactsDir,
|
|
138
164
|
projectName: projectName || "COOP Workspace",
|
|
139
165
|
projectId: resolvedProjectId || "workspace",
|
|
140
166
|
projectAliases,
|
|
@@ -249,6 +275,59 @@ function sanitizeTemplateValue(input3, fallback = "X") {
|
|
|
249
275
|
const normalized = input3.toUpperCase().replace(/[^A-Z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-");
|
|
250
276
|
return normalized || fallback;
|
|
251
277
|
}
|
|
278
|
+
function sanitizeSemanticWord(input3) {
|
|
279
|
+
return input3.toUpperCase().replace(/[^A-Z0-9]+/g, "").trim();
|
|
280
|
+
}
|
|
281
|
+
function semanticWords(input3) {
|
|
282
|
+
const rawWords = input3.split(/[^a-zA-Z0-9]+/g).map(sanitizeSemanticWord).filter(Boolean);
|
|
283
|
+
if (rawWords.length <= 1) {
|
|
284
|
+
return rawWords;
|
|
285
|
+
}
|
|
286
|
+
const filtered = rawWords.filter((word) => !SEMANTIC_STOP_WORDS.has(word));
|
|
287
|
+
return filtered.length > 0 ? filtered : rawWords;
|
|
288
|
+
}
|
|
289
|
+
function semanticTitleToken(input3, maxLength = DEFAULT_TITLE_TOKEN_LENGTH) {
|
|
290
|
+
const safeMaxLength = Number.isFinite(maxLength) ? Math.max(4, Math.floor(maxLength)) : DEFAULT_TITLE_TOKEN_LENGTH;
|
|
291
|
+
const words = semanticWords(input3);
|
|
292
|
+
if (words.length === 0) {
|
|
293
|
+
return "UNTITLED";
|
|
294
|
+
}
|
|
295
|
+
if (words.length === 1) {
|
|
296
|
+
return words[0].slice(0, safeMaxLength) || "UNTITLED";
|
|
297
|
+
}
|
|
298
|
+
const segments = [];
|
|
299
|
+
let used = 0;
|
|
300
|
+
for (const word of words) {
|
|
301
|
+
const remaining = safeMaxLength - used - (segments.length > 0 ? 1 : 0);
|
|
302
|
+
if (remaining < 2) {
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
const segment = word.slice(0, Math.min(SEMANTIC_WORD_MAX, remaining));
|
|
306
|
+
if (!segment) {
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
segments.push(segment);
|
|
310
|
+
used += segment.length + (segments.length > 1 ? 1 : 0);
|
|
311
|
+
}
|
|
312
|
+
if (segments.length > 0) {
|
|
313
|
+
return segments.join("-");
|
|
314
|
+
}
|
|
315
|
+
return words[0].slice(0, safeMaxLength) || "UNTITLED";
|
|
316
|
+
}
|
|
317
|
+
function namingTokenExamples(title = "Natural-language COOP command recommender") {
|
|
318
|
+
return {
|
|
319
|
+
"<TYPE>": "IDEA",
|
|
320
|
+
"<TITLE>": semanticTitleToken(title, DEFAULT_TITLE_TOKEN_LENGTH),
|
|
321
|
+
"<TITLE16>": semanticTitleToken(title, 16),
|
|
322
|
+
"<TITLE24>": semanticTitleToken(title, 24),
|
|
323
|
+
"<TRACK>": "MVP",
|
|
324
|
+
"<SEQ>": "1",
|
|
325
|
+
"<USER>": "PKVSI",
|
|
326
|
+
"<YYMMDD>": "260320",
|
|
327
|
+
"<RAND>": "AB12CD34",
|
|
328
|
+
"<PREFIX>": "PM"
|
|
329
|
+
};
|
|
330
|
+
}
|
|
252
331
|
function sequenceForPattern(existingIds, prefix, suffix) {
|
|
253
332
|
const escapedPrefix = prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
254
333
|
const escapedSuffix = suffix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -284,7 +363,9 @@ function buildIdContext(root, config, context) {
|
|
|
284
363
|
ENTITY: sanitizeTemplateValue(entityType, entityType),
|
|
285
364
|
USER: normalizeIdPart(actor, "USER", 16),
|
|
286
365
|
YYMMDD: shortDateToken(now),
|
|
287
|
-
TITLE:
|
|
366
|
+
TITLE: semanticTitleToken(title, DEFAULT_TITLE_TOKEN_LENGTH),
|
|
367
|
+
TITLE16: semanticTitleToken(title, 16),
|
|
368
|
+
TITLE24: semanticTitleToken(title, 24),
|
|
288
369
|
TRACK: sanitizeTemplateValue(track || "UNASSIGNED", "UNASSIGNED"),
|
|
289
370
|
STATUS: sanitizeTemplateValue(status || "TODO", "TODO"),
|
|
290
371
|
TASK_TYPE: sanitizeTemplateValue(taskType || "FEATURE", "FEATURE"),
|
|
@@ -307,6 +388,9 @@ function replaceTemplateToken(token, contextMap) {
|
|
|
307
388
|
if (upper === "USER") return contextMap.USER;
|
|
308
389
|
if (upper === "TYPE" || upper === "ENTITY") return contextMap.TYPE;
|
|
309
390
|
if (upper === "TITLE") return contextMap.TITLE;
|
|
391
|
+
if (/^TITLE\d+$/.test(upper)) {
|
|
392
|
+
return contextMap[upper] || contextMap.TITLE;
|
|
393
|
+
}
|
|
310
394
|
if (upper === "TRACK") return contextMap.TRACK;
|
|
311
395
|
if (upper === "TASK_TYPE") return contextMap.TASK_TYPE;
|
|
312
396
|
if (upper === "STATUS") return contextMap.STATUS;
|
|
@@ -316,7 +400,7 @@ function replaceTemplateToken(token, contextMap) {
|
|
|
316
400
|
return sanitizeTemplateValue(upper);
|
|
317
401
|
}
|
|
318
402
|
function renderNamingTemplate(template, contextMap) {
|
|
319
|
-
const normalizedTemplate = template.trim().length > 0 ? template :
|
|
403
|
+
const normalizedTemplate = template.trim().length > 0 ? template : DEFAULT_ID_NAMING_TEMPLATE;
|
|
320
404
|
const replaced = normalizedTemplate.replace(/<([^>]+)>/g, (_, token) => replaceTemplateToken(token, contextMap));
|
|
321
405
|
return sanitizeTemplateValue(replaced, "COOP-ID");
|
|
322
406
|
}
|
|
@@ -324,6 +408,17 @@ function defaultCoopAuthor(root) {
|
|
|
324
408
|
const actor = inferActor(root);
|
|
325
409
|
return actor.trim() || "unknown";
|
|
326
410
|
}
|
|
411
|
+
function previewNamingTemplate(template, context, root = process.cwd()) {
|
|
412
|
+
const config = readCoopConfig(root);
|
|
413
|
+
const contextMap = buildIdContext(root, config, context);
|
|
414
|
+
const rendered = renderNamingTemplate(template, {
|
|
415
|
+
...contextMap,
|
|
416
|
+
RAND: "AB12CD34",
|
|
417
|
+
YYMMDD: "260320",
|
|
418
|
+
USER: "PKVSI"
|
|
419
|
+
});
|
|
420
|
+
return rendered.replace(SEQ_MARKER, "1");
|
|
421
|
+
}
|
|
327
422
|
function generateConfiguredId(root, existingIds, context) {
|
|
328
423
|
const config = readCoopConfig(root);
|
|
329
424
|
const contextMap = buildIdContext(root, config, context);
|
|
@@ -1550,6 +1645,7 @@ var INDEX_DATA_KEY = "index.data";
|
|
|
1550
1645
|
var ID_NAMING_KEY = "id.naming";
|
|
1551
1646
|
var AI_PROVIDER_KEY = "ai.provider";
|
|
1552
1647
|
var AI_MODEL_KEY = "ai.model";
|
|
1648
|
+
var ARTIFACTS_DIR_KEY = "artifacts.dir";
|
|
1553
1649
|
var PROJECT_NAME_KEY = "project.name";
|
|
1554
1650
|
var PROJECT_ID_KEY = "project.id";
|
|
1555
1651
|
var PROJECT_ALIASES_KEY = "project.aliases";
|
|
@@ -1558,6 +1654,7 @@ var SUPPORTED_KEYS = [
|
|
|
1558
1654
|
ID_NAMING_KEY,
|
|
1559
1655
|
AI_PROVIDER_KEY,
|
|
1560
1656
|
AI_MODEL_KEY,
|
|
1657
|
+
ARTIFACTS_DIR_KEY,
|
|
1561
1658
|
PROJECT_NAME_KEY,
|
|
1562
1659
|
PROJECT_ID_KEY,
|
|
1563
1660
|
PROJECT_ALIASES_KEY
|
|
@@ -1585,6 +1682,9 @@ function readAiModelValue(root) {
|
|
|
1585
1682
|
function readProjectNameValue(root) {
|
|
1586
1683
|
return readCoopConfig(root).projectName;
|
|
1587
1684
|
}
|
|
1685
|
+
function readArtifactsDirValue(root) {
|
|
1686
|
+
return readCoopConfig(root).artifactsDir;
|
|
1687
|
+
}
|
|
1588
1688
|
function readProjectIdValue(root) {
|
|
1589
1689
|
return readCoopConfig(root).projectId;
|
|
1590
1690
|
}
|
|
@@ -1653,6 +1753,18 @@ function writeProjectNameValue(root, value) {
|
|
|
1653
1753
|
next.project = projectRaw;
|
|
1654
1754
|
writeCoopConfig(root, next);
|
|
1655
1755
|
}
|
|
1756
|
+
function writeArtifactsDirValue(root, value) {
|
|
1757
|
+
const nextValue = value.trim().replace(/\\/g, "/").replace(/\/+/g, "/").replace(/^\/+|\/+$/g, "");
|
|
1758
|
+
if (!nextValue) {
|
|
1759
|
+
throw new Error("Invalid value for artifacts.dir. Provide a non-empty relative path.");
|
|
1760
|
+
}
|
|
1761
|
+
const config = readCoopConfig(root).raw;
|
|
1762
|
+
const next = { ...config };
|
|
1763
|
+
const artifactsRaw = typeof next.artifacts === "object" && next.artifacts !== null ? { ...next.artifacts } : {};
|
|
1764
|
+
artifactsRaw.dir = nextValue;
|
|
1765
|
+
next.artifacts = artifactsRaw;
|
|
1766
|
+
writeCoopConfig(root, next);
|
|
1767
|
+
}
|
|
1656
1768
|
function writeProjectIdValue(root, value) {
|
|
1657
1769
|
const nextValue = value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-");
|
|
1658
1770
|
if (!nextValue) {
|
|
@@ -1702,6 +1814,10 @@ function registerConfigCommand(program) {
|
|
|
1702
1814
|
console.log(`${AI_PROVIDER_KEY}=${readAiProviderValue(root)}`);
|
|
1703
1815
|
return;
|
|
1704
1816
|
}
|
|
1817
|
+
if (keyPath === ARTIFACTS_DIR_KEY) {
|
|
1818
|
+
console.log(`${ARTIFACTS_DIR_KEY}=${readArtifactsDirValue(root)}`);
|
|
1819
|
+
return;
|
|
1820
|
+
}
|
|
1705
1821
|
if (keyPath === PROJECT_NAME_KEY) {
|
|
1706
1822
|
console.log(`${PROJECT_NAME_KEY}=${readProjectNameValue(root)}`);
|
|
1707
1823
|
return;
|
|
@@ -1732,6 +1848,11 @@ function registerConfigCommand(program) {
|
|
|
1732
1848
|
console.log(`${AI_PROVIDER_KEY}=${readAiProviderValue(root)}`);
|
|
1733
1849
|
return;
|
|
1734
1850
|
}
|
|
1851
|
+
if (keyPath === ARTIFACTS_DIR_KEY) {
|
|
1852
|
+
writeArtifactsDirValue(root, value);
|
|
1853
|
+
console.log(`${ARTIFACTS_DIR_KEY}=${readArtifactsDirValue(root)}`);
|
|
1854
|
+
return;
|
|
1855
|
+
}
|
|
1735
1856
|
if (keyPath === PROJECT_NAME_KEY) {
|
|
1736
1857
|
writeProjectNameValue(root, value);
|
|
1737
1858
|
console.log(`${PROJECT_NAME_KEY}=${readProjectNameValue(root)}`);
|
|
@@ -2778,6 +2899,7 @@ function registerGraphCommand(program) {
|
|
|
2778
2899
|
}
|
|
2779
2900
|
|
|
2780
2901
|
// src/utils/ai-help.ts
|
|
2902
|
+
import path7 from "path";
|
|
2781
2903
|
var catalog = {
|
|
2782
2904
|
purpose: "COOP is a Git-native planning, backlog, execution, and orchestration CLI. It stores canonical data in .coop/projects/<project.id>/ and should be treated as the source of truth for work selection and lifecycle state.",
|
|
2783
2905
|
selection_rules: [
|
|
@@ -2792,15 +2914,49 @@ var catalog = {
|
|
|
2792
2914
|
"Multi-project workspaces require `--project <id>` or `coop project use <id>`.",
|
|
2793
2915
|
"Canonical storage is under `.coop/projects/<project.id>/...`."
|
|
2794
2916
|
],
|
|
2917
|
+
allowed_lifecycle_commands: [
|
|
2918
|
+
"coop start task <id>",
|
|
2919
|
+
"coop review task <id>",
|
|
2920
|
+
"coop complete task <id>",
|
|
2921
|
+
"coop block task <id>",
|
|
2922
|
+
"coop unblock task <id>",
|
|
2923
|
+
"coop cancel task <id>",
|
|
2924
|
+
"coop reopen task <id>",
|
|
2925
|
+
"coop transition task <id> <status>"
|
|
2926
|
+
],
|
|
2927
|
+
lifecycle_requirements: [
|
|
2928
|
+
"`coop start task <id>` moves a ready task into `in_progress`.",
|
|
2929
|
+
"`coop review task <id>` requires the task to already be `in_progress` and moves it to `in_review`.",
|
|
2930
|
+
"`coop complete task <id>` requires the task to already be `in_review` and moves it to `done`.",
|
|
2931
|
+
"Do not call `coop complete task <id>` directly from `in_progress` unless COOP explicitly adds that transition later."
|
|
2932
|
+
],
|
|
2933
|
+
unsupported_command_warnings: [
|
|
2934
|
+
"Do not invent COOP commands such as `coop complete` when they are not present in `help-ai` output.",
|
|
2935
|
+
"If a desired lifecycle action is not represented by an allowed command, state that explicitly instead of paraphrasing.",
|
|
2936
|
+
"Quote exact COOP command names from `coop help-ai --format json` before proposing them."
|
|
2937
|
+
],
|
|
2938
|
+
artifact_policy: {
|
|
2939
|
+
config_key: "artifacts.dir",
|
|
2940
|
+
default_dir: "docs",
|
|
2941
|
+
required_for: ["contract reviews", "spike findings", "audit notes", "planning artifacts"],
|
|
2942
|
+
guidance: [
|
|
2943
|
+
"Write agent-produced artifacts under the configured artifact directory.",
|
|
2944
|
+
"If `artifacts.dir` is unset, use `docs`.",
|
|
2945
|
+
"Do not invent ad hoc artifact paths outside the configured directory unless the user explicitly asks for a different location."
|
|
2946
|
+
]
|
|
2947
|
+
},
|
|
2795
2948
|
command_groups: [
|
|
2796
2949
|
{
|
|
2797
2950
|
name: "Workspace",
|
|
2798
2951
|
description: "Initialize and select the active COOP project.",
|
|
2799
2952
|
commands: [
|
|
2800
2953
|
{ usage: "coop init --yes", purpose: "Initialize a COOP workspace with repo-name defaults." },
|
|
2954
|
+
{ usage: "coop init --naming <TYPE>-<TITLE16>-<SEQ>", purpose: "Initialize a workspace with an explicit ID naming template." },
|
|
2801
2955
|
{ usage: "coop project list", purpose: "List projects in the current workspace." },
|
|
2802
2956
|
{ usage: "coop project show", purpose: "Show the active project id, name, path, and layout." },
|
|
2803
|
-
{ usage: "coop project use <id>", purpose: "Switch the active project in a multi-project workspace." }
|
|
2957
|
+
{ usage: "coop project use <id>", purpose: "Switch the active project in a multi-project workspace." },
|
|
2958
|
+
{ usage: "coop naming", purpose: "Explain the current naming template, tokens, and examples." },
|
|
2959
|
+
{ usage: 'coop naming preview "Natural-language COOP command recommender"', purpose: "Preview a semantic ID before creating an item." }
|
|
2804
2960
|
]
|
|
2805
2961
|
},
|
|
2806
2962
|
{
|
|
@@ -2838,7 +2994,13 @@ var catalog = {
|
|
|
2838
2994
|
{ usage: "coop next task", purpose: "Show the top ready task using the default track or full workspace context." },
|
|
2839
2995
|
{ usage: "coop graph next --delivery MVP", purpose: "Show the ready queue for a delivery with scores and blockers." },
|
|
2840
2996
|
{ usage: "coop pick task --delivery MVP --claim --actor dev1 --user lead-user", purpose: "Select the top ready task, optionally assign it, and move it to in_progress." },
|
|
2841
|
-
{ usage: "coop start task PM-101 --claim --actor dev1 --user lead-user", purpose: "Start a specific task or the top ready task if no id is provided." }
|
|
2997
|
+
{ usage: "coop start task PM-101 --claim --actor dev1 --user lead-user", purpose: "Start a specific task or the top ready task if no id is provided." },
|
|
2998
|
+
{ usage: "coop review task PM-101", purpose: "Move an in-progress task into in_review using a DX-friendly verb." },
|
|
2999
|
+
{ usage: "coop complete task PM-101", purpose: "Move a task in review into done using a DX-friendly verb." },
|
|
3000
|
+
{ usage: "coop block task PM-101", purpose: "Mark a task as blocked." },
|
|
3001
|
+
{ usage: "coop unblock task PM-101", purpose: "Move a blocked task back to todo." },
|
|
3002
|
+
{ usage: "coop cancel task PM-101", purpose: "Cancel a task." },
|
|
3003
|
+
{ usage: "coop reopen task PM-101", purpose: "Move a canceled task back to todo." }
|
|
2842
3004
|
]
|
|
2843
3005
|
},
|
|
2844
3006
|
{
|
|
@@ -2858,6 +3020,13 @@ var catalog = {
|
|
|
2858
3020
|
name: "Execute And Integrate",
|
|
2859
3021
|
description: "Hand execution to an AI provider or code-agent CLI and expose COOP through MCP/API.",
|
|
2860
3022
|
commands: [
|
|
3023
|
+
{ usage: "coop help-ai --selection --format markdown", purpose: "Show the focused rules for deterministic task selection and workspace resolution." },
|
|
3024
|
+
{ usage: "coop help-ai --state-transitions --format json", purpose: "Show exact lifecycle commands, prerequisites, and warnings against invented transitions." },
|
|
3025
|
+
{ usage: "coop help-ai --artifacts --format markdown", purpose: "Show where contract-review, audit, and planning artifacts must be written." },
|
|
3026
|
+
{ usage: "coop help-ai --post-execution --format markdown", purpose: "Show what an agent must do after finishing the selected task." },
|
|
3027
|
+
{ usage: "coop help-ai --naming --format markdown", purpose: "Show naming guidance when IDs and naming templates matter." },
|
|
3028
|
+
{ usage: "coop help-ai --initial-prompt --strict --repo C:/path/to/repo --delivery MVP --command coop.cmd", purpose: "Generate a strict agent bootstrap prompt for a repository and delivery." },
|
|
3029
|
+
{ usage: "coop help-ai --initial-prompt --rigour balanced --repo C:/path/to/repo --delivery MVP --command coop.cmd", purpose: "Generate the recommended balanced agent bootstrap prompt for day-to-day implementation work." },
|
|
2861
3030
|
{ usage: "coop config ai.provider codex_cli", purpose: "Use the installed Codex CLI as the execution/refinement backend." },
|
|
2862
3031
|
{ usage: "coop config ai.provider claude_cli", purpose: "Use the installed Claude CLI as the execution/refinement backend." },
|
|
2863
3032
|
{ usage: "coop config ai.provider gemini_cli", purpose: "Use the installed Gemini CLI as the execution/refinement backend." },
|
|
@@ -2883,9 +3052,201 @@ var catalog = {
|
|
|
2883
3052
|
execution_model: [
|
|
2884
3053
|
"Agents or services may send drafts through files or stdin, but COOP owns canonical writes.",
|
|
2885
3054
|
"Use `coop create ... --from-file|--stdin` and `coop apply draft` instead of editing `.coop` task or idea files directly.",
|
|
2886
|
-
"Use `coop log --last --verbose` when command execution fails and the concise error points to a stack trace."
|
|
3055
|
+
"Use `coop log --last --verbose` when command execution fails and the concise error points to a stack trace.",
|
|
3056
|
+
"If a workflow depends on stable human-readable IDs, inspect `coop naming` or `coop config id.naming` before creating items."
|
|
3057
|
+
],
|
|
3058
|
+
post_execution_rules: [
|
|
3059
|
+
"After completing the currently selected task, stop and wait instead of automatically selecting the next task.",
|
|
3060
|
+
"Do not auto-start a second task in the same run unless the user explicitly asks you to continue.",
|
|
3061
|
+
"If you changed task state in COOP, report the exact command used and the resulting new state before doing anything else."
|
|
2887
3062
|
]
|
|
2888
3063
|
};
|
|
3064
|
+
function renderTopicPayload(topic) {
|
|
3065
|
+
if (topic === "state-transitions") {
|
|
3066
|
+
return {
|
|
3067
|
+
topic,
|
|
3068
|
+
allowed_lifecycle_commands: catalog.allowed_lifecycle_commands,
|
|
3069
|
+
lifecycle_requirements: catalog.lifecycle_requirements,
|
|
3070
|
+
warnings: catalog.unsupported_command_warnings
|
|
3071
|
+
};
|
|
3072
|
+
}
|
|
3073
|
+
if (topic === "artifacts") {
|
|
3074
|
+
return {
|
|
3075
|
+
topic,
|
|
3076
|
+
artifact_policy: catalog.artifact_policy
|
|
3077
|
+
};
|
|
3078
|
+
}
|
|
3079
|
+
if (topic === "post-execution") {
|
|
3080
|
+
return {
|
|
3081
|
+
topic,
|
|
3082
|
+
post_execution_rules: catalog.post_execution_rules
|
|
3083
|
+
};
|
|
3084
|
+
}
|
|
3085
|
+
if (topic === "selection") {
|
|
3086
|
+
return {
|
|
3087
|
+
topic,
|
|
3088
|
+
selection_rules: catalog.selection_rules,
|
|
3089
|
+
workspace_rules: catalog.workspace_rules
|
|
3090
|
+
};
|
|
3091
|
+
}
|
|
3092
|
+
return {
|
|
3093
|
+
topic,
|
|
3094
|
+
naming_guidance: [
|
|
3095
|
+
"Use `coop naming` to inspect the current naming template and token behavior.",
|
|
3096
|
+
'Use `coop naming preview "<title>"` before creating a new idea, task, or delivery if predictable IDs matter.',
|
|
3097
|
+
"Use `coop config id.naming ...` to override the default semantic naming template."
|
|
3098
|
+
]
|
|
3099
|
+
};
|
|
3100
|
+
}
|
|
3101
|
+
function renderAiHelpTopic(format, topic) {
|
|
3102
|
+
const payload = renderTopicPayload(topic);
|
|
3103
|
+
if (format === "json") {
|
|
3104
|
+
return `${JSON.stringify(payload, null, 2)}
|
|
3105
|
+
`;
|
|
3106
|
+
}
|
|
3107
|
+
const lines = [];
|
|
3108
|
+
const bullet = (value) => `- ${value}`;
|
|
3109
|
+
const title = topic.split("-").map((part) => part.slice(0, 1).toUpperCase() + part.slice(1)).join(" ");
|
|
3110
|
+
lines.push(format === "markdown" ? `# COOP AI Help: ${title}` : `COOP AI Help: ${title}`, "");
|
|
3111
|
+
if (topic === "state-transitions") {
|
|
3112
|
+
lines.push(format === "markdown" ? "## Allowed Lifecycle Commands" : "Allowed Lifecycle Commands");
|
|
3113
|
+
for (const item of catalog.allowed_lifecycle_commands) {
|
|
3114
|
+
lines.push(bullet(`\`${item}\``));
|
|
3115
|
+
}
|
|
3116
|
+
lines.push("");
|
|
3117
|
+
lines.push(format === "markdown" ? "## Lifecycle Requirements" : "Lifecycle Requirements");
|
|
3118
|
+
for (const item of catalog.lifecycle_requirements) {
|
|
3119
|
+
lines.push(bullet(item));
|
|
3120
|
+
}
|
|
3121
|
+
lines.push("");
|
|
3122
|
+
lines.push(format === "markdown" ? "## Warnings" : "Warnings");
|
|
3123
|
+
for (const item of catalog.unsupported_command_warnings) {
|
|
3124
|
+
lines.push(bullet(item));
|
|
3125
|
+
}
|
|
3126
|
+
lines.push("");
|
|
3127
|
+
return `${lines.join("\n")}
|
|
3128
|
+
`;
|
|
3129
|
+
}
|
|
3130
|
+
if (topic === "artifacts") {
|
|
3131
|
+
lines.push(bullet(`Config key: \`${catalog.artifact_policy.config_key}\``));
|
|
3132
|
+
lines.push(bullet(`Default dir: \`${catalog.artifact_policy.default_dir}\``));
|
|
3133
|
+
lines.push(bullet(`Required for: ${catalog.artifact_policy.required_for.join(", ")}`));
|
|
3134
|
+
for (const item of catalog.artifact_policy.guidance) {
|
|
3135
|
+
lines.push(bullet(item));
|
|
3136
|
+
}
|
|
3137
|
+
lines.push("");
|
|
3138
|
+
return `${lines.join("\n")}
|
|
3139
|
+
`;
|
|
3140
|
+
}
|
|
3141
|
+
if (topic === "post-execution") {
|
|
3142
|
+
for (const item of catalog.post_execution_rules) {
|
|
3143
|
+
lines.push(bullet(item));
|
|
3144
|
+
}
|
|
3145
|
+
lines.push("");
|
|
3146
|
+
return `${lines.join("\n")}
|
|
3147
|
+
`;
|
|
3148
|
+
}
|
|
3149
|
+
if (topic === "selection") {
|
|
3150
|
+
lines.push(format === "markdown" ? "## Selection Rules" : "Selection Rules");
|
|
3151
|
+
for (const item of catalog.selection_rules) {
|
|
3152
|
+
lines.push(bullet(item));
|
|
3153
|
+
}
|
|
3154
|
+
lines.push("");
|
|
3155
|
+
lines.push(format === "markdown" ? "## Workspace Rules" : "Workspace Rules");
|
|
3156
|
+
for (const item of catalog.workspace_rules) {
|
|
3157
|
+
lines.push(bullet(item));
|
|
3158
|
+
}
|
|
3159
|
+
lines.push("");
|
|
3160
|
+
return `${lines.join("\n")}
|
|
3161
|
+
`;
|
|
3162
|
+
}
|
|
3163
|
+
for (const item of renderTopicPayload("naming").naming_guidance) {
|
|
3164
|
+
lines.push(bullet(item));
|
|
3165
|
+
}
|
|
3166
|
+
lines.push("");
|
|
3167
|
+
return `${lines.join("\n")}
|
|
3168
|
+
`;
|
|
3169
|
+
}
|
|
3170
|
+
function normalizeRepoPath(repoPath) {
|
|
3171
|
+
return repoPath ? path7.resolve(repoPath) : process.cwd();
|
|
3172
|
+
}
|
|
3173
|
+
function formatSelectionCommand(commandName, delivery, track) {
|
|
3174
|
+
if (delivery) {
|
|
3175
|
+
return `${commandName} graph next --delivery ${delivery}`;
|
|
3176
|
+
}
|
|
3177
|
+
if (track) {
|
|
3178
|
+
return `${commandName} next task --track ${track}`;
|
|
3179
|
+
}
|
|
3180
|
+
return `${commandName} next task`;
|
|
3181
|
+
}
|
|
3182
|
+
function renderInitialPrompt(options = {}) {
|
|
3183
|
+
const rigour = options.rigour ?? "balanced";
|
|
3184
|
+
const repoPath = normalizeRepoPath(options.repoPath);
|
|
3185
|
+
const commandName = options.commandName?.trim() || "coop";
|
|
3186
|
+
const artifactsDir = options.artifactsDir?.trim() || "docs";
|
|
3187
|
+
const selectionCommand = formatSelectionCommand(commandName, options.delivery, options.track);
|
|
3188
|
+
const planningCommand = options.delivery ? `${commandName} plan delivery ${options.delivery}` : `${commandName} status`;
|
|
3189
|
+
const scopeLine = options.delivery ? `- Active delivery: ${options.delivery}` : options.track ? `- Active track: ${options.track}` : "- Work scope: use the workspace default track or delivery context from COOP";
|
|
3190
|
+
const lines = [
|
|
3191
|
+
`You are working in:`,
|
|
3192
|
+
repoPath,
|
|
3193
|
+
"",
|
|
3194
|
+
"COOP is the source of truth for backlog, planning, and execution in this repository.",
|
|
3195
|
+
"",
|
|
3196
|
+
"Before doing anything else:",
|
|
3197
|
+
`1. Run \`${commandName} help-ai --format markdown\``,
|
|
3198
|
+
`2. Run \`${commandName} project show\``,
|
|
3199
|
+
`3. Run \`${selectionCommand}\``,
|
|
3200
|
+
`4. Run \`${planningCommand}\``,
|
|
3201
|
+
"",
|
|
3202
|
+
"Context:",
|
|
3203
|
+
scopeLine,
|
|
3204
|
+
`- Artifact directory: ${artifactsDir}`,
|
|
3205
|
+
"",
|
|
3206
|
+
"Rules:",
|
|
3207
|
+
"- Learn COOP capabilities from `coop help-ai` before proposing commands.",
|
|
3208
|
+
`- If you are unsure how to pick work, run \`${commandName} help-ai --selection --format markdown\`.`,
|
|
3209
|
+
`- If you are unsure about lifecycle changes, run \`${commandName} help-ai --state-transitions --format markdown\`.`,
|
|
3210
|
+
`- If you are unsure where artifacts should go, run \`${commandName} help-ai --artifacts --format markdown\`.`,
|
|
3211
|
+
`- If you are unsure whether to continue after a task, run \`${commandName} help-ai --post-execution --format markdown\`.`,
|
|
3212
|
+
"- Use only commands that actually exist in COOP. Do not invent command names.",
|
|
3213
|
+
"- Do not reprioritize work outside COOP unless the user explicitly overrides it.",
|
|
3214
|
+
"- Select only the first ready task from the COOP readiness output.",
|
|
3215
|
+
"- Inspect the selected task with `coop show task <id>` before implementation.",
|
|
3216
|
+
"- Do not create, edit, move, or normalize files directly inside `.coop/`; use COOP commands, MCP, or API surfaces so COOP remains the canonical writer.",
|
|
3217
|
+
"- Respect lifecycle prerequisites: use `coop review task <id>` before `coop complete task <id>`; do not complete directly from `in_progress`.",
|
|
3218
|
+
`- Write any contract-review, audit, or planning artifact under \`${artifactsDir}\` unless the user explicitly chooses another location.`
|
|
3219
|
+
];
|
|
3220
|
+
if (rigour === "strict") {
|
|
3221
|
+
lines.push(
|
|
3222
|
+
"- Before mentioning a COOP command, verify the exact command name from `coop help-ai --format json`.",
|
|
3223
|
+
"- If a needed workflow is not supported by an exact COOP command, say that explicitly instead of inventing one.",
|
|
3224
|
+
"- Do not switch to another task unless COOP state changes or the user explicitly redirects you.",
|
|
3225
|
+
"- After executing or completing the selected task, stop and wait for user confirmation before picking another task."
|
|
3226
|
+
);
|
|
3227
|
+
} else if (rigour === "balanced") {
|
|
3228
|
+
lines.push(
|
|
3229
|
+
"- Prefer exact COOP commands from `coop help-ai --format json` when describing next actions.",
|
|
3230
|
+
"- If COOP lacks a named command for a desired action, state the limitation plainly.",
|
|
3231
|
+
"- After finishing the selected task, report the resulting COOP state before proposing follow-up work."
|
|
3232
|
+
);
|
|
3233
|
+
} else {
|
|
3234
|
+
lines.push("- Keep COOP as the authority for task selection and lifecycle state.");
|
|
3235
|
+
}
|
|
3236
|
+
lines.push(
|
|
3237
|
+
"",
|
|
3238
|
+
"Output format:",
|
|
3239
|
+
"- Active project",
|
|
3240
|
+
"- Selected task",
|
|
3241
|
+
"- Why selected",
|
|
3242
|
+
"- Task acceptance/tests/refs summary",
|
|
3243
|
+
"- Immediate implementation steps",
|
|
3244
|
+
"",
|
|
3245
|
+
"Do not start code changes until you have reported the selected task and plan."
|
|
3246
|
+
);
|
|
3247
|
+
return `${lines.join("\n")}
|
|
3248
|
+
`;
|
|
3249
|
+
}
|
|
2889
3250
|
function renderAiHelp(format) {
|
|
2890
3251
|
if (format === "json") {
|
|
2891
3252
|
return `${JSON.stringify(catalog, null, 2)}
|
|
@@ -2906,6 +3267,29 @@ function renderAiHelp(format) {
|
|
|
2906
3267
|
lines.push(bullet(rule));
|
|
2907
3268
|
}
|
|
2908
3269
|
lines.push("");
|
|
3270
|
+
lines.push(format === "markdown" ? "## Allowed Lifecycle Commands" : "Allowed Lifecycle Commands");
|
|
3271
|
+
for (const item of catalog.allowed_lifecycle_commands) {
|
|
3272
|
+
lines.push(bullet(`\`${item}\``));
|
|
3273
|
+
}
|
|
3274
|
+
lines.push("");
|
|
3275
|
+
lines.push(format === "markdown" ? "## Lifecycle Requirements" : "Lifecycle Requirements");
|
|
3276
|
+
for (const item of catalog.lifecycle_requirements) {
|
|
3277
|
+
lines.push(bullet(item));
|
|
3278
|
+
}
|
|
3279
|
+
lines.push("");
|
|
3280
|
+
lines.push(format === "markdown" ? "## Unsupported / Invented Command Warnings" : "Unsupported / Invented Command Warnings");
|
|
3281
|
+
for (const item of catalog.unsupported_command_warnings) {
|
|
3282
|
+
lines.push(bullet(item));
|
|
3283
|
+
}
|
|
3284
|
+
lines.push("");
|
|
3285
|
+
lines.push(format === "markdown" ? "## Artifact Policy" : "Artifact Policy");
|
|
3286
|
+
lines.push(`${format === "markdown" ? "- " : "- "}Config key: \`${catalog.artifact_policy.config_key}\``);
|
|
3287
|
+
lines.push(`${format === "markdown" ? "- " : "- "}Default dir: \`${catalog.artifact_policy.default_dir}\``);
|
|
3288
|
+
lines.push(`${format === "markdown" ? "- " : "- "}Required for: ${catalog.artifact_policy.required_for.join(", ")}`);
|
|
3289
|
+
for (const item of catalog.artifact_policy.guidance) {
|
|
3290
|
+
lines.push(bullet(item));
|
|
3291
|
+
}
|
|
3292
|
+
lines.push("");
|
|
2909
3293
|
for (const group of catalog.command_groups) {
|
|
2910
3294
|
lines.push(format === "markdown" ? `## ${group.name}` : group.name);
|
|
2911
3295
|
lines.push(group.description);
|
|
@@ -2925,23 +3309,82 @@ function renderAiHelp(format) {
|
|
|
2925
3309
|
lines.push(bullet(item));
|
|
2926
3310
|
}
|
|
2927
3311
|
lines.push("");
|
|
3312
|
+
lines.push(format === "markdown" ? "## Post-Execution Rules" : "Post-Execution Rules");
|
|
3313
|
+
for (const item of catalog.post_execution_rules) {
|
|
3314
|
+
lines.push(bullet(item));
|
|
3315
|
+
}
|
|
3316
|
+
lines.push("");
|
|
2928
3317
|
return `${lines.join("\n")}
|
|
2929
3318
|
`;
|
|
2930
3319
|
}
|
|
3320
|
+
function renderAiInitialPrompt(options = {}) {
|
|
3321
|
+
return renderInitialPrompt(options);
|
|
3322
|
+
}
|
|
2931
3323
|
|
|
2932
3324
|
// src/commands/help-ai.ts
|
|
2933
3325
|
function registerHelpAiCommand(program) {
|
|
2934
|
-
program.command("help-ai").description("Print AI-oriented COOP capability and usage help").option("--format <format>", "Output format: text | json | markdown", "text").action((options) => {
|
|
3326
|
+
program.command("help-ai").description("Print AI-oriented COOP capability and usage help").option("--format <format>", "Output format: text | json | markdown", "text").option("--initial-prompt", "Print an agent bootstrap prompt instead of the capability catalog").option("--topic <topic>", "Focused help topic: state-transitions | artifacts | post-execution | selection | naming").option("--state-transitions", "Focused help for lifecycle and state transition usage").option("--artifacts", "Focused help for artifact placement and policy").option("--post-execution", "Focused help for what to do after finishing the selected task").option("--selection", "Focused help for task selection and workspace resolution").option("--naming", "Focused help for naming IDs and naming templates").option("--repo <path>", "Repository path to embed in the initial prompt").option("--delivery <delivery>", "Delivery to embed in the initial prompt").option("--track <track>", "Track to embed in the initial prompt").option("--command <name>", "Command name to embed, e.g. coop or coop.cmd", "coop").option("--rigour <level>", "Prompt rigour: strict | balanced | light", "balanced").option("--strict", "Shortcut for --rigour strict").option("--balanced", "Shortcut for --rigour balanced").option("--light", "Shortcut for --rigour light").action((options) => {
|
|
2935
3327
|
const format = options.format ?? "text";
|
|
2936
3328
|
if (format !== "text" && format !== "json" && format !== "markdown") {
|
|
2937
3329
|
throw new Error(`Unsupported help-ai format '${format}'. Expected text|json|markdown.`);
|
|
2938
3330
|
}
|
|
2939
|
-
|
|
3331
|
+
const rigour = options.strict ? "strict" : options.light ? "light" : options.balanced ? "balanced" : options.rigour ?? "balanced";
|
|
3332
|
+
if (rigour !== "strict" && rigour !== "balanced" && rigour !== "light") {
|
|
3333
|
+
throw new Error(`Unsupported help-ai rigour '${rigour}'. Expected strict|balanced|light.`);
|
|
3334
|
+
}
|
|
3335
|
+
const topic = resolveHelpTopic(options);
|
|
3336
|
+
let artifactsDir = "docs";
|
|
3337
|
+
const repoRoot = options.repo ? resolveRepoRoot(options.repo) : resolveRepoRoot();
|
|
3338
|
+
try {
|
|
3339
|
+
ensureCoopInitialized(repoRoot);
|
|
3340
|
+
artifactsDir = readCoopConfig(repoRoot).artifactsDir;
|
|
3341
|
+
} catch {
|
|
3342
|
+
artifactsDir = "docs";
|
|
3343
|
+
}
|
|
3344
|
+
const output3 = options.initialPrompt ? renderAiInitialPrompt({
|
|
3345
|
+
repoPath: options.repo,
|
|
3346
|
+
delivery: options.delivery,
|
|
3347
|
+
track: options.track,
|
|
3348
|
+
commandName: options.command,
|
|
3349
|
+
rigour,
|
|
3350
|
+
artifactsDir
|
|
3351
|
+
}) : topic ? renderAiHelpTopic(format, topic) : renderAiHelp(format);
|
|
3352
|
+
console.log(output3.trimEnd());
|
|
2940
3353
|
});
|
|
2941
3354
|
}
|
|
3355
|
+
function resolveHelpTopic(options) {
|
|
3356
|
+
const requestedTopics = [];
|
|
3357
|
+
if (options.topic) {
|
|
3358
|
+
requestedTopics.push(options.topic);
|
|
3359
|
+
}
|
|
3360
|
+
if (options.stateTransitions) {
|
|
3361
|
+
requestedTopics.push("state-transitions");
|
|
3362
|
+
}
|
|
3363
|
+
if (options.artifacts) {
|
|
3364
|
+
requestedTopics.push("artifacts");
|
|
3365
|
+
}
|
|
3366
|
+
if (options.postExecution) {
|
|
3367
|
+
requestedTopics.push("post-execution");
|
|
3368
|
+
}
|
|
3369
|
+
if (options.selection) {
|
|
3370
|
+
requestedTopics.push("selection");
|
|
3371
|
+
}
|
|
3372
|
+
if (options.naming) {
|
|
3373
|
+
requestedTopics.push("naming");
|
|
3374
|
+
}
|
|
3375
|
+
const unique2 = [...new Set(requestedTopics)];
|
|
3376
|
+
if (unique2.length > 1) {
|
|
3377
|
+
throw new Error(`Specify only one focused help-ai topic at a time. Received: ${unique2.join(", ")}.`);
|
|
3378
|
+
}
|
|
3379
|
+
const topic = unique2[0];
|
|
3380
|
+
if (topic !== void 0 && topic !== "state-transitions" && topic !== "artifacts" && topic !== "post-execution" && topic !== "selection" && topic !== "naming") {
|
|
3381
|
+
throw new Error(`Unsupported help-ai topic '${topic}'. Expected state-transitions|artifacts|post-execution|selection|naming.`);
|
|
3382
|
+
}
|
|
3383
|
+
return topic;
|
|
3384
|
+
}
|
|
2942
3385
|
|
|
2943
3386
|
// src/commands/index.ts
|
|
2944
|
-
import
|
|
3387
|
+
import path8 from "path";
|
|
2945
3388
|
import { IndexManager as IndexManager2 } from "@kitsy/coop-core";
|
|
2946
3389
|
function runStatus(options) {
|
|
2947
3390
|
const root = resolveRepoRoot();
|
|
@@ -2951,7 +3394,7 @@ function runStatus(options) {
|
|
|
2951
3394
|
const freshness = status.stale ? "stale" : "fresh";
|
|
2952
3395
|
const existsText = status.exists ? "present" : "missing";
|
|
2953
3396
|
console.log(`[COOP] index ${existsText}, ${freshness}`);
|
|
2954
|
-
console.log(`[COOP] graph: ${
|
|
3397
|
+
console.log(`[COOP] graph: ${path8.relative(root, status.graph_path)}`);
|
|
2955
3398
|
if (status.generated_at) {
|
|
2956
3399
|
console.log(`[COOP] generated_at: ${status.generated_at}`);
|
|
2957
3400
|
}
|
|
@@ -2974,7 +3417,7 @@ function runRebuild() {
|
|
|
2974
3417
|
const graph = manager.build_full_index();
|
|
2975
3418
|
const elapsed = Date.now() - start;
|
|
2976
3419
|
console.log(`[COOP] index rebuilt: ${graph.nodes.size} tasks (${elapsed} ms)`);
|
|
2977
|
-
console.log(`[COOP] graph: ${
|
|
3420
|
+
console.log(`[COOP] graph: ${path8.relative(root, manager.graphPath)}`);
|
|
2978
3421
|
}
|
|
2979
3422
|
function registerIndexCommand(program) {
|
|
2980
3423
|
const index = program.command("index").description("Index management commands");
|
|
@@ -2991,7 +3434,7 @@ function registerIndexCommand(program) {
|
|
|
2991
3434
|
|
|
2992
3435
|
// src/commands/init.ts
|
|
2993
3436
|
import fs8 from "fs";
|
|
2994
|
-
import
|
|
3437
|
+
import path11 from "path";
|
|
2995
3438
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
2996
3439
|
import { createInterface } from "readline/promises";
|
|
2997
3440
|
import { stdin as input, stdout as output } from "process";
|
|
@@ -2999,7 +3442,7 @@ import { CURRENT_SCHEMA_VERSION, write_schema_version } from "@kitsy/coop-core";
|
|
|
2999
3442
|
|
|
3000
3443
|
// src/hooks/pre-commit.ts
|
|
3001
3444
|
import fs6 from "fs";
|
|
3002
|
-
import
|
|
3445
|
+
import path9 from "path";
|
|
3003
3446
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
3004
3447
|
import { detect_cycle, parseTaskContent, parseTaskFile as parseTaskFile7, validateStructural as validateStructural5 } from "@kitsy/coop-core";
|
|
3005
3448
|
var HOOK_BLOCK_START = "# COOP_PRE_COMMIT_START";
|
|
@@ -3030,15 +3473,15 @@ function projectRootFromRelativePath(repoRoot, relativePath) {
|
|
|
3030
3473
|
const normalized = toPosixPath2(relativePath);
|
|
3031
3474
|
const projectMatch = /^\.coop\/projects\/([^/]+)\/tasks\/.+\.md$/i.exec(normalized);
|
|
3032
3475
|
if (projectMatch?.[1]) {
|
|
3033
|
-
return
|
|
3476
|
+
return path9.join(repoRoot, ".coop", "projects", projectMatch[1]);
|
|
3034
3477
|
}
|
|
3035
3478
|
if (normalized.startsWith(".coop/tasks/")) {
|
|
3036
|
-
return
|
|
3479
|
+
return path9.join(repoRoot, ".coop");
|
|
3037
3480
|
}
|
|
3038
3481
|
throw new Error(`Unsupported staged COOP task path '${relativePath}'.`);
|
|
3039
3482
|
}
|
|
3040
3483
|
function listTaskFilesForProject(projectRoot) {
|
|
3041
|
-
const tasksDir =
|
|
3484
|
+
const tasksDir = path9.join(projectRoot, "tasks");
|
|
3042
3485
|
if (!fs6.existsSync(tasksDir)) return [];
|
|
3043
3486
|
const out = [];
|
|
3044
3487
|
const stack = [tasksDir];
|
|
@@ -3046,12 +3489,12 @@ function listTaskFilesForProject(projectRoot) {
|
|
|
3046
3489
|
const current = stack.pop();
|
|
3047
3490
|
const entries = fs6.readdirSync(current, { withFileTypes: true });
|
|
3048
3491
|
for (const entry of entries) {
|
|
3049
|
-
const fullPath =
|
|
3492
|
+
const fullPath = path9.join(current, entry.name);
|
|
3050
3493
|
if (entry.isDirectory()) {
|
|
3051
3494
|
stack.push(fullPath);
|
|
3052
3495
|
continue;
|
|
3053
3496
|
}
|
|
3054
|
-
if (entry.isFile() &&
|
|
3497
|
+
if (entry.isFile() && path9.extname(entry.name).toLowerCase() === ".md") {
|
|
3055
3498
|
out.push(fullPath);
|
|
3056
3499
|
}
|
|
3057
3500
|
}
|
|
@@ -3070,7 +3513,7 @@ function parseStagedTasks(repoRoot, relativePaths) {
|
|
|
3070
3513
|
const errors = [];
|
|
3071
3514
|
const staged = [];
|
|
3072
3515
|
for (const relativePath of relativePaths) {
|
|
3073
|
-
const absolutePath =
|
|
3516
|
+
const absolutePath = path9.join(repoRoot, ...relativePath.split("/"));
|
|
3074
3517
|
const projectRoot = projectRootFromRelativePath(repoRoot, relativePath);
|
|
3075
3518
|
const stagedBlob = readGitBlob(repoRoot, `:${relativePath}`);
|
|
3076
3519
|
if (!stagedBlob) {
|
|
@@ -3134,7 +3577,7 @@ function collectTasksForCycleCheck(projectRoot, stagedTasks) {
|
|
|
3134
3577
|
}
|
|
3135
3578
|
const tasks = [];
|
|
3136
3579
|
for (const filePath of listTaskFilesForProject(projectRoot)) {
|
|
3137
|
-
const normalized = toPosixPath2(
|
|
3580
|
+
const normalized = toPosixPath2(path9.resolve(filePath));
|
|
3138
3581
|
const stagedTask = stagedByPath.get(normalized);
|
|
3139
3582
|
if (stagedTask) {
|
|
3140
3583
|
tasks.push(stagedTask);
|
|
@@ -3164,7 +3607,7 @@ function runPreCommitChecks(repoRoot) {
|
|
|
3164
3607
|
const graph = buildGraphForCycleCheck(tasks);
|
|
3165
3608
|
const cycle = detect_cycle(graph);
|
|
3166
3609
|
if (cycle) {
|
|
3167
|
-
const projectLabel = toPosixPath2(
|
|
3610
|
+
const projectLabel = toPosixPath2(path9.relative(repoRoot, projectRoot));
|
|
3168
3611
|
errors.push(`[COOP] Dependency cycle detected in ${projectLabel}: ${cycle.join(" -> ")}.`);
|
|
3169
3612
|
}
|
|
3170
3613
|
} catch (error) {
|
|
@@ -3196,8 +3639,8 @@ function hookScriptBlock() {
|
|
|
3196
3639
|
].join("\n");
|
|
3197
3640
|
}
|
|
3198
3641
|
function installPreCommitHook(repoRoot) {
|
|
3199
|
-
const hookPath =
|
|
3200
|
-
const hookDir =
|
|
3642
|
+
const hookPath = path9.join(repoRoot, ".git", "hooks", "pre-commit");
|
|
3643
|
+
const hookDir = path9.dirname(hookPath);
|
|
3201
3644
|
if (!fs6.existsSync(hookDir)) {
|
|
3202
3645
|
return {
|
|
3203
3646
|
installed: false,
|
|
@@ -3229,13 +3672,13 @@ function installPreCommitHook(repoRoot) {
|
|
|
3229
3672
|
|
|
3230
3673
|
// src/hooks/post-merge-validate.ts
|
|
3231
3674
|
import fs7 from "fs";
|
|
3232
|
-
import
|
|
3675
|
+
import path10 from "path";
|
|
3233
3676
|
import { list_projects } from "@kitsy/coop-core";
|
|
3234
3677
|
import { load_graph as load_graph4, validate_graph as validate_graph2 } from "@kitsy/coop-core";
|
|
3235
3678
|
var HOOK_BLOCK_START2 = "# COOP_POST_MERGE_START";
|
|
3236
3679
|
var HOOK_BLOCK_END2 = "# COOP_POST_MERGE_END";
|
|
3237
3680
|
function runPostMergeValidate(repoRoot) {
|
|
3238
|
-
const workspaceDir =
|
|
3681
|
+
const workspaceDir = path10.join(repoRoot, ".coop");
|
|
3239
3682
|
if (!fs7.existsSync(workspaceDir)) {
|
|
3240
3683
|
return {
|
|
3241
3684
|
ok: true,
|
|
@@ -3288,8 +3731,8 @@ function postMergeHookBlock() {
|
|
|
3288
3731
|
].join("\n");
|
|
3289
3732
|
}
|
|
3290
3733
|
function installPostMergeHook(repoRoot) {
|
|
3291
|
-
const hookPath =
|
|
3292
|
-
const hookDir =
|
|
3734
|
+
const hookPath = path10.join(repoRoot, ".git", "hooks", "post-merge");
|
|
3735
|
+
const hookDir = path10.dirname(hookPath);
|
|
3293
3736
|
if (!fs7.existsSync(hookDir)) {
|
|
3294
3737
|
return {
|
|
3295
3738
|
installed: false,
|
|
@@ -3339,7 +3782,7 @@ function aliasesYaml(aliases) {
|
|
|
3339
3782
|
return `
|
|
3340
3783
|
${aliases.map((alias) => ` - "${alias}"`).join("\n")}`;
|
|
3341
3784
|
}
|
|
3342
|
-
function buildProjectConfig(projectId, projectName, projectAliases) {
|
|
3785
|
+
function buildProjectConfig(projectId, projectName, projectAliases, namingTemplate) {
|
|
3343
3786
|
return `version: 2
|
|
3344
3787
|
project:
|
|
3345
3788
|
name: ${JSON.stringify(projectName)}
|
|
@@ -3353,7 +3796,7 @@ id_prefixes:
|
|
|
3353
3796
|
run: "RUN"
|
|
3354
3797
|
|
|
3355
3798
|
id:
|
|
3356
|
-
naming:
|
|
3799
|
+
naming: ${JSON.stringify(namingTemplate)}
|
|
3357
3800
|
seq_padding: 0
|
|
3358
3801
|
|
|
3359
3802
|
defaults:
|
|
@@ -3482,7 +3925,7 @@ function writeIfMissing(filePath, content) {
|
|
|
3482
3925
|
}
|
|
3483
3926
|
}
|
|
3484
3927
|
function ensureGitignoreEntry(root, entry) {
|
|
3485
|
-
const gitignorePath =
|
|
3928
|
+
const gitignorePath = path11.join(root, ".gitignore");
|
|
3486
3929
|
if (!fs8.existsSync(gitignorePath)) {
|
|
3487
3930
|
fs8.writeFileSync(gitignorePath, `${entry}
|
|
3488
3931
|
`, "utf8");
|
|
@@ -3497,7 +3940,7 @@ function ensureGitignoreEntry(root, entry) {
|
|
|
3497
3940
|
}
|
|
3498
3941
|
}
|
|
3499
3942
|
function ensureGitattributesEntry(root, entry) {
|
|
3500
|
-
const attrsPath =
|
|
3943
|
+
const attrsPath = path11.join(root, ".gitattributes");
|
|
3501
3944
|
if (!fs8.existsSync(attrsPath)) {
|
|
3502
3945
|
fs8.writeFileSync(attrsPath, `${entry}
|
|
3503
3946
|
`, "utf8");
|
|
@@ -3559,10 +4002,12 @@ async function promptInitIdentity(root, options) {
|
|
|
3559
4002
|
throw new Error("Invalid project id. Use letters, numbers, and hyphens.");
|
|
3560
4003
|
}
|
|
3561
4004
|
const aliasesInput = options.aliases !== void 0 ? options.aliases : await ask2("Aliases (csv, optional)", "");
|
|
4005
|
+
const namingTemplate = options.naming?.trim() || await ask2("ID naming template", DEFAULT_ID_NAMING_TEMPLATE);
|
|
3562
4006
|
return {
|
|
3563
4007
|
projectName,
|
|
3564
4008
|
projectId,
|
|
3565
|
-
projectAliases: parseAliases2(aliasesInput)
|
|
4009
|
+
projectAliases: parseAliases2(aliasesInput),
|
|
4010
|
+
namingTemplate
|
|
3566
4011
|
};
|
|
3567
4012
|
} finally {
|
|
3568
4013
|
rl.close();
|
|
@@ -3573,6 +4018,7 @@ async function resolveInitIdentity(root, options) {
|
|
|
3573
4018
|
const fallbackName = options.name?.trim() || defaultName;
|
|
3574
4019
|
const fallbackId = normalizeProjectId(options.id?.trim() || fallbackName) || repoIdentityId(root);
|
|
3575
4020
|
const fallbackAliases = parseAliases2(options.aliases);
|
|
4021
|
+
const fallbackNaming = options.naming?.trim() || DEFAULT_ID_NAMING_TEMPLATE;
|
|
3576
4022
|
const interactive = Boolean(options.interactive || !options.yes && input.isTTY && output.isTTY);
|
|
3577
4023
|
if (interactive) {
|
|
3578
4024
|
return promptInitIdentity(root, options);
|
|
@@ -3580,16 +4026,17 @@ async function resolveInitIdentity(root, options) {
|
|
|
3580
4026
|
return {
|
|
3581
4027
|
projectName: fallbackName,
|
|
3582
4028
|
projectId: fallbackId,
|
|
3583
|
-
projectAliases: fallbackAliases
|
|
4029
|
+
projectAliases: fallbackAliases,
|
|
4030
|
+
namingTemplate: fallbackNaming
|
|
3584
4031
|
};
|
|
3585
4032
|
}
|
|
3586
4033
|
function registerInitCommand(program) {
|
|
3587
|
-
program.command("init").description("Initialize .coop/ structure in the current repository").option("--name <name>", "Project display name").option("--id <id>", "Project id").option("--aliases <csv>", "Comma-separated project aliases").option("-y, --yes", "Use defaults without prompting").option("--interactive", "Prompt for missing project identity values").action(async (options) => {
|
|
4034
|
+
program.command("init").description("Initialize .coop/ structure in the current repository").option("--name <name>", "Project display name").option("--id <id>", "Project id").option("--aliases <csv>", "Comma-separated project aliases").option("--naming <template>", "ID naming template (default: <TYPE>-<TITLE16>-<SEQ>)").option("-y, --yes", "Use defaults without prompting").option("--interactive", "Prompt for missing project identity values").action(async (options) => {
|
|
3588
4035
|
const root = resolveRepoRoot();
|
|
3589
4036
|
const workspaceDir = coopWorkspaceDir(root);
|
|
3590
4037
|
const identity = await resolveInitIdentity(root, options);
|
|
3591
4038
|
const projectId = identity.projectId;
|
|
3592
|
-
const projectRoot =
|
|
4039
|
+
const projectRoot = path11.join(workspaceDir, "projects", projectId);
|
|
3593
4040
|
const dirs = [
|
|
3594
4041
|
"ideas",
|
|
3595
4042
|
"tasks",
|
|
@@ -3605,23 +4052,23 @@ function registerInitCommand(program) {
|
|
|
3605
4052
|
"history/deliveries",
|
|
3606
4053
|
".index"
|
|
3607
4054
|
];
|
|
3608
|
-
ensureDir(
|
|
4055
|
+
ensureDir(path11.join(workspaceDir, "projects"));
|
|
3609
4056
|
for (const dir of dirs) {
|
|
3610
|
-
ensureDir(
|
|
4057
|
+
ensureDir(path11.join(projectRoot, dir));
|
|
3611
4058
|
}
|
|
3612
4059
|
writeIfMissing(
|
|
3613
|
-
|
|
3614
|
-
buildProjectConfig(projectId, identity.projectName, identity.projectAliases)
|
|
4060
|
+
path11.join(projectRoot, "config.yml"),
|
|
4061
|
+
buildProjectConfig(projectId, identity.projectName, identity.projectAliases, identity.namingTemplate)
|
|
3615
4062
|
);
|
|
3616
|
-
if (!fs8.existsSync(
|
|
4063
|
+
if (!fs8.existsSync(path11.join(projectRoot, "schema-version"))) {
|
|
3617
4064
|
write_schema_version(projectRoot, CURRENT_SCHEMA_VERSION);
|
|
3618
4065
|
}
|
|
3619
|
-
writeIfMissing(
|
|
3620
|
-
writeIfMissing(
|
|
3621
|
-
writeIfMissing(
|
|
3622
|
-
writeIfMissing(
|
|
3623
|
-
writeIfMissing(
|
|
3624
|
-
writeIfMissing(
|
|
4066
|
+
writeIfMissing(path11.join(projectRoot, "templates/task.md"), TASK_TEMPLATE);
|
|
4067
|
+
writeIfMissing(path11.join(projectRoot, "templates/idea.md"), IDEA_TEMPLATE);
|
|
4068
|
+
writeIfMissing(path11.join(projectRoot, "plugins/console-log.yml"), PLUGIN_CONSOLE_TEMPLATE);
|
|
4069
|
+
writeIfMissing(path11.join(projectRoot, "plugins/github-pr.yml"), PLUGIN_GITHUB_TEMPLATE);
|
|
4070
|
+
writeIfMissing(path11.join(workspaceDir, ".ignore"), COOP_IGNORE_TEMPLATE);
|
|
4071
|
+
writeIfMissing(path11.join(workspaceDir, ".gitignore"), COOP_IGNORE_TEMPLATE);
|
|
3625
4072
|
writeWorkspaceConfig(root, { version: 2, current_project: projectId });
|
|
3626
4073
|
ensureGitignoreEntry(root, ".coop/logs/");
|
|
3627
4074
|
ensureGitignoreEntry(root, ".coop/tmp/");
|
|
@@ -3631,17 +4078,18 @@ function registerInitCommand(program) {
|
|
|
3631
4078
|
const project = resolveProject(root, projectId);
|
|
3632
4079
|
console.log("Initialized COOP workspace.");
|
|
3633
4080
|
console.log(`- Root: ${root}`);
|
|
3634
|
-
console.log(`- Workspace: ${
|
|
3635
|
-
console.log(`- Project: ${project.id} (${
|
|
4081
|
+
console.log(`- Workspace: ${path11.relative(root, workspaceDir)}`);
|
|
4082
|
+
console.log(`- Project: ${project.id} (${path11.relative(root, project.root)})`);
|
|
3636
4083
|
console.log(`- Name: ${identity.projectName}`);
|
|
3637
4084
|
console.log(`- Aliases: ${identity.projectAliases.length > 0 ? identity.projectAliases.join(", ") : "(none)"}`);
|
|
4085
|
+
console.log(`- ID naming: ${identity.namingTemplate}`);
|
|
3638
4086
|
console.log(`- ${preCommitHook.message}`);
|
|
3639
4087
|
if (preCommitHook.installed) {
|
|
3640
|
-
console.log(`- Hook: ${
|
|
4088
|
+
console.log(`- Hook: ${path11.relative(root, preCommitHook.hookPath)}`);
|
|
3641
4089
|
}
|
|
3642
4090
|
console.log(`- ${postMergeHook.message}`);
|
|
3643
4091
|
if (postMergeHook.installed) {
|
|
3644
|
-
console.log(`- Hook: ${
|
|
4092
|
+
console.log(`- Hook: ${path11.relative(root, postMergeHook.hookPath)}`);
|
|
3645
4093
|
}
|
|
3646
4094
|
console.log(`- ${mergeDrivers}`);
|
|
3647
4095
|
console.log("- Next steps:");
|
|
@@ -3651,9 +4099,70 @@ function registerInitCommand(program) {
|
|
|
3651
4099
|
});
|
|
3652
4100
|
}
|
|
3653
4101
|
|
|
4102
|
+
// src/commands/lifecycle.ts
|
|
4103
|
+
import path12 from "path";
|
|
4104
|
+
import { parseTaskFile as parseTaskFile8 } from "@kitsy/coop-core";
|
|
4105
|
+
var lifecycleVerbs = [
|
|
4106
|
+
{
|
|
4107
|
+
name: "review",
|
|
4108
|
+
description: "Move a task into review",
|
|
4109
|
+
targetStatus: "in_review"
|
|
4110
|
+
},
|
|
4111
|
+
{
|
|
4112
|
+
name: "complete",
|
|
4113
|
+
description: "Complete a task",
|
|
4114
|
+
targetStatus: "done"
|
|
4115
|
+
},
|
|
4116
|
+
{
|
|
4117
|
+
name: "block",
|
|
4118
|
+
description: "Mark a task as blocked",
|
|
4119
|
+
targetStatus: "blocked"
|
|
4120
|
+
},
|
|
4121
|
+
{
|
|
4122
|
+
name: "unblock",
|
|
4123
|
+
description: "Move a blocked task back to todo",
|
|
4124
|
+
targetStatus: "todo"
|
|
4125
|
+
},
|
|
4126
|
+
{
|
|
4127
|
+
name: "reopen",
|
|
4128
|
+
description: "Move a task back to todo",
|
|
4129
|
+
targetStatus: "todo"
|
|
4130
|
+
},
|
|
4131
|
+
{
|
|
4132
|
+
name: "cancel",
|
|
4133
|
+
description: "Cancel a task",
|
|
4134
|
+
targetStatus: "canceled"
|
|
4135
|
+
}
|
|
4136
|
+
];
|
|
4137
|
+
function currentTaskSelection(root, id) {
|
|
4138
|
+
const reference = resolveReference(root, id, "task");
|
|
4139
|
+
const filePath = path12.join(root, ...reference.file.split("/"));
|
|
4140
|
+
const parsed = parseTaskFile8(filePath);
|
|
4141
|
+
return formatSelectedTask(
|
|
4142
|
+
{
|
|
4143
|
+
task: parsed.task,
|
|
4144
|
+
score: 0,
|
|
4145
|
+
readiness: parsed.task.status === "blocked" ? "blocked" : parsed.task.status === "in_review" ? "waiting_review" : parsed.task.status === "done" || parsed.task.status === "canceled" ? "done" : parsed.task.status === "in_progress" ? "in_progress" : "ready",
|
|
4146
|
+
fits_capacity: true,
|
|
4147
|
+
fits_wip: true
|
|
4148
|
+
},
|
|
4149
|
+
{}
|
|
4150
|
+
);
|
|
4151
|
+
}
|
|
4152
|
+
function registerLifecycleCommands(program) {
|
|
4153
|
+
for (const verb of lifecycleVerbs) {
|
|
4154
|
+
program.command(verb.name).description(verb.description).command("task").description(`${verb.description} by task id or alias`).argument("<id>", "Task ID or alias").option("--actor <actor>", "Actor performing the transition").option("--user <user>", "Current user for advisory authorization checks").option("--force", "Override advisory authorization checks").action(async (id, options) => {
|
|
4155
|
+
const root = resolveRepoRoot();
|
|
4156
|
+
console.log(currentTaskSelection(root, id));
|
|
4157
|
+
const result = await transitionTaskByReference(root, id, verb.targetStatus, options);
|
|
4158
|
+
console.log(`Updated ${result.task.id}: ${result.from} -> ${result.to}`);
|
|
4159
|
+
});
|
|
4160
|
+
}
|
|
4161
|
+
}
|
|
4162
|
+
|
|
3654
4163
|
// src/commands/list.ts
|
|
3655
|
-
import
|
|
3656
|
-
import { parseIdeaFile as parseIdeaFile3, parseTaskFile as
|
|
4164
|
+
import path13 from "path";
|
|
4165
|
+
import { parseIdeaFile as parseIdeaFile3, parseTaskFile as parseTaskFile9 } from "@kitsy/coop-core";
|
|
3657
4166
|
import chalk2 from "chalk";
|
|
3658
4167
|
|
|
3659
4168
|
// src/utils/not-implemented.ts
|
|
@@ -3683,7 +4192,7 @@ function sortByIdAsc(items) {
|
|
|
3683
4192
|
}
|
|
3684
4193
|
function loadTasks(root) {
|
|
3685
4194
|
return listTaskFiles(root).map((filePath) => ({
|
|
3686
|
-
task:
|
|
4195
|
+
task: parseTaskFile9(filePath).task,
|
|
3687
4196
|
filePath
|
|
3688
4197
|
}));
|
|
3689
4198
|
}
|
|
@@ -3723,7 +4232,7 @@ function listTasks(options) {
|
|
|
3723
4232
|
statusColor(entry.status),
|
|
3724
4233
|
entry.priority,
|
|
3725
4234
|
entry.track,
|
|
3726
|
-
|
|
4235
|
+
path13.relative(root, entry.filePath)
|
|
3727
4236
|
])
|
|
3728
4237
|
)
|
|
3729
4238
|
);
|
|
@@ -3758,7 +4267,7 @@ function listIdeas(options) {
|
|
|
3758
4267
|
statusColor(entry.status),
|
|
3759
4268
|
entry.priority,
|
|
3760
4269
|
entry.track,
|
|
3761
|
-
|
|
4270
|
+
path13.relative(root, entry.filePath)
|
|
3762
4271
|
])
|
|
3763
4272
|
)
|
|
3764
4273
|
);
|
|
@@ -3783,24 +4292,24 @@ function registerListCommand(program) {
|
|
|
3783
4292
|
|
|
3784
4293
|
// src/utils/logger.ts
|
|
3785
4294
|
import fs9 from "fs";
|
|
3786
|
-
import
|
|
4295
|
+
import path14 from "path";
|
|
3787
4296
|
function resolveRepoSafe(start = process.cwd()) {
|
|
3788
4297
|
try {
|
|
3789
4298
|
return resolveRepoRoot(start);
|
|
3790
4299
|
} catch {
|
|
3791
|
-
return
|
|
4300
|
+
return path14.resolve(start);
|
|
3792
4301
|
}
|
|
3793
4302
|
}
|
|
3794
4303
|
function resolveCliLogFile(start = process.cwd()) {
|
|
3795
4304
|
const root = resolveRepoSafe(start);
|
|
3796
4305
|
const workspace = coopWorkspaceDir(root);
|
|
3797
4306
|
if (fs9.existsSync(workspace)) {
|
|
3798
|
-
return
|
|
4307
|
+
return path14.join(workspace, "logs", "cli.log");
|
|
3799
4308
|
}
|
|
3800
|
-
return
|
|
4309
|
+
return path14.join(resolveCoopHome(), "logs", "cli.log");
|
|
3801
4310
|
}
|
|
3802
4311
|
function appendLogEntry(entry, logFile) {
|
|
3803
|
-
fs9.mkdirSync(
|
|
4312
|
+
fs9.mkdirSync(path14.dirname(logFile), { recursive: true });
|
|
3804
4313
|
fs9.appendFileSync(logFile, `${JSON.stringify(entry)}
|
|
3805
4314
|
`, "utf8");
|
|
3806
4315
|
}
|
|
@@ -3877,7 +4386,7 @@ function registerLogCommand(program) {
|
|
|
3877
4386
|
|
|
3878
4387
|
// src/commands/migrate.ts
|
|
3879
4388
|
import fs10 from "fs";
|
|
3880
|
-
import
|
|
4389
|
+
import path15 from "path";
|
|
3881
4390
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
3882
4391
|
import { stdin as input2, stdout as output2 } from "process";
|
|
3883
4392
|
import { CURRENT_SCHEMA_VERSION as CURRENT_SCHEMA_VERSION2, IndexManager as IndexManager3, migrate_repository, parseYamlFile as parseYamlFile2, writeYamlFile as writeYamlFile4 } from "@kitsy/coop-core";
|
|
@@ -3901,7 +4410,7 @@ function writeIfMissing2(filePath, content) {
|
|
|
3901
4410
|
}
|
|
3902
4411
|
}
|
|
3903
4412
|
function ensureGitignoreEntry2(root, entry) {
|
|
3904
|
-
const gitignorePath =
|
|
4413
|
+
const gitignorePath = path15.join(root, ".gitignore");
|
|
3905
4414
|
if (!fs10.existsSync(gitignorePath)) {
|
|
3906
4415
|
fs10.writeFileSync(gitignorePath, `${entry}
|
|
3907
4416
|
`, "utf8");
|
|
@@ -3936,7 +4445,7 @@ function legacyWorkspaceProjectEntries(root) {
|
|
|
3936
4445
|
"backlog",
|
|
3937
4446
|
"plans",
|
|
3938
4447
|
"releases"
|
|
3939
|
-
].filter((entry) => fs10.existsSync(
|
|
4448
|
+
].filter((entry) => fs10.existsSync(path15.join(workspaceDir, entry)));
|
|
3940
4449
|
}
|
|
3941
4450
|
function normalizeProjectId2(value) {
|
|
3942
4451
|
return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-");
|
|
@@ -3981,7 +4490,7 @@ async function promptProjectIdentity(defaults, options) {
|
|
|
3981
4490
|
async function resolveMigrationIdentity(root, options) {
|
|
3982
4491
|
const existing = readCoopConfig(root);
|
|
3983
4492
|
const defaults = {
|
|
3984
|
-
projectName: existing.projectName ||
|
|
4493
|
+
projectName: existing.projectName || path15.basename(root),
|
|
3985
4494
|
projectId: normalizeProjectId2(existing.projectId || repoIdentityId(root)) || repoIdentityId(root),
|
|
3986
4495
|
projectAliases: options.aliases !== void 0 ? parseAliases3(options.aliases) : existing.projectAliases
|
|
3987
4496
|
};
|
|
@@ -4004,7 +4513,7 @@ async function migrateWorkspaceLayout(root, options) {
|
|
|
4004
4513
|
if (!fs10.existsSync(workspaceDir)) {
|
|
4005
4514
|
throw new Error("Missing .coop directory. Run 'coop init' first.");
|
|
4006
4515
|
}
|
|
4007
|
-
const projectsDir =
|
|
4516
|
+
const projectsDir = path15.join(workspaceDir, "projects");
|
|
4008
4517
|
const legacyEntries = legacyWorkspaceProjectEntries(root);
|
|
4009
4518
|
if (legacyEntries.length === 0 && fs10.existsSync(projectsDir)) {
|
|
4010
4519
|
console.log("[COOP] workspace layout already uses v2.");
|
|
@@ -4012,11 +4521,11 @@ async function migrateWorkspaceLayout(root, options) {
|
|
|
4012
4521
|
}
|
|
4013
4522
|
const identity = await resolveMigrationIdentity(root, options);
|
|
4014
4523
|
const projectId = identity.projectId;
|
|
4015
|
-
const projectRoot =
|
|
4524
|
+
const projectRoot = path15.join(projectsDir, projectId);
|
|
4016
4525
|
if (fs10.existsSync(projectRoot) && !options.force) {
|
|
4017
|
-
throw new Error(`Project destination '${
|
|
4526
|
+
throw new Error(`Project destination '${path15.relative(root, projectRoot)}' already exists. Re-run with --force.`);
|
|
4018
4527
|
}
|
|
4019
|
-
const changed = legacyEntries.map((entry) => `${
|
|
4528
|
+
const changed = legacyEntries.map((entry) => `${path15.join(".coop", entry)} -> ${path15.join(".coop", "projects", projectId, entry)}`);
|
|
4020
4529
|
changed.push(`.coop/config.yml -> workspace current_project=${projectId}`);
|
|
4021
4530
|
console.log(`Workspace layout migration (${options.dryRun ? "DRY RUN" : "APPLY"})`);
|
|
4022
4531
|
console.log(`- from: v1 flat layout`);
|
|
@@ -4035,17 +4544,17 @@ async function migrateWorkspaceLayout(root, options) {
|
|
|
4035
4544
|
fs10.mkdirSync(projectsDir, { recursive: true });
|
|
4036
4545
|
fs10.mkdirSync(projectRoot, { recursive: true });
|
|
4037
4546
|
for (const entry of legacyEntries) {
|
|
4038
|
-
const source =
|
|
4039
|
-
const destination =
|
|
4547
|
+
const source = path15.join(workspaceDir, entry);
|
|
4548
|
+
const destination = path15.join(projectRoot, entry);
|
|
4040
4549
|
if (fs10.existsSync(destination)) {
|
|
4041
4550
|
if (!options.force) {
|
|
4042
|
-
throw new Error(`Destination '${
|
|
4551
|
+
throw new Error(`Destination '${path15.relative(root, destination)}' already exists.`);
|
|
4043
4552
|
}
|
|
4044
4553
|
fs10.rmSync(destination, { recursive: true, force: true });
|
|
4045
4554
|
}
|
|
4046
4555
|
fs10.renameSync(source, destination);
|
|
4047
4556
|
}
|
|
4048
|
-
const movedConfigPath =
|
|
4557
|
+
const movedConfigPath = path15.join(projectRoot, "config.yml");
|
|
4049
4558
|
if (fs10.existsSync(movedConfigPath)) {
|
|
4050
4559
|
const movedConfig = parseYamlFile2(movedConfigPath);
|
|
4051
4560
|
const nextProject = typeof movedConfig.project === "object" && movedConfig.project !== null ? { ...movedConfig.project } : {};
|
|
@@ -4063,13 +4572,13 @@ async function migrateWorkspaceLayout(root, options) {
|
|
|
4063
4572
|
}
|
|
4064
4573
|
const workspace = readWorkspaceConfig(root);
|
|
4065
4574
|
writeWorkspaceConfig(root, { ...workspace, version: 2, current_project: projectId });
|
|
4066
|
-
writeIfMissing2(
|
|
4067
|
-
writeIfMissing2(
|
|
4575
|
+
writeIfMissing2(path15.join(workspaceDir, ".ignore"), COOP_IGNORE_TEMPLATE2);
|
|
4576
|
+
writeIfMissing2(path15.join(workspaceDir, ".gitignore"), COOP_IGNORE_TEMPLATE2);
|
|
4068
4577
|
ensureGitignoreEntry2(root, ".coop/logs/");
|
|
4069
4578
|
ensureGitignoreEntry2(root, ".coop/tmp/");
|
|
4070
4579
|
const manager = new IndexManager3(projectRoot);
|
|
4071
4580
|
manager.build_full_index();
|
|
4072
|
-
console.log(`[COOP] migrated workspace to v2 at ${
|
|
4581
|
+
console.log(`[COOP] migrated workspace to v2 at ${path15.relative(root, projectRoot)}`);
|
|
4073
4582
|
}
|
|
4074
4583
|
function registerMigrateCommand(program) {
|
|
4075
4584
|
const migrate = program.command("migrate").description("Migrate COOP data and workspace layouts").option("--dry-run", "Preview migration without writing files").option("--to <version>", "Target schema version", String(CURRENT_SCHEMA_VERSION2)).action((options) => {
|
|
@@ -4088,7 +4597,7 @@ function registerMigrateCommand(program) {
|
|
|
4088
4597
|
if (report.changed_files.length > 0) {
|
|
4089
4598
|
console.log("- changed files:");
|
|
4090
4599
|
for (const filePath of report.changed_files) {
|
|
4091
|
-
console.log(` - ${
|
|
4600
|
+
console.log(` - ${path15.relative(root, filePath)}`);
|
|
4092
4601
|
}
|
|
4093
4602
|
}
|
|
4094
4603
|
if (report.dry_run) {
|
|
@@ -4108,6 +4617,75 @@ function registerMigrateCommand(program) {
|
|
|
4108
4617
|
});
|
|
4109
4618
|
}
|
|
4110
4619
|
|
|
4620
|
+
// src/commands/naming.ts
|
|
4621
|
+
function printNamingOverview() {
|
|
4622
|
+
const root = resolveRepoRoot();
|
|
4623
|
+
const config = readCoopConfig(root);
|
|
4624
|
+
const sampleTitle = "Natural-language COOP command recommender";
|
|
4625
|
+
const sampleTaskTitle = "Implement billing payment contract review";
|
|
4626
|
+
console.log("COOP Naming");
|
|
4627
|
+
console.log(`Current template: ${config.idNamingTemplate}`);
|
|
4628
|
+
console.log(`Default template: ${DEFAULT_ID_NAMING_TEMPLATE}`);
|
|
4629
|
+
console.log("Tokens:");
|
|
4630
|
+
console.log(" <TYPE> entity type such as IDEA, TASK, DELIVERY");
|
|
4631
|
+
console.log(" <TITLE> semantic title token (defaults to TITLE16)");
|
|
4632
|
+
console.log(" <TITLE16> semantic title token capped to 16 chars");
|
|
4633
|
+
console.log(" <TITLE24> semantic title token capped to 24 chars");
|
|
4634
|
+
console.log(" <TRACK> task track");
|
|
4635
|
+
console.log(" <SEQ> sequential number within the rendered pattern");
|
|
4636
|
+
console.log(" <USER> actor/user namespace");
|
|
4637
|
+
console.log(" <YYMMDD> short date token");
|
|
4638
|
+
console.log(" <RAND> random uniqueness token");
|
|
4639
|
+
console.log(" <PREFIX> entity prefix override");
|
|
4640
|
+
console.log("Examples:");
|
|
4641
|
+
const tokenExamples = namingTokenExamples(sampleTitle);
|
|
4642
|
+
for (const [token, value] of Object.entries(tokenExamples)) {
|
|
4643
|
+
console.log(` ${token.padEnd(10)} ${value}`);
|
|
4644
|
+
}
|
|
4645
|
+
console.log(`Preview (${DEFAULT_ID_NAMING_TEMPLATE}): ${previewNamingTemplate(DEFAULT_ID_NAMING_TEMPLATE, {
|
|
4646
|
+
entityType: "idea",
|
|
4647
|
+
title: sampleTitle
|
|
4648
|
+
}, root)}`);
|
|
4649
|
+
console.log(`Preview (${config.idNamingTemplate}): ${previewNamingTemplate(config.idNamingTemplate, {
|
|
4650
|
+
entityType: "task",
|
|
4651
|
+
title: sampleTaskTitle,
|
|
4652
|
+
track: "mvp",
|
|
4653
|
+
status: "todo",
|
|
4654
|
+
taskType: "spike"
|
|
4655
|
+
}, root)}`);
|
|
4656
|
+
console.log("Try:");
|
|
4657
|
+
console.log(` coop naming preview "${sampleTitle}"`);
|
|
4658
|
+
console.log(' coop config id.naming "<TYPE>-<TITLE24>"');
|
|
4659
|
+
console.log(' coop config id.naming "<TRACK>-<SEQ>"');
|
|
4660
|
+
}
|
|
4661
|
+
function registerNamingCommand(program) {
|
|
4662
|
+
const naming = program.command("naming").description("Explain COOP ID naming templates and preview examples");
|
|
4663
|
+
naming.action(() => {
|
|
4664
|
+
printNamingOverview();
|
|
4665
|
+
});
|
|
4666
|
+
naming.command("preview").description("Preview the current or supplied naming template for a sample title").argument("<title>", "Sample title to render").option("--template <template>", "Override naming template").option("--entity <entity>", "Entity type: idea|task|track|delivery|run", "task").option("--track <track>", "Track token value").option("--status <status>", "Status token value").option("--task-type <taskType>", "Task type token value").action(
|
|
4667
|
+
(title, options) => {
|
|
4668
|
+
const root = resolveRepoRoot();
|
|
4669
|
+
const config = readCoopConfig(root);
|
|
4670
|
+
const entity = options.entity?.trim().toLowerCase() || "task";
|
|
4671
|
+
const template = options.template?.trim() || config.idNamingTemplate;
|
|
4672
|
+
console.log(
|
|
4673
|
+
previewNamingTemplate(
|
|
4674
|
+
template,
|
|
4675
|
+
{
|
|
4676
|
+
entityType: entity,
|
|
4677
|
+
title,
|
|
4678
|
+
track: options.track,
|
|
4679
|
+
status: options.status,
|
|
4680
|
+
taskType: options.taskType
|
|
4681
|
+
},
|
|
4682
|
+
root
|
|
4683
|
+
)
|
|
4684
|
+
);
|
|
4685
|
+
}
|
|
4686
|
+
);
|
|
4687
|
+
}
|
|
4688
|
+
|
|
4111
4689
|
// src/commands/plan.ts
|
|
4112
4690
|
import chalk3 from "chalk";
|
|
4113
4691
|
import {
|
|
@@ -4343,7 +4921,7 @@ function registerPlanCommand(program) {
|
|
|
4343
4921
|
|
|
4344
4922
|
// src/commands/project.ts
|
|
4345
4923
|
import fs11 from "fs";
|
|
4346
|
-
import
|
|
4924
|
+
import path16 from "path";
|
|
4347
4925
|
import { CURRENT_SCHEMA_VERSION as CURRENT_SCHEMA_VERSION3, write_schema_version as write_schema_version2 } from "@kitsy/coop-core";
|
|
4348
4926
|
var TASK_TEMPLATE2 = `---
|
|
4349
4927
|
id: TASK-001
|
|
@@ -4376,7 +4954,7 @@ linked_tasks: []
|
|
|
4376
4954
|
## Problem
|
|
4377
4955
|
What problem are you solving?
|
|
4378
4956
|
`;
|
|
4379
|
-
var PROJECT_CONFIG_TEMPLATE = (projectId, projectName) => `version: 2
|
|
4957
|
+
var PROJECT_CONFIG_TEMPLATE = (projectId, projectName, namingTemplate) => `version: 2
|
|
4380
4958
|
project:
|
|
4381
4959
|
name: "${projectName}"
|
|
4382
4960
|
id: "${projectId}"
|
|
@@ -4389,7 +4967,7 @@ id_prefixes:
|
|
|
4389
4967
|
run: "RUN"
|
|
4390
4968
|
|
|
4391
4969
|
id:
|
|
4392
|
-
naming:
|
|
4970
|
+
naming: ${JSON.stringify(namingTemplate)}
|
|
4393
4971
|
seq_padding: 0
|
|
4394
4972
|
|
|
4395
4973
|
defaults:
|
|
@@ -4462,19 +5040,19 @@ function writeIfMissing3(filePath, content) {
|
|
|
4462
5040
|
function normalizeProjectId3(value) {
|
|
4463
5041
|
return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-");
|
|
4464
5042
|
}
|
|
4465
|
-
function createProject(root, projectId, projectName) {
|
|
5043
|
+
function createProject(root, projectId, projectName, namingTemplate = DEFAULT_ID_NAMING_TEMPLATE) {
|
|
4466
5044
|
const workspaceDir = coopWorkspaceDir(root);
|
|
4467
|
-
const projectRoot =
|
|
4468
|
-
ensureDir2(
|
|
5045
|
+
const projectRoot = path16.join(workspaceDir, "projects", projectId);
|
|
5046
|
+
ensureDir2(path16.join(workspaceDir, "projects"));
|
|
4469
5047
|
for (const dir of PROJECT_DIRS) {
|
|
4470
|
-
ensureDir2(
|
|
5048
|
+
ensureDir2(path16.join(projectRoot, dir));
|
|
4471
5049
|
}
|
|
4472
|
-
writeIfMissing3(
|
|
4473
|
-
if (!fs11.existsSync(
|
|
5050
|
+
writeIfMissing3(path16.join(projectRoot, "config.yml"), PROJECT_CONFIG_TEMPLATE(projectId, projectName, namingTemplate));
|
|
5051
|
+
if (!fs11.existsSync(path16.join(projectRoot, "schema-version"))) {
|
|
4474
5052
|
write_schema_version2(projectRoot, CURRENT_SCHEMA_VERSION3);
|
|
4475
5053
|
}
|
|
4476
|
-
writeIfMissing3(
|
|
4477
|
-
writeIfMissing3(
|
|
5054
|
+
writeIfMissing3(path16.join(projectRoot, "templates/task.md"), TASK_TEMPLATE2);
|
|
5055
|
+
writeIfMissing3(path16.join(projectRoot, "templates/idea.md"), IDEA_TEMPLATE2);
|
|
4478
5056
|
return projectRoot;
|
|
4479
5057
|
}
|
|
4480
5058
|
function registerProjectCommand(program) {
|
|
@@ -4502,7 +5080,7 @@ function registerProjectCommand(program) {
|
|
|
4502
5080
|
}
|
|
4503
5081
|
console.log(`id=${active.id}`);
|
|
4504
5082
|
console.log(`name=${active.name}`);
|
|
4505
|
-
console.log(`path=${
|
|
5083
|
+
console.log(`path=${path16.relative(root, active.root)}`);
|
|
4506
5084
|
console.log(`layout=${active.layout}`);
|
|
4507
5085
|
});
|
|
4508
5086
|
project.command("use").description("Set the active COOP project").argument("<id>", "Project id").action((id) => {
|
|
@@ -4516,7 +5094,7 @@ function registerProjectCommand(program) {
|
|
|
4516
5094
|
writeWorkspaceConfig(root, { ...workspace, version: 2, current_project: match.id });
|
|
4517
5095
|
console.log(`current_project=${match.id}`);
|
|
4518
5096
|
});
|
|
4519
|
-
project.command("create").description("Create a new COOP project in the current workspace").argument("<id>", "Project id").option("--name <name>", "Project display name").action((id, options) => {
|
|
5097
|
+
project.command("create").description("Create a new COOP project in the current workspace").argument("<id>", "Project id").option("--name <name>", "Project display name").option("--naming <template>", "ID naming template (default: <TYPE>-<TITLE16>-<SEQ>)").action((id, options) => {
|
|
4520
5098
|
const root = resolveRepoRoot();
|
|
4521
5099
|
const projectId = normalizeProjectId3(id);
|
|
4522
5100
|
if (!projectId) {
|
|
@@ -4527,33 +5105,33 @@ function registerProjectCommand(program) {
|
|
|
4527
5105
|
throw new Error(`Project '${projectId}' already exists.`);
|
|
4528
5106
|
}
|
|
4529
5107
|
const projectName = options.name?.trim() || repoDisplayName(root);
|
|
4530
|
-
const projectRoot = createProject(root, projectId, projectName);
|
|
5108
|
+
const projectRoot = createProject(root, projectId, projectName, options.naming?.trim() || DEFAULT_ID_NAMING_TEMPLATE);
|
|
4531
5109
|
const workspace = readWorkspaceConfig(root);
|
|
4532
5110
|
writeWorkspaceConfig(root, {
|
|
4533
5111
|
...workspace,
|
|
4534
5112
|
version: 2,
|
|
4535
5113
|
current_project: workspace.current_project || projectId
|
|
4536
5114
|
});
|
|
4537
|
-
console.log(`Created project '${projectId}' at ${
|
|
5115
|
+
console.log(`Created project '${projectId}' at ${path16.relative(root, projectRoot)}`);
|
|
4538
5116
|
});
|
|
4539
5117
|
}
|
|
4540
5118
|
|
|
4541
5119
|
// src/commands/refine.ts
|
|
4542
5120
|
import fs12 from "fs";
|
|
4543
|
-
import
|
|
4544
|
-
import { parseIdeaFile as parseIdeaFile4, parseTaskFile as
|
|
5121
|
+
import path17 from "path";
|
|
5122
|
+
import { parseIdeaFile as parseIdeaFile4, parseTaskFile as parseTaskFile10 } from "@kitsy/coop-core";
|
|
4545
5123
|
import { create_provider_refinement_client, refine_idea_to_draft, refine_task_to_draft } from "@kitsy/coop-ai";
|
|
4546
5124
|
function resolveTaskFile(root, idOrAlias) {
|
|
4547
5125
|
const target = resolveReference(root, idOrAlias, "task");
|
|
4548
|
-
return
|
|
5126
|
+
return path17.join(root, ...target.file.split("/"));
|
|
4549
5127
|
}
|
|
4550
5128
|
function resolveIdeaFile2(root, idOrAlias) {
|
|
4551
5129
|
const target = resolveReference(root, idOrAlias, "idea");
|
|
4552
|
-
return
|
|
5130
|
+
return path17.join(root, ...target.file.split("/"));
|
|
4553
5131
|
}
|
|
4554
5132
|
async function readSupplementalInput(root, options) {
|
|
4555
5133
|
if (options.inputFile?.trim()) {
|
|
4556
|
-
return fs12.readFileSync(
|
|
5134
|
+
return fs12.readFileSync(path17.resolve(root, options.inputFile.trim()), "utf8");
|
|
4557
5135
|
}
|
|
4558
5136
|
if (options.stdin) {
|
|
4559
5137
|
return readStdinText();
|
|
@@ -4571,7 +5149,7 @@ function loadAuthorityContext(root, refs) {
|
|
|
4571
5149
|
for (const ref of refs ?? []) {
|
|
4572
5150
|
const filePart = extractRefFile(ref);
|
|
4573
5151
|
if (!filePart) continue;
|
|
4574
|
-
const fullPath =
|
|
5152
|
+
const fullPath = path17.resolve(root, filePart);
|
|
4575
5153
|
if (!fs12.existsSync(fullPath) || !fs12.statSync(fullPath).isFile()) continue;
|
|
4576
5154
|
out.push({
|
|
4577
5155
|
ref,
|
|
@@ -4610,7 +5188,7 @@ function registerRefineCommand(program) {
|
|
|
4610
5188
|
const root = resolveRepoRoot();
|
|
4611
5189
|
const projectDir = ensureCoopInitialized(root);
|
|
4612
5190
|
const taskFile = resolveTaskFile(root, id);
|
|
4613
|
-
const parsed =
|
|
5191
|
+
const parsed = parseTaskFile10(taskFile);
|
|
4614
5192
|
const supplemental = await readSupplementalInput(root, options);
|
|
4615
5193
|
const client = create_provider_refinement_client(readCoopConfig(root).raw);
|
|
4616
5194
|
const draft = await refine_task_to_draft(
|
|
@@ -4638,15 +5216,15 @@ function registerRefineCommand(program) {
|
|
|
4638
5216
|
const written = applyRefinementDraft(root, projectDir, draft);
|
|
4639
5217
|
console.log(`[COOP] applied draft from ${draftInput.source}: ${written.length} task file(s) updated`);
|
|
4640
5218
|
for (const filePath of written) {
|
|
4641
|
-
console.log(`- ${
|
|
5219
|
+
console.log(`- ${path17.relative(root, filePath)}`);
|
|
4642
5220
|
}
|
|
4643
5221
|
});
|
|
4644
5222
|
}
|
|
4645
5223
|
|
|
4646
5224
|
// src/commands/run.ts
|
|
4647
5225
|
import fs13 from "fs";
|
|
4648
|
-
import
|
|
4649
|
-
import { load_graph as load_graph6, parseTaskFile as
|
|
5226
|
+
import path18 from "path";
|
|
5227
|
+
import { load_graph as load_graph6, parseTaskFile as parseTaskFile11 } from "@kitsy/coop-core";
|
|
4650
5228
|
import {
|
|
4651
5229
|
build_contract,
|
|
4652
5230
|
create_provider_agent_client,
|
|
@@ -4655,11 +5233,11 @@ import {
|
|
|
4655
5233
|
} from "@kitsy/coop-ai";
|
|
4656
5234
|
function loadTask(root, idOrAlias) {
|
|
4657
5235
|
const target = resolveReference(root, idOrAlias, "task");
|
|
4658
|
-
const taskFile =
|
|
5236
|
+
const taskFile = path18.join(root, ...target.file.split("/"));
|
|
4659
5237
|
if (!fs13.existsSync(taskFile)) {
|
|
4660
5238
|
throw new Error(`Task file not found: ${target.file}`);
|
|
4661
5239
|
}
|
|
4662
|
-
return
|
|
5240
|
+
return parseTaskFile11(taskFile).task;
|
|
4663
5241
|
}
|
|
4664
5242
|
function printContract(contract) {
|
|
4665
5243
|
console.log(JSON.stringify(contract, null, 2));
|
|
@@ -4695,26 +5273,26 @@ function registerRunCommand(program) {
|
|
|
4695
5273
|
on_progress: (message) => console.log(`[COOP] ${message}`)
|
|
4696
5274
|
});
|
|
4697
5275
|
if (result.status === "failed") {
|
|
4698
|
-
throw new Error(`Run failed: ${result.run.id}. Log: ${
|
|
5276
|
+
throw new Error(`Run failed: ${result.run.id}. Log: ${path18.relative(root, result.log_path)}`);
|
|
4699
5277
|
}
|
|
4700
5278
|
if (result.status === "paused") {
|
|
4701
5279
|
console.log(`[COOP] run paused: ${result.run.id}`);
|
|
4702
|
-
console.log(`[COOP] log: ${
|
|
5280
|
+
console.log(`[COOP] log: ${path18.relative(root, result.log_path)}`);
|
|
4703
5281
|
return;
|
|
4704
5282
|
}
|
|
4705
5283
|
console.log(`[COOP] run completed: ${result.run.id}`);
|
|
4706
|
-
console.log(`[COOP] log: ${
|
|
5284
|
+
console.log(`[COOP] log: ${path18.relative(root, result.log_path)}`);
|
|
4707
5285
|
});
|
|
4708
5286
|
}
|
|
4709
5287
|
|
|
4710
5288
|
// src/server/api.ts
|
|
4711
5289
|
import fs14 from "fs";
|
|
4712
5290
|
import http2 from "http";
|
|
4713
|
-
import
|
|
5291
|
+
import path19 from "path";
|
|
4714
5292
|
import {
|
|
4715
5293
|
analyze_feasibility as analyze_feasibility2,
|
|
4716
5294
|
load_graph as load_graph7,
|
|
4717
|
-
parseTaskFile as
|
|
5295
|
+
parseTaskFile as parseTaskFile12,
|
|
4718
5296
|
resolve_external_dependencies
|
|
4719
5297
|
} from "@kitsy/coop-core";
|
|
4720
5298
|
function json(res, statusCode, payload) {
|
|
@@ -4756,12 +5334,12 @@ function taskSummary(graph, task, external = []) {
|
|
|
4756
5334
|
};
|
|
4757
5335
|
}
|
|
4758
5336
|
function taskFileById(root, id) {
|
|
4759
|
-
const tasksDir =
|
|
5337
|
+
const tasksDir = path19.join(resolveProject(root).root, "tasks");
|
|
4760
5338
|
if (!fs14.existsSync(tasksDir)) return null;
|
|
4761
5339
|
const entries = fs14.readdirSync(tasksDir, { withFileTypes: true });
|
|
4762
5340
|
const target = `${id}.md`.toLowerCase();
|
|
4763
5341
|
const match = entries.find((entry) => entry.isFile() && entry.name.toLowerCase() === target);
|
|
4764
|
-
return match ?
|
|
5342
|
+
return match ? path19.join(tasksDir, match.name) : null;
|
|
4765
5343
|
}
|
|
4766
5344
|
function loadRemoteConfig(root) {
|
|
4767
5345
|
const raw = readCoopConfig(root).raw;
|
|
@@ -4842,7 +5420,7 @@ function createApiServer(root, options = {}) {
|
|
|
4842
5420
|
notFound(res, `Task '${taskId}' not found.`);
|
|
4843
5421
|
return;
|
|
4844
5422
|
}
|
|
4845
|
-
const parsed =
|
|
5423
|
+
const parsed = parseTaskFile12(filePath);
|
|
4846
5424
|
const task = graph.nodes.get(parsed.task.id) ?? parsed.task;
|
|
4847
5425
|
json(res, 200, {
|
|
4848
5426
|
...workspaceMeta(repoRoot),
|
|
@@ -4851,7 +5429,7 @@ function createApiServer(root, options = {}) {
|
|
|
4851
5429
|
created: task.created,
|
|
4852
5430
|
updated: task.updated,
|
|
4853
5431
|
body: parsed.body,
|
|
4854
|
-
file_path:
|
|
5432
|
+
file_path: path19.relative(repoRoot, filePath).replace(/\\/g, "/")
|
|
4855
5433
|
}
|
|
4856
5434
|
});
|
|
4857
5435
|
return;
|
|
@@ -4940,8 +5518,8 @@ function registerServeCommand(program) {
|
|
|
4940
5518
|
|
|
4941
5519
|
// src/commands/show.ts
|
|
4942
5520
|
import fs15 from "fs";
|
|
4943
|
-
import
|
|
4944
|
-
import { parseIdeaFile as parseIdeaFile5, parseTaskFile as
|
|
5521
|
+
import path20 from "path";
|
|
5522
|
+
import { parseIdeaFile as parseIdeaFile5, parseTaskFile as parseTaskFile13 } from "@kitsy/coop-core";
|
|
4945
5523
|
function stringify(value) {
|
|
4946
5524
|
if (value === null || value === void 0) return "-";
|
|
4947
5525
|
if (Array.isArray(value)) return value.length > 0 ? value.join(", ") : "-";
|
|
@@ -4959,7 +5537,7 @@ function pushListSection(lines, title, values) {
|
|
|
4959
5537
|
}
|
|
4960
5538
|
}
|
|
4961
5539
|
function loadComputedFromIndex(root, taskId) {
|
|
4962
|
-
const indexPath =
|
|
5540
|
+
const indexPath = path20.join(ensureCoopInitialized(root), ".index", "tasks.json");
|
|
4963
5541
|
if (!fs15.existsSync(indexPath)) {
|
|
4964
5542
|
return null;
|
|
4965
5543
|
}
|
|
@@ -5004,8 +5582,8 @@ function showTask(taskId) {
|
|
|
5004
5582
|
const root = resolveRepoRoot();
|
|
5005
5583
|
const coop = ensureCoopInitialized(root);
|
|
5006
5584
|
const target = resolveReference(root, taskId, "task");
|
|
5007
|
-
const taskFile =
|
|
5008
|
-
const parsed =
|
|
5585
|
+
const taskFile = path20.join(root, ...target.file.split("/"));
|
|
5586
|
+
const parsed = parseTaskFile13(taskFile);
|
|
5009
5587
|
const task = parsed.task;
|
|
5010
5588
|
const body = parsed.body.trim();
|
|
5011
5589
|
const computed = loadComputedFromIndex(root, task.id);
|
|
@@ -5023,7 +5601,7 @@ function showTask(taskId) {
|
|
|
5023
5601
|
`Tags: ${stringify(task.tags)}`,
|
|
5024
5602
|
`Created: ${task.created}`,
|
|
5025
5603
|
`Updated: ${task.updated}`,
|
|
5026
|
-
`File: ${
|
|
5604
|
+
`File: ${path20.relative(root, taskFile)}`,
|
|
5027
5605
|
""
|
|
5028
5606
|
];
|
|
5029
5607
|
pushListSection(lines, "Acceptance", task.acceptance);
|
|
@@ -5048,7 +5626,7 @@ function showTask(taskId) {
|
|
|
5048
5626
|
"Computed:"
|
|
5049
5627
|
);
|
|
5050
5628
|
if (!computed) {
|
|
5051
|
-
lines.push(`index not built (${
|
|
5629
|
+
lines.push(`index not built (${path20.relative(root, path20.join(coop, ".index", "tasks.json"))} missing)`);
|
|
5052
5630
|
} else {
|
|
5053
5631
|
for (const [key, value] of Object.entries(computed)) {
|
|
5054
5632
|
lines.push(`- ${key}: ${stringify(value)}`);
|
|
@@ -5060,7 +5638,7 @@ function showIdea(ideaId) {
|
|
|
5060
5638
|
const root = resolveRepoRoot();
|
|
5061
5639
|
ensureCoopInitialized(root);
|
|
5062
5640
|
const target = resolveReference(root, ideaId, "idea");
|
|
5063
|
-
const ideaFile =
|
|
5641
|
+
const ideaFile = path20.join(root, ...target.file.split("/"));
|
|
5064
5642
|
const parsed = parseIdeaFile5(ideaFile);
|
|
5065
5643
|
const idea = parsed.idea;
|
|
5066
5644
|
const body = parsed.body.trim();
|
|
@@ -5074,7 +5652,7 @@ function showIdea(ideaId) {
|
|
|
5074
5652
|
`Tags: ${stringify(idea.tags)}`,
|
|
5075
5653
|
`Linked Tasks: ${stringify(idea.linked_tasks)}`,
|
|
5076
5654
|
`Created: ${idea.created}`,
|
|
5077
|
-
`File: ${
|
|
5655
|
+
`File: ${path20.relative(root, ideaFile)}`,
|
|
5078
5656
|
"",
|
|
5079
5657
|
"Body:",
|
|
5080
5658
|
body || "-"
|
|
@@ -5191,8 +5769,8 @@ function registerTransitionCommand(program) {
|
|
|
5191
5769
|
}
|
|
5192
5770
|
|
|
5193
5771
|
// src/commands/taskflow.ts
|
|
5194
|
-
import
|
|
5195
|
-
import { parseTaskFile as
|
|
5772
|
+
import path21 from "path";
|
|
5773
|
+
import { parseTaskFile as parseTaskFile14 } from "@kitsy/coop-core";
|
|
5196
5774
|
function normalizeExecutor(value) {
|
|
5197
5775
|
if (!value?.trim()) return void 0;
|
|
5198
5776
|
const normalized = value.trim().toLowerCase();
|
|
@@ -5229,8 +5807,8 @@ async function claimAndStart(root, taskId, options) {
|
|
|
5229
5807
|
}
|
|
5230
5808
|
}
|
|
5231
5809
|
const reference = resolveReference(root, taskId, "task");
|
|
5232
|
-
const filePath =
|
|
5233
|
-
const parsed =
|
|
5810
|
+
const filePath = path21.join(root, ...reference.file.split("/"));
|
|
5811
|
+
const parsed = parseTaskFile14(filePath);
|
|
5234
5812
|
if (parsed.task.status === "in_progress") {
|
|
5235
5813
|
console.log(`Task ${parsed.task.id} is already in_progress.`);
|
|
5236
5814
|
return;
|
|
@@ -5276,8 +5854,8 @@ function registerTaskFlowCommands(program) {
|
|
|
5276
5854
|
today: options.today
|
|
5277
5855
|
}).entry.task.id;
|
|
5278
5856
|
const reference = resolveReference(root, taskId, "task");
|
|
5279
|
-
const filePath =
|
|
5280
|
-
const parsed =
|
|
5857
|
+
const filePath = path21.join(root, ...reference.file.split("/"));
|
|
5858
|
+
const parsed = parseTaskFile14(filePath);
|
|
5281
5859
|
console.log(
|
|
5282
5860
|
formatSelectedTask(
|
|
5283
5861
|
{
|
|
@@ -5301,18 +5879,18 @@ function registerTaskFlowCommands(program) {
|
|
|
5301
5879
|
|
|
5302
5880
|
// src/commands/ui.ts
|
|
5303
5881
|
import fs16 from "fs";
|
|
5304
|
-
import
|
|
5882
|
+
import path22 from "path";
|
|
5305
5883
|
import { createRequire } from "module";
|
|
5306
5884
|
import { fileURLToPath } from "url";
|
|
5307
5885
|
import { spawn } from "child_process";
|
|
5308
5886
|
import { IndexManager as IndexManager4 } from "@kitsy/coop-core";
|
|
5309
5887
|
function findPackageRoot(entryPath) {
|
|
5310
|
-
let current =
|
|
5888
|
+
let current = path22.dirname(entryPath);
|
|
5311
5889
|
while (true) {
|
|
5312
|
-
if (fs16.existsSync(
|
|
5890
|
+
if (fs16.existsSync(path22.join(current, "package.json"))) {
|
|
5313
5891
|
return current;
|
|
5314
5892
|
}
|
|
5315
|
-
const parent =
|
|
5893
|
+
const parent = path22.dirname(current);
|
|
5316
5894
|
if (parent === current) {
|
|
5317
5895
|
throw new Error(`Unable to locate package root for ${entryPath}.`);
|
|
5318
5896
|
}
|
|
@@ -5361,9 +5939,9 @@ async function startUiServer(repoRoot, host, port, shouldOpen) {
|
|
|
5361
5939
|
const project = resolveProject(repoRoot);
|
|
5362
5940
|
ensureIndex(repoRoot);
|
|
5363
5941
|
const uiRoot = resolveUiPackageRoot();
|
|
5364
|
-
const requireFromUi = createRequire(
|
|
5942
|
+
const requireFromUi = createRequire(path22.join(uiRoot, "package.json"));
|
|
5365
5943
|
const vitePackageJson = requireFromUi.resolve("vite/package.json");
|
|
5366
|
-
const viteBin =
|
|
5944
|
+
const viteBin = path22.join(path22.dirname(vitePackageJson), "bin", "vite.js");
|
|
5367
5945
|
const url = `http://${host}:${port}`;
|
|
5368
5946
|
console.log(`COOP UI: ${url}`);
|
|
5369
5947
|
const child = spawn(process.execPath, [viteBin, "--host", host, "--port", String(port)], {
|
|
@@ -5722,7 +6300,7 @@ function registerWebhookCommand(program) {
|
|
|
5722
6300
|
// src/merge-driver/merge-driver.ts
|
|
5723
6301
|
import fs17 from "fs";
|
|
5724
6302
|
import os2 from "os";
|
|
5725
|
-
import
|
|
6303
|
+
import path23 from "path";
|
|
5726
6304
|
import { spawnSync as spawnSync5 } from "child_process";
|
|
5727
6305
|
import { stringifyFrontmatter as stringifyFrontmatter4, parseFrontmatterContent as parseFrontmatterContent3, parseYamlContent as parseYamlContent3, stringifyYamlContent as stringifyYamlContent2 } from "@kitsy/coop-core";
|
|
5728
6306
|
var STATUS_RANK = {
|
|
@@ -5812,11 +6390,11 @@ function mergeTaskFile(ancestorPath, oursPath, theirsPath) {
|
|
|
5812
6390
|
const ours = parseTaskDocument(oursRaw, oursPath);
|
|
5813
6391
|
const theirs = parseTaskDocument(theirsRaw, theirsPath);
|
|
5814
6392
|
const mergedFrontmatter = mergeTaskFrontmatter(ancestor.frontmatter, ours.frontmatter, theirs.frontmatter);
|
|
5815
|
-
const tempDir = fs17.mkdtempSync(
|
|
6393
|
+
const tempDir = fs17.mkdtempSync(path23.join(os2.tmpdir(), "coop-merge-body-"));
|
|
5816
6394
|
try {
|
|
5817
|
-
const ancestorBody =
|
|
5818
|
-
const oursBody =
|
|
5819
|
-
const theirsBody =
|
|
6395
|
+
const ancestorBody = path23.join(tempDir, "ancestor.md");
|
|
6396
|
+
const oursBody = path23.join(tempDir, "ours.md");
|
|
6397
|
+
const theirsBody = path23.join(tempDir, "theirs.md");
|
|
5820
6398
|
fs17.writeFileSync(ancestorBody, ancestor.body, "utf8");
|
|
5821
6399
|
fs17.writeFileSync(oursBody, ours.body, "utf8");
|
|
5822
6400
|
fs17.writeFileSync(theirsBody, theirs.body, "utf8");
|
|
@@ -5854,7 +6432,7 @@ function runMergeDriver(kind, ancestorPath, oursPath, theirsPath) {
|
|
|
5854
6432
|
// src/index.ts
|
|
5855
6433
|
function readVersion() {
|
|
5856
6434
|
const currentFile = fileURLToPath2(import.meta.url);
|
|
5857
|
-
const packageJsonPath =
|
|
6435
|
+
const packageJsonPath = path24.resolve(path24.dirname(currentFile), "..", "package.json");
|
|
5858
6436
|
try {
|
|
5859
6437
|
const parsed = JSON.parse(fs18.readFileSync(packageJsonPath, "utf8"));
|
|
5860
6438
|
return parsed.version ?? "0.0.0";
|
|
@@ -5887,7 +6465,9 @@ function createProgram() {
|
|
|
5887
6465
|
registerHelpAiCommand(program);
|
|
5888
6466
|
registerIndexCommand(program);
|
|
5889
6467
|
registerLogCommand(program);
|
|
6468
|
+
registerLifecycleCommands(program);
|
|
5890
6469
|
registerMigrateCommand(program);
|
|
6470
|
+
registerNamingCommand(program);
|
|
5891
6471
|
registerPlanCommand(program);
|
|
5892
6472
|
registerProjectCommand(program);
|
|
5893
6473
|
registerRefineCommand(program);
|
|
@@ -5954,7 +6534,7 @@ async function runCli(argv = process.argv) {
|
|
|
5954
6534
|
function isMainModule() {
|
|
5955
6535
|
const entry = process.argv[1];
|
|
5956
6536
|
if (!entry) return false;
|
|
5957
|
-
return
|
|
6537
|
+
return path24.resolve(entry) === fileURLToPath2(import.meta.url);
|
|
5958
6538
|
}
|
|
5959
6539
|
if (isMainModule()) {
|
|
5960
6540
|
await runCli(process.argv);
|