@angar2/taskery 0.8.1 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +437 -188
  2. package/bin/add.js +27 -99
  3. package/bin/backlog.js +240 -113
  4. package/bin/check.js +182 -0
  5. package/bin/commands.js +151 -0
  6. package/bin/finish.js +365 -0
  7. package/bin/init.js +99 -190
  8. package/bin/install.js +143 -0
  9. package/bin/lib.js +591 -1336
  10. package/bin/mcp.js +31 -263
  11. package/bin/orche.js +240 -0
  12. package/bin/plan.js +58 -120
  13. package/bin/prepare.js +329 -0
  14. package/bin/prune.js +46 -176
  15. package/bin/status.js +57 -151
  16. package/bin/taskery.js +76 -120
  17. package/bin/update.js +66 -269
  18. package/package.json +5 -5
  19. package/template/.taskery/BACKLOG.md +38 -0
  20. package/template/.taskery/rules/CHANGELOG_RULE.md +31 -0
  21. package/template/.taskery/rules/DEV_RULE.local.md +8 -0
  22. package/template/.taskery/rules/GIT_RULE.md +82 -0
  23. package/template/.taskery/rules/MOCKUP_RULE.md +19 -0
  24. package/template/.taskery/rules/TASKERY_RULE.md +112 -0
  25. package/template/.taskery/rules/TASK_DOC_RULE.md +104 -0
  26. package/template/.taskery/rules/TEST_RULE.local.md +16 -0
  27. package/template/AGENTS.md +15 -53
  28. package/template/shared/skills/add-backlog/SKILL.md +8 -38
  29. package/template/shared/skills/log-friction/SKILL.md +10 -88
  30. package/template/shared/skills/plan-init/SKILL.md +10 -98
  31. package/template/shared/skills/project-init/SKILL.md +13 -342
  32. package/template/shared/skills/task-close/SKILL.md +16 -319
  33. package/template/shared/skills/task-dev/SKILL.md +19 -233
  34. package/template/shared/skills/task-init/SKILL.md +11 -68
  35. package/template/shared/skills/task-orche/SKILL.md +35 -0
  36. package/template/shared/skills/task-plan/SKILL.md +19 -498
  37. package/template/shared/skills/task-test/SKILL.md +23 -342
  38. package/bin/close.js +0 -86
  39. package/bin/fork.js +0 -112
  40. package/bin/legacy-quotes.js +0 -60
  41. package/bin/migrate.js +0 -481
  42. package/bin/set-status.js +0 -98
  43. package/template/.claude/hooks/closed-immutable.sh +0 -77
  44. package/template/.claude/settings.json +0 -18
  45. package/template/.claude/skills/run-team/SKILL.md +0 -145
  46. package/template/.codex/agents/task-tester.toml +0 -41
  47. package/template/.codex/agents/test-plan-author.toml +0 -34
  48. package/template/.codex/config.toml +0 -17
  49. package/template/.codex/hooks/closed-immutable.sh +0 -66
  50. package/template/.project/changelog/.gitkeep +0 -0
  51. package/template/.project/flows/.gitkeep +0 -0
  52. package/template/.project/plans/.gitkeep +0 -0
  53. package/template/.project/rules/CHANGELOG_RULE.md +0 -97
  54. package/template/.project/rules/GIT_RULE.md +0 -325
  55. package/template/.project/rules/MOCKUP_RULE.md +0 -86
  56. package/template/.project/rules/TASKERY_RULE.md +0 -237
  57. package/template/.project/rules/TASK_DOC_RULE.md +0 -668
  58. package/template/.project/shared/received/completed/.gitkeep +0 -0
  59. package/template/.project/shared/sent/completed/.gitkeep +0 -0
  60. package/template/.project/tasks/.gitkeep +0 -0
  61. package/template/shared/hooks/git-guard.sh +0 -108
package/bin/add.js CHANGED
@@ -1,113 +1,41 @@
1
1
  #!/usr/bin/env node
