@dev-loops/core 1.0.1 → 1.0.2-slim.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/package.json +5 -2
- package/src/analysis/diff-analyzer.mjs +85 -137
- package/src/claude/asset-generation.mjs +7 -7
- package/src/claude/hook-decisions.mjs +167 -50
- package/src/config/config.mjs +388 -787
- package/src/config/extension-defaults.yaml +14 -9
- package/src/github/comment-id-guard.mjs +39 -1
- package/src/github/copilot-helpers.mjs +90 -158
- package/src/github/gh.mjs +49 -0
- package/src/loop/bash-command-classify.mjs +34 -49
- package/src/loop/commit-msg-guard.mjs +1 -1
- package/src/loop/conductor-routing.mjs +15 -23
- package/src/loop/copilot-loop-state.mjs +46 -94
- package/src/loop/gate-carry-forward.mjs +46 -22
- package/src/loop/gate-evidence-reconcile.mjs +75 -0
- package/src/loop/gate-fanin.mjs +266 -435
- package/src/loop/handoff-envelope.mjs +21 -21
- package/src/loop/issue-refinement-artifact.mjs +449 -284
- package/src/loop/lifecycle-state.mjs +10 -21
- package/src/loop/pr-gate-coordination.mjs +49 -49
- package/src/loop/queue-board-sync.mjs +16 -82
- package/src/loop/review-dispatch-plan.mjs +61 -122
- package/src/loop/review-lineage.mjs +19 -44
- package/src/loop/spec-authority.mjs +729 -0
- package/src/loop/steering.mjs +16 -68
- package/src/loop/ui-e2e-scoping.mjs +1 -0
- package/src/loop/worktree-guard.mjs +55 -0
- package/src/projects/list-queue-items.mjs +16 -175
- package/src/projects/move-queue-item.mjs +16 -171
- package/src/projects/projects-access.mjs +202 -0
- package/src/security/secret-scan.mjs +13 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dev-loops/core",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2-slim.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=24"
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"./loop/copilot-loop-iterations": "./src/loop/copilot-loop-iterations.mjs",
|
|
36
36
|
"./loop/copilot-loop-state": "./src/loop/copilot-loop-state.mjs",
|
|
37
37
|
"./loop/gate-carry-forward": "./src/loop/gate-carry-forward.mjs",
|
|
38
|
+
"./loop/gate-evidence-reconcile": "./src/loop/gate-evidence-reconcile.mjs",
|
|
38
39
|
"./loop/gate-fanin": "./src/loop/gate-fanin.mjs",
|
|
39
40
|
"./loop/handoff-envelope": "./src/loop/handoff-envelope.mjs",
|
|
40
41
|
"./loop/lifecycle-state": "./src/loop/lifecycle-state.mjs",
|
|
@@ -67,6 +68,7 @@
|
|
|
67
68
|
"./loop/run-context": "./src/loop/run-context.mjs",
|
|
68
69
|
"./loop/run-inspection": "./src/loop/run-inspection.mjs",
|
|
69
70
|
"./loop/run-post-merge-actions": "./src/loop/run-post-merge-actions.mjs",
|
|
71
|
+
"./loop/spec-authority": "./src/loop/spec-authority.mjs",
|
|
70
72
|
"./loop/spike-exit-contract": "./src/loop/spike-exit-contract.mjs",
|
|
71
73
|
"./loop/spike-intake-contract": "./src/loop/spike-intake-contract.mjs",
|
|
72
74
|
"./loop/steering": "./src/loop/steering.mjs",
|
|
@@ -81,6 +83,7 @@
|
|
|
81
83
|
"./security/secret-scan": "./src/security/secret-scan.mjs",
|
|
82
84
|
"./projects/list-queue-items": "./src/projects/list-queue-items.mjs",
|
|
83
85
|
"./projects/move-queue-item": "./src/projects/move-queue-item.mjs",
|
|
86
|
+
"./projects/projects-access": "./src/projects/projects-access.mjs",
|
|
84
87
|
"./projects/resolve-project": "./src/projects/resolve-project.mjs",
|
|
85
88
|
"./harness": "./src/harness/index.mjs",
|
|
86
89
|
"./tracker": "./src/tracker/index.mjs",
|
|
@@ -100,7 +103,7 @@
|
|
|
100
103
|
"bin/**/*.mjs"
|
|
101
104
|
],
|
|
102
105
|
"scripts": {
|
|
103
|
-
"test": "
|
|
106
|
+
"test": "bun ../../scripts/run-bun-test.mjs ./test/*.test.mjs"
|
|
104
107
|
},
|
|
105
108
|
"publishConfig": {
|
|
106
109
|
"access": "public",
|
|
@@ -1,54 +1,41 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Diff analysis for dynamic gate angle resolution.
|
|
3
3
|
*
|
|
4
|
-
* T0
|
|
5
|
-
*
|
|
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
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
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
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
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
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
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 (
|
|
41
|
-
* docs
|
|
42
|
-
*
|
|
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
|
-
//
|
|
50
|
-
//
|
|
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
|
-
*
|
|
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
|
|
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
|
-
//
|
|
98
|
-
//
|
|
99
|
-
//
|
|
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
|
-
//
|
|
122
|
-
//
|
|
123
|
-
//
|
|
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
|
|
135
|
-
//
|
|
136
|
-
//
|
|
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
|
-
*
|
|
206
|
-
*
|
|
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
|
|
220
|
-
// influenced input is where trust-boundary bugs concentrate
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
//
|
|
224
|
-
//
|
|
225
|
-
//
|
|
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 (
|
|
249
|
-
*
|
|
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
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
*
|
|
265
|
-
*
|
|
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
|
|
277
|
-
//
|
|
278
|
-
//
|
|
279
|
-
//
|
|
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
|
|
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
|
|
341
|
-
// the
|
|
342
|
-
//
|
|
343
|
-
//
|
|
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
|
-
//
|
|
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
|
|
386
|
-
//
|
|
387
|
-
//
|
|
388
|
-
//
|
|
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:
|
|
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 (
|
|
440
|
-
*
|
|
441
|
-
*
|
|
442
|
-
*
|
|
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
|
|
470
|
-
//
|
|
471
|
-
//
|
|
472
|
-
//
|
|
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
|
-
//
|
|
507
|
-
//
|
|
508
|
-
//
|
|
509
|
-
//
|
|
510
|
-
//
|
|
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
|
-
//
|
|
523
|
-
// pure-code diff (
|
|
524
|
-
//
|
|
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
|
|
531
|
-
//
|
|
532
|
-
//
|
|
533
|
-
//
|
|
534
|
-
//
|
|
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
|
-
*
|
|
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
|
|
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
|
|
79
|
-
*
|
|
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
|
|
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
|
|
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.
|