@dev-loops/core 1.0.2-pre.0 → 1.0.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dev-loops/core",
3
- "version": "1.0.2-pre.0",
3
+ "version": "1.0.2",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=24"
@@ -22,6 +22,7 @@
22
22
  "./debt/signal": "./src/debt/debt-signal.mjs",
23
23
  "./github/copilot-helpers": "./src/github/copilot-helpers.mjs",
24
24
  "./github/comment-id-guard": "./src/github/comment-id-guard.mjs",
25
+ "./github/closing-ref-guard": "./src/github/closing-ref-guard.mjs",
25
26
  "./github/gh": "./src/github/gh.mjs",
26
27
  "./github/issue-ops": "./src/github/issue-ops.mjs",
27
28
  "./github/ownership-helpers": "./src/github/ownership-helpers.mjs",
@@ -83,6 +84,7 @@
83
84
  "./security/secret-scan": "./src/security/secret-scan.mjs",
84
85
  "./projects/list-queue-items": "./src/projects/list-queue-items.mjs",
85
86
  "./projects/move-queue-item": "./src/projects/move-queue-item.mjs",
87
+ "./projects/projects-access": "./src/projects/projects-access.mjs",
86
88
  "./projects/resolve-project": "./src/projects/resolve-project.mjs",
87
89
  "./harness": "./src/harness/index.mjs",
88
90
  "./tracker": "./src/tracker/index.mjs",
@@ -1,54 +1,41 @@
1
1
  /**
2
2
  * Diff analysis for dynamic gate angle resolution.
3
3
  *
4
- * T0 — file-level: classifies files by extension and directory.
5
- * T1 — hunk-level: classifies hunks by change type (comments, imports, config, etc.).
6
- *
7
- * T2 (AST-level) is deferred to a follow-up.
8
- *
9
- * This module is intentionally pure and side-effect free.
4
+ * T0 file-level classifies files by extension/directory; T1 hunk-level
5
+ * classifies hunks by change type. This module is intentionally pure and
6
+ * side-effect free.
10
7
  */
11
8
 
12
9
  // ---------------------------------------------------------------------------
13
10
  // T0: File-level analysis
14
11
  // ---------------------------------------------------------------------------
15
12
 
16
- // Extensionless dotfile configs: static allowlist, no content sniffing. Matched
17
- // against the basename only, never a prefix/suffix guess. Deliberately just
18
- // .devloops (the reported consumer shape): runtime-version files like .nvmrc
19
- // were considered and REJECTED — classifying them config would let ci-guard/
20
- // determinism carry stale clean verdicts across a runtime bump; unknown
21
- // fails closed toward a full re-review, which is what a version bump needs.
13
+ // Extensionless dotfile configs: static allowlist matched on basename only.
14
+ // Runtime-version files like .nvmrc are REJECTED as config: classifying them
15
+ // config would carry stale clean verdicts across a runtime bump. Unknown fails
16
+ // closed to a full re-review.
22
17
  const DOTFILE_CONFIG_BASENAMES = new Set([".devloops"]);
23
18
 
24
- // #1442 (ADR 0041 prose half): the prose surface that triggers the required
25
- // `deslop` gate angle. skills/docs/** is deliberately excluded — those are
26
- // normative contracts (owned by the contract style guide + contradiction
27
- // lens), not prose, and deslop's contrast-cutting must not fight RFC-2119
28
- // modality.
19
+ // Prose surface that arms the required `deslop` gate angle.
20
+ // skills/docs/** is excluded via SKILLS_DOCS_EXEMPT_RE: those are normative
21
+ // contracts, not prose.
29
22
  const PROSE_PATH_RE = /^docs\/(articles|presentations)\//;
30
23
  const NARRATIVE_DOC_RE = /^docs\/[^/]+\.(md|markdown)$/;