2
- // bin/add.js
3
- // `npx @angar2/taskery add <platform>` — 기존 설치 리포에 다른 플랫폼 자산 추가.
4
- //
5
- // 동작:
6
- // 1. <platform> 인자 검증 (claude | codex)
7
- // 2. .taskery-manifest.json 필수 — 없으면 'init' 안내
8
- // 3. 이미 manifest.platforms에 있으면 변경 없이 종료
9
- // 4. 해당 플랫폼 전용 자산만 카피 (공통 .project/는 init이 이미 설치)
10
- // 5. manifest.platforms push + files 갱신
11
-
2
+ // add — 설치된 리포에 플랫폼(claude·codex)의 스킬·설정을 더한다
12
3
  const fs = require('fs');
13
4
  const path = require('path');
14
- const readline = require('readline');
15
-
16
- const {
17
- MANIFEST_NAME,
18
- findTemplateDir,
19
- walkTemplate,
20
- copyFile,
21
- writeManifest,
22
- readManifest,
23
- getPackageVersion,
24
- resolveInstallPlan,
25
- PLATFORMS,
26
- } = require('./lib');
5
+ const L = require('./lib');
6
+ const I = require('./install');
27
7
 
28
- async function confirm(msg) {
29
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
30
- return new Promise((resolve) => {
31
- rl.question(`${msg} (y/N) `, (ans) => {
32
- rl.close();
33
- resolve(ans.trim().toLowerCase() === 'y');
34
- });
35
- });
36
- }
37
-
38
- async function main() {
39
- const cwd = process.cwd();
8
+ function main() {
40
9
  const platform = (process.argv[2] || '').trim();
41
-
42
- // 1. 플랫폼 인자 검증
43
- if (!PLATFORMS.includes(platform)) {
44
- console.error(
45
- `taskery add: 플랫폼 인자 필요 (${PLATFORMS.join(' | ')}). 예: npx @angar2/taskery add codex`,
46
- );
47
- process.exit(1);
48
- }
49
-
50
- // 2. manifest 필수
51
- const manifestPath = path.join(cwd, MANIFEST_NAME);
52
- const manifest = readManifest(manifestPath);
53
- if (!manifest) {
54
- console.error(
55
- `taskery add: ${MANIFEST_NAME} 없음 — 'npx @angar2/taskery init' 먼저 실행 필요.`,
56
- );
57
- process.exit(1);
58
- }
59
-
60
- // 3. 이미 설치된 플랫폼?
10
+ if (!I.PLATFORMS.includes(platform)) L.fail(`add: 플랫폼을 넣는다 (${I.PLATFORMS.join(' | ')}). 예: npx @angar2/taskery add codex`);
11
+ const main = L.findMain(process.cwd());
12
+ const manifest = L.readManifest(main);
13
+ if (!manifest) L.fail("add: taskery가 설치돼 있지 않다. 'npx @angar2/taskery init'을 먼저 부른다.");
61
14
  const platforms = Array.isArray(manifest.platforms) ? manifest.platforms : ['claude'];
62
15
  if (platforms.includes(platform)) {
63
- console.log(`taskery add: '${platform}' 이미 설치됨 — 변경 없음.`);
64
- process.exit(0);
16
+ console.log(`'${platform}'은 이미 설치돼 있다 — 바꾼 것 없음.`);
17
+ return;
65
18
  }
66
-
67
- // 4. 해당 플랫폼 전용 + 공통(shared/) 자산만 카피 (agnostic은 init이 이미 설치)
68
- const templateDir = findTemplateDir();
69
- const templateFiles = walkTemplate(templateDir);
70
- const plan = resolveInstallPlan(templateFiles, [platform], { includeAgnostic: false });
71
-
72
- console.log(`\ntaskery v${getPackageVersion()} add ${platform}`);
73
- console.log(`카피 대상: ${plan.length}개 파일\n`);
74
-
75
19
  const files = { ...(manifest.files || {}) };
76
- let copied = 0;
77
- let skipped = 0;
78
-
79
- for (const { templateRel, installRel, hash } of plan) {
80
- const src = path.join(templateDir, templateRel);
81
- const dst = path.join(cwd, installRel);
82
- if (fs.existsSync(dst)) {
83
- const ok = await confirm(` '${installRel}' 이미 존재 — 덮어쓸까?`);
84
- if (!ok) {
85
- console.log(` skip: ${installRel}`);
86
- skipped++;
87
- continue;
88
- }
20
+ const notes = [];
21
+ for (const it of I.installPlan([platform], { agnostic: false })) {
22
+ if (fs.existsSync(path.join(main, it.dst))) {
23
+ notes.push(`건너뜀: ${it.dst} (기존 파일 유지)`);
24
+ continue;
89
25
  }
90
- copyFile(src, dst);
91
- files[installRel] = { hash, core: true, managed: true };
92
- copied++;
93
- console.log(` copy: ${installRel}`);
94
- }
95
-
96
- // 5. manifest 갱신
97
- manifest.platforms = [...platforms, platform];
98
- manifest.files = files;
99
- manifest.updated_at = new Date().toISOString();
100
- writeManifest(manifest, manifestPath);
101
-
102
- console.log(`\n✅ taskery add ${platform} 완료 (카피 ${copied} / 스킵 ${skipped})`);
103
- console.log(` platforms: ${manifest.platforms.join(', ')}`);
104
- if (platform === 'codex') {
105
- console.log(` 다음: Codex 최초 1회 '/hooks'로 hook trust 승인 (.codex/config.toml)`);
106
- console.log(` AGENTS.md 정독 (코덱스 진입점)`);
26
+ const text = I.readTemplate(it.src);
27
+ I.writeFile(main, it.dst, text);
28
+ files[it.dst] = I.hashText(text);
107
29
  }
30
+ notes.push(...I.writeConfigs(main, [platform]));
31
+ L.writeManifest(main, { ...manifest, platforms: [...platforms, platform], files, updated_at: new Date().toISOString() });
32
+ console.log(`taskery add ${platform} 완료`);
33
+ for (const n of notes) console.log(`- ${n}`);
108
34
  }
109
35
 
110
- main().catch((e) => {
111
- console.error(`taskery add 실패: ${e.message}`);
36
+ try {
37
+ main();
38
+ } catch (e) {
39
+ console.error(e instanceof L.TaskeryError ? e.message : `taskery add 실패: ${e.stack || e.message}`);
112
40
  process.exit(1);
113
- });
41
+ }
package/bin/backlog.js CHANGED
@@ -1,131 +1,258 @@
1
- #!/usr/bin/env node
2
- // 활성 plan별 BACKLOG.md 조작 CLI — add(추가) / get(조회) / mark(확인 마킹) 묶음
3
- /**
4
- * bin/backlog.js
5
- * `npx @angar2/taskery backlog-add|backlog-get|backlog-mark ...` — 백로그 조작.
6
- *
7
- * dispatcher(taskery.js)가 서브op(add|get|mark)를 첫 인자로 주입한다.
8
- * add --type <t> --title <제목> --slug <slug> --summary <개요> --target <대상영역>
9
- * → withMetaLock + BL-NNN 채번 + append. stdout: { blNum, blId }
10
- * get <BL-NNN> → 항목 메타 JSON ({ blId, status, type, title, slug, summary, target, taskNums }). 없으면 exit 1
11
- * mark <BL-NNN> <TASK-NNN> → withMetaLock + [ ]→[x] + TASK 마크. stdout: { blId, taskNum }
12
- *
13
- * 채번·서식·락은 전부 코드(lib.js)가 보장 — LLM 손 파싱/Edit 제거.
14
- * 실패(인자 미달 / 활성 plan 부재 / 항목 없음 등) 시 stderr + exit 1.
15
- */
16
-
1
+ // backlog-add · backlog-get · backlog-mark — 프로젝트 백로그 하나(.taskery/BACKLOG.md, 부록 B)를 명령이 다룬다(§5-3, §6-3)
2
+ // BACKLOG.md 쓰기는 모두 번호 잠금 안에서 한다 — prepare-task --from·close-task도 같은 잠금 안에서 아래 텍스트 함수를 부른다
3
+ const fs = require('fs');
17
4
  const path = require('path');
5
+ const L = require('./lib');
18
6
 
19
- const {
20
- MANIFEST_NAME,
21
- readManifest,
22
- getMainWorktreePath,
23
- appendBacklogItem,
24
- parseBacklogItem,
25
- markBacklogChecked,
26
- } = require('./lib');
27
-
28
- function parseFlags(argv) {
29
- // --key value | --key=value 둘 다 지원. 정의된 키만 수집.
30
- const KEYS = ['type', 'title', 'slug', 'summary', 'target'];
31
- const out = {};
32
- for (let i = 0; i < argv.length; i++) {
33
- const a = argv[i];
34
- if (!a.startsWith('--')) continue;
35
- const eq = a.indexOf('=');
36
- let key;
37
- let val;
38
- if (eq !== -1) {
39
- key = a.slice(2, eq);
40
- val = a.slice(eq + 1);
41
- } else {
42
- key = a.slice(2);
43
- val = argv[++i];
44
- }
45
- if (KEYS.includes(key)) out[key] = val;
7
+ const OPEN = '## 열린 항목';
8
+ const DONE = '## 끝난 항목';
9
+ const NONE = '–';
10
+
11
+ function backlogFile(main) {
12
+ return path.join(main, '.taskery', 'BACKLOG.md');
13
+ }
14
+
15
+ // BACKLOG.md가 없으면 설치 직후 모습(부록 B)으로 만든다
16
+ function readBacklog(main) {
17
+ const file = backlogFile(main);
18
+ if (!fs.existsSync(file)) {
19
+ const tpl = fs.readFileSync(path.resolve(__dirname, '..', 'template', '.taskery', 'BACKLOG.md'), 'utf8');
20
+ fs.mkdirSync(path.dirname(file), { recursive: true });
21
+ fs.writeFileSync(file, tpl);
46
22
  }
47
- return out;
23
+ return fs.readFileSync(file, 'utf8');
48
24
  }
49
25
 
50
- function parseTaskNum(token) {
51
- // 'TASK-007' | 'TASK-7' | '7' → 7
52
- const m = String(token).match(/(\d+)/);
53
- return m ? parseInt(m[1], 10) : NaN;
26
+ function writeBacklog(main, text) {
27
+ fs.writeFileSync(backlogFile(main), text);
54
28
  }
55
29
 
56
- function resolveMainWt() {
57
- let mainWt;
58
- try {
59
- mainWt = getMainWorktreePath();
60
- } catch (e) {
61
- console.error(`taskery backlog 실패: ${e.message}`);
62
- process.exit(1);
63
- }
64
- if (!readManifest(path.join(mainWt, MANIFEST_NAME))) {
65
- console.error(`${MANIFEST_NAME} 없음 — 'npx @angar2/taskery init' 먼저 실행 필요.`);
66
- process.exit(1);
67
- }
68
- return mainWt;
69
- }
70
-
71
- async function doAdd(rest) {
72
- const meta = parseFlags(rest);
73
- const missing = ['type', 'title', 'slug', 'summary', 'target'].filter((k) => !meta[k]);
74
- if (missing.length) {
75
- console.error(
76
- `사용법: npx @angar2/taskery backlog-add --type <t> --title <제목> --slug <slug> --summary <개요> --target <대상영역>`,
77
- );
78
- console.error(`누락: ${missing.map((k) => '--' + k).join(', ')}`);
79
- process.exit(1);
30
+ function parseBlNum(token) {
31
+ const m = String(token || '').trim().match(/^(?:BL-?)?(\d+)$/i);
32
+ if (!m) L.fail(`백로그 번호를 읽지 못했다: '${token}'. 'BL-3'이나 '3'처럼 넘긴다.`);
33
+ return parseInt(m[1], 10);
34
+ }
35
+
36
+ // 항목 블록 = `### BL-N` 줄부터 다음 `### `·`## ` 줄 앞까지(뒤 빈 줄 포함)
37
+ function parse(lines) {
38
+ const items = [];
39
+ let sectionName = null;
40
+ let cur = null;
41
+ lines.forEach((l, i) => {
42
+ if (/^## /.test(l)) {
43
+ if (cur) cur.end = i;
44
+ cur = null;
45
+ sectionName = l.trim() === OPEN ? 'open' : l.trim() === DONE ? 'done' : null;
46
+ return;
47
+ }
48
+ if (/^### /.test(l)) {
49
+ if (cur) cur.end = i;
50
+ cur = null;
51
+ const m = l.match(/^### BL-(\d+)\b\s*(?:\[([^\]]*)\])?\s*(.*)$/);
52
+ if (m) {
53
+ cur = { num: parseInt(m[1], 10), type: (m[2] || '').trim(), title: m[3].trim(), start: i, end: lines.length, section: sectionName };
54
+ items.push(cur);
55
+ }
56
+ }
57
+ });
58
+ return items;
59
+ }
60
+
61
+ const FIELD = (name) => new RegExp(`^- ${name}\\s*:\\s*(.*)$`);
62
+
63
+ function getField(lines, item, name) {
64
+ for (let i = item.start + 1; i < item.end; i++) {
65
+ const m = lines[i].match(FIELD(name));
66
+ if (m) return m[1].trim();
80
67
  }
81
- const mainWt = resolveMainWt();
82
- const blNum = await appendBacklogItem(mainWt, meta);
83
- const blId = `BL-${String(blNum).padStart(3, '0')}`;
84
- process.stdout.write(JSON.stringify({ blNum, blId }) + '\n');
68
+ return null;
85
69
  }
86
70
 
87
- function doGet(rest) {
88
- const blId = rest[0];
89
- if (!blId || !/^BL-\d+$/.test(blId)) {
90
- console.error(`사용법: npx @angar2/taskery backlog-get <BL-NNN>`);
91
- process.exit(1);
71
+ // 칸 값을 바꾼다. 칸이 없으면 항목의 마지막 칸 줄 뒤에 넣는다
72
+ function setField(lines, item, name, value) {
73
+ let last = item.start;
74
+ for (let i = item.start + 1; i < item.end; i++) {
75
+ if (FIELD(name).test(lines[i])) {
76
+ lines[i] = `- ${name}: ${value}`;
77
+ return;
78
+ }
79
+ if (lines[i].trim()) last = i;
92
80
  }
93
- const mainWt = resolveMainWt();
94
- const item = parseBacklogItem(mainWt, blId);
95
- if (!item) {
96
- console.error(`${blId} 항목 없음.`);
97
- process.exit(1);
81
+ lines.splice(last + 1, 0, `- ${name}: ${value}`);
82
+ item.end++;
83
+ }
84
+
85
+ // 섹션 머리 줄 번호. 없으면 파일 끝에 만든다(끝난 항목은 열린 항목 뒤)
86
+ function ensureSection(lines, title) {
87
+ let i = lines.findIndex((l) => l.trim() === title);
88
+ if (i !== -1) return i;
89
+ while (lines.length && lines[lines.length - 1].trim() === '') lines.pop();
90
+ lines.push('', title, '');
91
+ return lines.length - 2;
92
+ }
93
+
94
+ // 섹션 맨 위(머리 줄과 빈 줄 하나 뒤)에 항목 블록을 넣는다
95
+ function insertTop(lines, title, block) {
96
+ const h = ensureSection(lines, title);
97
+ let at = h + 1;
98
+ if (lines[at] !== undefined && lines[at].trim() === '') at++;
99
+ else lines.splice(at++, 0, '');
100
+ const body = block.slice();
101
+ while (body.length && body[body.length - 1].trim() === '') body.pop();
102
+ lines.splice(at, 0, ...body, '');
103
+ }
104
+
105
+ function moveTo(lines, item, title) {
106
+ const block = lines.splice(item.start, item.end - item.start);
107
+ insertTop(lines, title, block);
108
+ }
109
+
110
+ function findItem(lines, num) {
111
+ const item = parse(lines).find((it) => it.num === num);
112
+ if (!item) L.fail(`BACKLOG.md에 BL-${num}이 없다. 'backlog-get'으로 열린 항목 번호를 확인한다.`);
113
+ return item;
114
+ }
115
+
116
+ function linkTokens(value) {
117
+ if (!value || value === NONE || value === '-') return [];
118
+ return value.split(/\s*,\s*/).filter(Boolean);
119
+ }
120
+
121
+ // 태스크를 항목에 잇는다 — 연결 태스크 칸에 더하고 상태를 진행으로. 끝난 항목이면 열린 항목으로 되돌린다(§5-3)
122
+ function linkText(text, num, label) {
123
+ const lines = text.split('\n');
124
+ const item = findItem(lines, num);
125
+ const tokens = linkTokens(getField(lines, item, '연결 태스크'));
126
+ if (!tokens.some((t) => t.split(/\s/)[0] === label)) tokens.push(label);
127
+ setField(lines, item, '연결 태스크', tokens.join(', '));
128
+ setField(lines, item, '상태', '진행');
129
+ const reopened = item.section === 'done';
130
+ if (reopened) moveTo(lines, item, OPEN);
131
+ return { text: lines.join('\n'), reopened };
132
+ }
133
+
134
+ // 닫힌 태스크의 연결 표시 — (완료)·(포기). 연결 태스크가 모두 닫히면 끝난 항목으로 옮기거나 대기로 되돌린다(§5-3)
135
+ function closeText(text, label, finished) {
136
+ const lines = text.split('\n');
137
+ const notes = [];
138
+ for (const { num } of parse(lines)) {
139
+ const item = parse(lines).find((it) => it.num === num);
140
+ const tokens = linkTokens(getField(lines, item, '연결 태스크'));
141
+ let hit = false;
142
+ const next = tokens.map((t) => {
143
+ if (t === label) {
144
+ hit = true;
145
+ return `${label} (${finished ? '완료' : '포기'})`;
146
+ }
147
+ return t;
148
+ });
149
+ if (!hit) continue;
150
+ setField(lines, item, '연결 태스크', next.join(', '));
151
+ if (next.every((t) => /\((완료|포기)\)$/.test(t))) {
152
+ if (next.some((t) => t.endsWith('(완료)'))) {
153
+ setField(lines, item, '상태', '완료');
154
+ if (item.section !== 'done') moveTo(lines, item, DONE);
155
+ notes.push(`BL-${num} — 연결 태스크가 모두 닫혀 끝난 항목으로 옮겼다`);
156
+ } else {
157
+ setField(lines, item, '상태', '대기');
158
+ notes.push(`BL-${num} — 연결 태스크가 모두 포기로 닫혀 대기로 되돌렸다`);
159
+ }
160
+ } else {
161
+ notes.push(`BL-${num} — ${label} ${finished ? '(완료)' : '(포기)'} 표시`);
162
+ }
98
163
  }
99
- process.stdout.write(JSON.stringify(item) + '\n');
164
+ return { text: lines.join('\n'), notes };
165
+ }
166
+
167
+ function itemText(lines, item) {
168
+ const block = lines.slice(item.start, item.end);
169
+ while (block.length && block[block.length - 1].trim() === '') block.pop();
170
+ return block.join('\n');
100
171
  }
101
172
 
102
- async function doMark(rest) {
103
- const blId = rest[0];
104
- const taskNum = parseTaskNum(rest[1]);
105
- if (!blId || !/^BL-\d+$/.test(blId) || !Number.isInteger(taskNum)) {
106
- console.error(`사용법: npx @angar2/taskery backlog-mark <BL-NNN> <TASK-NNN>`);
107
- process.exit(1);
173
+ // --from 목록 → 번호 배열. 없는 번호는 거부한다(태스크를 만들기 전에 확인)
174
+ function parseFromList(main, from) {
175
+ const nums = String(from)
176
+ .split(/[\s,]+/)
177
+ .filter(Boolean)
178
+ .map(parseBlNum);
179
+ const have = new Set(parse(readBacklog(main).split('\n')).map((it) => it.num));
180
+ const missing = nums.filter((n) => !have.has(n));
181
+ if (missing.length) L.fail(`prepare-task: --from의 ${missing.map((n) => `BL-${n}`).join(', ')}이 BACKLOG.md에 없다. 'backlog-get'으로 번호를 확인한다.`);
182
+ return [...new Set(nums)];
183
+ }
184
+
185
+ // prepare-task·backlog-mark가 부른다(번호 잠금 안)
186
+ function linkTasks(main, nums, label) {
187
+ let text = readBacklog(main);
188
+ const notes = [];
189
+ for (const n of nums) {
190
+ const r = linkText(text, n, label);
191
+ text = r.text;
192
+ if (r.reopened) notes.push(`BL-${n}은 끝난 항목이었다 — 열린 항목으로 되돌리고 진행으로 바꿨다`);
108
193
  }
109
- const mainWt = resolveMainWt();
110
- await markBacklogChecked(mainWt, blId, taskNum);
111
- process.stdout.write(JSON.stringify({ blId, taskNum }) + '\n');
112
- }
113
-
114
- async function main() {
115
- const op = process.argv[2];
116
- const rest = process.argv.slice(3);
117
- try {
118
- if (op === 'add') await doAdd(rest);
119
- else if (op === 'get') doGet(rest);
120
- else if (op === 'mark') await doMark(rest);
121
- else {
122
- console.error(`backlog: 알 수 없는 op '${op}' (add | get | mark)`);
123
- process.exit(1);
124
- }
125
- } catch (e) {
126
- console.error(`taskery backlog-${op} 실패: ${e.message}`);
127
- process.exit(1);
194
+ writeBacklog(main, text);
195
+ return notes;
196
+ }
197
+
198
+ // close-task가 부른다(번호 잠금 안)
199
+ function markClosed(main, label, finished) {
200
+ if (!fs.existsSync(backlogFile(main))) return [];
201
+ const { text, notes } = closeText(readBacklog(main), label, finished);
202
+ if (notes.length) writeBacklog(main, text);
203
+ return notes;
204
+ }
205
+
206
+ async function backlogAdd(ctx, a) {
207
+ const main = ctx.main;
208
+ L.requireInstalled(main);
209
+ const title = String(a.title || '').trim();
210
+ if (!title) L.fail('backlog-add: <제목>을 넣는다 — 무엇이 문제인지, 또는 무엇을 원하는지 한 줄.');
211
+ if (a.type && !L.TYPES.includes(a.type)) L.fail(`backlog-add: --type '${a.type}'을 모른다. ${L.TYPES.join(' · ')} 중 하나를 넣는다.`);
212
+ return L.withLock(main, 'number', async () => {
213
+ const lines = readBacklog(main).split('\n');
214
+ const num = parse(lines).reduce((m, it) => Math.max(m, it.num), 0) + 1;
215
+ const block = [
216
+ `### BL-${num} [${a.type || '<종류>'}] ${title}`,
217
+ '- 상태: 대기',
218
+ `- 등록: ${L.todayLocal()}`,
219
+ '- 현상: <현상>',
220
+ `- 연결 태스크: ${NONE}`,
221
+ ];
222
+ insertTop(lines, OPEN, block);
223
+ writeBacklog(main, lines.join('\n'));
224
+ const left = a.type ? '현상' : '종류·현상';
225
+ return `BL-${num}을 ${backlogFile(main)}의 열린 항목 맨 위에 넣었다.\n다음: 필수 칸(${left})의 <…>를 채운다. 선택 칸은 근거가 있을 때만 줄을 더해 적는다.`;
226
+ });
227
+ }
228
+
229
+ async function backlogGet(ctx, a) {
230
+ const main = ctx.main;
231
+ L.requireInstalled(main);
232
+ const lines = readBacklog(main).split('\n');
233
+ if (a.id) {
234
+ const item = findItem(lines, parseBlNum(a.id));
235
+ return itemText(lines, item);
128
236
  }
237
+ const open = parse(lines).filter((it) => it.section === 'open');
238
+ if (!open.length) return '열린 항목 없음.';
239
+ return [
240
+ `열린 항목 ${open.length}개 (${backlogFile(main)})`,
241
+ ...open.map((it) => `- BL-${it.num} [${it.type || '?'}] ${it.title} — ${getField(lines, it, '상태') || '?'}`),
242
+ ].join('\n');
243
+ }
244
+
245
+ async function backlogMark(ctx, a) {
246
+ const main = ctx.main;
247
+ L.requireInstalled(main);
248
+ if (!a.id) L.fail('backlog-mark: <BL-번호>와 <TASK>를 넣는다. 예: backlog-mark BL-3 TASK-012');
249
+ const num = parseBlNum(a.id);
250
+ const st = L.readState(main, L.parseTaskNum(a.task));
251
+ const label = L.taskLabel(st.num);
252
+ return L.withLock(main, 'number', async () => {
253
+ const notes = linkTasks(main, [num], label);
254
+ return [`BL-${num}에 ${label}을 연결했다 — 상태 진행.`, ...notes].join('\n');
255
+ });
129
256
  }
130
257
 
131
- main();
258
+ module.exports = { backlogAdd, backlogGet, backlogMark, parseFromList, linkTasks, markClosed, parseBlNum };