@ksoftm/create-arc 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -3
- package/bin/cli.js +542 -32
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -32,11 +32,21 @@ Requires Node ≥ 18.
|
|
|
32
32
|
| Command | What it does |
|
|
33
33
|
|---|---|
|
|
34
34
|
| `init [dir]` | Scaffold `ARC.md` + `.arc/` (index, template, standing maintenance arc, `notes/`, `archive/`). Idempotent — never overwrites. |
|
|
35
|
-
| `new "Title" [--
|
|
35
|
+
| `new "Title" [--goal …] [--task …] [--tags a,b]` | Take the next ID, create the arc, register its row. `--goal`/`--task` prefill the plan and tasks. |
|
|
36
|
+
| `start <arc>` | Set an arc to in-progress and log it. `<arc>` is an id or slug. |
|
|
37
|
+
| `task <arc> <n> [done\|start\|block\|cancel\|pending]` | Toggle a task marker; `--add "text"` appends a new task. |
|
|
38
|
+
| `block <arc> [--reason …]` | Set an arc to blocked, recording the reason in the worklog. |
|
|
39
|
+
| `refine <arc> "…"` | Fold a new instruction into an arc: append to §1, bump plan_version, log §3, set refining. |
|
|
40
|
+
| `note <arc> "…"` | Quick-append to §1 Raw Instructions (or §5 Worklog with `--worklog`). |
|
|
41
|
+
| `log <arc> [--json]` | Show the arc's worklog timeline. |
|
|
42
|
+
| `done <arc>` | Mark done, log it, move the file to `archive/`, move its index row. |
|
|
43
|
+
| `archive <arc> [--cancelled]` | Archive an arc (outcome done, or cancelled). |
|
|
44
|
+
| `show <arc> [--json]` | Print one arc's plan, tasks, and status notes. |
|
|
45
|
+
| `next [--json]` | Suggest what to work on next. |
|
|
36
46
|
| `status [dir] [--json]` | Print a table (or JSON) of every arc: ID, status, plan version, task progress, and which to resume. |
|
|
37
|
-
| `doctor [dir]` | Consistency checks — index ↔ file bijection, ID/`next_id` sanity, valid statuses. Exits non-zero on problems (CI-friendly). |
|
|
47
|
+
| `doctor [dir] [--fix]` | Consistency checks — index ↔ file bijection, ID/`next_id` sanity, valid statuses. Exits non-zero on problems (CI-friendly). `--fix` auto-repairs drift. |
|
|
38
48
|
|
|
39
|
-
Common options: `--owner NAME` (defaults to `git config user.name`); `--
|
|
49
|
+
Common options: `--owner NAME` (defaults to `git config user.name`); `--goal`/`--task` on `new`; `--reason` on `block`/`archive`; `--cancelled` on `archive`; `--fix` on `doctor`; `--json` on `status`; `--agents a,b` and `--force` on `agent-init`; `--version`, `--help`. Per-command help: `arc help <command>`.
|
|
40
50
|
|
|
41
51
|
Installs two equivalent binaries: **`arc`** and **`create-arc`**.
|
|
42
52
|
|
package/bin/cli.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import { execFileSync } from "node:child_process";
|
|
14
14
|
import {
|
|
15
|
-
existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync,
|
|
15
|
+
existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync,
|
|
16
16
|
} from "node:fs";
|
|
17
17
|
import { basename, join, resolve } from "node:path";
|
|
18
18
|
import { fileURLToPath } from "node:url";
|
|
@@ -55,19 +55,24 @@ function detectOwner(dir) {
|
|
|
55
55
|
return "user";
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
const BOOLEAN_FLAGS = new Set(["json", "help", "version"]);
|
|
58
|
+
const BOOLEAN_FLAGS = new Set(["json", "help", "version", "fix", "force", "cancelled", "cancel", "worklog"]);
|
|
59
|
+
const REPEATABLE_FLAGS = new Set(["task"]);
|
|
59
60
|
|
|
60
61
|
function parseArgv(argv) {
|
|
61
62
|
const flags = {}; const pos = [];
|
|
63
|
+
const set = (key, val) => {
|
|
64
|
+
if (REPEATABLE_FLAGS.has(key)) (flags[key] ??= []).push(val);
|
|
65
|
+
else flags[key] = val;
|
|
66
|
+
};
|
|
62
67
|
for (let i = 0; i < argv.length; i++) {
|
|
63
68
|
const a = argv[i];
|
|
64
69
|
if (a.startsWith("--")) {
|
|
65
70
|
const eq = a.indexOf("=");
|
|
66
|
-
if (eq > -1) {
|
|
71
|
+
if (eq > -1) { set(a.slice(2, eq), a.slice(eq + 1)); continue; }
|
|
67
72
|
const key = a.slice(2);
|
|
68
73
|
const next = argv[i + 1];
|
|
69
74
|
if (!BOOLEAN_FLAGS.has(key) && next !== undefined && !next.startsWith("--")) {
|
|
70
|
-
|
|
75
|
+
set(key, next); i++; // --owner NAME form
|
|
71
76
|
} else flags[key] = true; // --json / trailing flag form
|
|
72
77
|
} else pos.push(a);
|
|
73
78
|
}
|
|
@@ -92,6 +97,175 @@ function listArcFiles(arcDir) {
|
|
|
92
97
|
return { active: ls(arcDir), archived: ls(join(arcDir, "archive")) };
|
|
93
98
|
}
|
|
94
99
|
|
|
100
|
+
/* ---------------------- shared arc-mutation helpers ---------------------- */
|
|
101
|
+
|
|
102
|
+
// Resolve a project dir's .arc, failing clearly if uninitialized.
|
|
103
|
+
function arcDirOf(flags, target) {
|
|
104
|
+
const dir = resolve(flags?.dir ?? target ?? ".");
|
|
105
|
+
const arcDir = join(dir, ".arc");
|
|
106
|
+
return { dir, arcDir, exists: existsSync(arcDir) };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Find an arc file by id ("ARC-0007" / "7" / "0007") or by a slug substring.
|
|
110
|
+
// Searches active first, then archive. Returns { path, archived } or null.
|
|
111
|
+
function findArc(arcDir, ref) {
|
|
112
|
+
const { active, archived } = listArcFiles(arcDir);
|
|
113
|
+
const all = [...active.map((p) => ({ p, archived: false })),
|
|
114
|
+
...archived.map((p) => ({ p, archived: true }))];
|
|
115
|
+
if (!ref) return null;
|
|
116
|
+
const raw = String(ref).trim();
|
|
117
|
+
const num = raw.replace(/^ARC-/i, "").replace(/[^0-9]/g, "");
|
|
118
|
+
const id = num ? `ARC-${num.padStart(4, "0")}` : null;
|
|
119
|
+
// exact id match on filename
|
|
120
|
+
if (id) {
|
|
121
|
+
const hit = all.find(({ p }) => basename(p).toUpperCase().startsWith(id));
|
|
122
|
+
if (hit) return { path: hit.p, archived: hit.archived };
|
|
123
|
+
}
|
|
124
|
+
// slug substring match (case-insensitive), unique-or-first
|
|
125
|
+
const matches = all.filter(({ p }) => basename(p).toLowerCase().includes(raw.toLowerCase()));
|
|
126
|
+
if (matches.length) return { path: matches[0].p, archived: matches[0].archived };
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Set a frontmatter field, bump `updated` to today, and (optionally) sync the
|
|
131
|
+
// INDEX.md row for this arc. Returns the new frontmatter values of interest.
|
|
132
|
+
function updateArcFrontmatter(arcPath, changes) {
|
|
133
|
+
let text = readText(arcPath);
|
|
134
|
+
const fm = text.match(/^---\n([\s\S]*?)\n---/);
|
|
135
|
+
if (!fm) return fail(`${basename(arcPath)}: no YAML frontmatter`);
|
|
136
|
+
let front = fm[1];
|
|
137
|
+
for (const [k, v] of Object.entries(changes)) front = setField(front, k, v);
|
|
138
|
+
if (!("updated" in changes)) front = setField(front, "updated", today());
|
|
139
|
+
text = text.slice(0, fm.index) + `---\n${front}\n---` + text.slice(fm.index + fm[0].length);
|
|
140
|
+
writeFileSync(arcPath, text);
|
|
141
|
+
return front;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Sync the INDEX.md row (status, plan v, updated) for a given arc id.
|
|
145
|
+
function syncIndexRow(arcDir, id, { status, planVersion } = {}) {
|
|
146
|
+
const indexPath = join(arcDir, "INDEX.md");
|
|
147
|
+
if (!existsSync(indexPath)) return;
|
|
148
|
+
const lines = readText(indexPath).split("\n");
|
|
149
|
+
let changed = false;
|
|
150
|
+
for (let i = 0; i < lines.length; i++) {
|
|
151
|
+
const cells = lines[i].split("|");
|
|
152
|
+
if (cells.length >= 8 && cells[1].trim().toUpperCase() === id) {
|
|
153
|
+
if (status !== undefined) cells[3] = ` ${status} `;
|
|
154
|
+
if (planVersion !== undefined) cells[4] = ` ${planVersion} `;
|
|
155
|
+
cells[5] = ` ${today()} `;
|
|
156
|
+
lines[i] = cells.join("|");
|
|
157
|
+
changed = true;
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (changed) writeFileSync(indexPath, lines.join("\n"));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Move an arc row from the Active table to the Archived table in INDEX.md.
|
|
165
|
+
function moveIndexRowToArchived(arcDir, id, outcome) {
|
|
166
|
+
const indexPath = join(arcDir, "INDEX.md");
|
|
167
|
+
if (!existsSync(indexPath)) return;
|
|
168
|
+
const lines = readText(indexPath).split("\n");
|
|
169
|
+
const archivedHeadIdx = lines.findIndex((l) => /^##\s+Archived/i.test(l));
|
|
170
|
+
let rowIdx = -1;
|
|
171
|
+
for (let i = 0; i < lines.length; i++) {
|
|
172
|
+
if (archivedHeadIdx !== -1 && i >= archivedHeadIdx) break;
|
|
173
|
+
const cells = lines[i].split("|");
|
|
174
|
+
if (cells.length >= 8 && cells[1].trim().toUpperCase() === id) { rowIdx = i; break; }
|
|
175
|
+
}
|
|
176
|
+
if (rowIdx === -1) return;
|
|
177
|
+
const cells = lines[rowIdx].split("|");
|
|
178
|
+
const title = cells[2].trim();
|
|
179
|
+
const file = cells[7].trim(); // [name](path)
|
|
180
|
+
const archivedRow = `| ${id} | ${title} | ${outcome} | ${today()} | ${file} |`;
|
|
181
|
+
lines.splice(rowIdx, 1); // remove from Active
|
|
182
|
+
// re-find Archived head (index shifted), then drop a placeholder "— | —" row if present
|
|
183
|
+
let head = lines.findIndex((l) => /^##\s+Archived/i.test(l));
|
|
184
|
+
if (head === -1) { lines.push("", "## Archived", "", "| ID | Title | Outcome | Closed | File |", "|---|---|---|---|---|"); head = lines.length - 2; }
|
|
185
|
+
// insert after the Archived table separator (first |---| after head)
|
|
186
|
+
let sep = -1;
|
|
187
|
+
for (let i = head + 1; i < lines.length; i++) {
|
|
188
|
+
if (/^\|?\s*:?-{2,}/.test(lines[i])) { sep = i; break; }
|
|
189
|
+
}
|
|
190
|
+
const insertAt = sep !== -1 ? sep : head;
|
|
191
|
+
// remove an empty placeholder archived row ("| — | — | …")
|
|
192
|
+
if (lines[insertAt + 1] && /^\|\s*—\s*\|/.test(lines[insertAt + 1])) lines.splice(insertAt + 1, 1);
|
|
193
|
+
lines.splice(insertAt + 1, 0, archivedRow);
|
|
194
|
+
writeFileSync(indexPath, lines.join("\n"));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Append a worklog entry under "## 5 · Worklog".
|
|
198
|
+
function appendWorklog(arcPath, note) {
|
|
199
|
+
let text = readText(arcPath);
|
|
200
|
+
const stamp = new Date().toISOString().slice(0, 16).replace("T", " ");
|
|
201
|
+
const entry = `\n### ${stamp} — ${note}\n`;
|
|
202
|
+
const m = text.match(/^## 5 · Worklog\n/m);
|
|
203
|
+
if (!m) { writeFileSync(arcPath, text + entry); return; }
|
|
204
|
+
// insert right after the Worklog heading (and its leading HTML comment if present)
|
|
205
|
+
const afterHead = m.index + m[0].length;
|
|
206
|
+
text = text.slice(0, afterHead) + entry + text.slice(afterHead);
|
|
207
|
+
writeFileSync(arcPath, text);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Get the current plan_version int from an arc.
|
|
211
|
+
function planVersionOf(arcPath) {
|
|
212
|
+
return parseInt(field(frontmatter(readText(arcPath)), "plan_version", "1"), 10) || 1;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Count existing instructions (I1, I2, …) in §1 to compute the next index.
|
|
216
|
+
function nextInstructionIndex(text) {
|
|
217
|
+
const nums = [...text.matchAll(/^### I(\d+)\b/gm)].map((m) => parseInt(m[1], 10));
|
|
218
|
+
return (nums.length ? Math.max(...nums) : 0) + 1;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Append a verbatim instruction to "## 1 · Raw Instructions". Returns the new I-index.
|
|
222
|
+
function appendInstruction(arcPath, instruction, source = "chat") {
|
|
223
|
+
let text = readText(arcPath);
|
|
224
|
+
const idx = nextInstructionIndex(text);
|
|
225
|
+
const quoted = String(instruction).trim().split("\n").map((l) => `> ${l}`).join("\n");
|
|
226
|
+
const entry = `\n### I${idx} — ${today()} (source: ${source})\n${quoted}\n`;
|
|
227
|
+
const sec = text.match(/(^## 1 · Raw Instructions\n)([\s\S]*?)(?=^## )/m);
|
|
228
|
+
if (!sec) { writeFileSync(arcPath, text + entry); return idx; }
|
|
229
|
+
// append at the end of §1, before the next "## "
|
|
230
|
+
const insertAt = sec.index + sec[1].length + sec[2].replace(/\n*$/, "\n").length;
|
|
231
|
+
const head = text.slice(0, sec.index + sec[1].length);
|
|
232
|
+
const bodyTrimmed = sec[2].replace(/\n+$/, "\n");
|
|
233
|
+
text = head + bodyTrimmed + entry + "\n" + text.slice(sec.index + sec[0].length);
|
|
234
|
+
writeFileSync(arcPath, text);
|
|
235
|
+
return idx;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Append a Refinement Log entry under "## 3 · Refinement Log" for a version bump.
|
|
239
|
+
function appendRefinement(arcPath, version, changed, instructionIdx) {
|
|
240
|
+
let text = readText(arcPath);
|
|
241
|
+
const trigger = instructionIdx ? ` — triggered by I${instructionIdx}` : "";
|
|
242
|
+
const entry = `\n### v${version} — ${today()}${trigger}\n- changed: ${changed}\n`;
|
|
243
|
+
const m = text.match(/^## 3 · Refinement Log\n/m);
|
|
244
|
+
if (!m) { writeFileSync(arcPath, text + entry); return; }
|
|
245
|
+
// insert right after the heading + its leading HTML comment (the example block)
|
|
246
|
+
let afterHead = m.index + m[0].length;
|
|
247
|
+
const rest = text.slice(afterHead);
|
|
248
|
+
const lead = rest.match(/^(\s*<!--[\s\S]*?-->\n)/); // skip the template's example comment
|
|
249
|
+
if (lead) afterHead += lead[0].length;
|
|
250
|
+
text = text.slice(0, afterHead) + entry + text.slice(afterHead);
|
|
251
|
+
writeFileSync(arcPath, text);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Extract worklog entries (### timestamp — note + bullet lines) from §5.
|
|
255
|
+
function readWorklog(arcPath) {
|
|
256
|
+
const text = readText(arcPath);
|
|
257
|
+
const sec = text.match(/^## 5 · Worklog\n([\s\S]*?)(?=^## |\Z)/m)?.[1] ?? "";
|
|
258
|
+
const body = sec.replace(/<!--[\s\S]*?-->/g, "").trim();
|
|
259
|
+
if (!body) return [];
|
|
260
|
+
// split on entry headers, keep the header text
|
|
261
|
+
const parts = body.split(/(?=^### )/m).map((s) => s.trim()).filter(Boolean);
|
|
262
|
+
return parts.map((p) => {
|
|
263
|
+
const head = p.match(/^### (.+)/)?.[1] ?? "";
|
|
264
|
+
const lines = p.split("\n").slice(1).map((l) => l.trim()).filter(Boolean);
|
|
265
|
+
return { head, lines };
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
95
269
|
/* -------------------------------- commands ------------------------------- */
|
|
96
270
|
|
|
97
271
|
function cmdInit(target, flags) {
|
|
@@ -160,6 +334,18 @@ function cmdNew(title, flags) {
|
|
|
160
334
|
front = setField(front, "tags", `[${tags}]`);
|
|
161
335
|
}
|
|
162
336
|
body = body.replace("# ARC-0000 · <Title>", `# ${id} · ${title}`);
|
|
337
|
+
|
|
338
|
+
// Optional prefill: --goal sets the Plan goal line; --task adds first tasks.
|
|
339
|
+
if (flags.goal) {
|
|
340
|
+
body = body.replace(/\*\*Goal:\*\* <[^>]*>/, `**Goal:** ${String(flags.goal).trim()}`);
|
|
341
|
+
}
|
|
342
|
+
const tasks = flags.task ? (Array.isArray(flags.task) ? flags.task : [flags.task]) : [];
|
|
343
|
+
if (tasks.length) {
|
|
344
|
+
const block = tasks.map((t, i) => `- [ ] T${i + 1} ${String(t).trim()}`).join("\n");
|
|
345
|
+
// Replace the placeholder "- [ ] T1 <…>" lines under "## 4 · Tasks" with real tasks.
|
|
346
|
+
body = body.replace(/^- \[ \] T\d+ <[^>]*>(?:\n- \[ \] T\d+ <[^>]*>)*/m, block);
|
|
347
|
+
}
|
|
348
|
+
|
|
163
349
|
writeFileSync(dest, `---\n${front}\n---\n${body}`);
|
|
164
350
|
|
|
165
351
|
// update the index: bump next_id, insert registry row
|
|
@@ -272,16 +458,18 @@ function cmdStatus(target, flags) {
|
|
|
272
458
|
return 0;
|
|
273
459
|
}
|
|
274
460
|
|
|
275
|
-
function cmdDoctor(target) {
|
|
276
|
-
const dir = resolve(target ?? ".");
|
|
461
|
+
function cmdDoctor(target, flags = {}) {
|
|
462
|
+
const dir = resolve(flags.dir ?? target ?? ".");
|
|
277
463
|
const arcDir = join(dir, ".arc");
|
|
278
464
|
const indexPath = join(arcDir, "INDEX.md");
|
|
279
|
-
|
|
465
|
+
const doFix = !!flags.fix;
|
|
466
|
+
let failures = 0, warnings = 0, fixed = 0;
|
|
280
467
|
const ok = (msg) => console.log(` OK ${msg}`);
|
|
281
468
|
const bad = (msg) => { console.log(` FAIL ${msg}`); failures++; };
|
|
282
469
|
const warn = (msg) => { console.log(` WARN ${msg}`); warnings++; };
|
|
470
|
+
const fix = (msg) => { console.log(` FIX ${msg}`); fixed++; };
|
|
283
471
|
|
|
284
|
-
if (!existsSync(arcDir)) return fail(`${arcDir} not found — run \`
|
|
472
|
+
if (!existsSync(arcDir)) return fail(`${arcDir} not found — run \`arc init\` first`);
|
|
285
473
|
if (!existsSync(indexPath)) { bad(".arc/INDEX.md missing"); return finish(); }
|
|
286
474
|
|
|
287
475
|
const index = readText(indexPath);
|
|
@@ -292,19 +480,39 @@ function cmdDoctor(target) {
|
|
|
292
480
|
const nm = index.match(/^next_id:\s*ARC-(\d+)\s*$/m);
|
|
293
481
|
const maxId = Math.max(-1, ...all.map(({ p }) => parseInt(basename(p).match(/^ARC-(\d+)/)?.[1] ?? "-1", 10)));
|
|
294
482
|
if (!nm) bad("next_id missing or malformed in INDEX.md");
|
|
295
|
-
else if (parseInt(nm[1], 10) <= maxId)
|
|
296
|
-
|
|
483
|
+
else if (parseInt(nm[1], 10) <= maxId) {
|
|
484
|
+
if (doFix) {
|
|
485
|
+
const next = `ARC-${String(maxId + 1).padStart(4, "0")}`;
|
|
486
|
+
writeFileSync(indexPath, readText(indexPath).replace(/^next_id:\s*ARC-\d+\s*$/m, `next_id: ${next}`));
|
|
487
|
+
fix(`next_id → ${next} (was ARC-${nm[1]})`);
|
|
488
|
+
} else bad(`next_id ARC-${nm[1]} is not greater than highest existing arc ARC-${String(maxId).padStart(4, "0")}`);
|
|
489
|
+
} else ok(`next_id ARC-${nm[1]} > highest arc id`);
|
|
490
|
+
|
|
491
|
+
// index row status map (id → status cell), for status-drift repair
|
|
492
|
+
const rowStatus = new Map();
|
|
493
|
+
for (const line of index.split("\n")) {
|
|
494
|
+
const cells = line.split("|");
|
|
495
|
+
if (cells.length >= 8 && /^ARC-\d{4}$/.test(cells[1].trim())) rowStatus.set(cells[1].trim(), cells[3].trim());
|
|
496
|
+
}
|
|
297
497
|
|
|
298
498
|
// file <-> index bijection, frontmatter integrity
|
|
299
499
|
const indexIds = new Set([...index.matchAll(/^\|\s*(ARC-\d{4})\s*\|/gm)].map((x) => x[1]));
|
|
300
|
-
for (const { p } of all) {
|
|
500
|
+
for (const { p, archived: isArch } of all) {
|
|
301
501
|
const fileId = basename(p).match(/^(ARC-\d{4})/)?.[1];
|
|
302
502
|
const front = frontmatter(readText(p));
|
|
303
503
|
const fmId = field(front, "id");
|
|
304
504
|
const status = field(front, "status");
|
|
305
|
-
if (fmId !== fileId)
|
|
505
|
+
if (fmId !== fileId) {
|
|
506
|
+
if (doFix) { updateArcFrontmatter(p, { id: fileId }); fix(`${basename(p)}: frontmatter id '${fmId}' → '${fileId}'`); }
|
|
507
|
+
else bad(`${basename(p)}: frontmatter id '${fmId}' != filename id '${fileId}'`);
|
|
508
|
+
}
|
|
306
509
|
if (!VALID_STATUSES.has(status)) bad(`${basename(p)}: invalid status '${status}'`);
|
|
307
510
|
if (!indexIds.has(fileId)) bad(`${basename(p)}: no row in INDEX.md`);
|
|
511
|
+
// status drift: active arc's index cell disagrees with its frontmatter
|
|
512
|
+
else if (!isArch && VALID_STATUSES.has(status) && rowStatus.get(fileId) && rowStatus.get(fileId) !== status) {
|
|
513
|
+
if (doFix) { syncIndexRow(arcDir, fileId, { status }); fix(`${fileId}: index status '${rowStatus.get(fileId)}' → '${status}'`); }
|
|
514
|
+
else warn(`${fileId}: index status '${rowStatus.get(fileId)}' != frontmatter '${status}' (use --fix)`);
|
|
515
|
+
}
|
|
308
516
|
const inProg = [...readText(p).matchAll(/^- \[>\]/gm)].length;
|
|
309
517
|
if (inProg > 2) warn(`${basename(p)}: ${inProg} tasks marked [>] — keep at most 1–2 in progress`);
|
|
310
518
|
}
|
|
@@ -315,13 +523,242 @@ function cmdDoctor(target) {
|
|
|
315
523
|
return finish();
|
|
316
524
|
|
|
317
525
|
function finish() {
|
|
526
|
+
const fx = fixed ? `, ${fixed} fixed` : "";
|
|
318
527
|
console.log(failures
|
|
319
|
-
? `\ndoctor: ${failures} problem(s), ${warnings} warning(s)`
|
|
320
|
-
: `\ndoctor: healthy (${warnings} warning(s))`);
|
|
528
|
+
? `\ndoctor: ${failures} problem(s), ${warnings} warning(s)${fx}`
|
|
529
|
+
: `\ndoctor: healthy (${warnings} warning(s)${fx})`);
|
|
321
530
|
return failures ? 1 : 0;
|
|
322
531
|
}
|
|
323
532
|
}
|
|
324
533
|
|
|
534
|
+
/* ------------------------ lifecycle / task commands ----------------------- */
|
|
535
|
+
|
|
536
|
+
// Shared: resolve an arc by reference for a mutating command.
|
|
537
|
+
function resolveForMutation(ref, flags) {
|
|
538
|
+
const { arcDir, exists } = arcDirOf(flags);
|
|
539
|
+
if (!exists) return { err: fail("`.arc` not found — run `arc init` first") };
|
|
540
|
+
if (!ref) return { err: fail("which arc? pass an id or slug, e.g. `ARC-0007` or `rate-limit`") };
|
|
541
|
+
const found = findArc(arcDir, ref);
|
|
542
|
+
if (!found) return { err: fail(`no arc matching '${ref}' (try \`arc status\` to list them)`) };
|
|
543
|
+
const id = basename(found.path).match(/^(ARC-\d{4})/)?.[1];
|
|
544
|
+
return { arcDir, path: found.path, archived: found.archived, id };
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function setStatus(ref, flags, status, { note } = {}) {
|
|
548
|
+
const r = resolveForMutation(ref, flags);
|
|
549
|
+
if (r.err) return r.err;
|
|
550
|
+
if (r.archived) return fail(`${r.id} is archived — restore it first`);
|
|
551
|
+
updateArcFrontmatter(r.path, { status });
|
|
552
|
+
syncIndexRow(r.arcDir, r.id, { status });
|
|
553
|
+
if (note) appendWorklog(r.path, note);
|
|
554
|
+
console.log(`${r.id} → ${status}`);
|
|
555
|
+
return 0;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const cmdStart = (ref, flags) => setStatus(ref, flags, "in-progress", { note: "started (status → in-progress)" });
|
|
559
|
+
const cmdReview = (ref, flags) => setStatus(ref, flags, "review", { note: "moved to review" });
|
|
560
|
+
|
|
561
|
+
function cmdBlock(ref, flags) {
|
|
562
|
+
const reason = flags.reason || flags.r;
|
|
563
|
+
return setStatus(ref, flags, "blocked", {
|
|
564
|
+
note: reason ? `blocked: ${reason}` : "blocked",
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function cmdDone(ref, flags) {
|
|
569
|
+
const r = resolveForMutation(ref, flags);
|
|
570
|
+
if (r.err) return r.err;
|
|
571
|
+
if (r.archived) { console.log(`${r.id} is already archived`); return 0; }
|
|
572
|
+
updateArcFrontmatter(r.path, { status: "done" });
|
|
573
|
+
appendWorklog(r.path, "completed (status → done)");
|
|
574
|
+
// move file into archive/ and move the INDEX row to Archived
|
|
575
|
+
const archiveDir = join(r.arcDir, "archive");
|
|
576
|
+
mkdirSync(archiveDir, { recursive: true });
|
|
577
|
+
const destPath = join(archiveDir, basename(r.path));
|
|
578
|
+
renameSync(r.path, destPath);
|
|
579
|
+
moveIndexRowToArchived(r.arcDir, r.id, "done");
|
|
580
|
+
console.log(`${r.id} → done · moved to .arc/archive/${basename(r.path)}`);
|
|
581
|
+
return 0;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function cmdArchive(ref, flags) {
|
|
585
|
+
const r = resolveForMutation(ref, flags);
|
|
586
|
+
if (r.err) return r.err;
|
|
587
|
+
if (r.archived) { console.log(`${r.id} is already archived`); return 0; }
|
|
588
|
+
const outcome = flags.cancelled || flags.cancel ? "cancelled" : "done";
|
|
589
|
+
if (outcome === "cancelled") {
|
|
590
|
+
updateArcFrontmatter(r.path, { status: "cancelled" });
|
|
591
|
+
appendWorklog(r.path, flags.reason ? `cancelled: ${flags.reason}` : "cancelled");
|
|
592
|
+
}
|
|
593
|
+
const archiveDir = join(r.arcDir, "archive");
|
|
594
|
+
mkdirSync(archiveDir, { recursive: true });
|
|
595
|
+
renameSync(r.path, join(archiveDir, basename(r.path)));
|
|
596
|
+
moveIndexRowToArchived(r.arcDir, r.id, outcome);
|
|
597
|
+
console.log(`${r.id} → ${outcome} · archived`);
|
|
598
|
+
return 0;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// arc task <ref> <n> [done|start|block|cancel|pending] — toggle one task marker
|
|
602
|
+
// arc task <ref> --add "text" — append a new task
|
|
603
|
+
function cmdTask(ref, flags, rest) {
|
|
604
|
+
const r = resolveForMutation(ref, flags);
|
|
605
|
+
if (r.err) return r.err;
|
|
606
|
+
let text = readText(r.path);
|
|
607
|
+
const secRe = /(^## 4 · Tasks\n)([\s\S]*?)(?=^## |\Z)/m;
|
|
608
|
+
const sec = text.match(secRe);
|
|
609
|
+
if (!sec) return fail(`${r.id}: no "## 4 · Tasks" section`);
|
|
610
|
+
let body = sec[2];
|
|
611
|
+
|
|
612
|
+
if (flags.add) {
|
|
613
|
+
const nums = [...body.matchAll(/^- \[[ >x!-]\]\s*T(\d+)/gm)].map((m) => parseInt(m[1], 10));
|
|
614
|
+
const next = (nums.length ? Math.max(...nums) : 0) + 1;
|
|
615
|
+
const line = `- [ ] T${next} ${String(flags.add).trim()}\n`;
|
|
616
|
+
body = body.replace(/\n*$/, "\n") + line;
|
|
617
|
+
text = text.slice(0, sec.index) + sec[1] + body + text.slice(sec.index + sec[0].length);
|
|
618
|
+
writeFileSync(r.path, text);
|
|
619
|
+
updateArcFrontmatter(r.path, {});
|
|
620
|
+
console.log(`${r.id}: added T${next}`);
|
|
621
|
+
return 0;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
const n = rest?.[0];
|
|
625
|
+
const action = (rest?.[1] || "done").toLowerCase();
|
|
626
|
+
if (!n) return fail("usage: arc task <arc> <task-number> [done|start|block|cancel|pending] (or --add \"text\")");
|
|
627
|
+
const marker = { done: "x", start: ">", block: "!", cancel: "-", pending: " " }[action];
|
|
628
|
+
if (marker === undefined) return fail(`unknown task action '${action}'`);
|
|
629
|
+
const tnum = String(n).replace(/[^0-9]/g, "");
|
|
630
|
+
const re = new RegExp(`^(- \\[)[ >x!-](\\]\\s*T${tnum}\\b.*)$`, "m");
|
|
631
|
+
if (!re.test(body)) return fail(`${r.id}: task T${tnum} not found`);
|
|
632
|
+
body = body.replace(re, `$1${marker}$2`);
|
|
633
|
+
text = text.slice(0, sec.index) + sec[1] + body + text.slice(sec.index + sec[0].length);
|
|
634
|
+
writeFileSync(r.path, text);
|
|
635
|
+
updateArcFrontmatter(r.path, {});
|
|
636
|
+
console.log(`${r.id}: T${tnum} → [${marker}]`);
|
|
637
|
+
return 0;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// arc refine <arc> "instruction" [--changed "…"] [--source chat|voice|issue|review]
|
|
641
|
+
// Appends the instruction verbatim to §1, bumps plan_version, logs §3, sets refining.
|
|
642
|
+
function cmdRefine(ref, flags, rest) {
|
|
643
|
+
const r = resolveForMutation(ref, flags);
|
|
644
|
+
if (r.err) return r.err;
|
|
645
|
+
if (r.archived) return fail(`${r.id} is archived — restore it before refining`);
|
|
646
|
+
const instruction = (rest || []).join(" ").trim() || (typeof flags.note === "string" ? flags.note : "");
|
|
647
|
+
if (!instruction) return fail('what changed? usage: arc refine <arc> "the new instruction" [--changed "plan delta"]');
|
|
648
|
+
|
|
649
|
+
const iIdx = appendInstruction(r.path, instruction, flags.source || "chat");
|
|
650
|
+
const nextVer = planVersionOf(r.path) + 1;
|
|
651
|
+
const changed = (typeof flags.changed === "string" && flags.changed) || instruction;
|
|
652
|
+
appendRefinement(r.path, nextVer, changed, iIdx);
|
|
653
|
+
updateArcFrontmatter(r.path, { plan_version: nextVer, status: "refining" });
|
|
654
|
+
syncIndexRow(r.arcDir, r.id, { status: "refining", planVersion: nextVer });
|
|
655
|
+
appendWorklog(r.path, `refined to plan v${nextVer} (I${iIdx})`);
|
|
656
|
+
console.log(`${r.id} → refining · plan v${nextVer} · recorded I${iIdx}`);
|
|
657
|
+
console.log(`Next: update §2 Plan to reflect v${nextVer}, then adjust Tasks.`);
|
|
658
|
+
return 0;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// arc note <arc> "text" [--worklog] — quick-append an instruction (default) or a worklog note.
|
|
662
|
+
function cmdNote(ref, flags, rest) {
|
|
663
|
+
const r = resolveForMutation(ref, flags);
|
|
664
|
+
if (r.err) return r.err;
|
|
665
|
+
const note = (rest || []).join(" ").trim() || (typeof flags.note === "string" ? flags.note : "");
|
|
666
|
+
if (!note) return fail('usage: arc note <arc> "text" [--worklog]');
|
|
667
|
+
if (flags.worklog) {
|
|
668
|
+
appendWorklog(r.path, note);
|
|
669
|
+
updateArcFrontmatter(r.path, {});
|
|
670
|
+
console.log(`${r.id}: worklog note added`);
|
|
671
|
+
} else {
|
|
672
|
+
const iIdx = appendInstruction(r.path, note, flags.source || "chat");
|
|
673
|
+
updateArcFrontmatter(r.path, {});
|
|
674
|
+
console.log(`${r.id}: recorded I${iIdx} in Raw Instructions`);
|
|
675
|
+
}
|
|
676
|
+
return 0;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// arc log <arc> [--json] — show an arc's worklog timeline.
|
|
680
|
+
function cmdLog(ref, flags) {
|
|
681
|
+
const r = resolveForMutation(ref, flags);
|
|
682
|
+
if (r.err) return r.err;
|
|
683
|
+
const entries = readWorklog(r.path);
|
|
684
|
+
const a = parseArc(r.path, r.archived);
|
|
685
|
+
if (flags.json) { console.log(JSON.stringify({ id: a.id, title: a.title, worklog: entries }, null, 2)); return 0; }
|
|
686
|
+
console.log(`${a.id} · ${a.title} — worklog (${entries.length} entr${entries.length === 1 ? "y" : "ies"})`);
|
|
687
|
+
if (!entries.length) { console.log(" (no worklog entries yet)"); return 0; }
|
|
688
|
+
for (const e of entries) {
|
|
689
|
+
console.log(`\n• ${e.head}`);
|
|
690
|
+
for (const l of e.lines) console.log(` ${l}`);
|
|
691
|
+
}
|
|
692
|
+
return 0;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// arc show <ref> — print one arc's plan, tasks, and status notes.
|
|
696
|
+
function cmdShow(ref, flags) {
|
|
697
|
+
const r = resolveForMutation(ref, flags);
|
|
698
|
+
if (r.err) return r.err;
|
|
699
|
+
const text = readText(r.path);
|
|
700
|
+
const a = parseArc(r.path, r.archived);
|
|
701
|
+
const sec = (n, title) => text.match(new RegExp(`^## ${n} · ${title}\\n([\\s\\S]*?)(?=^## |\\Z)`, "m"))?.[1]?.trim() ?? "";
|
|
702
|
+
if (flags.json) {
|
|
703
|
+
const clean = (s) => s.replace(/<!--[\s\S]*?-->/g, "").trim();
|
|
704
|
+
console.log(JSON.stringify({
|
|
705
|
+
...a,
|
|
706
|
+
plan: clean(sec(2, "Plan \\(current[^)]*\\)") || sec(2, "Plan.*")),
|
|
707
|
+
tasks: clean(sec(4, "Tasks")),
|
|
708
|
+
status_notes: clean(sec(6, "Status Notes")),
|
|
709
|
+
}, null, 2));
|
|
710
|
+
return 0;
|
|
711
|
+
}
|
|
712
|
+
console.log(`${a.id} · ${a.title}`);
|
|
713
|
+
console.log(`status: ${a.status}${r.archived ? " (archived)" : ""} · plan v${a.plan_version} · tasks ${a.tasks_total ? `${a.tasks_done}/${a.tasks_total}` : "—"} · updated ${a.updated}`);
|
|
714
|
+
const plan = sec(2, "Plan \\(current[^)]*\\)") || sec(2, "Plan.*");
|
|
715
|
+
if (plan) { console.log("\n— Plan —"); console.log(plan.replace(/<!--[\s\S]*?-->/g, "").trim()); }
|
|
716
|
+
const tasks = sec(4, "Tasks");
|
|
717
|
+
if (tasks) { console.log("\n— Tasks —"); console.log(tasks.replace(/<!--[\s\S]*?-->/g, "").trim()); }
|
|
718
|
+
const notes = sec(6, "Status Notes");
|
|
719
|
+
if (notes) { console.log("\n— Status —"); console.log(notes.replace(/<!--[\s\S]*?-->/g, "").trim()); }
|
|
720
|
+
console.log(`\nfile: ${r.path}`);
|
|
721
|
+
return 0;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// arc next — suggest what to work on (active_focus, then in-progress, then planned).
|
|
725
|
+
function cmdNext(flags) {
|
|
726
|
+
const { arcDir, exists } = arcDirOf(flags);
|
|
727
|
+
if (!exists) return fail("`.arc` not found — run `arc init` first");
|
|
728
|
+
const { active } = listArcFiles(arcDir);
|
|
729
|
+
if (!active.length) return (console.log("no active arcs — `arc new \"Title\"` to begin"), 0);
|
|
730
|
+
const arcs = active.map((p) => parseArc(p, false))
|
|
731
|
+
// The standing maintenance arc (ARC-0000) is always in-progress; only surface
|
|
732
|
+
// it as "next" when it actually has unfinished tasks and nothing else is active.
|
|
733
|
+
.filter((a) => !(a.id === "ARC-0000" && a.tasks_in_progress === 0));
|
|
734
|
+
if (!arcs.length) return (console.log("no actionable arcs — `arc new \"Title\"` to begin"), 0);
|
|
735
|
+
const idx = existsSync(join(arcDir, "INDEX.md")) ? readText(join(arcDir, "INDEX.md")) : "";
|
|
736
|
+
const focus = idx.match(/^active_focus:\s*(.+)$/m)?.[1]?.trim();
|
|
737
|
+
|
|
738
|
+
const byStatus = (s) => arcs.filter((a) => a.status === s);
|
|
739
|
+
const pick =
|
|
740
|
+
(focus && focus !== "—" && arcs.find((a) => a.id === focus || a.title.includes(focus))) ||
|
|
741
|
+
byStatus("in-progress")[0] || byStatus("refining")[0] ||
|
|
742
|
+
byStatus("planned")[0] || byStatus("review")[0] || byStatus("draft")[0];
|
|
743
|
+
|
|
744
|
+
if (flags.json) {
|
|
745
|
+
console.log(JSON.stringify({
|
|
746
|
+
next: pick ? { id: pick.id, title: pick.title, status: pick.status,
|
|
747
|
+
tasks_done: pick.tasks_done, tasks_total: pick.tasks_total,
|
|
748
|
+
tasks_blocked: pick.tasks_blocked } : null,
|
|
749
|
+
blocked: byStatus("blocked").map((a) => a.id),
|
|
750
|
+
}, null, 2));
|
|
751
|
+
return 0;
|
|
752
|
+
}
|
|
753
|
+
if (!pick) { console.log("nothing actionable — all arcs are blocked or done"); return 0; }
|
|
754
|
+
console.log(`next: ${pick.id} · ${pick.title} [${pick.status}]`);
|
|
755
|
+
console.log(` tasks ${pick.tasks_total ? `${pick.tasks_done}/${pick.tasks_total}` : "—"}${pick.tasks_blocked ? ` · ${pick.tasks_blocked} blocked` : ""}`);
|
|
756
|
+
console.log(` open: arc show ${pick.id}`);
|
|
757
|
+
const blocked = byStatus("blocked");
|
|
758
|
+
if (blocked.length) console.log(` (blocked: ${blocked.map((a) => a.id).join(", ")})`);
|
|
759
|
+
return 0;
|
|
760
|
+
}
|
|
761
|
+
|
|
325
762
|
/* --------------------------------- main ----------------------------------- */
|
|
326
763
|
|
|
327
764
|
function fail(msg) { console.error(`error: ${msg}`); return 1; }
|
|
@@ -335,7 +772,7 @@ const ARC_COMMANDS = {
|
|
|
335
772
|
|
|
336
773
|
"$ARGUMENTS"
|
|
337
774
|
|
|
338
|
-
Steps:
|
|
775
|
+
Steps: run \`arc status\` to see existing arcs; if an open arc already covers this, append the instruction verbatim and refine its plan; otherwise create a new arc with \`arc new "<short title>" --goal "<one-line goal>" --task "<first task>" --task "<next task>"\`. Then record the raw instruction verbatim in §1, finish the Plan with checkable acceptance criteria, and refine the Tasks. Do not start coding until the plan is acknowledged.`,
|
|
339
776
|
},
|
|
340
777
|
"arc-refine": {
|
|
341
778
|
desc: "ARC: fold a new instruction into an existing arc (Refine)",
|
|
@@ -343,11 +780,11 @@ Steps: read .arc/INDEX.md; if an open arc already covers this, append the instru
|
|
|
343
780
|
|
|
344
781
|
"$ARGUMENTS"
|
|
345
782
|
|
|
346
|
-
|
|
783
|
+
Run \`arc refine <arc> "$ARGUMENTS" --changed "<one-line plan delta>"\` — this appends the instruction verbatim to §1, bumps plan_version, adds a §3 Refinement Log entry, and sets the arc to refining. Then rewrite §2 Plan to reflect only the current intent, adjust §4 Tasks (move dropped scope to "Out of scope"), and resume construction only once the plan and tasks absorb the change. Never edit the append-only sections retroactively.`,
|
|
347
784
|
},
|
|
348
785
|
"arc-build": {
|
|
349
786
|
desc: "ARC: do the work for the active arc (Read Before / Update After Editing)",
|
|
350
|
-
body: `Read ./ARC.md. Before editing: read .arc/INDEX.md, fully read the arc(s) covering the files you'll touch, and read the real source files.
|
|
787
|
+
body: `Read ./ARC.md. Before editing: run \`arc status\` (or read .arc/INDEX.md), fully read the arc(s) covering the files you'll touch, and read the real source files. Mark a task in progress with \`arc task <arc> <n> start\`, do the work, then tick it with \`arc task <arc> <n> done\`. After editing: append a Worklog entry (tasks, files read, files changed, summary, decisions, follow-ups), keep the arc's Status Notes current, and reference the arc id in the commit. When the arc is finished, run \`arc done <arc>\` to mark it done and archive it. An edit without a worklog entry is unfinished.
|
|
351
788
|
|
|
352
789
|
Focus: $ARGUMENTS`,
|
|
353
790
|
},
|
|
@@ -404,22 +841,81 @@ function cmdAgentInit(flags) {
|
|
|
404
841
|
return 0;
|
|
405
842
|
}
|
|
406
843
|
|
|
407
|
-
|
|
844
|
+
const COMMAND_HELP = {
|
|
845
|
+
init: `arc init [dir] [--owner=NAME]
|
|
846
|
+
Scaffold ARC.md + .arc/ in dir (default: current). Idempotent — never overwrites.
|
|
847
|
+
--owner NAME arc owner (default: git config user.name)`,
|
|
848
|
+
new: `arc new "Short imperative title" [options]
|
|
849
|
+
Create the next arc and register it in INDEX.md.
|
|
850
|
+
--goal "…" prefill the Plan goal line
|
|
851
|
+
--task "…" add a first task (repeatable: --task a --task b)
|
|
852
|
+
--tags a,b frontmatter tags
|
|
853
|
+
--owner NAME arc owner
|
|
854
|
+
--dir DIR project dir (default: current)`,
|
|
855
|
+
start: `arc start <arc>
|
|
856
|
+
Set an arc to in-progress and log it. <arc> is an id or slug (e.g. ARC-0007 or rate-limit).`,
|
|
857
|
+
done: `arc done <arc>
|
|
858
|
+
Mark an arc done, log it, move the file to .arc/archive/, and move its INDEX row to Archived.`,
|
|
859
|
+
block: `arc block <arc> [--reason "…"]
|
|
860
|
+
Set an arc to blocked, recording the reason in the worklog.`,
|
|
861
|
+
refine: `arc refine <arc> "the new instruction" [--changed "plan delta"] [--source chat|voice|issue|review]
|
|
862
|
+
Fold a new instruction into an arc: append it verbatim to §1, bump plan_version,
|
|
863
|
+
add a §3 Refinement Log entry, and set status to refining.`,
|
|
864
|
+
note: `arc note <arc> "text" [--worklog] [--source …]
|
|
865
|
+
Quick-append a note. Default goes to §1 Raw Instructions; --worklog appends to §5 Worklog.`,
|
|
866
|
+
log: `arc log <arc> [--json]
|
|
867
|
+
Show an arc's worklog timeline (newest entries as recorded).`,
|
|
868
|
+
archive: `arc archive <arc> [--cancelled] [--reason "…"]
|
|
869
|
+
Archive an arc. Default outcome is "done"; --cancelled archives it as cancelled.`,
|
|
870
|
+
task: `arc task <arc> <n> [done|start|block|cancel|pending]
|
|
871
|
+
Toggle task T<n>'s marker. Default action is "done".
|
|
872
|
+
arc task <arc> --add "text" append a new task`,
|
|
873
|
+
show: `arc show <arc>
|
|
874
|
+
Print one arc's plan, tasks, and status notes.`,
|
|
875
|
+
next: `arc next
|
|
876
|
+
Suggest what to work on (active_focus → in-progress → planned).`,
|
|
877
|
+
status: `arc status [dir] [--json]
|
|
878
|
+
Table of every arc: id, status, plan version, task progress.`,
|
|
879
|
+
doctor: `arc doctor [dir] [--fix]
|
|
880
|
+
Consistency checks (exit 1 on problems). --fix auto-repairs index/status drift.`,
|
|
881
|
+
"agent-init": `arc agent-init [--agents=a,b] [--force]
|
|
882
|
+
Write /arc-* slash commands for AI agents (agents: ${Object.keys(AGENT_TARGETS).join(", ")}; default: all).`,
|
|
883
|
+
};
|
|
884
|
+
|
|
885
|
+
function help(topic) {
|
|
886
|
+
if (topic && COMMAND_HELP[topic]) { console.log(COMMAND_HELP[topic]); return 0; }
|
|
408
887
|
console.log(`create-arc v${PKG.version} — ARC plan-driven development (Align → Refine → Construct)
|
|
409
888
|
(alias: \`arc\`)
|
|
410
889
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
890
|
+
Setup
|
|
891
|
+
init [dir] scaffold ARC.md + .arc/ (idempotent)
|
|
892
|
+
agent-init [--agents=…] write /arc-* slash commands for AI agents
|
|
893
|
+
|
|
894
|
+
Capture & plan
|
|
895
|
+
new "Title" [--goal …] [--task …] [--tags a,b]
|
|
896
|
+
create + register the next arc
|
|
897
|
+
|
|
898
|
+
Work the arc
|
|
899
|
+
start <arc> → in-progress
|
|
900
|
+
task <arc> <n> [action] tick a task ([done]/start/block/cancel/pending); --add "text"
|
|
901
|
+
refine <arc> "…" fold in a new instruction (bumps plan version → refining)
|
|
902
|
+
note <arc> "…" quick-append to Raw Instructions; --worklog for a worklog note
|
|
903
|
+
block <arc> [--reason …] → blocked
|
|
904
|
+
done <arc> → done + archive (file + index row)
|
|
905
|
+
archive <arc> [--cancelled]
|
|
906
|
+
|
|
907
|
+
Inspect
|
|
908
|
+
status [dir] [--json] table of every arc
|
|
909
|
+
show <arc> [--json] one arc's plan, tasks, status
|
|
910
|
+
log <arc> [--json] an arc's worklog timeline
|
|
911
|
+
next [--json] what to work on next
|
|
912
|
+
doctor [dir] [--fix] consistency checks (+ auto-repair)
|
|
913
|
+
|
|
914
|
+
<arc> is an id or slug: ARC-0007, 7, or a slug substring like "rate-limit".
|
|
915
|
+
Per-command help: arc help <command> (e.g. arc help new)
|
|
916
|
+
|
|
917
|
+
Run with npx: npx @ksoftm/create-arc <command>
|
|
918
|
+
Install: npm i -g @ksoftm/create-arc then use \`arc\`
|
|
423
919
|
Protocol reference: ARC.md in your project root after init.`);
|
|
424
920
|
return 0;
|
|
425
921
|
}
|
|
@@ -427,14 +923,28 @@ Protocol reference: ARC.md in your project root after init.`);
|
|
|
427
923
|
const { pos, flags } = parseArgv(process.argv.slice(2));
|
|
428
924
|
const cmd = pos[0];
|
|
429
925
|
|
|
926
|
+
// per-command help: `arc help new` or `arc new --help`
|
|
927
|
+
if (flags.help && cmd && cmd !== "help") { process.exit(help(cmd)); }
|
|
928
|
+
|
|
430
929
|
let code;
|
|
431
930
|
if (flags.version || cmd === "version") { console.log(PKG.version); code = 0; }
|
|
432
|
-
else if (!cmd || cmd === "help"
|
|
931
|
+
else if (!cmd || cmd === "help") code = help(pos[1]);
|
|
433
932
|
else if (cmd === "init") code = cmdInit(pos[1], flags);
|
|
434
933
|
else if (cmd === "new") code = cmdNew(pos.slice(1).join(" "), flags);
|
|
435
934
|
else if (cmd === "status") code = cmdStatus(pos[1], flags);
|
|
436
|
-
else if (cmd === "doctor") code = cmdDoctor(pos[1]);
|
|
935
|
+
else if (cmd === "doctor") code = cmdDoctor(pos[1], flags);
|
|
437
936
|
else if (cmd === "agent-init" || cmd === "agents") code = cmdAgentInit(flags);
|
|
937
|
+
else if (cmd === "start") code = cmdStart(pos[1], flags);
|
|
938
|
+
else if (cmd === "done") code = cmdDone(pos[1], flags);
|
|
939
|
+
else if (cmd === "block") code = cmdBlock(pos[1], flags);
|
|
940
|
+
else if (cmd === "review") code = cmdReview(pos[1], flags);
|
|
941
|
+
else if (cmd === "refine") code = cmdRefine(pos[1], flags, pos.slice(2));
|
|
942
|
+
else if (cmd === "note") code = cmdNote(pos[1], flags, pos.slice(2));
|
|
943
|
+
else if (cmd === "log") code = cmdLog(pos[1], flags);
|
|
944
|
+
else if (cmd === "archive") code = cmdArchive(pos[1], flags);
|
|
945
|
+
else if (cmd === "task") code = cmdTask(pos[1], flags, pos.slice(2));
|
|
946
|
+
else if (cmd === "show" || cmd === "view") code = cmdShow(pos[1], flags);
|
|
947
|
+
else if (cmd === "next") code = cmdNext(flags);
|
|
438
948
|
else code = fail(`unknown command '${cmd}' — try: arc help`);
|
|
439
949
|
|
|
440
950
|
process.exit(code);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ksoftm/create-arc",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Scaffold and manage ARC — plan-driven development for AI agents (Align → Refine → Construct). Pure-Markdown plans, tasks, worklogs and statuses in .arc/, for any language and any agent.",
|
|
6
6
|
"keywords": [
|