arcane-cli 0.35.0 → 0.36.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/dist/assets/.github/prompts/spell-close-session.prompt.md +2 -0
- package/dist/assets/.github/prompts/spell-commit-work.prompt.md +6 -0
- package/dist/assets/.github/prompts/spell-create-pull-request.prompt.md +27 -0
- package/dist/assets/report/show-report.template.html +31 -5
- package/dist/index.js +45 -12
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -71,6 +71,8 @@ Workflow:
|
|
|
71
71
|
|
|
72
72
|
4c. **Follow-up promotion sweep (LH-09; only when this repo has its own `scripts/check-followups.ts` — Arcane's own repo, or one that adopted the same convention; skip gracefully otherwise).** Run `npm run check:followups` (or the closest journal-scoped equivalent) and check whether this session's own journal entry or closure notes contain a deferral phrase ("filed as a separate", "follow-up", "out of scope", "not fixed here", "left open", "deferred", "revisit", "future work") with no tracker token nearby (`TODO`, `IDEAS`, `Q-NNN`, `ARC-NNN`, `EF-NN`, `RCA-NNN`, a PR URL) and no explicit `(untracked: <reason>)` opt-out. Promote each real hit to `TODO.md`/`IDEAS.md` before writing the handoff, or add the opt-out inline. Report-only tool — a hit is a prompt to check, not proof of an untracked gap; skip anything that's already tracked elsewhere in the same document.
|
|
73
73
|
|
|
74
|
+
4d. **Record the epic's reader-facing line (only when this session shipped an epic of a tracked program — one with a `docs/plans/<slug>/PLAN.md`; skip entirely otherwise).** Copy the `## For the record` sentence from the epic's own PR description verbatim into that epic's `PLAN.md` entry as its `**Report:**` line, in the format `**Report:** <sentence> · category: <spell|feature|governance|decision|fix|process|docs|platform>`, alongside the PR number the epic ticked with. Authored at PR time (see `spell-create-pull-request` Step 4) and copied here — never re-invented at close, and never substituted with the commit subject, which describes the change to an engineer rather than to the program's reader. **If the PR carried no such section**, leave the line absent rather than writing one from memory: the generator renders a missing description visibly as `unwritten`, and a visible gap someone can fill later is worth more than a plausible sentence nobody verified. Where a completion report is generated (`npm run check:report` / `spell report`), regenerate it after this edit so the committed artifact matches its sources.
|
|
75
|
+
|
|
74
76
|
5. **Update [ai-context/system-prompt-context.md](../../ai-context/system-prompt-context.md).**
|
|
75
77
|
- If priorities, environment state, or agent rules changed during the session, update the file to reflect current reality.
|
|
76
78
|
- If no changes are needed, skip this step.
|
|
@@ -205,6 +205,12 @@ Workflow:
|
|
|
205
205
|
- Structured `###` sections for each logical change area.
|
|
206
206
|
- A `### Testing` checklist (`- [x]` / `- [ ]`).
|
|
207
207
|
- Tables/code blocks where they improve clarity.
|
|
208
|
+
- `## For the record` — **only when this PR is one epic of a tracked program** (one with a
|
|
209
|
+
`docs/plans/<slug>/PLAN.md`); omit the heading entirely otherwise. One plain sentence a
|
|
210
|
+
non-engineer reading the finished program would understand, plus `· category: <…>`.
|
|
211
|
+
It is copied verbatim into the epic's `PLAN.md` entry as its `**Report:**` line when the epic
|
|
212
|
+
is recorded. See `spell-create-pull-request`'s Step 4 for the full convention — defined there,
|
|
213
|
+
referenced here.
|
|
208
214
|
|
|
209
215
|
g. Capture PR ID/URL from command output.
|
|
210
216
|
- Always render PRs as clickable markdown links with the full URL.
|
|
@@ -125,6 +125,12 @@ Standard template:
|
|
|
125
125
|
## Testing
|
|
126
126
|
|
|
127
127
|
- <coverage / test status if known, else "See CI pipeline results.">
|
|
128
|
+
|
|
129
|
+
## For the record
|
|
130
|
+
|
|
131
|
+
<One plain sentence a non-engineer reviewing the finished program would read — what changed for
|
|
132
|
+
them, not how it was built. Omit this section entirely when the PR is not one epic of a tracked
|
|
133
|
+
program.> · category: <spell|feature|governance|decision|fix|process|docs|platform>
|
|
128
134
|
```
|
|
129
135
|
|
|
130
136
|
`--docs-only` template (skip Testing):
|
|
@@ -138,8 +144,29 @@ Standard template:
|
|
|
138
144
|
|
|
139
145
|
## Notes
|
|
140
146
|
Documentation-only PR. No functional code changed.
|
|
147
|
+
|
|
148
|
+
## For the record
|
|
149
|
+
|
|
150
|
+
<Same as above — include only when this PR is one epic of a tracked program.> · category: docs
|
|
141
151
|
```
|
|
142
152
|
|
|
153
|
+
**About `## For the record`** — include it **only when this PR is one epic of a tracked program**
|
|
154
|
+
(one with a `docs/plans/<slug>/PLAN.md`); omit the heading entirely otherwise, since an empty
|
|
155
|
+
section is worse than none. Write the sentence that program's completion report will show for this
|
|
156
|
+
epic, in the reader's language rather than the implementation's — what changed for someone reading
|
|
157
|
+
the finished program, not how it was built. Authoring it here, while the work is fresh, is the
|
|
158
|
+
point: it is copied verbatim into the epic's `PLAN.md` entry as its `**Report:**` line when the
|
|
159
|
+
epic is recorded (`spell-close-session` step 4d). An epic that never gets one renders visibly as
|
|
160
|
+
**unwritten** in the report — never as a pasted commit subject — so a missing line stays a visible
|
|
161
|
+
gap rather than a silently fabricated one.
|
|
162
|
+
|
|
163
|
+
The line ends at `category:`. It carried a trailing `· glyph: <emoji>` until ARC-043, which
|
|
164
|
+
dropped it: `category` is a closed set of eight and already selects the mark the report draws, so a
|
|
165
|
+
per-epic emoji was a second, redundant decision on every line — and an emoji renders differently on
|
|
166
|
+
every platform, prints badly, and cannot take the report's own colour. Do **not** author one.
|
|
167
|
+
Lines written before ARC-043 still carry theirs; the parser reads past it and the report ignores it,
|
|
168
|
+
so they need no edit.
|
|
169
|
+
|
|
143
170
|
## Step 5 — Create the PR
|
|
144
171
|
|
|
145
172
|
- **GitHub:**
|
|
@@ -180,7 +180,6 @@
|
|
|
180
180
|
list-style: none;
|
|
181
181
|
}
|
|
182
182
|
.legend-item { display: inline-flex; align-items: center; gap: 0.4rem; font-size: 0.76rem; color: var(--ink-soft); }
|
|
183
|
-
.swatch { width: 0.6rem; height: 0.6rem; border-radius: 50%; flex: none; }
|
|
184
183
|
|
|
185
184
|
section.phase { margin-top: 2.5rem; }
|
|
186
185
|
|
|
@@ -219,7 +218,18 @@
|
|
|
219
218
|
.row:nth-child(even) { background: var(--row-alt); }
|
|
220
219
|
|
|
221
220
|
.item-title { display: flex; align-items: flex-start; gap: 0.55rem; font-weight: 600; font-size: 0.94rem; color: var(--ink); }
|
|
222
|
-
|
|
221
|
+
/* One icon per category, drawn from the sprite below and coloured by the
|
|
222
|
+
same token as the row's pill. Decorative: the pill carries the category in
|
|
223
|
+
text, so the icon is aria-hidden and never the sole carrier of meaning. */
|
|
224
|
+
.cat-icon { width: 1rem; height: 1rem; flex: none; margin-top: 0.12rem; }
|
|
225
|
+
.cat-icon.spell { color: var(--cat-spell); }
|
|
226
|
+
.cat-icon.feature { color: var(--cat-feature); }
|
|
227
|
+
.cat-icon.governance { color: var(--cat-governance); }
|
|
228
|
+
.cat-icon.decision { color: var(--cat-decision); }
|
|
229
|
+
.cat-icon.fix { color: var(--cat-fix); }
|
|
230
|
+
.cat-icon.process { color: var(--cat-process); }
|
|
231
|
+
.cat-icon.docs { color: var(--cat-docs); }
|
|
232
|
+
.cat-icon.platform { color: var(--cat-platform); }
|
|
223
233
|
.item-title a { color: inherit; text-decoration: none; }
|
|
224
234
|
.item-title a:hover { text-decoration: underline; }
|
|
225
235
|
.item-id { font-family: "IBM Plex Mono", monospace; font-size: 0.7rem; color: var(--ink-faint); margin-left: 0.4rem; font-weight: 500; }
|
|
@@ -273,6 +283,22 @@
|
|
|
273
283
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,500;9..144,600&family=IBM+Plex+Sans:wght@400;500;600&family=IBM+Plex+Mono:wght@500;600&display=swap">
|
|
274
284
|
</head>
|
|
275
285
|
<body>
|
|
286
|
+
<!-- Category icon sprite. Eight symbols, one per ReportCategory, referenced by
|
|
287
|
+
`<use href="#cat-<category>">` from each row and from the legend. Inline
|
|
288
|
+
rather than an emoji per row: an emoji renders differently on every
|
|
289
|
+
platform, often falls back to a box or a monochrome outline in print, and
|
|
290
|
+
cannot take the report's own category colour. All eight together are about
|
|
291
|
+
2 KB, and the file stays self-contained with no network fetch. -->
|
|
292
|
+
<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false"><defs>
|
|
293
|
+
<symbol id="cat-spell" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M8 1.75v3.5M8 10.75v3.5M1.75 8h3.5M10.75 8h3.5M3.6 3.6l2.4 2.4M10 10l2.4 2.4M12.4 3.6 10 6M6 10l-2.4 2.4"/></symbol>
|
|
294
|
+
<symbol id="cat-feature" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="2.25" y="2.25" width="11.5" height="11.5" rx="1"/><path d="M8 5.25v5.5M5.25 8h5.5"/></symbol>
|
|
295
|
+
<symbol id="cat-governance" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M8 1.75 13.25 3.75v4.2c0 3.05-2.2 5.3-5.25 6.3-3.05-1-5.25-3.25-5.25-6.3v-4.2z"/></symbol>
|
|
296
|
+
<symbol id="cat-decision" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M8 1.75 14.25 8 8 14.25 1.75 8z"/></symbol>
|
|
297
|
+
<symbol id="cat-fix" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M8 1.75 13.4 4.9v6.2L8 14.25 2.6 11.1V4.9z"/><circle cx="8" cy="8" r="2.25"/></symbol>
|
|
298
|
+
<symbol id="cat-process" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M13 8a5 5 0 0 1-8.65 3.45M3 8a5 5 0 0 1 8.65-3.45"/><path d="M11.65 2.15v2.4h-2.4M4.35 13.85v-2.4h2.4"/></symbol>
|
|
299
|
+
<symbol id="cat-docs" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3.75 1.75h5.75l2.75 2.75v9.75h-8.5z"/><path d="M9.5 1.75v3h3M6 8h4M6 10.75h4"/></symbol>
|
|
300
|
+
<symbol id="cat-platform" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="2.25" y="2.25" width="11.5" height="3.75" rx=".75"/><rect x="2.25" y="8.25" width="11.5" height="3.75" rx=".75"/><path d="M4.75 4.1h.01M4.75 10.1h.01M8 12v2.25"/></symbol>
|
|
301
|
+
</defs></svg>
|
|
276
302
|
<div class="page">
|
|
277
303
|
<main>
|
|
278
304
|
|
|
@@ -307,7 +333,7 @@
|
|
|
307
333
|
|
|
308
334
|
<ul class="legend" aria-label="Category legend">
|
|
309
335
|
{{#legend}}
|
|
310
|
-
<li class="legend-item"><
|
|
336
|
+
<li class="legend-item"><svg class="cat-icon {{key}}" aria-hidden="true" focusable="false"><use href="#cat-{{key}}"></use></svg>{{label}}</li>
|
|
311
337
|
{{/legend}}
|
|
312
338
|
</ul>
|
|
313
339
|
</header>
|
|
@@ -319,7 +345,7 @@
|
|
|
319
345
|
<ol class="ledger">
|
|
320
346
|
{{#rows}}
|
|
321
347
|
<li class="row">
|
|
322
|
-
<div class="item-title"
|
|
348
|
+
<div class="item-title"><svg class="cat-icon {{category}}" aria-hidden="true" focusable="false"><use href="#cat-{{category}}"></use></svg><span>{{#hasHref}}<a href="{{href}}">{{title}}</a>{{/hasHref}}{{^hasHref}}{{title}}{{/hasHref}}<span class="item-id">{{id}}</span></span></div>
|
|
323
349
|
{{#isUnwritten}}<div class="item-desc unwritten">unwritten</div>{{/isUnwritten}}
|
|
324
350
|
{{^isUnwritten}}<div class="item-desc">{{{descriptionHtml}}}</div>{{/isUnwritten}}
|
|
325
351
|
<span class="pill {{category}}">{{categoryLabel}}</span>
|
|
@@ -337,7 +363,7 @@
|
|
|
337
363
|
<ol class="ledger">
|
|
338
364
|
{{#highlights}}
|
|
339
365
|
<li class="row">
|
|
340
|
-
<div class="item-title"><span>{{title}}</span></div>
|
|
366
|
+
<div class="item-title"><svg class="cat-icon {{category}}" aria-hidden="true" focusable="false"><use href="#cat-{{category}}"></use></svg><span>{{title}}</span></div>
|
|
341
367
|
{{#isUnwritten}}<div class="item-desc unwritten">unwritten</div>{{/isUnwritten}}
|
|
342
368
|
{{^isUnwritten}}<div class="item-desc">{{{descriptionHtml}}}</div>{{/isUnwritten}}
|
|
343
369
|
<span class="pill {{category}}">{{categoryLabel}}</span>
|
package/dist/index.js
CHANGED
|
@@ -5123,7 +5123,7 @@ function parseFrontmatter(content) {
|
|
|
5123
5123
|
var EPIC_START = /^- \[([ x])\] \*\*([A-Z]+-\d+) — /gm;
|
|
5124
5124
|
var EPIC_TITLE = /^- \[[ x]\] \*\*[A-Z]+-\d+ — ([\s\S]+?)\.\*\*/;
|
|
5125
5125
|
var HEADING_LINE = /^(#{2,3}) (.+)$/gm;
|
|
5126
|
-
var REPORT_LINE = /\*\*Report:\*\*\s*([\s\S]*?)\s*·\s*category:\s*(
|
|
5126
|
+
var REPORT_LINE = /\*\*Report:\*\*\s*([\s\S]*?)\s*·\s*category:\s*([a-z]+)/;
|
|
5127
5127
|
var TITLE_OVERRIDE_LINE = /^\s*title:\s*(.+)$/m;
|
|
5128
5128
|
var PR_LINK = /\[PR #(\d+)\]\((https:\/\/github\.com\/[^)\s]+\/pull\/\d+)\)/g;
|
|
5129
5129
|
function parseEpics(content) {
|
|
@@ -5173,7 +5173,6 @@ function parseEpics(content) {
|
|
|
5173
5173
|
report: reportMatch ? {
|
|
5174
5174
|
description: reportMatch[1].replace(/\s+/g, " ").trim(),
|
|
5175
5175
|
category: reportMatch[2],
|
|
5176
|
-
glyph: reportMatch[3],
|
|
5177
5176
|
titleOverride: titleOverrideMatch ? titleOverrideMatch[1].trim() : void 0
|
|
5178
5177
|
} : null,
|
|
5179
5178
|
prLinks,
|
|
@@ -5205,11 +5204,32 @@ function parseParkedSection(content) {
|
|
|
5205
5204
|
reason: stripMarkdown(m[3].trim())
|
|
5206
5205
|
}));
|
|
5207
5206
|
}
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
|
|
5207
|
+
return unwrapBullets(body).flatMap((bullet) => {
|
|
5208
|
+
const match = /^\*\*(.+?)\*\*(.*?)\s*—\s*(.+)$/.exec(bullet);
|
|
5209
|
+
if (!match) return [];
|
|
5210
|
+
const tail = match[2].replace(/\([^)]*\)/g, "").replace(/\s+/g, " ").trim();
|
|
5211
|
+
const title = tail ? `${match[1].trim()} ${tail}` : match[1].trim();
|
|
5212
|
+
return [{ title: stripMarkdown(title), reason: stripMarkdown(match[3].trim()) }];
|
|
5213
|
+
});
|
|
5214
|
+
}
|
|
5215
|
+
function unwrapBullets(body) {
|
|
5216
|
+
const bullets = [];
|
|
5217
|
+
let current = null;
|
|
5218
|
+
const flush = () => {
|
|
5219
|
+
if (current) bullets.push(current.join(" ").replace(/\s+/g, " ").trim());
|
|
5220
|
+
current = null;
|
|
5221
|
+
};
|
|
5222
|
+
for (const line of body.split(/\r?\n/)) {
|
|
5223
|
+
if (/^- /.test(line)) {
|
|
5224
|
+
flush();
|
|
5225
|
+
current = [line.slice(2).trim()];
|
|
5226
|
+
} else if (current !== null) {
|
|
5227
|
+
if (/^\s+\S/.test(line)) current.push(line.trim());
|
|
5228
|
+
else flush();
|
|
5229
|
+
}
|
|
5230
|
+
}
|
|
5231
|
+
flush();
|
|
5232
|
+
return bullets;
|
|
5213
5233
|
}
|
|
5214
5234
|
function computeCoverageOutcome(content) {
|
|
5215
5235
|
const body = extractSection(content, "## Coverage Map");
|
|
@@ -5377,7 +5397,6 @@ function epicToRow(epic) {
|
|
|
5377
5397
|
href: epic.prLinks.length > 0 ? epic.prLinks[epic.prLinks.length - 1] : void 0,
|
|
5378
5398
|
refs: epic.prLinks
|
|
5379
5399
|
};
|
|
5380
|
-
if (epic.report?.glyph) row.glyph = epic.report.glyph;
|
|
5381
5400
|
return row;
|
|
5382
5401
|
}
|
|
5383
5402
|
function buildSections(epics) {
|
|
@@ -5424,13 +5443,20 @@ function buildNeedsYou(queueEntries) {
|
|
|
5424
5443
|
return entry;
|
|
5425
5444
|
});
|
|
5426
5445
|
}
|
|
5446
|
+
function truncateAtWord(text, limit) {
|
|
5447
|
+
if (text.length <= limit) return text;
|
|
5448
|
+
const window = text.slice(0, limit - 1);
|
|
5449
|
+
const lastSpace = window.lastIndexOf(" ");
|
|
5450
|
+
const head = lastSpace > 0 ? window.slice(0, lastSpace) : window;
|
|
5451
|
+
return `${head.replace(/[\s,;:.—-]+$/, "")}\u2026`;
|
|
5452
|
+
}
|
|
5427
5453
|
function buildCorrections(ledgerRows, programName) {
|
|
5428
5454
|
if (ledgerRows.length === 0) return void 0;
|
|
5429
5455
|
const corrected = ledgerRows.filter((r) => r.result === "corrected");
|
|
5430
5456
|
const unverifiable = ledgerRows.filter((r) => r.result === "unverifiable").length;
|
|
5431
5457
|
const highlights = corrected.slice(0, 3).map((row, i) => ({
|
|
5432
5458
|
id: `correction-${i + 1}`,
|
|
5433
|
-
title: row.claim
|
|
5459
|
+
title: truncateAtWord(row.claim, 80),
|
|
5434
5460
|
description: row.correction,
|
|
5435
5461
|
descriptionState: row.correction ? "authored" : "unwritten",
|
|
5436
5462
|
category: "fix",
|
|
@@ -5556,8 +5582,7 @@ function toRowView(row) {
|
|
|
5556
5582
|
categoryLabel: CATEGORY_LABELS[row.category] ?? row.category,
|
|
5557
5583
|
descriptionHtml: row.description === null ? null : inlineMarkupToHtml(row.description),
|
|
5558
5584
|
isUnwritten: row.descriptionState === "unwritten",
|
|
5559
|
-
hasHref: typeof row.href === "string" && row.href.length > 0
|
|
5560
|
-
hasGlyph: typeof row.glyph === "string" && row.glyph.length > 0
|
|
5585
|
+
hasHref: typeof row.href === "string" && row.href.length > 0
|
|
5561
5586
|
};
|
|
5562
5587
|
}
|
|
5563
5588
|
function buildShowReportView(model) {
|
|
@@ -5675,8 +5700,11 @@ async function runReportGeneration(options) {
|
|
|
5675
5700
|
const templateHtml = await readFile13(options.templatePath, "utf8");
|
|
5676
5701
|
const drifted = [];
|
|
5677
5702
|
const repaired = [];
|
|
5703
|
+
const unwritten = [];
|
|
5678
5704
|
for (const program2 of options.programs) {
|
|
5679
5705
|
const generated = await generateReport(options.rootDir, program2, templateHtml);
|
|
5706
|
+
const missing = JSON.parse(generated.json).sections.flatMap((section) => section.rows).filter((row) => row.descriptionState === "unwritten").map((row) => row.id);
|
|
5707
|
+
if (missing.length > 0) unwritten.push({ slug: program2.slug, ids: missing });
|
|
5680
5708
|
const outAbs = options.outDir ? isAbsolute(options.outDir) ? options.outDir : resolve3(options.rootDir, options.outDir) : resolve3(options.rootDir, dirname4(program2.planRelPath));
|
|
5681
5709
|
const outRel = toPosix(relative3(options.rootDir, outAbs));
|
|
5682
5710
|
const outputs = [
|
|
@@ -5695,7 +5723,7 @@ async function runReportGeneration(options) {
|
|
|
5695
5723
|
}
|
|
5696
5724
|
}
|
|
5697
5725
|
}
|
|
5698
|
-
return { drifted, repaired };
|
|
5726
|
+
return { drifted, repaired, unwritten };
|
|
5699
5727
|
}
|
|
5700
5728
|
|
|
5701
5729
|
// src/commands/report.ts
|
|
@@ -5746,6 +5774,11 @@ async function runReport(targetDir, options = {}, assetsDir) {
|
|
|
5746
5774
|
programs,
|
|
5747
5775
|
outDir: options.out
|
|
5748
5776
|
});
|
|
5777
|
+
for (const u of result.unwritten) {
|
|
5778
|
+
console.warn(
|
|
5779
|
+
`\u26A0 arcane: ${u.slug} has ${u.ids.length} epic(s) with no \`**Report:**\` line in PLAN.md -- they render as "unwritten": ${u.ids.join(", ")}.`
|
|
5780
|
+
);
|
|
5781
|
+
}
|
|
5749
5782
|
if (result.repaired.length === 0) {
|
|
5750
5783
|
console.log(`Show report: ${programs.length} program(s) already up to date.`);
|
|
5751
5784
|
return;
|