@ksoftm/create-arc 1.1.0 → 1.2.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.
Files changed (3) hide show
  1. package/README.md +10 -3
  2. package/bin/cli.js +398 -29
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -32,11 +32,18 @@ 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" [--dir=.] [--tags=a,b]` | Take the next sequential ID, create the arc from the template, and register its row in the index. |
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
+ | `done <arc>` | Mark done, log it, move the file to `archive/`, move its index row. |
40
+ | `archive <arc> [--cancelled]` | Archive an arc (outcome done, or cancelled). |
41
+ | `show <arc>` | Print one arc's plan, tasks, and status notes. |
42
+ | `next` | Suggest what to work on next. |
36
43
  | `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). |
44
+ | `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
45
 
39
- Common options: `--owner NAME` (defaults to `git config user.name`); `--tags a,b` on `new`; `--json` on `status`; `--agents a,b` and `--force` on `agent-init`; `--version`, `--help`.
46
+ 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
47
 
41
48
  Installs two equivalent binaries: **`arc`** and **`create-arc`**.
42
49
 
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"]);
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) { flags[a.slice(2, eq)] = a.slice(eq + 1); continue; }
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
- flags[key] = next; i++; // --owner NAME form
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,121 @@ 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
+
95
215
  /* -------------------------------- commands ------------------------------- */
96
216
 
