session-orchestrator 3.21.0 → 3.22.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor/rules/000-session-orchestrator.mdc +3 -2
- package/.cursor/rules/040-discovery.mdc +6 -6
- package/.cursor/rules/050-plan.mdc +8 -8
- package/CHANGELOG.md +101 -0
- package/README.md +10 -10
- package/agents/memory-proposal-collector.md +6 -4
- package/commands/eli5.md +33 -0
- package/commands/release.md +5 -3
- package/commands/test.md +2 -2
- package/docs/components.md +6 -5
- package/docs/scope-collision-guard.md +3 -3
- package/docs/session-config-reference.md +31 -8
- package/hooks/_lib/lock-bootstrap.mjs +19 -13
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks.json +11 -1
- package/hooks/on-session-end.mjs +24 -92
- package/hooks/on-session-start.mjs +195 -104
- package/hooks/pre-auq-clarity.mjs +787 -0
- package/hooks/pre-bash-issue-budget.mjs +17 -18
- package/package.json +3 -1
- package/pi/prompts/eli5.md +12 -0
- package/scripts/auq-audit.mjs +825 -0
- package/scripts/autopilot.mjs +7 -8
- package/scripts/lib/auq/clarity.mjs +1314 -0
- package/scripts/lib/auq/parse.mjs +1006 -0
- package/scripts/lib/auq/schema.mjs +1457 -0
- package/scripts/lib/ci-status-banner.mjs +63 -57
- package/scripts/lib/config/dispatcher-autonomy-capture.mjs +32 -9
- package/scripts/lib/config/vault-integration.mjs +12 -1
- package/scripts/lib/dispatcher/rank.mjs +4 -7
- package/scripts/lib/gates/gate-full.mjs +3 -3
- package/scripts/lib/gates/gate-helpers.mjs +17 -6
- package/scripts/lib/io.mjs +239 -0
- package/scripts/lib/issue-budget.mjs +63 -9
- package/scripts/lib/owner-interview.mjs +78 -32
- package/scripts/lib/peer-discovery.mjs +73 -22
- package/scripts/lib/project-hygiene.mjs +64 -4
- package/scripts/lib/reconcile/renderer.mjs +17 -4
- package/scripts/lib/resource-probe/evaluate.mjs +330 -149
- package/scripts/lib/resource-probe/probe-platform.mjs +35 -0
- package/scripts/lib/resource-probe.mjs +18 -2
- package/scripts/lib/spiral-carryover.mjs +23 -2
- package/scripts/lib/state-md/mission-status.mjs +147 -50
- package/scripts/lib/validate/check-auq-clarity.mjs +274 -0
- package/scripts/lib/validate/check-hooks-symmetry.mjs +30 -0
- package/scripts/lib/validate/check-rules.mjs +153 -9
- package/scripts/lib/vault-backfill/glab.mjs +91 -58
- package/scripts/lib/vault-backfill/manifest.mjs +28 -8
- package/scripts/lib/vcs-repo-spec.mjs +182 -13
- package/scripts/lib/wave-resource-gate.mjs +67 -73
- package/scripts/materialize-wave-scope.mjs +281 -0
- package/scripts/release.mjs +443 -122
- package/scripts/run-quality-gate.mjs +14 -0
- package/scripts/validate-plugin.mjs +3 -0
- package/scripts/validate-wave-scope.mjs +6 -1
- package/scripts/vault-backfill.mjs +32 -5
- package/skills/_shared/parallel-aware-auq.md +30 -24
- package/skills/_shared/parallel-aware-preamble.md +31 -2
- package/skills/_shared/state-ownership.md +32 -6
- package/skills/bootstrap/SKILL.md +2 -1
- package/skills/brainstorm/SKILL.md +18 -18
- package/skills/brainstorm/soul.md +12 -0
- package/skills/discovery/SKILL.md +28 -24
- package/skills/eli5/SKILL.md +43 -0
- package/skills/evolve/SKILL.md +8 -9
- package/skills/gitlab-ops/SKILL.md +30 -26
- package/skills/grill/SKILL.md +6 -6
- package/skills/grill/soul.md +16 -0
- package/skills/memory-cleanup/SKILL.md +2 -2
- package/skills/npm-publish/SKILL.md +4 -4
- package/skills/peekaboo-driver/SKILL.md +3 -3
- package/skills/plan/SKILL.md +18 -16
- package/skills/plan/mode-feature.md +1 -1
- package/skills/plan/mode-new.md +35 -23
- package/skills/plan/soul.md +12 -0
- package/skills/reconcile/SKILL.md +3 -3
- package/skills/session-end/SKILL.md +53 -20
- package/skills/session-end/phase-3-6-tail.md +37 -2
- package/skills/session-start/SKILL.md +69 -35
- package/skills/session-start/phase-2-5-docs-planning.md +8 -8
- package/skills/session-start/phase-4-5-resource-health.md +82 -19
- package/skills/session-start/soul.md +110 -0
- package/skills/test-runner/SKILL.md +2 -2
- package/skills/using-orchestrator/SKILL.md +1 -1
- package/skills/wave-executor/wave-loop.md +27 -5
- package/skills/write-executable-plan/SKILL.md +6 -6
- package/scripts/tests/fixtures/fetch-baseline/sample-rule.md +0 -8
- package/skills/vault-sync/tests/fixtures/archive-test-vault/90-archive/bad-archived.md +0 -8
- package/skills/vault-sync/tests/fixtures/archive-test-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/archive-test-vault/live-note.md +0 -8
- package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/bad-type.md +0 -8
- package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/good-note.md +0 -8
- package/skills/vault-sync/tests/fixtures/clean-vault/.obsidian/config.md +0 -8
- package/skills/vault-sync/tests/fixtures/clean-vault/01-projects/foo/projects-baseline.md +0 -10
- package/skills/vault-sync/tests/fixtures/clean-vault/03-daily/daily-2026-04-13.md +0 -8
- package/skills/vault-sync/tests/fixtures/clean-vault/README.md +0 -3
- package/skills/vault-sync/tests/fixtures/clean-vault/hello-world.md +0 -11
- package/skills/vault-sync/tests/fixtures/dangling-link-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/dangling-link-vault/has-dangling.md +0 -9
- package/skills/vault-sync/tests/fixtures/dangling-link-vault/real-target.md +0 -8
- package/skills/vault-sync/tests/fixtures/empty-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/missing-field-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/missing-field-vault/missing-id.md +0 -7
- package/skills/vault-sync/tests/fixtures/nested-tag-vault/03-daily/daily-2026-04-13.md +0 -9
- package/skills/vault-sync/tests/fixtures/nested-tag-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/nested-tag-vault/nested-tags-note.md +0 -11
- package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/README.md +0 -3
- package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/_MOC.md +0 -3
- package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/with-moc-vault/_MOC.md +0 -11
- package/skills/vault-sync/tests/fixtures/with-moc-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/with-moc-vault/hello-world.md +0 -11
- package/skills/vault-sync/tests/schema-drift.test.mjs +0 -133
|
@@ -67,6 +67,7 @@ import { discoverActiveSessions } from './session-discovery.mjs';
|
|
|
67
67
|
import { readLock, isLockLive, LOCK_PATH } from './session-lock.mjs';
|
|
68
68
|
import { checkPeerStateMd } from './state-md-peer-guard.mjs';
|
|
69
69
|
import { listWorktreesChecked } from './worktree/listing.mjs';
|
|
70
|
+
import { parseSessionId } from './session-id.mjs';
|
|
70
71
|
|
|
71
72
|
/** Closed enum of provenance sources. */
|
|
72
73
|
const SOURCE_DISCOVERED = 'discovered'; // lock + registry unified (irreversibly merged upstream)
|
|
@@ -120,24 +121,64 @@ function _peerFromDiscovered(s, nowMs) {
|
|
|
120
121
|
return peer;
|
|
121
122
|
}
|
|
122
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Resolve a semantic hint to the concrete local raw ID for discovered surfaces.
|
|
126
|
+
*
|
|
127
|
+
* Semantic labels are attribution, not ownership. A mapping is usable only when
|
|
128
|
+
* current-session.json names the hint and its raw id exactly matches the local
|
|
129
|
+
* readable session.lock. Otherwise return null so a potentially foreign record
|
|
130
|
+
* stays visible. Raw caller ids retain the legacy direct comparison unchanged.
|
|
131
|
+
*
|
|
132
|
+
* This is a consistency check on two repo-global files, NOT a proof of ownership;
|
|
133
|
+
* a colliding semantic label from a foreign live session satisfies it. See the
|
|
134
|
+
* call site in findPeers for the measured case and why it is not closed here.
|
|
135
|
+
*
|
|
136
|
+
* @param {string|null} mySessionId
|
|
137
|
+
* @param {string} repoRoot
|
|
138
|
+
* @returns {string|null}
|
|
139
|
+
*/
|
|
140
|
+
function _discoveredSelfSessionId(mySessionId, repoRoot) {
|
|
141
|
+
if (typeof mySessionId !== 'string' || mySessionId.length === 0) return null;
|
|
142
|
+
if (parseSessionId(mySessionId)?.format !== 'semantic') return mySessionId;
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
const raw = fs.readFileSync(
|
|
146
|
+
path.join(repoRoot, '.orchestrator', 'current-session.json'),
|
|
147
|
+
'utf8',
|
|
148
|
+
);
|
|
149
|
+
const current = JSON.parse(raw);
|
|
150
|
+
const lock = readLock({ repoRoot });
|
|
151
|
+
if (
|
|
152
|
+
current &&
|
|
153
|
+
typeof current === 'object' &&
|
|
154
|
+
!Array.isArray(current) &&
|
|
155
|
+
typeof current.session_id === 'string' &&
|
|
156
|
+
current.session_id.length > 0 &&
|
|
157
|
+
current.semantic_session_id === mySessionId &&
|
|
158
|
+
lock !== null &&
|
|
159
|
+
typeof lock.session_id === 'string' &&
|
|
160
|
+
lock.session_id === current.session_id
|
|
161
|
+
) {
|
|
162
|
+
return lock.session_id;
|
|
163
|
+
}
|
|
164
|
+
} catch {
|
|
165
|
+
// Missing or malformed local state cannot prove a semantic alias.
|
|
166
|
+
}
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
|
|
123
170
|
/**
|
|
124
171
|
* findPeers — union of all 3 peer-discovery surfaces, fail-open per surface.
|
|
125
172
|
*
|
|
126
173
|
* @param {string} repoRoot Absolute path to the repository root.
|
|
127
174
|
* @param {object} [opts] passthrough seams shared with the underlying surfaces.
|
|
128
|
-
* @param {string|null} [opts.mySessionId] Current session id
|
|
129
|
-
* self-
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
* the SAME id-space `mySessionId` was derived from (see the PRECONDITION
|
|
136
|
-
* note in state-md-peer-guard.mjs). Passing a semantic id here self-excludes
|
|
137
|
-
* correctly on Surface C but NOT on Surface A+B (the UUID lock/registry
|
|
138
|
-
* entry for the same session will still surface as a 'discovered' peer of
|
|
139
|
-
* itself) — callers that need both surfaces to self-exclude MUST pass the
|
|
140
|
-
* UUID.
|
|
175
|
+
* @param {string|null} [opts.mySessionId] Current session id hint. Raw
|
|
176
|
+
* ids self-exclude directly on Surface A+B. A semantic hint maps to a raw
|
|
177
|
+
* discovered-surface id only after the local current-session.json semantic
|
|
178
|
+
* label matches it AND that file's raw id exactly equals local
|
|
179
|
+
* session.lock.session_id. Missing, malformed, or mismatched local state
|
|
180
|
+
* maps nothing, leaving discovered entries visible. Surface C receives this
|
|
181
|
+
* original hint unchanged and compares it to STATE.md's `session:` field.
|
|
141
182
|
* @param {number} [opts.now] ms-since-epoch (test seam for freshness/age).
|
|
142
183
|
* @param {number} [opts.freshnessMin] Registry-entry freshness threshold (minutes).
|
|
143
184
|
* @param {number} [opts.maxAgeHours] STATE.md abandonment threshold (hours).
|
|
@@ -155,6 +196,7 @@ function _peerFromDiscovered(s, nowMs) {
|
|
|
155
196
|
export async function findPeers(repoRoot, opts = {}) {
|
|
156
197
|
const nowMs = typeof opts.now === 'number' ? opts.now : Date.now();
|
|
157
198
|
const mySessionId = opts.mySessionId ?? null;
|
|
199
|
+
const discoveredSelfSessionId = _discoveredSelfSessionId(mySessionId, repoRoot);
|
|
158
200
|
|
|
159
201
|
const peers = [];
|
|
160
202
|
|
|
@@ -172,15 +214,24 @@ export async function findPeers(repoRoot, opts = {}) {
|
|
|
172
214
|
});
|
|
173
215
|
if (Array.isArray(discovered)) {
|
|
174
216
|
for (const s of discovered) {
|
|
175
|
-
// Self-exclusion (#798):
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
|
|
217
|
+
// Self-exclusion (#798, #1085): discovered entries carry raw ids. A
|
|
218
|
+
// semantic caller hint filters one such id only after the local raw
|
|
219
|
+
// binding was verified above.
|
|
220
|
+
//
|
|
221
|
+
// The binding proves CONSISTENCY of the two local files, not OWNERSHIP.
|
|
222
|
+
// Both are repo-global, so if a foreign live session wrote them last and
|
|
223
|
+
// its semantic label collides with this hint (labels like
|
|
224
|
+
// `main-2026-08-21-session-2` are routinely shared — see
|
|
225
|
+
// hooks/on-session-start.mjs), that foreign raw id is filtered and the
|
|
226
|
+
// peer PSA-002 exists to surface goes missing. Measured 2026-08-21:
|
|
227
|
+
// null hint -> peer visible, colliding semantic hint -> peers: [].
|
|
228
|
+
// Closing it needs a per-process ownership proof, not a stronger read
|
|
229
|
+
// of the same two files.
|
|
230
|
+
if (
|
|
231
|
+
s &&
|
|
232
|
+
typeof s.sessionId === 'string' &&
|
|
233
|
+
s.sessionId !== discoveredSelfSessionId
|
|
234
|
+
) {
|
|
184
235
|
peers.push(_peerFromDiscovered(s, nowMs));
|
|
185
236
|
}
|
|
186
237
|
}
|
|
@@ -212,6 +212,21 @@ function duBytes(absPath) {
|
|
|
212
212
|
}
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
+
/**
|
|
216
|
+
* Human-readable byte size. Sub-megabyte totals are the common case once the
|
|
217
|
+
* aged subset is sized correctly, and `Math.round(bytes / MB)` renders every
|
|
218
|
+
* one of them as "0 MB" — which is why the unit is chosen, not fixed.
|
|
219
|
+
* @param {number} bytes
|
|
220
|
+
* @returns {string}
|
|
221
|
+
*/
|
|
222
|
+
function formatBytes(bytes) {
|
|
223
|
+
const mb = bytes / (1024 * 1024);
|
|
224
|
+
if (mb >= 10) return `${Math.round(mb)} MB`;
|
|
225
|
+
if (mb >= 1) return `${mb.toFixed(2)} MB`;
|
|
226
|
+
if (bytes >= 1024) return `${Math.round(bytes / 1024)} KB`;
|
|
227
|
+
return `${bytes} B`;
|
|
228
|
+
}
|
|
229
|
+
|
|
215
230
|
/**
|
|
216
231
|
* H3 — Aged orchestrator artifacts.
|
|
217
232
|
*
|
|
@@ -219,17 +234,48 @@ function duBytes(absPath) {
|
|
|
219
234
|
* 147 MB were Playwright test-run captures dating back seven weeks, plus
|
|
220
235
|
* 592 files older than 30 days.
|
|
221
236
|
*
|
|
237
|
+
* Counts and sizes ONLY untracked aged files. Two properties are load-bearing
|
|
238
|
+
* and were both defects until 2026-08-22:
|
|
239
|
+
*
|
|
240
|
+
* 1. The byte total describes the set the sentence names — the aged files —
|
|
241
|
+
* not the whole directory. Sizing the directory overstated the reachable
|
|
242
|
+
* win by a factor of ~18 in this repo (11 MB claimed, 0.68 MB real).
|
|
243
|
+
* 2. Version-tracked paths are excluded. `.orchestrator/policy/*.json` and
|
|
244
|
+
* `.orchestrator/steering/*.md` are read at runtime by hooks and skills;
|
|
245
|
+
* their age is a sign of stability, not decay. Proposing them for
|
|
246
|
+
* "pruning" is proposing to delete source.
|
|
247
|
+
*
|
|
222
248
|
* @param {string} repoRoot
|
|
223
249
|
* @param {number} ageDays
|
|
224
250
|
* @param {number} now
|
|
225
|
-
* @returns {
|
|
251
|
+
* @returns {{check: string, fixable: boolean, agedFiles: number, agedBytes: number, message: string}|null}
|
|
226
252
|
*/
|
|
227
253
|
export function checkStaleArtifacts(repoRoot, ageDays = DEFAULT_ARTIFACT_AGE_DAYS, now = Date.now()) {
|
|
228
254
|
const dir = join(repoRoot, '.orchestrator');
|
|
229
255
|
if (!existsSync(dir)) return null;
|
|
230
256
|
|
|
257
|
+
// Resolve the tracked set ONCE per call — `git ls-files --error-unmatch` per
|
|
258
|
+
// file would cost one process per candidate for the same answer.
|
|
259
|
+
//
|
|
260
|
+
// FAIL-SAFE, NOT FAIL-OPEN: when git cannot answer (no repo, git absent,
|
|
261
|
+
// non-zero exit) source and artifact are indistinguishable, so the probe
|
|
262
|
+
// stays SILENT rather than falling back to "nothing is tracked" — that
|
|
263
|
+
// fallback IS the defect this exclusion closes, and it would return
|
|
264
|
+
// invisibly. Losing an advisory finding costs nothing; proposing to delete
|
|
265
|
+
// versioned files costs a restore. `-z` suppresses git's path quoting, so
|
|
266
|
+
// non-ASCII and space-bearing paths compare byte-exactly.
|
|
267
|
+
const trackedRaw = git(['ls-files', '-z', '--', '.orchestrator'], repoRoot);
|
|
268
|
+
if (trackedRaw === null) return null;
|
|
269
|
+
const tracked = new Set(
|
|
270
|
+
trackedRaw
|
|
271
|
+
.split('\0')
|
|
272
|
+
.filter(Boolean)
|
|
273
|
+
.map((p) => join(repoRoot, p)),
|
|
274
|
+
);
|
|
275
|
+
|
|
231
276
|
const cutoff = now - ageDays * 24 * 60 * 60 * 1000;
|
|
232
277
|
let aged = 0;
|
|
278
|
+
let agedBytes = 0;
|
|
233
279
|
let scanned = 0;
|
|
234
280
|
|
|
235
281
|
/** @param {string} d @param {number} depth */
|
|
@@ -248,9 +294,16 @@ export function checkStaleArtifacts(repoRoot, ageDays = DEFAULT_ARTIFACT_AGE_DAY
|
|
|
248
294
|
if (e.isDirectory()) {
|
|
249
295
|
walk(full, depth + 1);
|
|
250
296
|
} else if (e.isFile()) {
|
|
297
|
+
// Counted before the tracked-skip so the 20k bound still measures the
|
|
298
|
+
// walk, not the reportable subset.
|
|
251
299
|
scanned++;
|
|
300
|
+
if (tracked.has(full)) continue;
|
|
252
301
|
try {
|
|
253
|
-
|
|
302
|
+
const st = statSync(full);
|
|
303
|
+
if (st.mtimeMs < cutoff) {
|
|
304
|
+
aged++;
|
|
305
|
+
agedBytes += st.size;
|
|
306
|
+
}
|
|
254
307
|
} catch {
|
|
255
308
|
/* vanished mid-scan — ignore */
|
|
256
309
|
}
|
|
@@ -261,11 +314,18 @@ export function checkStaleArtifacts(repoRoot, ageDays = DEFAULT_ARTIFACT_AGE_DAY
|
|
|
261
314
|
|
|
262
315
|
if (aged === 0) return null;
|
|
263
316
|
|
|
264
|
-
|
|
317
|
+
// `fixable: true` stays. The only consumer is skills/session-start/SKILL.md
|
|
318
|
+
// (Phase 4), which routes fixable findings to "safe batch work" instead of
|
|
319
|
+
// the operator Q&A — prose guidance to the coordinator, not an automatic
|
|
320
|
+
// deletion run; `grep -rn "fixable" scripts/ skills/ hooks/` finds no other
|
|
321
|
+
// reader than the `mechanical` count below. With tracked paths excluded the
|
|
322
|
+
// claim is now true: every reported path is an untracked artifact.
|
|
265
323
|
return {
|
|
266
324
|
check: 'stale-artifacts',
|
|
267
325
|
fixable: true,
|
|
268
|
-
|
|
326
|
+
agedFiles: aged,
|
|
327
|
+
agedBytes,
|
|
328
|
+
message: `${aged} untracked file(s) under .orchestrator/ older than ${ageDays}d${agedBytes > 0 ? ` (${formatBytes(agedBytes)} total)` : ''} — candidates for pruning`,
|
|
269
329
|
};
|
|
270
330
|
}
|
|
271
331
|
|
|
@@ -262,10 +262,23 @@ export function renderRule(learning, metadata) {
|
|
|
262
262
|
// purely by `host-class:` (loads only on a matching host) — the intended
|
|
263
263
|
// behavior, and still never always-on (host-class IS an activation axis).
|
|
264
264
|
if (globs.length > 0) {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
265
|
+
// TWO keys, same list, on purpose (#1108). `globs:` is what THIS repo's
|
|
266
|
+
// `rule-loader.mjs` and Cursor read; `paths:` is the ONLY key Claude Code's
|
|
267
|
+
// own loader honours, and a rule without it is loaded UNCONDITIONALLY into
|
|
268
|
+
// every agent (https://code.claude.com/docs/en/memory § Path-specific
|
|
269
|
+
// rules). Emitting `globs:` alone therefore produced a rule that LOOKS
|
|
270
|
+
// scoped, IS scoped for us, and is always-on for the harness — measured
|
|
271
|
+
// 2026-08-22: 16 such rules put 72_195 bytes (~18_300 tokens) into every
|
|
272
|
+
// single dispatch. `check-rules.mjs` now hard-FAILs that shape, so a
|
|
273
|
+
// globs-only rule emitted here would be rejected by the gate the moment it
|
|
274
|
+
// lands. Keep the two lists IDENTICAL: divergence is its own FAIL class,
|
|
275
|
+
// because it would scope Claude Code and Cursor to different rule sets.
|
|
276
|
+
for (const key of ['globs', 'paths']) {
|
|
277
|
+
fm.push(`${key}:`);
|
|
278
|
+
for (const glob of globs) {
|
|
279
|
+
// Always double-quote — keeps `*`/`[`/`{` safe; loader strips quotes.
|
|
280
|
+
fm.push(` - "${glob}"`);
|
|
281
|
+
}
|
|
269
282
|
}
|
|
270
283
|
}
|
|
271
284
|
if (hostClass !== undefined) {
|