31
- // #1442 review finding: the skills/docs/** exemption must hold for the README
32
- // rule too. PROSE_PATH_RE / NARRATIVE_DOC_RE are anchored to `docs/` so
33
- // skills/docs files never match them; the README basename rule is the ONLY
34
- // path by which a skills/docs file could be classified prose (e.g. a future
35
- // `skills/docs/README-*.md`). Carve the normative-contract subtree out
36
- // explicitly so a README-named contract never arms deslop.
24
+ // The README basename rule below is the only path by which a skills/docs file
25
+ // could be classified prose, so carve the normative-contract subtree out here
26
+ // so a README-named contract never arms deslop.
37
27
  const SKILLS_DOCS_EXEMPT_RE = /^(skills\/docs|\x2eclaude\/skills\/docs)\//;
38
28
 
39
29
  /**
40
- * Whether a file path is on the prose surface (#1442): docs/articles/**,
41
- * docs/presentations/**, README*, or a narrative direct-child docs/*.md.
42
- * skills/docs/** (and .claude/skills/docs/**) is exempt (normative
43
- * contracts, not prose) across ALL rules, including the README basename rule.
44
- *
30
+ * Whether a file path is on the prose surface. skills/docs/** (and
31
+ * .claude/skills/docs/**) is exempt across all rules, including the README
32
+ * basename rule.
45
33
  * @param {string} filePath
46
34
  * @returns {boolean}
47
35
  */