97
217
  function cmdInit(target, flags) {
@@ -160,6 +280,18 @@ function cmdNew(title, flags) {
160
280
  front = setField(front, "tags", `[${tags}]`);
161
281
  }
162
282
  body = body.replace("# ARC-0000 · <Title>", `# ${id} · ${title}`);
283
+
284
+ // Optional prefill: --goal sets the Plan goal line; --task adds first tasks.
285
+ if (flags.goal) {
286
+ body = body.replace(/\*\*Goal:\*\* <[^>]*>/, `**Goal:** ${String(flags.goal).trim()}`);
287
+ }
288
+ const tasks = flags.task ? (Array.isArray(flags.task) ? flags.task : [flags.task]) : [];
289
+ if (tasks.length) {
290
+ const block = tasks.map((t, i) => `- [ ] T${i + 1} ${String(t).trim()}`).join("\n");
291
+ // Replace the placeholder "- [ ] T1 <…>" lines under "## 4 · Tasks" with real tasks.
292
+ body = body.replace(/^- \[ \] T\d+ <[^>]*>(?:\n- \[ \] T\d+ <[^>]*>)*/m, block);
293
+ }
294
+
163
295
  writeFileSync(dest, `---\n${front}\n---\n${body}`);
164
296
 
165
297
  // update the index: bump next_id, insert registry row
@@ -272,16 +404,18 @@ function cmdStatus(target, flags) {
272
404
  return 0;
273
405
  }
274
406
 
275
- function cmdDoctor(target) {
276
- const dir = resolve(target ?? ".");
407
+ function cmdDoctor(target, flags = {}) {
408
+ const dir = resolve(flags.dir ?? target ?? ".");
277
409
  const arcDir = join(dir, ".arc");
278
410
  const indexPath = join(arcDir, "INDEX.md");
279
- let failures = 0, warnings = 0;
411
+ const doFix = !!flags.fix;
412
+ let failures = 0, warnings = 0, fixed = 0;
280
413
  const ok = (msg) => console.log(` OK ${msg}`);
281
414
  const bad = (msg) => { console.log(` FAIL ${msg}`); failures++; };
282
415
  const warn = (msg) => { console.log(` WARN ${msg}`); warnings++; };
416
+ const fix = (msg) => { console.log(` FIX ${msg}`); fixed++; };
283
417
 
284
- if (!existsSync(arcDir)) return fail(`${arcDir} not found — run \`create-arc init\` first`);
418
+ if (!existsSync(arcDir)) return fail(`${arcDir} not found — run \`arc init\` first`);
285
419
  if (!existsSync(indexPath)) { bad(".arc/INDEX.md missing"); return finish(); }
286
420
 
287
421
  const index = readText(indexPath);
@@ -292,19 +426,39 @@ function cmdDoctor(target) {
292
426
  const nm = index.match(/^next_id:\s*ARC-(\d+)\s*$/m);
293
427
  const maxId = Math.max(-1, ...all.map(({ p }) => parseInt(basename(p).match(/^ARC-(\d+)/)?.[1] ?? "-1", 10)));
294
428
  if (!nm) bad("next_id missing or malformed in INDEX.md");
295
- else if (parseInt(nm[1], 10) <= maxId) bad(`next_id ARC-${nm[1]} is not greater than highest existing arc ARC-${String(maxId).padStart(4, "0")}`);
296
- else ok(`next_id ARC-${nm[1]} > highest arc id`);
429
+ else if (parseInt(nm[1], 10) <= maxId) {
430
+ if (doFix) {
431
+ const next = `ARC-${String(maxId + 1).padStart(4, "0")}`;
432
+ writeFileSync(indexPath, readText(indexPath).replace(/^next_id:\s*ARC-\d+\s*$/m, `next_id: ${next}`));
433
+ fix(`next_id → ${next} (was ARC-${nm[1]})`);
434
+ } else bad(`next_id ARC-${nm[1]} is not greater than highest existing arc ARC-${String(maxId).padStart(4, "0")}`);
435
+ } else ok(`next_id ARC-${nm[1]} > highest arc id`);
436
+
437
+ // index row status map (id → status cell), for status-drift repair
438
+ const rowStatus = new Map();
439
+ for (const line of index.split("\n")) {
440
+ const cells = line.split("|");
441
+ if (cells.length >= 8 && /^ARC-\d{4}$/.test(cells[1].trim())) rowStatus.set(cells[1].trim(), cells[3].trim());
442
+ }
297
443
 
298
444
  // file <-> index bijection, frontmatter integrity
299
445
  const indexIds = new Set([...index.matchAll(/^\|\s*(ARC-\d{4})\s*\|/gm)].map((x) => x[1]));
300
- for (const { p } of all) {
446
+ for (const { p, archived: isArch } of all) {
301
447
  const fileId = basename(p).match(/^(ARC-\d{4})/)?.[1];
302
448
  const front = frontmatter(readText(p));
303
449
  const fmId = field(front, "id");
304
450
  const status = field(front, "status");
305
- if (fmId !== fileId) bad(`${basename(p)}: frontmatter id '${fmId}' != filename id '${fileId}'`);
451
+ if (fmId !== fileId) {
452
+ if (doFix) { updateArcFrontmatter(p, { id: fileId }); fix(`${basename(p)}: frontmatter id '${fmId}' → '${fileId}'`); }
453
+ else bad(`${basename(p)}: frontmatter id '${fmId}' != filename id '${fileId}'`);
454
+ }
306
455
  if (!VALID_STATUSES.has(status)) bad(`${basename(p)}: invalid status '${status}'`);
307
456
  if (!indexIds.has(fileId)) bad(`${basename(p)}: no row in INDEX.md`);
457
+ // status drift: active arc's index cell disagrees with its frontmatter
458
+ else if (!isArch && VALID_STATUSES.has(status) && rowStatus.get(fileId) && rowStatus.get(fileId) !== status) {
459
+ if (doFix) { syncIndexRow(arcDir, fileId, { status }); fix(`${fileId}: index status '${rowStatus.get(fileId)}' → '${status}'`); }
460
+ else warn(`${fileId}: index status '${rowStatus.get(fileId)}' != frontmatter '${status}' (use --fix)`);
461
+ }
308
462
  const inProg = [...readText(p).matchAll(/^- \[>\]/gm)].length;
309
463
  if (inProg > 2) warn(`${basename(p)}: ${inProg} tasks marked [>] — keep at most 1–2 in progress`);
310
464
  }
@@ -315,13 +469,168 @@ function cmdDoctor(target) {
315
469
  return finish();
316
470
 
317
471
  function finish() {
472
+ const fx = fixed ? `, ${fixed} fixed` : "";
318
473
  console.log(failures
319
- ? `\ndoctor: ${failures} problem(s), ${warnings} warning(s)`
320
- : `\ndoctor: healthy (${warnings} warning(s))`);
474
+ ? `\ndoctor: ${failures} problem(s), ${warnings} warning(s)${fx}`
475
+ : `\ndoctor: healthy (${warnings} warning(s)${fx})`);
321
476
  return failures ? 1 : 0;
322
477
  }
323
478
  }
324
479
 
480
+ /* ------------------------ lifecycle / task commands ----------------------- */
481
+
482
+ // Shared: resolve an arc by reference for a mutating command.
483
+ function resolveForMutation(ref, flags) {
484
+ const { arcDir, exists } = arcDirOf(flags);
485
+ if (!exists) return { err: fail("`.arc` not found — run `arc init` first") };
486
+ if (!ref) return { err: fail("which arc? pass an id or slug, e.g. `ARC-0007` or `rate-limit`") };
487
+ const found = findArc(arcDir, ref);
488
+ if (!found) return { err: fail(`no arc matching '${ref}' (try \`arc status\` to list them)`) };
489
+ const id = basename(found.path).match(/^(ARC-\d{4})/)?.[1];
490
+ return { arcDir, path: found.path, archived: found.archived, id };
491
+ }
492
+
493
+ function setStatus(ref, flags, status, { note } = {}) {
494
+ const r = resolveForMutation(ref, flags);
495
+ if (r.err) return r.err;
496
+ if (r.archived) return fail(`${r.id} is archived — restore it first`);
497
+ updateArcFrontmatter(r.path, { status });
498
+ syncIndexRow(r.arcDir, r.id, { status });
499
+ if (note) appendWorklog(r.path, note);
500
+ console.log(`${r.id} → ${status}`);
501
+ return 0;
502
+ }
503
+
504
+ const cmdStart = (ref, flags) => setStatus(ref, flags, "in-progress", { note: "started (status → in-progress)" });
505
+ const cmdReview = (ref, flags) => setStatus(ref, flags, "review", { note: "moved to review" });
506
+
507
+ function cmdBlock(ref, flags) {
508
+ const reason = flags.reason || flags.r;
509
+ return setStatus(ref, flags, "blocked", {
510
+ note: reason ? `blocked: ${reason}` : "blocked",
511
+ });
512
+ }
513
+
514
+ function cmdDone(ref, flags) {
515
+ const r = resolveForMutation(ref, flags);
516
+ if (r.err) return r.err;
517
+ if (r.archived) { console.log(`${r.id} is already archived`); return 0; }
518
+ updateArcFrontmatter(r.path, { status: "done" });
519
+ appendWorklog(r.path, "completed (status → done)");
520
+ // move file into archive/ and move the INDEX row to Archived
521
+ const archiveDir = join(r.arcDir, "archive");
522
+ mkdirSync(archiveDir, { recursive: true });
523
+ const destPath = join(archiveDir, basename(r.path));
524
+ renameSync(r.path, destPath);
525
+ moveIndexRowToArchived(r.arcDir, r.id, "done");
526
+ console.log(`${r.id} → done · moved to .arc/archive/${basename(r.path)}`);
527
+ return 0;
528
+ }
529
+
530
+ function cmdArchive(ref, flags) {
531
+ const r = resolveForMutation(ref, flags);
532
+ if (r.err) return r.err;
533
+ if (r.archived) { console.log(`${r.id} is already archived`); return 0; }
534
+ const outcome = flags.cancelled || flags.cancel ? "cancelled" : "done";
535
+ if (outcome === "cancelled") {
536
+ updateArcFrontmatter(r.path, { status: "cancelled" });
537
+ appendWorklog(r.path, flags.reason ? `cancelled: ${flags.reason}` : "cancelled");
538
+ }
539
+ const archiveDir = join(r.arcDir, "archive");
540
+ mkdirSync(archiveDir, { recursive: true });
541
+ renameSync(r.path, join(archiveDir, basename(r.path)));
542
+ moveIndexRowToArchived(r.arcDir, r.id, outcome);
543
+ console.log(`${r.id} → ${outcome} · archived`);
544
+ return 0;
545
+ }
546
+
547
+ // arc task <ref> <n> [done|start|block|cancel|pending] — toggle one task marker
548
+ // arc task <ref> --add "text" — append a new task
549
+ function cmdTask(ref, flags, rest) {
550
+ const r = resolveForMutation(ref, flags);
551
+ if (r.err) return r.err;
552
+ let text = readText(r.path);
553
+ const secRe = /(^## 4 · Tasks\n)([\s\S]*?)(?=^## |\Z)/m;
554
+ const sec = text.match(secRe);
555
+ if (!sec) return fail(`${r.id}: no "## 4 · Tasks" section`);
556
+ let body = sec[2];
557
+
558
+ if (flags.add) {
559
+ const nums = [...body.matchAll(/^- \[[ >x!-]\]\s*T(\d+)/gm)].map((m) => parseInt(m[1], 10));
560
+ const next = (nums.length ? Math.max(...nums) : 0) + 1;
561
+ const line = `- [ ] T${next} ${String(flags.add).trim()}\n`;
562
+ body = body.replace(/\n*$/, "\n") + line;
563
+ text = text.slice(0, sec.index) + sec[1] + body + text.slice(sec.index + sec[0].length);
564
+ writeFileSync(r.path, text);
565
+ updateArcFrontmatter(r.path, {});
566
+ console.log(`${r.id}: added T${next}`);
567
+ return 0;
568
+ }
569
+
570
+ const n = rest?.[0];
571
+ const action = (rest?.[1] || "done").toLowerCase();
572
+ if (!n) return fail("usage: arc task <arc> <task-number> [done|start|block|cancel|pending] (or --add \"text\")");
573
+ const marker = { done: "x", start: ">", block: "!", cancel: "-", pending: " " }[action];
574
+ if (marker === undefined) return fail(`unknown task action '${action}'`);
575
+ const tnum = String(n).replace(/[^0-9]/g, "");
576
+ const re = new RegExp(`^(- \\[)[ >x!-](\\]\\s*T${tnum}\\b.*)$`, "m");
577
+ if (!re.test(body)) return fail(`${r.id}: task T${tnum} not found`);
578
+ body = body.replace(re, `$1${marker}$2`);
579
+ text = text.slice(0, sec.index) + sec[1] + body + text.slice(sec.index + sec[0].length);
580
+ writeFileSync(r.path, text);
581
+ updateArcFrontmatter(r.path, {});
582
+ console.log(`${r.id}: T${tnum} → [${marker}]`);
583
+ return 0;
584
+ }
585
+
586
+ // arc show <ref> — print one arc's plan, tasks, and status notes.
587
+ function cmdShow(ref, flags) {
588
+ const r = resolveForMutation(ref, flags);
589
+ if (r.err) return r.err;
590
+ const text = readText(r.path);
591
+ const a = parseArc(r.path, r.archived);
592
+ const sec = (n, title) => text.match(new RegExp(`^## ${n} · ${title}\\n([\\s\\S]*?)(?=^## |\\Z)`, "m"))?.[1]?.trim() ?? "";
593
+ console.log(`${a.id} · ${a.title}`);
594
+ 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}`);
595
+ const plan = sec(2, "Plan \\(current[^)]*\\)") || sec(2, "Plan.*");
596
+ if (plan) { console.log("\n— Plan —"); console.log(plan.replace(/<!--[\s\S]*?-->/g, "").trim()); }
597
+ const tasks = sec(4, "Tasks");
598
+ if (tasks) { console.log("\n— Tasks —"); console.log(tasks.replace(/<!--[\s\S]*?-->/g, "").trim()); }
599
+ const notes = sec(6, "Status Notes");
600
+ if (notes) { console.log("\n— Status —"); console.log(notes.replace(/<!--[\s\S]*?-->/g, "").trim()); }
601
+ console.log(`\nfile: ${r.path}`);
602
+ return 0;
603
+ }
604
+
605
+ // arc next — suggest what to work on (active_focus, then in-progress, then planned).
606
+ function cmdNext(flags) {
607
+ const { arcDir, exists } = arcDirOf(flags);
608
+ if (!exists) return fail("`.arc` not found — run `arc init` first");
609
+ const { active } = listArcFiles(arcDir);
610
+ if (!active.length) return (console.log("no active arcs — `arc new \"Title\"` to begin"), 0);
611
+ const arcs = active.map((p) => parseArc(p, false))
612
+ // The standing maintenance arc (ARC-0000) is always in-progress; only surface
613
+ // it as "next" when it actually has unfinished tasks and nothing else is active.
614
+ .filter((a) => !(a.id === "ARC-0000" && a.tasks_in_progress === 0));
615
+ if (!arcs.length) return (console.log("no actionable arcs — `arc new \"Title\"` to begin"), 0);
616
+ const idx = existsSync(join(arcDir, "INDEX.md")) ? readText(join(arcDir, "INDEX.md")) : "";
617
+ const focus = idx.match(/^active_focus:\s*(.+)$/m)?.[1]?.trim();
618
+
619
+ const byStatus = (s) => arcs.filter((a) => a.status === s);
620
+ const pick =
621
+ (focus && focus !== "—" && arcs.find((a) => a.id === focus || a.title.includes(focus))) ||
622
+ byStatus("in-progress")[0] || byStatus("refining")[0] ||
623
+ byStatus("planned")[0] || byStatus("review")[0] || byStatus("draft")[0];
624
+
625
+ if (!pick) { console.log("nothing actionable — all arcs are blocked or done"); return 0; }
626
+ console.log(`next: ${pick.id} · ${pick.title} [${pick.status}]`);
627
+ console.log(` tasks ${pick.tasks_total ? `${pick.tasks_done}/${pick.tasks_total}` : "—"}${pick.tasks_blocked ? ` · ${pick.tasks_blocked} blocked` : ""}`);
628
+ console.log(` open: arc show ${pick.id}`);
629
+ const blocked = byStatus("blocked");
630
+ if (blocked.length) console.log(` (blocked: ${blocked.map((a) => a.id).join(", ")})`);
631
+ return 0;
632
+ }
633
+
325
634
  /* --------------------------------- main ----------------------------------- */
326
635
 
327
636
  function fail(msg) { console.error(`error: ${msg}`); return 1; }
@@ -335,7 +644,7 @@ const ARC_COMMANDS = {
335
644
 
336
645
  "$ARGUMENTS"
337
646
 
338
- Steps: read .arc/INDEX.md; if an open arc already covers this, append the instruction verbatim and refine its plan; otherwise create a new arc (use \`npx @ksoftm/create-arc new "<short title>"\`, or create the file from .arc/_TEMPLATE.md and register it in INDEX.md). Record the raw instruction verbatim, draft the Plan with checkable acceptance criteria, and list the Tasks. Do not start coding until the plan is acknowledged.`,
647
+ 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
648
  },
340
649
  "arc-refine": {
341
650
  desc: "ARC: fold a new instruction into an existing arc (Refine)",
@@ -347,7 +656,7 @@ Append it verbatim as the next Raw Instruction, add a Refinement Log entry (new
347
656
  },
348
657
  "arc-build": {
349
658
  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. Work the task list in order, marking one task in progress. After editing: advance task states, append a Worklog entry (tasks, files read, files changed, summary, decisions, follow-ups), update the arc frontmatter and its INDEX row, and reference the arc id in the commit. An edit without a worklog entry is unfinished.
659
+ 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
660
 
352
661
  Focus: $ARGUMENTS`,
353
662
  },
@@ -404,22 +713,71 @@ function cmdAgentInit(flags) {
404
713
  return 0;
405
714
  }
406
715
 
407
- function help() {
716
+ const COMMAND_HELP = {
717
+ init: `arc init [dir] [--owner=NAME]
718
+ Scaffold ARC.md + .arc/ in dir (default: current). Idempotent — never overwrites.
719
+ --owner NAME arc owner (default: git config user.name)`,
720
+ new: `arc new "Short imperative title" [options]
721
+ Create the next arc and register it in INDEX.md.
722
+ --goal "…" prefill the Plan goal line
723
+ --task "…" add a first task (repeatable: --task a --task b)
724
+ --tags a,b frontmatter tags
725
+ --owner NAME arc owner
726
+ --dir DIR project dir (default: current)`,
727
+ start: `arc start <arc>
728
+ Set an arc to in-progress and log it. <arc> is an id or slug (e.g. ARC-0007 or rate-limit).`,
729
+ done: `arc done <arc>
730
+ Mark an arc done, log it, move the file to .arc/archive/, and move its INDEX row to Archived.`,
731
+ block: `arc block <arc> [--reason "…"]
732
+ Set an arc to blocked, recording the reason in the worklog.`,
733
+ archive: `arc archive <arc> [--cancelled] [--reason "…"]
734
+ Archive an arc. Default outcome is "done"; --cancelled archives it as cancelled.`,
735
+ task: `arc task <arc> <n> [done|start|block|cancel|pending]
736
+ Toggle task T<n>'s marker. Default action is "done".
737
+ arc task <arc> --add "text" append a new task`,
738
+ show: `arc show <arc>
739
+ Print one arc's plan, tasks, and status notes.`,
740
+ next: `arc next
741
+ Suggest what to work on (active_focus → in-progress → planned).`,
742
+ status: `arc status [dir] [--json]
743
+ Table of every arc: id, status, plan version, task progress.`,
744
+ doctor: `arc doctor [dir] [--fix]
745
+ Consistency checks (exit 1 on problems). --fix auto-repairs index/status drift.`,
746
+ "agent-init": `arc agent-init [--agents=a,b] [--force]
747
+ Write /arc-* slash commands for AI agents (agents: ${Object.keys(AGENT_TARGETS).join(", ")}; default: all).`,
748
+ };
749
+
750
+ function help(topic) {
751
+ if (topic && COMMAND_HELP[topic]) { console.log(COMMAND_HELP[topic]); return 0; }
408
752
  console.log(`create-arc v${PKG.version} — ARC plan-driven development (Align → Refine → Construct)
409
753
  (alias: \`arc\`)
410
754
 
411
- Usage:
412
- arc init [dir] [--owner=NAME] scaffold ARC.md + .arc/ (idempotent)
413
- arc new "Title" [--dir=.] [--tags=a,b] create + register the next arc
414
- arc status [dir] [--json] status table across all arcs
415
- arc doctor [dir] consistency checks (exit 1 on problems)
416
- arc agent-init [--agents=a,b] [--force] write /slash commands for AI agents
417
- (agents: ${ALL_AGENTS.join(", ")}; default: all)
755
+ Setup
756
+ init [dir] scaffold ARC.md + .arc/ (idempotent)
757
+ agent-init [--agents=…] write /arc-* slash commands for AI agents
418
758
 
419
- Run with npx (no install): npx @ksoftm/create-arc <command>
420
- Install globally: npm i -g @ksoftm/create-arc then: arc <command>
421
- Project dev dependency: npm i -D @ksoftm/create-arc then: npx arc <command>
759
+ Capture & plan
760
+ new "Title" [--goal …] [--task …] [--tags a,b]
761
+ create + register the next arc
422
762
 
763
+ Work the arc
764
+ start <arc> → in-progress
765
+ task <arc> <n> [action] tick a task ([done]/start/block/cancel/pending); --add "text"
766
+ block <arc> [--reason …] → blocked
767
+ done <arc> → done + archive (file + index row)
768
+ archive <arc> [--cancelled]
769
+
770
+ Inspect
771
+ status [dir] [--json] table of every arc
772
+ show <arc> one arc's plan, tasks, status
773
+ next what to work on next
774
+ doctor [dir] [--fix] consistency checks (+ auto-repair)
775
+
776
+ <arc> is an id or slug: ARC-0007, 7, or a slug substring like "rate-limit".
777
+ Per-command help: arc help <command> (e.g. arc help new)
778
+
779
+ Run with npx: npx @ksoftm/create-arc <command>
780
+ Install: npm i -g @ksoftm/create-arc then use \`arc\`
423
781
  Protocol reference: ARC.md in your project root after init.`);
424
782
  return 0;
425
783
  }
@@ -427,14 +785,25 @@ Protocol reference: ARC.md in your project root after init.`);
427
785
  const { pos, flags } = parseArgv(process.argv.slice(2));
428
786
  const cmd = pos[0];
429
787
 
788
+ // per-command help: `arc help new` or `arc new --help`
789
+ if (flags.help && cmd && cmd !== "help") { process.exit(help(cmd)); }
790
+
430
791
  let code;
431
792
  if (flags.version || cmd === "version") { console.log(PKG.version); code = 0; }
432
- else if (!cmd || cmd === "help" || flags.help) code = help();
793
+ else if (!cmd || cmd === "help") code = help(pos[1]);
433
794
  else if (cmd === "init") code = cmdInit(pos[1], flags);
434
795
  else if (cmd === "new") code = cmdNew(pos.slice(1).join(" "), flags);
435
796
  else if (cmd === "status") code = cmdStatus(pos[1], flags);
436
- else if (cmd === "doctor") code = cmdDoctor(pos[1]);
797
+ else if (cmd === "doctor") code = cmdDoctor(pos[1], flags);
437
798
  else if (cmd === "agent-init" || cmd === "agents") code = cmdAgentInit(flags);
799
+ else if (cmd === "start") code = cmdStart(pos[1], flags);
800
+ else if (cmd === "done") code = cmdDone(pos[1], flags);
801
+ else if (cmd === "block") code = cmdBlock(pos[1], flags);
802
+ else if (cmd === "review") code = cmdReview(pos[1], flags);
803
+ else if (cmd === "archive") code = cmdArchive(pos[1], flags);
804
+ else if (cmd === "task") code = cmdTask(pos[1], flags, pos.slice(2));
805
+ else if (cmd === "show" || cmd === "view") code = cmdShow(pos[1], flags);
806
+ else if (cmd === "next") code = cmdNext(flags);
438
807
  else code = fail(`unknown command '${cmd}' — try: arc help`);
439
808
 
440
809
  process.exit(code);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ksoftm/create-arc",
3
- "version": "1.1.0",
3
+ "version": "1.2.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": [