@dev-loops/core 1.0.2 → 1.0.4-pre.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 +10 -1
- package/src/analysis/change-classifier.mjs +35 -0
- package/src/analysis/diff-analyzer.mjs +89 -16
- package/src/claude/asset-generation.mjs +64 -3
- package/src/claude/hook-decisions.mjs +213 -65
- package/src/config/config.mjs +473 -43
- package/src/config/extension-defaults.yaml +48 -0
- package/src/github/copilot-helpers.mjs +79 -1
- package/src/github/issue-ops.mjs +4 -0
- package/src/github/repo-slug.mjs +25 -4
- package/src/github/test-mode-write-guard.mjs +81 -0
- package/src/loop/bash-command-classify.mjs +396 -42
- package/src/loop/child-launch-bound.mjs +152 -0
- package/src/loop/copilot-ci-status.mjs +116 -6
- package/src/loop/copilot-loop-state.mjs +20 -4
- package/src/loop/execution-record.mjs +412 -0
- package/src/loop/finding-cluster.mjs +296 -0
- package/src/loop/fixer-disposition.mjs +200 -0
- package/src/loop/gate-carry-forward.mjs +39 -6
- package/src/loop/gate-fanin.mjs +82 -3
- package/src/loop/issue-refinement-artifact.mjs +117 -9
- package/src/loop/merge-approval.mjs +399 -0
- package/src/loop/pr-gate-coordination.mjs +123 -12
- package/src/loop/queue-board-sync.mjs +6 -3
- package/src/loop/reviewer-unit-bound.mjs +308 -0
- package/src/loop/role-budget-bound.mjs +242 -0
- package/src/loop/run-inspection.mjs +6 -0
- package/src/loop/size-budget-merge-gate.mjs +48 -12
- package/src/loop/spec-authority.mjs +19 -6
- package/src/loop/ui-e2e-scoping.mjs +1 -0
- package/src/loop/watcher-exclusivity.mjs +302 -0
- package/src/security/secret-scan.mjs +13 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dev-loops/core",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4-pre.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=24"
|
|
@@ -28,19 +28,25 @@
|
|
|
28
28
|
"./github/ownership-helpers": "./src/github/ownership-helpers.mjs",
|
|
29
29
|
"./github/repo-slug": "./src/github/repo-slug.mjs",
|
|
30
30
|
"./github/review-threads": "./src/github/review-threads.mjs",
|
|
31
|
+
"./github/test-mode-write-guard": "./src/github/test-mode-write-guard.mjs",
|
|
31
32
|
"./loop/async-start-contract": "./src/loop/async-start-contract.mjs",
|
|
32
33
|
"./loop/agent-stall": "./src/loop/agent-stall.mjs",
|
|
33
34
|
"./loop/bash-command-classify": "./src/loop/bash-command-classify.mjs",
|
|
35
|
+
"./loop/child-launch-bound": "./src/loop/child-launch-bound.mjs",
|
|
36
|
+
"./loop/reviewer-unit-bound": "./src/loop/reviewer-unit-bound.mjs",
|
|
34
37
|
"./loop/conductor-routing": "./src/loop/conductor-routing.mjs",
|
|
35
38
|
"./loop/copilot-ci-status": "./src/loop/copilot-ci-status.mjs",
|
|
36
39
|
"./loop/copilot-loop-iterations": "./src/loop/copilot-loop-iterations.mjs",
|
|
37
40
|
"./loop/copilot-loop-state": "./src/loop/copilot-loop-state.mjs",
|
|
41
|
+
"./loop/finding-cluster": "./src/loop/finding-cluster.mjs",
|
|
42
|
+
"./loop/fixer-disposition": "./src/loop/fixer-disposition.mjs",
|
|
38
43
|
"./loop/gate-carry-forward": "./src/loop/gate-carry-forward.mjs",
|
|
39
44
|
"./loop/gate-evidence-reconcile": "./src/loop/gate-evidence-reconcile.mjs",
|
|
40
45
|
"./loop/gate-fanin": "./src/loop/gate-fanin.mjs",
|
|
41
46
|
"./loop/handoff-envelope": "./src/loop/handoff-envelope.mjs",
|
|
42
47
|
"./loop/lifecycle-state": "./src/loop/lifecycle-state.mjs",
|
|
43
48
|
"./loop/markdown-sections": "./src/loop/markdown-sections.mjs",
|
|
49
|
+
"./loop/merge-approval": "./src/loop/merge-approval.mjs",
|
|
44
50
|
"./loop/issue-refinement-artifact": "./src/loop/issue-refinement-artifact.mjs",
|
|
45
51
|
"./loop/phase-files": "./src/loop/phase-files.mjs",
|
|
46
52
|
"./loop/policy-constants": "./src/loop/policy-constants.mjs",
|
|
@@ -63,9 +69,11 @@
|
|
|
63
69
|
"./loop/queue-state": "./src/loop/queue-state.mjs",
|
|
64
70
|
"./loop/size-budget-merge-gate": "./src/loop/size-budget-merge-gate.mjs",
|
|
65
71
|
"./loop/cache-telemetry-evidence": "./src/loop/cache-telemetry-evidence.mjs",
|
|
72
|
+
"./loop/execution-record": "./src/loop/execution-record.mjs",
|
|
66
73
|
"./loop/primer-evidence": "./src/loop/primer-evidence.mjs",
|
|
67
74
|
"./loop/review-dispatch-plan": "./src/loop/review-dispatch-plan.mjs",
|
|
68
75
|
"./loop/reviewer-loop-state": "./src/loop/reviewer-loop-state.mjs",
|
|
76
|
+
"./loop/role-budget-bound": "./src/loop/role-budget-bound.mjs",
|
|
69
77
|
"./loop/run-context": "./src/loop/run-context.mjs",
|
|
70
78
|
"./loop/run-inspection": "./src/loop/run-inspection.mjs",
|
|
71
79
|
"./loop/run-post-merge-actions": "./src/loop/run-post-merge-actions.mjs",
|
|
@@ -75,6 +83,7 @@
|
|
|
75
83
|
"./loop/steering": "./src/loop/steering.mjs",
|
|
76
84
|
"./loop/timeout-policy": "./src/loop/timeout-policy.mjs",
|
|
77
85
|
"./loop/tracker-pr-state": "./src/loop/tracker-pr-state.mjs",
|
|
86
|
+
"./loop/watcher-exclusivity": "./src/loop/watcher-exclusivity.mjs",
|
|
78
87
|
"./loop/ui-e2e-scoping": "./src/loop/ui-e2e-scoping.mjs",
|
|
79
88
|
"./loop/ui-review-provision": "./src/loop/ui-review-provision.mjs",
|
|
80
89
|
"./loop/ui-review-drive": "./src/loop/ui-review-drive.mjs",
|
|
@@ -117,6 +117,13 @@ export const ALWAYS_INCLUDE = new Set(["gate-evidence", "renderer-security", "pr
|
|
|
117
117
|
* When `anglePool` is omitted, additive mode is off and `addedAngles` is
|
|
118
118
|
* always empty.
|
|
119
119
|
*
|
|
120
|
+
* A configured angle that is NOT in CATEGORY_ANGLE_MAP (a consumer-defined
|
|
121
|
+
* angle) can still be recommended BY change category or file kind when it
|
|
122
|
+
* declares `categories`/`kinds` via `angleDeclarations`. This is purely
|
|
123
|
+
* additive: it can only SELECT such an angle when the diff intersects its
|
|
124
|
+
* declaration; it never drops any angle the catalog map, ALWAYS_INCLUDE, or the
|
|
125
|
+
* fallback-to-all path would otherwise recommend.
|
|
126
|
+
*
|
|
120
127
|
* @param {object} options
|
|
121
128
|
* @param {string[]} options.configuredAngles — all angles configured for this gate
|
|
122
129
|
* @param {string[]} options.changeCategories — from diff analysis
|
|
@@ -124,6 +131,11 @@ export const ALWAYS_INCLUDE = new Set(["gate-evidence", "renderer-security", "pr
|
|
|
124
131
|
* @param {string[]} [options.anglePool] — catalog of angles eligible for additive
|
|
125
132
|
* selection (caller pre-filters this against excludeAngles); when undefined,
|
|
126
133
|
* additive selection is disabled
|
|
134
|
+
* @param {Record<string, {categories?: string[], kinds?: string[]}>} [options.angleDeclarations]
|
|
135
|
+
* — per-angle category/file-kind bindings for consumer angles absent from
|
|
136
|
+
* CATEGORY_ANGLE_MAP; a match recommends the angle deterministically
|
|
137
|
+
* @param {string[]} [options.fileKinds] — file kinds present in the diff
|
|
138
|
+
* (classifyFile output), used to honor an angle's `kinds` declaration
|
|
127
139
|
* @returns {DynamicAngleResult}
|
|
128
140
|
*/
|
|
129
141
|
export function resolveDynamicAngles({
|
|
@@ -131,6 +143,8 @@ export function resolveDynamicAngles({
|
|
|
131
143
|
changeCategories,
|
|
132
144
|
ambiguous = false,
|
|
133
145
|
anglePool,
|
|
146
|
+
angleDeclarations = {},
|
|
147
|
+
fileKinds = [],
|
|
134
148
|
}) {
|
|
135
149
|
// Fallback: ambiguous diff → all angles
|
|
136
150
|
if (ambiguous) {
|
|
@@ -177,6 +191,27 @@ export function resolveDynamicAngles({
|
|
|
177
191
|
}
|
|
178
192
|
}
|
|
179
193
|
|
|
194
|
+
// Consumer angles bound by declaration: a configured angle absent from
|
|
195
|
+
// CATEGORY_ANGLE_MAP can name the change-categories / file-kinds that select
|
|
196
|
+
// it. Recommend it when the diff intersects that declaration, so it need not
|
|
197
|
+
// be forced `mandatory` to survive dynamic pruning. Additive only: never
|
|
198
|
+
// removes an angle already recommended above.
|
|
199
|
+
const changeCatSet = new Set(changeCategories);
|
|
200
|
+
const fileKindSet = new Set(fileKinds);
|
|
201
|
+
for (const angle of configuredAngles) {
|
|
202
|
+
if (recommended.has(angle)) continue;
|
|
203
|
+
const decl = angleDeclarations[angle];
|
|
204
|
+
if (!decl) continue;
|
|
205
|
+
const catHit = (decl.categories ?? []).some((c) => changeCatSet.has(c));
|
|
206
|
+
const kindHit = (decl.kinds ?? []).some((k) => fileKindSet.has(k));
|
|
207
|
+
if (catHit || kindHit) {
|
|
208
|
+
recommended.add(angle);
|
|
209
|
+
if (!triggers.has(angle)) {
|
|
210
|
+
triggers.set(angle, catHit ? "declared-category" : "declared-kind");
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
180
215
|
// Filter to only angles that are configured
|
|
181
216
|
const recommendedAngles = configuredAngles.filter((a) => recommended.has(a));
|
|
182
217
|
const skippedAngles = configuredAngles.filter((a) => !recommended.has(a));
|
|
@@ -16,6 +16,57 @@
|
|
|
16
16
|
// closed to a full re-review.
|
|
17
17
|
const DOTFILE_CONFIG_BASENAMES = new Set([".devloops"]);
|
|
18
18
|
|
|
19
|
+
// Generic classifier tables. Files are classified by principle (broad extension
|
|
20
|
+
// tables + directory/basename conventions), not by a bespoke per-language rule.
|
|
21
|
+
// A new mainstream language is covered by adding its extension here, not by a new
|
|
22
|
+
// branch. Deliberate fail-closed exceptions (`.ruby-version`, `.nvmrc`,
|
|
23
|
+
// stylesheets) are simply absent from every table, so they fall through to
|
|
24
|
+
// "unknown".
|
|
25
|
+
|
|
26
|
+
// Source extensions across the common languages. A file with one of these is
|
|
27
|
+
// executable/product logic → "code".
|
|
28
|
+
const CODE_EXTENSIONS = new Set([
|
|
29
|
+
// JS/TS
|
|
30
|
+
".mjs", ".cjs", ".js", ".jsx", ".ts", ".mts", ".cts", ".tsx",
|
|
31
|
+
// Ruby (source + Rails view templates, which embed control flow / XSS surface)
|
|
32
|
+
".rb", ".rake", ".erb", ".haml", ".slim", ".jbuilder",
|
|
33
|
+
// Python
|
|
34
|
+
".py", ".pyi",
|
|
35
|
+
// Go
|
|
36
|
+
".go",
|
|
37
|
+
// Rust
|
|
38
|
+
".rs",
|
|
39
|
+
// Java / Kotlin / Scala
|
|
40
|
+
".java", ".kt", ".kts", ".scala", ".sc",
|
|
41
|
+
// C / C++ / C#
|
|
42
|
+
".c", ".h", ".cc", ".cpp", ".cxx", ".hpp", ".hh", ".hxx", ".cs",
|
|
43
|
+
// PHP / Swift / Elixir / Lua / Dart
|
|
44
|
+
".php", ".swift", ".ex", ".exs", ".lua", ".dart",
|
|
45
|
+
// Shell
|
|
46
|
+
".sh", ".bash", ".zsh", ".fish",
|
|
47
|
+
]);
|
|
48
|
+
// Basename-only source files (no discriminating extension).
|
|
49
|
+
const CODE_BASENAMES = new Set(["Rakefile"]);
|
|
50
|
+
|
|
51
|
+
// Data/config/manifest extensions. `.ru` (rackup) and `.gemspec` are Ruby
|
|
52
|
+
// packaging surface. `.lock` covers every lockfile (Gemfile.lock, Cargo.lock,
|
|
53
|
+
// poetry.lock, yarn.lock, …).
|
|
54
|
+
const CONFIG_EXTENSIONS = new Set([
|
|
55
|
+
".yml", ".yaml", ".json", ".toml", ".ini", ".cfg", ".lock", ".ru", ".gemspec",
|
|
56
|
+
]);
|
|
57
|
+
// Manifest basenames without a config extension.
|
|
58
|
+
const CONFIG_BASENAMES = new Set([
|
|
59
|
+
"Gemfile", "go.mod", "go.sum", "requirements.txt", "pom.xml", "Dockerfile", "Makefile",
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
// Prose/documentation extensions.
|
|
63
|
+
const DOCS_EXTENSIONS = new Set([".md", ".markdown", ".rst", ".adoc", ".txt"]);
|
|
64
|
+
|
|
65
|
+
// Test conventions. A directory segment named any of these, or a basename token
|
|
66
|
+
// (`*_test.*`, `*_spec.*`, `test_*`, `*.test.*`, `*.spec.*`), marks a test file.
|
|
67
|
+
const TEST_DIR_SEGMENTS = new Set(["test", "tests", "spec", "specs", "__tests__"]);
|
|
68
|
+
const TEST_BASENAME_RE = /(\.test\.|\.spec\.|_test\.|_spec\.|^test_)/;
|
|
69
|
+
|
|
19
70
|
// Prose surface that arms the required `deslop` gate angle.
|
|
20
71
|
// skills/docs/** is excluded via SKILLS_DOCS_EXEMPT_RE: those are normative
|
|
21
72
|
// contracts, not prose.
|
|
@@ -135,34 +186,56 @@ export function analyzeT0(nameStatusOutput) {
|
|
|
135
186
|
*/
|
|
136
187
|
export function classifyFile(filePath) {
|
|
137
188
|
const fp = normalizeSep(filePath);
|
|
189
|
+
const base = fp.split("/").pop();
|
|
190
|
+
// Extension excludes a leading-dot dotfile (`.nvmrc` → "", not ".nvmrc"), so
|
|
191
|
+
// runtime-version dotfiles never match an extension table and fall through to
|
|
192
|
+
// "unknown".
|
|
193
|
+
const dot = base.lastIndexOf(".");
|
|
194
|
+
const ext = dot > 0 ? base.slice(dot).toLowerCase() : "";
|
|
195
|
+
|
|
138
196
|
if (fp.startsWith(".github/")) {
|
|
139
197
|
return "ci";
|
|
140
198
|
}
|
|
141
|
-
//
|
|
142
|
-
//
|
|
143
|
-
// not prose.
|
|
199
|
+
// Config wins over every later surface: a manifest/data file (even one hosted
|
|
200
|
+
// under docs/, or a build.gradle.kts that also has a code extension) is config,
|
|
201
|
+
// not prose or code. `.ruby-version`/`.nvmrc` are absent from all tables and so
|
|
202
|
+
// stay "unknown" — a runtime bump must re-run ci-guard/determinism, not carry a
|
|
203
|
+
// stale clean verdict.
|
|
144
204
|
if (
|
|
145
|
-
|
|
146
|
-
|
|
205
|
+
CONFIG_EXTENSIONS.has(ext) ||
|
|
206
|
+
CONFIG_BASENAMES.has(base) ||
|
|
207
|
+
DOTFILE_CONFIG_BASENAMES.has(base) ||
|
|
208
|
+
base.startsWith("build.gradle")
|
|
147
209
|
) {
|
|
148
210
|
return "config";
|
|
149
211
|
}
|
|
150
|
-
|
|
151
|
-
|
|
212
|
+
// Generic test convention, part 1: a basename carrying a test/spec token
|
|
213
|
+
// (`*_test.*`, `*_spec.*`, `test_*`, `*.test.*`, `*.spec.*`) is a test wherever
|
|
214
|
+
// it lives — a strong per-file signal that subsumes the old `.test.` and Ruby
|
|
215
|
+
// `*_spec.rb`/`*_test.rb` rules and wins even under `docs/`.
|
|
216
|
+
if (TEST_BASENAME_RE.test(base)) {
|
|
217
|
+
return "test";
|
|
152
218
|
}
|
|
153
|
-
|
|
219
|
+
// Generic test convention, part 2: a `test`/`tests`/`spec`/`specs`/`__tests__`
|
|
220
|
+
// path segment at any depth. Subsumes the old root-anchored `test/`/`spec/`
|
|
221
|
+
// rules and broadens them to nested suites (`packages/core/test/foo.mjs`).
|
|
222
|
+
// Excluded under `docs/`: the old rule anchored test dirs at the ROOT, so a
|
|
223
|
+
// docs-tree prose file (`docs/specs/queue-mode/SPEC.md`) was never a test — the
|
|
224
|
+
// widened any-depth scan must not reclassify it. This only skips the
|
|
225
|
+
// DIRECTORY-based classification; a code/config file or a test-token basename
|
|
226
|
+
// under docs/ is still classified by the extension/basename rules above and
|
|
227
|
+
// below (`docs/example.mjs` → code, `docs/x.test.mjs` → test).
|
|
228
|
+
const dirs = fp.split("/").slice(0, -1);
|
|
229
|
+
if (!fp.startsWith("docs/") && dirs.some((seg) => TEST_DIR_SEGMENTS.has(seg))) {
|
|
154
230
|
return "test";
|
|
155
231
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
) {
|
|
232
|
+
// Broad source table: a file in any covered language is code, not prose, even
|
|
233
|
+
// under docs/. Stylesheets (`.scss`/`.sass`) are deliberately absent — a
|
|
234
|
+
// style/asset kind is out of scope.
|
|
235
|
+
if (CODE_EXTENSIONS.has(ext) || CODE_BASENAMES.has(base)) {
|
|
160
236
|
return "code";
|
|
161
237
|
}
|
|
162
|
-
if (
|
|
163
|
-
fp.startsWith("docs/") || fp.endsWith(".md") || fp.endsWith(".markdown") ||
|
|
164
|
-
fp === "README.md"
|
|
165
|
-
) {
|
|
238
|
+
if (fp.startsWith("docs/") || DOCS_EXTENSIONS.has(ext)) {
|
|
166
239
|
return "docs";
|
|
167
240
|
}
|
|
168
241
|
return "unknown";
|
|
@@ -114,6 +114,67 @@ export function rewriteGeneratedRepoDocLinks(body) {
|
|
|
114
114
|
return String(body).replace(/(\]\(<?)(\.\.\/docs\/)/g, "$1../$2");
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
/**
|
|
118
|
+
* Name of the resolver launcher committed at `.claude/bin/dev-loops-run`. It self-locates a
|
|
119
|
+
* source checkout by walking up from `process.cwd()` (unconditional priority), else by walking
|
|
120
|
+
* up from the launcher's own real binary location, else falls back to the plugin's auto-installed
|
|
121
|
+
* `dev-loops` package, and hard-stops (never falls back to raw `gh`) when none resolve.
|
|
122
|
+
*/
|
|
123
|
+
export const WRAPPER_LAUNCHER = "dev-loops-run";
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* CLI namespaces the routed `dev-loops <ns> <sub>` form recognizes — the real top-level keys of
|
|
127
|
+
* `SUBCOMMAND_ROUTES` in `cli/index.mjs` (locked by
|
|
128
|
+
* `test/contracts/claude-no-bare-invocation.test.mjs`'s recurrence guard, which reads those keys
|
|
129
|
+
* directly). `release` and `security` are NOT CLI namespaces (no such routes exist); `issue` and
|
|
130
|
+
* `inspect` are and were previously missing here, which left e.g. `dev-loops issue edit …` bare
|
|
131
|
+
* and unrouted in the generated tree.
|
|
132
|
+
*/
|
|
133
|
+
export const WRAPPER_NS = "gate|loop|pr|issue|queue|project|inspect|refine";
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Regex source (no flags) matching a bare `node scripts/<dir>/…/<file>.mjs` invocation, at ANY
|
|
137
|
+
* subdirectory depth (one-or-more `dir/` segments), not just one subdirectory level, so a
|
|
138
|
+
* nested wrapper is caught too. The `node`→`scripts/` gap matches ANY whitespace (`\s+`),
|
|
139
|
+
* including a source line-wrap (`node` at a line end, `scripts/…mjs` beginning the next), so a
|
|
140
|
+
* prose-wrapped invocation is routed and the guard test that shares this source catches it too —
|
|
141
|
+
* a single-space-only pattern silently left wrapped invocations bare and passed the guard falsely.
|
|
142
|
+
* The `.mjs` path anchor keeps false positives impossible. Capture group 1 is the matched
|
|
143
|
+
* `scripts/…mjs` path. Exported so the no-bare-invocation guard test can build the identical regex
|
|
144
|
+
* rather than re-deriving it (single source of truth).
|
|
145
|
+
*/
|
|
146
|
+
export const BARE_NODE_SCRIPTS_SOURCE = String.raw`\bnode\s+(scripts\/(?:[a-z0-9-]+\/)+[A-Za-z0-9._-]+\.mjs)`;
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Regex source (no flags) matching a bare, unrouted `dev-loops <namespace> <sub>` invocation.
|
|
150
|
+
* Requires an immediate lowercase-starting subcommand (lookahead) so prose (`dev-loops gate.`,
|
|
151
|
+
* `dev-loops gate — …`, `` `dev-loops queue` ``) and the already-pinned `npx dev-loops@<version>`
|
|
152
|
+
* CLI form never match. Exported so the no-bare-invocation guard test can build the identical
|
|
153
|
+
* regex from the same `WRAPPER_NS` rather than re-deriving it (single source of truth).
|
|
154
|
+
*/
|
|
155
|
+
export const BARE_DEV_LOOPS_NS_SOURCE = String.raw`\bdev-loops (${WRAPPER_NS}) (?=[a-z])`;
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Route real wrapper invocations in a generated body through the resolver launcher so a
|
|
159
|
+
* plugin-only install (no `scripts/`, no `node_modules`) still resolves every wrapper.
|
|
160
|
+
* Two disjoint forms; args are preserved byte-for-byte:
|
|
161
|
+
* node scripts/<dir>/<file>.mjs … → <launcher> scripts/<dir>/<file>.mjs …
|
|
162
|
+
* dev-loops <namespace> <sub> … → <launcher> cli/index.mjs <namespace> <sub> …
|
|
163
|
+
* The namespace form requires an immediate lowercase-starting subcommand (lookahead), so prose
|
|
164
|
+
* (`dev-loops gate.`, `dev-loops gate — …`, `` `dev-loops queue` ``) and the already-pinned
|
|
165
|
+
* `npx dev-loops@<version>` CLI form are never touched. Idempotent: the rewritten output never
|
|
166
|
+
* re-matches (the launcher name is followed by a path/`cli/index.mjs`, not `scripts/` or a bare
|
|
167
|
+
* namespace token).
|
|
168
|
+
* @param {string} body
|
|
169
|
+
* @param {string} [launcher]
|
|
170
|
+
* @returns {string}
|
|
171
|
+
*/
|
|
172
|
+
export function rewriteWrapperInvocation(body, launcher = WRAPPER_LAUNCHER) {
|
|
173
|
+
return String(body)
|
|
174
|
+
.replace(new RegExp(BARE_NODE_SCRIPTS_SOURCE, "g"), `${launcher} $1`)
|
|
175
|
+
.replace(new RegExp(BARE_DEV_LOOPS_NS_SOURCE, "g"), `${launcher} cli/index.mjs $1 `);
|
|
176
|
+
}
|
|
177
|
+
|
|
117
178
|
/**
|
|
118
179
|
* Map a single Pi tool name to its Claude tool name(s).
|
|
119
180
|
* @param {string} name
|
|
@@ -184,7 +245,7 @@ function normalizeToolList(value) {
|
|
|
184
245
|
*/
|
|
185
246
|
export function transformAgent({ source, raw, version = "latest", config = {} }) {
|
|
186
247
|
const { frontmatter, body: rawBody } = splitFrontmatter(raw, source);
|
|
187
|
-
const body = rewriteGeneratedRepoDocLinks(rewriteCliInvocation(stripPiOnlyBlocks(rawBody), version));
|
|
248
|
+
const body = rewriteWrapperInvocation(rewriteGeneratedRepoDocLinks(rewriteCliInvocation(stripPiOnlyBlocks(rawBody), version)));
|
|
188
249
|
const tools = mapTools(normalizeToolList(frontmatter.tools));
|
|
189
250
|
const model = resolveRoleModel(config, { role: String(frontmatter.name ?? ""), harness: "claude" });
|
|
190
251
|
|
|
@@ -219,7 +280,7 @@ export function transformAgent({ source, raw, version = "latest", config = {} })
|
|
|
219
280
|
*/
|
|
220
281
|
export function transformCommand({ source, raw, version = "latest" }) {
|
|
221
282
|
const { frontmatter, body: rawBody } = splitFrontmatter(raw, source);
|
|
222
|
-
const body = rewriteGeneratedRepoDocLinks(rewriteCliInvocation(stripPiOnlyBlocks(rawBody), version));
|
|
283
|
+
const body = rewriteWrapperInvocation(rewriteGeneratedRepoDocLinks(rewriteCliInvocation(stripPiOnlyBlocks(rawBody), version)));
|
|
223
284
|
|
|
224
285
|
const lines = ["---"];
|
|
225
286
|
if (frontmatter.description != null) {
|
|
@@ -241,7 +302,7 @@ export function transformCommand({ source, raw, version = "latest" }) {
|
|
|
241
302
|
*/
|
|
242
303
|
export function transformSkill({ source, raw, version = "latest" }) {
|
|
243
304
|
const { frontmatter, body: rawBody } = splitFrontmatter(raw, source);
|
|
244
|
-
const body = rewriteCliInvocation(stripPiOnlyBlocks(rawBody), version);
|
|
305
|
+
const body = rewriteWrapperInvocation(rewriteCliInvocation(stripPiOnlyBlocks(rawBody), version));
|
|
245
306
|
const tools = mapTools(normalizeToolList(frontmatter["allowed-tools"]));
|
|
246
307
|
|
|
247
308
|
const lines = ["---"];
|