48
36
  export function isProsePath(filePath) {
49
- // #1442 review finding (input-validation): exported trust-boundary guard.
50
- // Fail closed (false) on a non-string/empty argument rather than crashing in
51
- // normalizeSep(filePath).replaceAll(...).
37
+ // Exported trust-boundary guard: fail closed (false) on a non-string/empty
38
+ // argument rather than crashing in normalizeSep().
52
39
  if (typeof filePath !== "string" || filePath.length === 0) return false;
53
40
  const fp = normalizeSep(filePath);
54
41
  if (SKILLS_DOCS_EXEMPT_RE.test(fp)) return false;
@@ -69,18 +56,14 @@ export function isProsePath(filePath) {
69
56
  */
70
57
 
71
58
  /**
72
- * Parse `git diff --name-status` output into a T0 analysis.
73
- *
74
- * Each line format: `<status>\t<path>` or `<status>\t<old>\t<new>`.
75
- *
76
- * @param {string} nameStatusOutput — raw stdout from `git diff --name-status`
59
+ * Parse `git diff --name-status` output into a T0 analysis. Line format:
60
+ * `<status>\t<path>` or `<status>\t<old>\t<new>`.
61
+ * @param {string} nameStatusOutput
77
62
  * @returns {T0Result}
78
63
  */
79
64
 
80
65
  /**
81
- * Normalize file path separators to forward slashes.
82
- * Handles Windows backslash paths from git output on Windows.
83
- *
66
+ * Normalize path separators to forward slashes (Windows backslash paths).
84
67
  * @param {string} filePath
85
68
  * @returns {string}
86
69
  */
@@ -94,12 +77,9 @@ export function analyzeT0(nameStatusOutput) {
94
77
  const extensions = new Set();
95
78
  const directories = new Set();
96
79
  let renameCount = 0;
97
- // #1442 review finding: prose arming must be scoped to content-carrying
98
- // rows. A pure deletion (`D` name-status row) has no prose content for the
99
- // deslop reviewer to strip — arming deslop on it over-selects and re-runs
100
- // the fan-out over a content-free delta. Added/modified/renamed-dest rows
101
- // (A / M / M? / R-dest) carry content and still arm prose. Rename rows emit
102
- // `R<score>\told\tnew` (rawPath = parts[2], the new content-bearing path).
80
+ // Prose arming is scoped to content-carrying rows: a pure deletion (`D`) has
81
+ // no prose content to strip, so it must not arm deslop. Added/modified/
82
+ // renamed-dest rows carry content and arm prose.
103
83
  let prosePresent = false;
104
84
 
105
85
  for (const line of lines) {
@@ -118,12 +98,9 @@ export function analyzeT0(nameStatusOutput) {
118
98
  if (dir) directories.add(dir);
119
99
 
120
100
  if (status.startsWith("R")) renameCount++;
121
- // #1442: a content-carrying prose file arms PROSE_PRESENT → deslop. Pure
122
- // deletions (`D`) are excluded (no prose content to strip, avoids noise);
123
- // a pure `R100` rename (git's 100% similarity score — no content changed)
124
- // is likewise content-free and must not arm deslop. Renames with a score
125
- // below 100 (`R<score>\told\tnew`, rawPath = the new content-bearing path)
126
- // carry content and still arm prose.
101
+ // Pure deletions (`D`) and a pure `R100` rename (100% similarity, no content
102
+ // changed) are content-free and must not arm deslop. Renames scored below
103
+ // 100 carry content and still arm prose.
127
104
  if (prosePresent) continue;
128
105
  if (status === "D" || status.startsWith("D")) continue;
129
106
  if (status.startsWith("R") && status === "R100") continue;
@@ -131,9 +108,9 @@ export function analyzeT0(nameStatusOutput) {
131
108
  }
132
109
 
133
110
  const renameOnly = lines.length > 0 && renameCount === lines.length;
134
- // Derive from the shared classifier so this predicate can't drift from it: a
135
- // code/config/test file hosted under docs/ is not prose, so a mixed diff that
136
- // includes one is not docs-only (it still gets the code-review surface).
111
+ // Derive from the shared classifier so this predicate can't drift: a code/
112
+ // config/test file under docs/ is not docs, so a mixed diff including one is
113
+ // not docs-only.
137
114
  const allDocs = lines.length > 0 && files.every((f) => classifyFile(f) === "docs");
138
115
 
139
116
  return {
@@ -202,10 +179,8 @@ export function classifyFile(filePath) {
202
179
  */
203
180
 
204
181
  /**
205
- * Check whether a diff line content (after stripping the + / - prefix) is
206
- * a comment or blank line — i.e. not logic.
207
- *
208
- * @param {string} content — trimmed line content (without + / - prefix)
182
+ * Whether a diff line's content (prefix stripped) is a comment or blank line.
183
+ * @param {string} content
209
184
  * @returns {boolean}
210
185
  */
211
186
  function isNonLogicLine(content) {
@@ -216,15 +191,13 @@ function isNonLogicLine(content) {
216
191
  return false;
217
192
  }
218
193
 
219
- // Security-sensitive seams (#1336): touching these primitives on caller-/plan-
220
- // influenced input is where trust-boundary bugs concentrate (drove #1335's 8
221
- // serial Copilot rounds). A changed line matching any of these triggers the
222
- // SECURITY_SENSITIVE_SEAM category so an up-front adversarial threat-model angle
223
- // is selected. Fail-safe by design — over-selection just adds one review lens.
224
- // Plain readFile/writeFile are deliberately excluded (ubiquitous JSON I/O would
225
- // flag nearly every script diff); the browser/process/network/destructive-fs/
226
- // upload seams below cover the genuinely dangerous surface, including #1335's
227
- // Playwright driver.
194
+ // Security-sensitive seams: touching these primitives on caller-/plan-
195
+ // influenced input is where trust-boundary bugs concentrate. A changed line
196
+ // matching any triggers the SECURITY_SENSITIVE_SEAM category, adding an up-front
197
+ // adversarial threat-model angle. Fail-safe: over-selection just adds one lens.
198
+ // Plain readFile/writeFile are excluded (ubiquitous JSON I/O would flag nearly
199
+ // every diff); the browser/process/network/destructive-fs/upload seams below
200
+ // cover the genuinely dangerous surface.
228
201
  const SECURITY_SEAM_PATTERNS = [
229
202
  // Browser automation (driving a real browser over semi-trusted navigation)
230
203
  /\b(playwright|webkit|chromium|puppeteer)\b/i,
@@ -245,10 +218,8 @@ const SECURITY_SEAM_PATTERNS = [
245
218
  ];
246
219
 
247
220
  /**
248
- * Whether a changed diff line (content, prefix stripped) touches a
249
- * security-sensitive seam (#1336).
250
- *
251
- * @param {string} content — trimmed line content (without + / - prefix)
221
+ * Whether a changed diff line (prefix stripped) touches a security-sensitive seam.
222
+ * @param {string} content
252
223
  * @returns {boolean}
253
224
  */
254
225
  function isSecuritySensitiveSeamLine(content) {
@@ -256,29 +227,22 @@ function isSecuritySensitiveSeamLine(content) {
256
227
  }
257
228
 
258
229
  /**
259
- * Scan a unified diff for a security-sensitive seam (#1336) on any added/removed
260
- * LOGIC line of a CODE file. Two gates keep it precise: (1) file-gate — only a
261
- * file that `classifyFile()` calls `code` is scanned, so a yaml/markdown/json
262
- * line that merely names a primitive (e.g. `shell: true` in a persona prompt, or
263
- * `child_process` in a doc) never triggers; (2) `!isNonLogicLine` — within a code
264
- * file, a comment/blank line that names a primitive (e.g. `// spawn( a child`)
265
- * does not trigger either. Runs independently of the T0/T1 category path so it
266
- * also covers a pure-code diff (all files classify as `code`), which is the MOST
267
- * concentrated seam case (e.g. editing a Playwright/child_process driver) and the
268
- * one #1336 targets.
269
- *
270
- * @param {string} diffOutput — raw unified diff output
230
+ * Scan a unified diff for a security-sensitive seam on any added/removed LOGIC
231
+ * line of a CODE file. Two gates keep it precise: (1) only files classifyFile()
232
+ * calls `code` are scanned, so a yaml/json/md line naming a primitive never
233
+ * triggers; (2) !isNonLogicLine, so a comment naming a primitive never triggers.
234
+ * Runs independently of the T0/T1 path so it also covers a pure-code diff, the
235
+ * most concentrated seam case.
236
+ * @param {string} diffOutput
271
237
  * @returns {boolean}
272
238
  */
273
239
  export function diffHasSecuritySeam(diffOutput) {
274
240
  if (!diffOutput) return false;
275
241
  let inHunk = false;
276
- // Only CODE files can carry an executable seam — a YAML/markdown/JSON line that
277
- // merely names a primitive (e.g. `shell: true` in a persona prompt) is not a
278
- // seam. Track the current file from the unified-diff `--- a/`/`+++ b/` headers
279
- // and gate the scan on `classifyFile(...) === "code"`. Bare-hunk input (no file
280
- // header — used in tests / direct hunk analysis) defaults to code so it still
281
- // scans; a real `git diff` always carries headers, so it is gated per file.
242
+ // Only CODE files can carry an executable seam: a YAML/markdown/JSON line
243
+ // naming a primitive (e.g. `shell: true`) is not a seam. Track the current
244
+ // file from the `--- a/`/`+++ b/` headers and gate on classifyFile === "code".
245
+ // Bare-hunk input (no header, used in tests) defaults to code so it still scans.
282
246
  let currentFileIsCode = true;
283
247
  let fromPath = null;
284
248
  for (const line of diffOutput.split("\n")) {
@@ -312,9 +276,7 @@ export function diffHasSecuritySeam(diffOutput) {
312
276
  *
313
277
  * Detects:
314
278
  * - COMMENT_ONLY: only comment lines changed
315
- * - DOCS_ONLY: emitted when docs files are PRESENT in the diff (docs extensions
316
- * .md/.markdown, or prose under docs/) — presence-based via
317
- * t0PresentSurfaceCategories, not exclusivity
279
+ * - DOCS_ONLY: emitted when docs files are present (presence-based, not exclusive)
318
280
  * - CONFIG_ONLY: only config files changed
319
281
  * - TEST_ONLY: only test files changed
320
282
  * - RENAME_ONLY: all renames, no content changes
@@ -337,11 +299,10 @@ export function analyzeT1(diffOutput, t0) {
337
299
  let allChangedLinesAreNonLogic = true;
338
300
 
339
301
  for (const line of lines) {
340
- // A new file's header block (diff --git, index, --- a/..., +++ b/...) ends
341
- // the previous file's hunk run. Resetting here is what lets the counting
342
- // below treat EVERY +/- line inside a hunk as content: a removed line whose
343
- // content itself starts with "--" (a CLI flag, a YAML document separator)
344
- // renders as "---…" in the diff, and a prefix-based header exclusion would
302
+ // A new file's header block ends the previous file's hunk run. Resetting
303
+ // here lets the counting below treat EVERY +/- line inside a hunk as
304
+ // content: a removed line whose content starts with "--" (a CLI flag, a YAML
305
+ // separator) renders as "---…" and a prefix-based header exclusion would
345
306
  // silently drop it from the counts.
346
307
  if (line.startsWith("diff --git ")) {
347
308
  inHunk = false;
@@ -379,19 +340,16 @@ export function analyzeT1(diffOutput, t0) {
379
340
  // Build categories from T0 (shared with inferCategoriesFromT0) + hunk analysis.
380
341
  for (const c of t0FileCategories(t0)) categories.add(c);
381
342
  if (hasLogicChange) categories.add("LOGIC_CHANGE");
382
- // #1336: a diff touching a security-sensitive seam gets an up-front adversarial
343
+ // a diff touching a security-sensitive seam gets an up-front adversarial
383
344
  // threat-model angle, batched at draft time instead of drip-fed via Copilot.
384
345
  if (diffHasSecuritySeam(diffOutput)) categories.add("SECURITY_SENSITIVE_SEAM");
385
- // Mixed diffs never satisfy the exclusive `_ONLY` checks above (some files are
386
- // code), so their peripheral surfaces would be dropped. In this hunk-level path
387
- // (only reached for genuinely mixed diffs), also union each surface by PRESENCE
388
- // so e.g. a code+workflow diff pulls ci-guard alongside the LOGIC_CHANGE core
389
- // (AC: mixed logic+CI -> core union ci-guard). The pure single-surface path
390
- // (inferCategoriesFromT0) keeps exclusive semantics.
346
+ // Mixed diffs never satisfy the exclusive `_ONLY` checks (some files are code),
347
+ // so union each peripheral surface by PRESENCE (e.g. code+workflow pulls
348
+ // ci-guard alongside LOGIC_CHANGE). The single-surface path keeps exclusive
349
+ // semantics.
391
350
  for (const c of t0PresentSurfaceCategories(t0)) categories.add(c);
392
351
 
393
- // COMMENT_ONLY: hunkCount > 0 (real diff), has changed lines, all are non-logic,
394
- // and not a rename-only change
352
+ // COMMENT_ONLY: real diff, all changed lines non-logic, not a rename.
395
353
  if (hunkCount > 0 && hasAnyChangedLine && allChangedLinesAreNonLogic && !t0.renameOnly) {
396
354
  categories.add("COMMENT_ONLY");
397
355
  }
@@ -436,12 +394,10 @@ function t0FileCategories(t0) {
436
394
  }
437
395
 
438
396
  /**
439
- * Surface categories present in a MIXED diff (at least one file of the surface),
440
- * used only by the hunk-level path to union a mixed diff's peripheral lenses on
441
- * top of LOGIC_CHANGE. Reuses the same category names / angle mappings as the
442
- * exclusive path; presence (not exclusivity) is the correct trigger for a mixed
443
- * diff. Renames are handled by the exclusive path, so they are excluded here.
444
- *
397
+ * Surface categories present in a MIXED diff (>=1 file of the surface), used by
398
+ * the hunk-level path to union a mixed diff's peripheral lenses on top of
399
+ * LOGIC_CHANGE. Presence (not exclusivity) is the correct trigger for a mixed
400
+ * diff. Renames are handled by the exclusive path, so excluded here.
445
401
  * @param {T0Result} t0
446
402
  * @returns {string[]}
447
403
  */
@@ -466,12 +422,10 @@ function t0PresentSurfaceCategories(t0) {
466
422
  */
467
423
  function inferCategoriesFromT0(t0) {
468
424
  const categories = t0FileCategories(t0);
469
- // Pure code-only change: a diff whose files all classify as code (and is not a
470
- // rename) is a LOGIC_CHANGE. Without this, an all-code diff has a single file
471
- // category (so analyzeDiff never runs hunk-level T1) and produces no category,
472
- // which resolveDynamicAngles treats as "unclassifiable" → fallback-to-all. That
473
- // regressed the primary case: a code-only PR must resolve to the LOGIC_CHANGE
474
- // core review subset, not all angles.
425
+ // Pure code-only change (all files classify as code, not a rename) is a
426
+ // LOGIC_CHANGE. Without this an all-code diff yields no category, which
427
+ // resolveDynamicAngles treats as unclassifiable → fallback-to-all, regressing
428
+ // the primary case: a code-only PR must resolve to the LOGIC_CHANGE subset.
475
429
  if (!t0.renameOnly && t0.files.length > 0 && t0.files.every((f) => classifyFile(f) === "code")) {
476
430
  categories.push("LOGIC_CHANGE");
477
431
  }
@@ -503,14 +457,11 @@ export function analyzeDiff({ nameStatusOutput, diffOutput }) {
503
457
  // When t1 is null (unambiguous diff), infer categories from t0
504
458
  // so dynamic angle resolution can narrow for config-only / test-only etc.
505
459
  if (!t1) {
506
- // #1442 Copilot finding: a genuinely MIXED diff whose T1 never ran (no
507
- // diffOutput) must NOT get a T0-only PROSE_PRESENT category. Non-empty
508
- // categories set ambiguous=false, so a mixed code+prose diff would
509
- // under-select to just deslop + always-include and drop the code-review
510
- // core. T0-only inference is only safe for unambiguous diffs (docs-only /
511
- // single surface); a mixed diff without hunk content is unclassifiable, so
512
- // return empty categories and let resolveDynamicAngles fall back to the
513
- // full angle set (fail closed).
460
+ // A genuinely MIXED diff whose T1 never ran (no diffOutput) must NOT get a
461
+ // T0-only category: non-empty categories set ambiguous=false, so it would
462
+ // under-select and drop the code-review core. T0-only inference is safe only
463
+ // for unambiguous diffs; a mixed diff without hunk content is unclassifiable,
464
+ // so return empty categories and fall back to the full angle set (fail closed).
514
465
  const changeCategories = t0Ambiguous ? [] : inferCategoriesFromT0(t0);
515
466
  t1 = {
516
467
  changeCategories,
@@ -519,21 +470,18 @@ export function analyzeDiff({ nameStatusOutput, diffOutput }) {
519
470
  };
520
471
  }
521
472
 
522
- // #1336: seam detection runs on the raw diff regardless of the T0/T1 path, so a
523
- // pure-code diff (single `code` category, T1 skipped) editing a browser/exec/
524
- // fetch/fs-mutation driver still triggers the up-front threat-model angle — the
525
- // most concentrated seam case, and the one this feature targets.
473
+ // Seam detection runs on the raw diff regardless of the T0/T1 path, so a
474
+ // pure-code diff (T1 skipped) editing a browser/exec/fetch/fs-mutation driver
475
+ // still triggers the threat-model angle.
526
476
  if (!t1.changeCategories.includes("SECURITY_SENSITIVE_SEAM") && diffHasSecuritySeam(diffOutput)) {
527
477
  t1.changeCategories.push("SECURITY_SENSITIVE_SEAM");
528
478
  }
529
479
 
530
- // `ambiguous` flags one specific case: a diff T0 could not classify (mixed file
531
- // categories, so t0Ambiguous) AND whose hunk analysis still produced no
532
- // category. It is NOT the only fallback trigger — resolveDynamicAngles also
533
- // falls back whenever changeCategories is empty (e.g. a single lone unknown/
534
- // asset file yields no category yet is not t0Ambiguous). A mixed diff that
535
- // yields a category (e.g. LOGIC_CHANGE) is classified and not ambiguous, so
536
- // LOGIC_CHANGE never forces fallback-to-all via this flag.
480
+ // `ambiguous` flags one case: a diff T0 could not classify (mixed categories)
481
+ // AND whose hunk analysis produced no category. It is NOT the only fallback
482
+ // trigger — resolveDynamicAngles also falls back whenever changeCategories is
483
+ // empty. A mixed diff that yields a category (e.g. LOGIC_CHANGE) is classified
484
+ // and not ambiguous, so LOGIC_CHANGE never forces fallback-to-all via this flag.
537
485
  const ambiguous = t0Ambiguous && t1.changeCategories.length === 0;
538
486
 
539
487
  return { t0, t1, ambiguous };
@@ -7,8 +7,8 @@
7
7
  * Pi→Claude tool-name mapping (confirmed against Claude Code docs):
8
8
  * read→Read, search→Grep+Glob, execute→Bash, bash→Bash, edit→Edit, write→Write,
9
9
  * agent→Agent, subagent→Agent, todo→TodoWrite, review_loop→Agent (the review subagent).
10
- * Frontmatter *tool lists* are rewritten, and bodies are copied through `stripPiOnlyBlocks`
11
- * (#817): `<!-- pi-only -->`…`<!-- /pi-only -->` sections are removed for the Claude output so
10
+ * Frontmatter *tool lists* are rewritten, and bodies are copied through `stripPiOnlyBlocks`:
11
+ * `<!-- pi-only -->`…`<!-- /pi-only -->` sections are removed for the Claude output so
12
12
  * Pi-runtime-specific prose (e.g. `tools: [subagent]`/`maxSubagentDepth` assertions, the
13
13
  * `contact_supervisor`/`pi-intercom` bug guidance) doesn't contradict the Claude assets. The
14
14
  * source stays Pi-complete; general `subagent` prose is preserved (Claude has subagents too).
@@ -46,7 +46,7 @@ const GENERATED_NOTE = (source) =>
46
46
  `<!-- GENERATED from ${source} by scripts/claude/generate-claude-assets.mjs — do not edit; edit the source and regenerate. -->`;
47
47
 
48
48
  /**
49
- * Strip Pi-runtime-only prose blocks from a body for the Claude output (#817).
49
+ * Strip Pi-runtime-only prose blocks from a body for the Claude output.
50
50
  *
51
51
  * The canonical sources stay Pi-complete; sections that are Pi-runtime-specific and misleading
52
52
  * under Claude (e.g. `tools: [subagent]`/`maxSubagentDepth` assertions that contradict the
@@ -75,11 +75,11 @@ export function stripPiOnlyBlocks(body) {
75
75
  }
76
76
 
77
77
  /**
78
- * Rewrite the Pi package-local CLI invocation into the Claude version-pinned `npx` form (#801,
79
- * #833). The Pi runtime sources invoke the CLI as `node <dev-loops-package-root>/cli/index.mjs`
78
+ * Rewrite the Pi package-local CLI invocation into the Claude version-pinned `npx` form.
79
+ * The Pi runtime sources invoke the CLI as `node <dev-loops-package-root>/cli/index.mjs`
80
80
  * (resolves unambiguously from the installed package). The Claude plugin does NOT bundle `cli/`,
81
81
  * so for the generated tree those tokens become `npx dev-loops@<version>` — pinning the version
82
- * keeps the CLI from drifting against the published plugin version (#833). The Pi-only
82
+ * keeps the CLI from drifting against the published plugin version. The Pi-only
83
83
  * package-root resolution note is removed separately by `stripPiOnlyBlocks`.
84
84
  *
85
85
  * @param {string} body
@@ -210,7 +210,7 @@ export function transformAgent({ source, raw, version = "latest", config = {} })
210
210
 
211
211
  /**
212
212
  * Transform a canonical `commands/<name>.command.md` into a Claude `.claude/commands/<name>.md`
213
- * slash command (#972). Commands are thin wrappers over the public dev-loop contract: the body
213
+ * slash command. Commands are thin wrappers over the public dev-loop contract: the body
214
214
  * is a prompt (with `$ARGUMENTS`) that invokes the existing entrypoint, so there is NO routing
215
215
  * logic here. Frontmatter keeps Claude's command fields (`description`, `argument-hint`); the body
216
216
  * is passed through `stripPiOnlyBlocks` + `rewriteCliInvocation` like agents/skills.