@ksoftm/create-arc 1.0.7 → 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.
- package/README.md +20 -7
- package/bin/cli.js +511 -28
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -3,12 +3,16 @@
|
|
|
3
3
|
Scaffold and drive the **ARC framework** (Align → Refine → Construct) — plan-driven development for AI agents, in pure Markdown. Zero dependencies, any language, any agent.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
npm i -g @ksoftm/create-arc # install once, then use the short `arc` command
|
|
7
|
+
arc init # scaffold ARC into the current project
|
|
8
|
+
arc new "Add per-key rate limit" # open and register a new unit of work
|
|
9
|
+
arc status # every arc at a glance
|
|
10
|
+
arc doctor # verify the registry is consistent
|
|
11
|
+
arc agent-init # write /arc-* slash commands for your AI agent
|
|
10
12
|
```
|
|
11
13
|
|
|
14
|
+
No install? Use `npx @ksoftm/create-arc <command>`. Prefer pinning per-project? `npm i -D @ksoftm/create-arc` then `npx arc <command>`.
|
|
15
|
+
|
|
12
16
|
## What ARC is
|
|
13
17
|
|
|
14
18
|
Every unit of development is an **arc**: one Markdown file under `.arc/` that holds its whole story — the user's raw instructions (verbatim, append-only), the current plan, a refinement log, a task list, a worklog, and a status. The agent plans before it builds, refines the plan when the ask changes, and logs every edit — so any later session resumes from the arc, not the chat history. Full concept and protocol: **https://github.com/KsoftmHub/arc-framework-v1**.
|
|
@@ -28,11 +32,20 @@ Requires Node ≥ 18.
|
|
|
28
32
|
| Command | What it does |
|
|
29
33
|
|---|---|
|
|
30
34
|
| `init [dir]` | Scaffold `ARC.md` + `.arc/` (index, template, standing maintenance arc, `notes/`, `archive/`). Idempotent — never overwrites. |
|
|
31
|
-
| `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
|
+
| `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. |
|
|
32
43
|
| `status [dir] [--json]` | Print a table (or JSON) of every arc: ID, status, plan version, task progress, and which to resume. |
|
|
33
|
-
| `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. |
|
|
45
|
+
|
|
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>`.
|
|
34
47
|
|
|
35
|
-
|
|
48
|
+
Installs two equivalent binaries: **`arc`** and **`create-arc`**.
|
|
36
49
|
|
|
37
50
|
## What `init` produces
|
|
38
51
|
|
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) {
|
|
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,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,18 +280,59 @@ 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
|
|
166
298
|
index = index.replace(/^next_id:\s*ARC-\d+\s*$/m, `next_id: ARC-${String(num + 1).padStart(4, "0")}`);
|
|
167
299
|
const row = `| ${id} | ${title} | draft | 1 | ${date} | — | [${filename}](${filename}) |`;
|
|
168
300
|
const lines = index.split("\n");
|
|
301
|
+
const archivedIdx = lines.findIndex((l) => /^##\s+Archived/i.test(l));
|
|
302
|
+
const scanEnd = archivedIdx === -1 ? lines.length : archivedIdx;
|
|
303
|
+
|
|
304
|
+
// Preferred: insert right after the last existing "| ARC-…" row in the Active section.
|
|
169
305
|
let insertAt = -1;
|
|
170
|
-
for (let i = 0; i <
|
|
171
|
-
if (lines[i].startsWith("## Archived")) break;
|
|
306
|
+
for (let i = 0; i < scanEnd; i++) {
|
|
172
307
|
if (/^\|\s*ARC-/.test(lines[i])) insertAt = i;
|
|
173
308
|
}
|
|
174
|
-
|
|
309
|
+
|
|
310
|
+
if (insertAt === -1) {
|
|
311
|
+
// No arc rows yet (or a differently-shaped INDEX). Fall back, in order, to:
|
|
312
|
+
// the "## Active" heading's table separator, the heading itself, or next_id.
|
|
313
|
+
const activeIdx = lines.findIndex((l) => /^##\s+Active/i.test(l));
|
|
314
|
+
if (activeIdx !== -1) {
|
|
315
|
+
// find a markdown table separator (|---|) after the Active heading
|
|
316
|
+
let sep = -1;
|
|
317
|
+
for (let i = activeIdx + 1; i < scanEnd; i++) {
|
|
318
|
+
if (/^\|?\s*:?-{2,}/.test(lines[i]) || /^\|(\s*-+\s*\|)+/.test(lines[i])) { sep = i; break; }
|
|
319
|
+
if (/^\|\s*ID\s*\|/i.test(lines[i])) { sep = i + 1; } // header row → insert after its separator
|
|
320
|
+
}
|
|
321
|
+
insertAt = sep !== -1 ? sep : activeIdx; // after separator, else right after heading
|
|
322
|
+
} else {
|
|
323
|
+
// No Active section at all — synthesize one after next_id (or at top).
|
|
324
|
+
const nextIdIdx = lines.findIndex((l) => /^next_id:/.test(l));
|
|
325
|
+
const anchor = nextIdIdx !== -1 ? nextIdIdx : 0;
|
|
326
|
+
lines.splice(anchor + 1, 0,
|
|
327
|
+
"",
|
|
328
|
+
"## Active",
|
|
329
|
+
"",
|
|
330
|
+
"| ID | Title | Status | Plan v | Updated | Depends | File |",
|
|
331
|
+
"|---|---|---|---|---|---|---|");
|
|
332
|
+
insertAt = anchor + 5; // the separator line we just added
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
175
336
|
lines.splice(insertAt + 1, 0, row);
|
|
176
337
|
writeFileSync(indexPath, lines.join("\n"));
|
|
177
338
|
|
|
@@ -243,16 +404,18 @@ function cmdStatus(target, flags) {
|
|
|
243
404
|
return 0;
|
|
244
405
|
}
|
|
245
406
|
|
|
246
|
-
function cmdDoctor(target) {
|
|
247
|
-
const dir = resolve(target ?? ".");
|
|
407
|
+
function cmdDoctor(target, flags = {}) {
|
|
408
|
+
const dir = resolve(flags.dir ?? target ?? ".");
|
|
248
409
|
const arcDir = join(dir, ".arc");
|
|
249
410
|
const indexPath = join(arcDir, "INDEX.md");
|
|
250
|
-
|
|
411
|
+
const doFix = !!flags.fix;
|
|
412
|
+
let failures = 0, warnings = 0, fixed = 0;
|
|
251
413
|
const ok = (msg) => console.log(` OK ${msg}`);
|
|
252
414
|
const bad = (msg) => { console.log(` FAIL ${msg}`); failures++; };
|
|
253
415
|
const warn = (msg) => { console.log(` WARN ${msg}`); warnings++; };
|
|
416
|
+
const fix = (msg) => { console.log(` FIX ${msg}`); fixed++; };
|
|
254
417
|
|
|
255
|
-
if (!existsSync(arcDir)) return fail(`${arcDir} not found — run \`
|
|
418
|
+
if (!existsSync(arcDir)) return fail(`${arcDir} not found — run \`arc init\` first`);
|
|
256
419
|
if (!existsSync(indexPath)) { bad(".arc/INDEX.md missing"); return finish(); }
|
|
257
420
|
|
|
258
421
|
const index = readText(indexPath);
|
|
@@ -263,19 +426,39 @@ function cmdDoctor(target) {
|
|
|
263
426
|
const nm = index.match(/^next_id:\s*ARC-(\d+)\s*$/m);
|
|
264
427
|
const maxId = Math.max(-1, ...all.map(({ p }) => parseInt(basename(p).match(/^ARC-(\d+)/)?.[1] ?? "-1", 10)));
|
|
265
428
|
if (!nm) bad("next_id missing or malformed in INDEX.md");
|
|
266
|
-
else if (parseInt(nm[1], 10) <= maxId)
|
|
267
|
-
|
|
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
|
+
}
|
|
268
443
|
|
|
269
444
|
// file <-> index bijection, frontmatter integrity
|
|
270
445
|
const indexIds = new Set([...index.matchAll(/^\|\s*(ARC-\d{4})\s*\|/gm)].map((x) => x[1]));
|
|
271
|
-
for (const { p } of all) {
|
|
446
|
+
for (const { p, archived: isArch } of all) {
|
|
272
447
|
const fileId = basename(p).match(/^(ARC-\d{4})/)?.[1];
|
|
273
448
|
const front = frontmatter(readText(p));
|
|
274
449
|
const fmId = field(front, "id");
|
|
275
450
|
const status = field(front, "status");
|
|
276
|
-
if (fmId !== 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
|
+
}
|
|
277
455
|
if (!VALID_STATUSES.has(status)) bad(`${basename(p)}: invalid status '${status}'`);
|
|
278
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
|
+
}
|
|
279
462
|
const inProg = [...readText(p).matchAll(/^- \[>\]/gm)].length;
|
|
280
463
|
if (inProg > 2) warn(`${basename(p)}: ${inProg} tasks marked [>] — keep at most 1–2 in progress`);
|
|
281
464
|
}
|
|
@@ -286,27 +469,315 @@ function cmdDoctor(target) {
|
|
|
286
469
|
return finish();
|
|
287
470
|
|
|
288
471
|
function finish() {
|
|
472
|
+
const fx = fixed ? `, ${fixed} fixed` : "";
|
|
289
473
|
console.log(failures
|
|
290
|
-
? `\ndoctor: ${failures} problem(s), ${warnings} warning(s)`
|
|
291
|
-
: `\ndoctor: healthy (${warnings} warning(s))`);
|
|
474
|
+
? `\ndoctor: ${failures} problem(s), ${warnings} warning(s)${fx}`
|
|
475
|
+
: `\ndoctor: healthy (${warnings} warning(s)${fx})`);
|
|
292
476
|
return failures ? 1 : 0;
|
|
293
477
|
}
|
|
294
478
|
}
|
|
295
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
|
+
|
|
296
634
|
/* --------------------------------- main ----------------------------------- */
|
|
297
635
|
|
|
298
636
|
function fail(msg) { console.error(`error: ${msg}`); return 1; }
|
|
299
637
|
|
|
300
|
-
|
|
301
|
-
|
|
638
|
+
// Slash-commands generated for each supported agent. Each is a thin prompt that
|
|
639
|
+
// routes the agent through ARC.md; $ARGUMENTS / $1 carry the user's text.
|
|
640
|
+
const ARC_COMMANDS = {
|
|
641
|
+
"arc-new": {
|
|
642
|
+
desc: "ARC: capture a development instruction as a new arc (Align)",
|
|
643
|
+
body: `Read ./ARC.md, then run ARC Intake for this instruction:
|
|
644
|
+
|
|
645
|
+
"$ARGUMENTS"
|
|
646
|
+
|
|
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.`,
|
|
648
|
+
},
|
|
649
|
+
"arc-refine": {
|
|
650
|
+
desc: "ARC: fold a new instruction into an existing arc (Refine)",
|
|
651
|
+
body: `Read ./ARC.md, then refine the relevant arc with this instruction:
|
|
652
|
+
|
|
653
|
+
"$ARGUMENTS"
|
|
654
|
+
|
|
655
|
+
Append it verbatim as the next Raw Instruction, add a Refinement Log entry (new plan_version, what changed, task impact), rewrite the Plan to reflect only the current intent, and adjust the Tasks. Move dropped scope to "Out of scope". Never edit the append-only sections retroactively.`,
|
|
656
|
+
},
|
|
657
|
+
"arc-build": {
|
|
658
|
+
desc: "ARC: do the work for the active arc (Read Before / Update After Editing)",
|
|
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.
|
|
660
|
+
|
|
661
|
+
Focus: $ARGUMENTS`,
|
|
662
|
+
},
|
|
663
|
+
"arc-status": {
|
|
664
|
+
desc: "ARC: summarize all arcs and what to resume",
|
|
665
|
+
body: `Run \`npx @ksoftm/create-arc status\` (or read .arc/INDEX.md and each arc). Report every arc's id, status, plan version, task progress, and which in-progress/refining arcs to resume — reading each one's Status Notes and last Worklog entry.`,
|
|
666
|
+
},
|
|
667
|
+
"arc-resume": {
|
|
668
|
+
desc: "ARC: pick up the in-progress arc cold",
|
|
669
|
+
body: `Read ./ARC.md. Read .arc/INDEX.md, find arcs in in-progress or refining, open them, read Status Notes and the last Worklog entry, then continue from the open tasks — after running Read Before Editing. If code and the arc disagree, the code is truth: note the drift in the Worklog and correct the arc.`,
|
|
670
|
+
},
|
|
671
|
+
};
|
|
672
|
+
|
|
673
|
+
// Where each agent looks for project-level slash commands, and how to render one.
|
|
674
|
+
const AGENT_TARGETS = {
|
|
675
|
+
claude: { dir: ".claude/commands", ext: ".md", fmt: md },
|
|
676
|
+
opencode: { dir: ".opencode/command", ext: ".md", fmt: md },
|
|
677
|
+
cursor: { dir: ".cursor/commands", ext: ".md", fmt: md },
|
|
678
|
+
codex: { dir: ".codex/prompts", ext: ".md", fmt: plain }, // Codex custom prompts
|
|
679
|
+
gemini: { dir: ".gemini/commands", ext: ".toml", fmt: toml },
|
|
680
|
+
};
|
|
681
|
+
const ALL_AGENTS = Object.keys(AGENT_TARGETS);
|
|
682
|
+
|
|
683
|
+
function md(name, c) { return `---\ndescription: ${c.desc}\n---\n\n${c.body}\n`; }
|
|
684
|
+
function plain(name, c) { return `${c.body}\n`; }
|
|
685
|
+
function toml(name, c) {
|
|
686
|
+
const b = c.body.replace(/\\/g, "\\\\").replace(/"""/g, '\\"\\"\\"');
|
|
687
|
+
return `description = ${JSON.stringify(c.desc)}\nprompt = """\n${b}\n"""\n`;
|
|
688
|
+
}
|
|
302
689
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
690
|
+
function cmdAgentInit(flags) {
|
|
691
|
+
const dir = resolve(flags.dir ?? ".");
|
|
692
|
+
// --agents=claude,opencode (default: all)
|
|
693
|
+
const agents = (flags.agents ? String(flags.agents).split(",") : ALL_AGENTS)
|
|
694
|
+
.map((a) => a.trim().toLowerCase()).filter(Boolean);
|
|
695
|
+
const unknown = agents.filter((a) => !AGENT_TARGETS[a]);
|
|
696
|
+
if (unknown.length) return fail(`unknown agent(s): ${unknown.join(", ")} (known: ${ALL_AGENTS.join(", ")})`);
|
|
697
|
+
|
|
698
|
+
let created = 0, skipped = 0;
|
|
699
|
+
for (const agent of agents) {
|
|
700
|
+
const { dir: rel, ext, fmt } = AGENT_TARGETS[agent];
|
|
701
|
+
const outDir = join(dir, rel);
|
|
702
|
+
mkdirSync(outDir, { recursive: true });
|
|
703
|
+
for (const [name, c] of Object.entries(ARC_COMMANDS)) {
|
|
704
|
+
const dest = join(outDir, name + ext);
|
|
705
|
+
if (existsSync(dest) && !flags.force) { skipped++; continue; }
|
|
706
|
+
writeFileSync(dest, fmt(name, c));
|
|
707
|
+
created++;
|
|
708
|
+
}
|
|
709
|
+
console.log(` ${agent.padEnd(9)} ${rel}/ (${Object.keys(ARC_COMMANDS).length} commands)`);
|
|
710
|
+
}
|
|
711
|
+
console.log(`\nAgent commands written (created ${created}, skipped ${skipped}${flags.force ? "" : "; use --force to overwrite"}).`);
|
|
712
|
+
console.log(`Type /arc-new, /arc-build, /arc-status, /arc-refine, /arc-resume in your agent.`);
|
|
713
|
+
return 0;
|
|
714
|
+
}
|
|
715
|
+
|
|
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
|
+
};
|
|
309
749
|
|
|
750
|
+
function help(topic) {
|
|
751
|
+
if (topic && COMMAND_HELP[topic]) { console.log(COMMAND_HELP[topic]); return 0; }
|
|
752
|
+
console.log(`create-arc v${PKG.version} — ARC plan-driven development (Align → Refine → Construct)
|
|
753
|
+
(alias: \`arc\`)
|
|
754
|
+
|
|
755
|
+
Setup
|
|
756
|
+
init [dir] scaffold ARC.md + .arc/ (idempotent)
|
|
757
|
+
agent-init [--agents=…] write /arc-* slash commands for AI agents
|
|
758
|
+
|
|
759
|
+
Capture & plan
|
|
760
|
+
new "Title" [--goal …] [--task …] [--tags a,b]
|
|
761
|
+
create + register the next arc
|
|
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\`
|
|
310
781
|
Protocol reference: ARC.md in your project root after init.`);
|
|
311
782
|
return 0;
|
|
312
783
|
}
|
|
@@ -314,13 +785,25 @@ Protocol reference: ARC.md in your project root after init.`);
|
|
|
314
785
|
const { pos, flags } = parseArgv(process.argv.slice(2));
|
|
315
786
|
const cmd = pos[0];
|
|
316
787
|
|
|
788
|
+
// per-command help: `arc help new` or `arc new --help`
|
|
789
|
+
if (flags.help && cmd && cmd !== "help") { process.exit(help(cmd)); }
|
|
790
|
+
|
|
317
791
|
let code;
|
|
318
792
|
if (flags.version || cmd === "version") { console.log(PKG.version); code = 0; }
|
|
319
|
-
else if (!cmd || cmd === "help"
|
|
793
|
+
else if (!cmd || cmd === "help") code = help(pos[1]);
|
|
320
794
|
else if (cmd === "init") code = cmdInit(pos[1], flags);
|
|
321
795
|
else if (cmd === "new") code = cmdNew(pos.slice(1).join(" "), flags);
|
|
322
796
|
else if (cmd === "status") code = cmdStatus(pos[1], flags);
|
|
323
|
-
else if (cmd === "doctor") code = cmdDoctor(pos[1]);
|
|
324
|
-
else
|
|
797
|
+
else if (cmd === "doctor") code = cmdDoctor(pos[1], flags);
|
|
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);
|
|
807
|
+
else code = fail(`unknown command '${cmd}' — try: arc help`);
|
|
325
808
|
|
|
326
809
|
process.exit(code);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ksoftm/create-arc",
|
|
3
|
-
"version": "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": [
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
"author": "Ksoftm (Kajalan)",
|
|
20
20
|
"type": "module",
|
|
21
21
|
"bin": {
|
|
22
|
-
"create-arc": "bin/cli.js"
|
|
22
|
+
"create-arc": "bin/cli.js",
|
|
23
|
+
"arc": "bin/cli.js"
|
|
23
24
|
},
|
|
24
25
|
"files": [
|
|
25
26
|
"bin",
|