session-orchestrator 3.16.0 → 3.19.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/030-wave-execution.mdc +17 -1
- package/CHANGELOG.md +130 -412
- package/README.md +17 -12
- package/SECURITY.md +190 -27
- package/agents/AGENTS.md +20 -3
- package/agents/code-implementer.md +6 -6
- package/agents/db-specialist.md +1 -1
- package/agents/qa-strategist.md +31 -6
- package/agents/schemas/qa-strategist.schema.json +27 -0
- package/agents/schemas/test-writer.schema.json +60 -2
- package/agents/security-reviewer.md +1 -1
- package/agents/session-reviewer.md +1 -1
- package/agents/test-writer.md +29 -10
- package/agents/ui-developer.md +1 -1
- package/commands/contract-version-bump.md +28 -0
- package/commands/portfolio.md +1 -1
- package/docs/README.md +2 -1
- package/docs/USER-GUIDE.md +8 -3
- package/docs/ci-setup.md +121 -7
- package/docs/codex-setup.md +1 -1
- package/docs/components.md +7 -7
- package/docs/cursor-setup.md +22 -9
- package/docs/events-schema.md +5 -1
- package/docs/instruction-delivery.md +444 -0
- package/docs/pi-setup.md +1 -1
- package/docs/rule-authoring.md +58 -9
- package/docs/session-config-reference.md +306 -6
- package/docs/session-config-template.md +66 -3
- package/docs/telemetry/telemetry-claims.md +204 -0
- package/docs/telemetry.md +158 -0
- package/hooks/_lib/guard-source-loader.mjs +467 -0
- package/hooks/_lib/lock-bootstrap.mjs +21 -0
- package/hooks/_lib/vcs-create-matcher.mjs +119 -0
- package/hooks/config-protection.mjs +0 -0
- package/hooks/enforce-commands.mjs +10 -2
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks-cursor.json +11 -2
- package/hooks/hooks-pi.json +10 -0
- package/hooks/hooks.json +21 -1
- package/hooks/on-session-end.mjs +178 -18
- package/hooks/on-session-start.mjs +23 -0
- package/hooks/post-bash-write-verify.mjs +977 -0
- package/hooks/post-subagent-discovery-validator.mjs +256 -41
- package/hooks/pre-bash-destructive-guard.mjs +525 -160
- package/hooks/pre-bash-issue-budget.mjs +167 -0
- package/hooks/pre-bash-sessions-ledger-guard.mjs +627 -0
- package/hooks/pre-bash-templates-first.mjs +96 -63
- package/hooks/skill-invocation-telemetry.mjs +109 -10
- package/hooks/subagent-telemetry.mjs +527 -37
- package/package.json +16 -3
- package/pi/prompts/contract-version-bump.md +12 -0
- package/rules/README.md +32 -0
- package/scripts/archive-closed-prds.mjs +12 -22
- package/scripts/autopilot-multi.mjs +103 -20
- package/scripts/backfill-abandoned-sessions.mjs +160 -4
- package/scripts/check-doc-consistency.sh +17 -1
- package/scripts/compute-grounding-injection.sh +18 -3
- package/scripts/dialectic-deriver.mjs +7 -2
- package/scripts/eval-session.mjs +50 -9
- package/scripts/fleet-instruction-scan.mjs +141 -0
- package/scripts/lib/auto-dialectic.mjs +11 -2
- package/scripts/lib/auto-dream.mjs +16 -5
- package/scripts/lib/autopilot/mr-draft.mjs +31 -1
- package/scripts/lib/autopilot/worktree-pipeline.mjs +113 -5
- package/scripts/lib/backlog-scan.mjs +39 -6
- package/scripts/lib/blocked-commands-policy.mjs +340 -0
- package/scripts/lib/build-live-signals.mjs +7 -4
- package/scripts/lib/ci-status-banner.mjs +75 -12
- package/scripts/lib/claude-md-budget-lint.mjs +283 -34
- package/scripts/lib/command-blocker.mjs +1013 -58
- package/scripts/lib/config/config-protection.mjs +2 -1
- package/scripts/lib/config/context-coverage.mjs +82 -0
- package/scripts/lib/config/drift-check.mjs +9 -1
- package/scripts/lib/config/gitlab-portfolio.mjs +1 -1
- package/scripts/lib/config/issue-budget.mjs +123 -0
- package/scripts/lib/config/moc-staleness.mjs +98 -0
- package/scripts/lib/config/reconcile.mjs +21 -0
- package/scripts/lib/config/section-extractor.mjs +121 -1
- package/scripts/lib/config/worktree-orphans.mjs +138 -0
- package/scripts/lib/config-schema.mjs +23 -3
- package/scripts/lib/config.mjs +32 -0
- package/scripts/lib/context-coverage-banner.mjs +223 -0
- package/scripts/lib/convergence-monitor.mjs +49 -3
- package/scripts/lib/description-surface.mjs +535 -0
- package/scripts/lib/dispatcher/enumerate.mjs +136 -30
- package/scripts/lib/dispatcher/rank.mjs +22 -8
- package/scripts/lib/ecosystem-wizard/config-writer.mjs +26 -24
- package/scripts/lib/ecosystem-wizard/wizard-prompt.mjs +1 -1
- package/scripts/lib/eval/engine.mjs +47 -5
- package/scripts/lib/events.mjs +59 -7
- package/scripts/lib/evolve/autonomy-verdict.mjs +5 -0
- package/scripts/lib/evolve/autopilot-effectiveness.mjs +54 -7
- package/scripts/lib/gates/gate-full.mjs +15 -3
- package/scripts/lib/gates/gate-helpers.mjs +132 -6
- package/scripts/lib/gitlab-ops/stale-mr-sweep.mjs +28 -8
- package/scripts/lib/gitlab-portfolio/aggregator.mjs +8 -2
- package/scripts/lib/gitlab-portfolio/cli.mjs +1 -1
- package/scripts/lib/handover-gate.mjs +7 -3
- package/scripts/lib/harness-audit/categories/category4.mjs +22 -5
- package/scripts/lib/instruction-budget-guard.mjs +402 -51
- package/scripts/lib/io.mjs +345 -10
- package/scripts/lib/issue-budget.mjs +269 -0
- package/scripts/lib/issue-close-strip-labels.mjs +39 -9
- package/scripts/lib/label-scope.mjs +47 -0
- package/scripts/lib/learnings/schema.mjs +43 -3
- package/scripts/lib/lock-reaper.mjs +1 -2
- package/scripts/lib/memory-proposals/schema.mjs +36 -1
- package/scripts/lib/moc-staleness-banner.mjs +267 -0
- package/scripts/lib/peer-discovery.mjs +645 -0
- package/scripts/lib/pi-hook-bridge.mjs +146 -17
- package/scripts/lib/product-repo-detect.mjs +9 -8
- package/scripts/lib/project-hygiene.mjs +432 -0
- package/scripts/lib/quality-gate.mjs +167 -0
- package/scripts/lib/recommendations-v0.mjs +1 -1
- package/scripts/lib/reconcile/eligibility.mjs +1 -1
- package/scripts/lib/reconcile/emitter.mjs +23 -4
- package/scripts/lib/reconcile/engine.mjs +147 -39
- package/scripts/lib/reconcile/idempotency.mjs +114 -14
- package/scripts/lib/reconcile-nudge-banner.mjs +65 -9
- package/scripts/lib/resource-probe/evaluate.mjs +70 -4
- package/scripts/lib/resource-probe.mjs +19 -0
- package/scripts/lib/rule-loader.mjs +6 -0
- package/scripts/lib/scope-baseline.mjs +564 -0
- package/scripts/lib/scope-gate.mjs +399 -98
- package/scripts/lib/session-close-backfill.mjs +61 -6
- package/scripts/lib/session-end/phase-skip.mjs +1 -0
- package/scripts/lib/session-end/worktree-orphan-sweep.mjs +252 -0
- package/scripts/lib/session-id.mjs +221 -41
- package/scripts/lib/session-lock.mjs +304 -6
- package/scripts/lib/session-schema/constants.mjs +22 -3
- package/scripts/lib/session-schema/filters.mjs +88 -0
- package/scripts/lib/session-schema/validator.mjs +16 -0
- package/scripts/lib/session-schema.mjs +1 -0
- package/scripts/lib/sessions-integrity-banner.mjs +294 -0
- package/scripts/lib/sessions-staleness-banner.mjs +121 -12
- package/scripts/lib/skill-evolution/idempotency.mjs +135 -16
- package/scripts/lib/skill-evolution/mr-opener.mjs +9 -1
- package/scripts/lib/skill-health/join.mjs +35 -9
- package/scripts/lib/spiral-carryover.mjs +142 -30
- package/scripts/lib/state-md/mission-status.mjs +53 -3
- package/scripts/lib/subagents-schema.mjs +43 -9
- package/scripts/lib/telemetry/anon-id.mjs +141 -0
- package/scripts/lib/telemetry/consent.mjs +299 -0
- package/scripts/lib/telemetry/paths.mjs +27 -0
- package/scripts/lib/telemetry/queue.mjs +287 -0
- package/scripts/lib/telemetry/schema.mjs +384 -0
- package/scripts/lib/telemetry/sync.mjs +312 -0
- package/scripts/lib/test-runner/issue-reconcile.mjs +53 -13
- package/scripts/lib/tests-src-ratio.mjs +484 -0
- package/scripts/lib/validate/check-agents.mjs +56 -0
- package/scripts/lib/validate/check-hooks-symmetry.mjs +244 -10
- package/scripts/lib/validate/check-rules.mjs +217 -35
- package/scripts/lib/validate/check-test-value-bans.mjs +782 -0
- package/scripts/lib/validate/check-unicode-safety.mjs +1 -0
- package/scripts/lib/validate-vendored-rules.mjs +10 -2
- package/scripts/lib/vault-archive.mjs +17 -2
- package/scripts/lib/vault-backfill/glab.mjs +8 -0
- package/scripts/lib/vault-mirror/process.mjs +30 -0
- package/scripts/lib/vault-mirror/render-sessions.mjs +293 -36
- package/scripts/lib/vault-status/board-writer.mjs +63 -5
- package/scripts/lib/vault-status/narrative-mirror.mjs +13 -7
- package/scripts/lib/vcs-repo-spec.mjs +362 -0
- package/scripts/lib/wave-resource-gate.mjs +115 -11
- package/scripts/lib/worktree/listing.mjs +44 -7
- package/scripts/mcp-server.sh +32 -6
- package/scripts/measure-context-overhead.sh +151 -0
- package/scripts/memory-propose.mjs +72 -9
- package/scripts/print-applicable-rules.mjs +51 -12
- package/scripts/release.mjs +534 -0
- package/scripts/run-quality-gate.mjs +123 -5
- package/scripts/telemetry.mjs +250 -0
- package/scripts/validate-wave-scope.mjs +182 -17
- package/scripts/vault-integration-watcher.mjs +32 -10
- package/skills/_shared/config-reading.md +2 -2
- package/skills/bootstrap/fast-template.md +1 -1
- package/skills/claude-md-drift-check/checker.mjs +145 -28
- package/skills/contract-version-bump/SKILL.md +219 -0
- package/skills/discovery/SKILL.md +4 -4
- package/skills/discovery/issue-templates.md +11 -11
- package/skills/discovery/probes-audit.md +1 -1
- package/skills/discovery/probes-feature.md +1 -1
- package/skills/discovery/probes-session.md +26 -5
- package/skills/ecosystem-health/SKILL.md +1 -1
- package/skills/ecosystem-health/wizard.md +4 -4
- package/skills/evolve/SKILL.md +1 -0
- package/skills/gitlab-ops/SKILL.md +20 -12
- package/skills/gitlab-portfolio/SKILL.md +2 -2
- package/skills/hook-development/SKILL.md +1 -1
- package/skills/mode-selector/SKILL.md +1 -1
- package/skills/npm-publish/SKILL.md +97 -0
- package/skills/plan/SKILL.md +5 -5
- package/skills/plan/mode-feature.md +4 -4
- package/skills/plan/mode-new.md +10 -10
- package/skills/plan/mode-retro.md +1 -1
- package/skills/quality-gates/SKILL.md +1 -1
- package/skills/reconcile/SKILL.md +21 -4
- package/skills/session-end/SKILL.md +108 -14
- package/skills/session-end/discovery-scan.md +4 -2
- package/skills/session-end/drift-operations.md +4 -4
- package/skills/session-end/metrics-collection.md +13 -0
- package/skills/session-end/phase-3-2-docs-verification.md +1 -1
- package/skills/session-end/phase-3-6-tail.md +2 -1
- package/skills/session-end/plan-verification.md +5 -2
- package/skills/session-end/vault-operations.md +1 -1
- package/skills/session-end/verification-checklist.md +1 -1
- package/skills/session-plan/SKILL.md +6 -2
- package/skills/session-plan/wave-template.md +2 -0
- package/skills/session-start/SKILL.md +149 -7
- package/skills/session-start/phase-4-5-resource-health.md +15 -2
- package/skills/test-runner/SKILL.md +2 -2
- package/skills/vault-sync/SKILL.md +1 -1
- package/skills/vault-sync/package-lock.json +3 -3
- package/skills/vault-sync/validator.mjs +228 -40
- package/skills/wave-executor/SKILL.md +5 -2
- package/skills/wave-executor/circuit-breaker.md +2 -0
- package/skills/wave-executor/wave-loop.md +163 -10
- package/templates/_shared/loop.md +4 -4
|
@@ -15,6 +15,17 @@
|
|
|
15
15
|
* fileScope file (a JSON array of strings) and assert
|
|
16
16
|
* it is a subset of wave-scope.allowedPaths (#796).
|
|
17
17
|
* Fails (exit 1) with "missing: [...]" on violation.
|
|
18
|
+
* --expand-test-siblings With --assert-subset: ALSO require allowedPaths to
|
|
19
|
+
* grant the test sibling of every concrete production
|
|
20
|
+
* file in the agent fileScope (#970). Default OFF.
|
|
21
|
+
* Only ever ADDS a requirement — never relaxes the
|
|
22
|
+
* subset assertion above.
|
|
23
|
+
* GATED ON THE MANIFEST'S OWN `role`: the assertion
|
|
24
|
+
* fires only for the roles where expansion fires
|
|
25
|
+
* (scope-gate.mjs TEST_SIBLING_EXPANSION_ROLES), so
|
|
26
|
+
* the caller may pass the flag unconditionally on
|
|
27
|
+
* every pre-dispatch check. A skip is announced on
|
|
28
|
+
* stderr as a WARN.
|
|
18
29
|
*
|
|
19
30
|
* Exit codes:
|
|
20
31
|
* 0 — valid (validated JSON echoed to stdout)
|
|
@@ -22,9 +33,15 @@
|
|
|
22
33
|
* 2 — I/O error (file not found, unreadable stdin, unreadable --assert-subset file)
|
|
23
34
|
*/
|
|
24
35
|
|
|
36
|
+
import path from 'node:path';
|
|
25
37
|
import { readFileSync, existsSync, statSync } from 'node:fs';
|
|
26
38
|
import { warn } from './lib/common.mjs';
|
|
27
|
-
import {
|
|
39
|
+
import {
|
|
40
|
+
assertFileScopeSubset,
|
|
41
|
+
assertTestSiblingCoverage,
|
|
42
|
+
testSiblingExpansionApplies,
|
|
43
|
+
TEST_SIBLING_EXPANSION_ROLES,
|
|
44
|
+
} from './lib/scope-gate.mjs';
|
|
28
45
|
|
|
29
46
|
/**
|
|
30
47
|
* Write an error to stderr and exit with the given code.
|
|
@@ -40,19 +57,23 @@ function die(msg, code = 1) {
|
|
|
40
57
|
/**
|
|
41
58
|
* Parse CLI flags out of argv, leaving positional args behind.
|
|
42
59
|
*
|
|
43
|
-
* Recognised: `--assert-subset <path>` (#796)
|
|
44
|
-
*
|
|
45
|
-
* behaviour where argv[2] is the
|
|
60
|
+
* Recognised: `--assert-subset <path>` (#796) and `--expand-test-siblings`
|
|
61
|
+
* (#970). Everything else is treated as a positional argument (the
|
|
62
|
+
* wave-scope.json file path), preserving legacy behaviour where argv[2] is the
|
|
63
|
+
* input file.
|
|
46
64
|
*
|
|
47
65
|
* @param {string[]} argv - full process.argv
|
|
48
|
-
* @returns {{ assertSubset: string|null, positionals: string[] }}
|
|
66
|
+
* @returns {{ assertSubset: string|null, expandTestSiblings: boolean, positionals: string[] }}
|
|
49
67
|
*/
|
|
50
68
|
function parseArgs(argv) {
|
|
51
69
|
const positionals = [];
|
|
52
70
|
let assertSubset = null;
|
|
71
|
+
let expandTestSiblings = false;
|
|
53
72
|
for (let i = 2; i < argv.length; i++) {
|
|
54
73
|
const a = argv[i];
|
|
55
|
-
if (a === '--
|
|
74
|
+
if (a === '--expand-test-siblings') {
|
|
75
|
+
expandTestSiblings = true;
|
|
76
|
+
} else if (a === '--assert-subset') {
|
|
56
77
|
assertSubset = argv[i + 1];
|
|
57
78
|
if (assertSubset === undefined) {
|
|
58
79
|
die('--assert-subset requires a file-path argument', 1);
|
|
@@ -62,7 +83,7 @@ function parseArgs(argv) {
|
|
|
62
83
|
positionals.push(a);
|
|
63
84
|
}
|
|
64
85
|
}
|
|
65
|
-
return { assertSubset, positionals };
|
|
86
|
+
return { assertSubset, expandTestSiblings, positionals };
|
|
66
87
|
}
|
|
67
88
|
|
|
68
89
|
/**
|
|
@@ -139,9 +160,91 @@ function validateRequired(obj, errors) {
|
|
|
139
160
|
}
|
|
140
161
|
}
|
|
141
162
|
|
|
163
|
+
/**
|
|
164
|
+
* Literal filesystem-root forms — POSIX "/" and the Windows equivalents "\"
|
|
165
|
+
* and a bare drive root ("C:\", "C:\\", ...). Checked independently of
|
|
166
|
+
* `path.isAbsolute()` because that primitive is platform-native: on a POSIX
|
|
167
|
+
* host (this repo's dev/CI hosts) it never reports `C:\` as absolute, so a
|
|
168
|
+
* Windows-literal-root entry would otherwise slip past every check below.
|
|
169
|
+
* @type {ReadonlySet<string>}
|
|
170
|
+
*/
|
|
171
|
+
const FILESYSTEM_ROOT_LITERALS = new Set(['/', '\\']);
|
|
172
|
+
const WINDOWS_DRIVE_ROOT_RE = /^[A-Za-z]:\\+$/;
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* @param {string} entry
|
|
176
|
+
* @returns {boolean}
|
|
177
|
+
*/
|
|
178
|
+
function isFilesystemRootLiteral(entry) {
|
|
179
|
+
return FILESYSTEM_ROOT_LITERALS.has(entry) || WINDOWS_DRIVE_ROOT_RE.test(entry);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Well-known top-level system/home directories. A FIXED DENYLIST (not a
|
|
184
|
+
* segment-count threshold): the #792 legitimate grant
|
|
185
|
+
* (`/private/tmp/<session>/scratchpad/**`) is itself a single-segment-deep
|
|
186
|
+
* grant under an unusual root ("private"), so any segment-count heuristic
|
|
187
|
+
* tight enough to catch `/etc/**` risks catching that too, or must be tuned
|
|
188
|
+
* loosely enough to leave a gap. A fixed list of the directories a
|
|
189
|
+
* hallucinated/mis-copied wave-scope entry could plausibly land on is
|
|
190
|
+
* predictable, auditable in a one-line diff, and does not touch legitimate
|
|
191
|
+
* deep grants under any other root.
|
|
192
|
+
* @type {ReadonlySet<string>}
|
|
193
|
+
*/
|
|
194
|
+
const DENIED_ABSOLUTE_TOP_SEGMENTS = new Set([
|
|
195
|
+
'etc',
|
|
196
|
+
'Users',
|
|
197
|
+
'home',
|
|
198
|
+
'root',
|
|
199
|
+
'bin',
|
|
200
|
+
'sbin',
|
|
201
|
+
'usr',
|
|
202
|
+
'System',
|
|
203
|
+
'var',
|
|
204
|
+
'boot',
|
|
205
|
+
'dev',
|
|
206
|
+
'proc',
|
|
207
|
+
'sys',
|
|
208
|
+
'Library',
|
|
209
|
+
'Applications',
|
|
210
|
+
'Windows',
|
|
211
|
+
]);
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* The top-level path segment of an absolute POSIX-style entry (the segment
|
|
215
|
+
* immediately after the leading "/"), if it is on the denylist above.
|
|
216
|
+
* @param {string} entry
|
|
217
|
+
* @returns {string|null}
|
|
218
|
+
*/
|
|
219
|
+
function deniedTopSegment(entry) {
|
|
220
|
+
const first = entry.split('/').filter(Boolean)[0];
|
|
221
|
+
return first && DENIED_ABSOLUTE_TOP_SEGMENTS.has(first) ? first : null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Does this entry contain a glob wildcard? Mirrors this codebase's own glob
|
|
226
|
+
* convention (`isGlobScopeEntry` in scripts/lib/scope-gate.mjs / #796): `*`
|
|
227
|
+
* is the sole wildcard metachar used in allowedPaths/fileScope entries
|
|
228
|
+
* throughout this repo (no `?`/`[]`/`{}` glob syntax is supported or tested
|
|
229
|
+
* anywhere else in scope-gate.mjs or enforce-scope.mjs).
|
|
230
|
+
* @param {string} entry
|
|
231
|
+
* @returns {boolean}
|
|
232
|
+
*/
|
|
233
|
+
function hasWildcard(entry) {
|
|
234
|
+
return entry.includes('*');
|
|
235
|
+
}
|
|
236
|
+
|
|
142
237
|
/**
|
|
143
238
|
* Validate allowedPaths array: must exist, be an array of non-empty strings,
|
|
144
|
-
* with no
|
|
239
|
+
* with no path-traversal segments. Absolute (out-of-repo) entries are a
|
|
240
|
+
* SANCTIONED Gate 5b grant (#792) — WARN, not reject; see #870. A narrow
|
|
241
|
+
* catastrophic subclass of absolute entries is instead a hard ERROR
|
|
242
|
+
* (#870-followup security review, confidence 0.85): allowedPaths is not
|
|
243
|
+
* hand-authored — wave-loop.md's Scope Manifest computes it PROGRAMMATICALLY
|
|
244
|
+
* as the union of LLM-authored per-agent "Files:" scopes — so a
|
|
245
|
+
* hallucinated/mis-copied/injected entry reaching one of these shapes must
|
|
246
|
+
* hard-fail rather than rely on a stderr WARN nothing guarantees a human
|
|
247
|
+
* reads before dispatch.
|
|
145
248
|
* @param {Record<string, unknown>} obj
|
|
146
249
|
* @param {string[]} errors
|
|
147
250
|
* @param {string[]} warnings
|
|
@@ -161,11 +264,46 @@ function validateAllowedPaths(obj, errors, warnings) {
|
|
|
161
264
|
errors.push('allowedPaths contains empty string');
|
|
162
265
|
continue;
|
|
163
266
|
}
|
|
164
|
-
//
|
|
165
|
-
|
|
166
|
-
|
|
267
|
+
// #870: an explicit absolute entry is a SANCTIONED out-of-repo grant — mirrors
|
|
268
|
+
// hooks/enforce-scope.mjs Gate 5b (matchesAbsoluteAllowlist), which honours ANY
|
|
269
|
+
// syntactically-absolute allowedPaths entry (path.isAbsolute) against the
|
|
270
|
+
// realpath-resolved write candidate. Using `path.isAbsolute` (not a hand-rolled
|
|
271
|
+
// `startsWith('/')`) keeps the same platform-native semantics Gate 5b uses.
|
|
272
|
+
// WARN, not reject — the validator must not contradict the hook it validates
|
|
273
|
+
// for (#792 / #870) — EXCEPT for the narrow catastrophic subclass below
|
|
274
|
+
// (#870-followup), which hard-rejects regardless of what Gate 5b would do
|
|
275
|
+
// with it: pre-flight validation exists precisely to catch a grant this bad
|
|
276
|
+
// before the hook is ever consulted.
|
|
277
|
+
if (path.isAbsolute(entry) || isFilesystemRootLiteral(entry)) {
|
|
278
|
+
if (isFilesystemRootLiteral(entry)) {
|
|
279
|
+
errors.push(
|
|
280
|
+
`allowedPaths grants the entire filesystem root: ${entry} — refused unconditionally, this can never be a valid wave scope`,
|
|
281
|
+
);
|
|
282
|
+
} else {
|
|
283
|
+
const denied = deniedTopSegment(entry);
|
|
284
|
+
if (denied) {
|
|
285
|
+
errors.push(
|
|
286
|
+
`allowedPaths contains a well-known system/home directory grant: ${entry} (top-level segment "${denied}" is denylisted) — refused, scope a narrower path instead`,
|
|
287
|
+
);
|
|
288
|
+
} else if (!hasWildcard(entry)) {
|
|
289
|
+
errors.push(
|
|
290
|
+
`allowedPaths contains a bare absolute file grant with no wildcard: ${entry} — a single concrete out-of-repo file has no established legitimate use in this codebase; scope a glob instead`,
|
|
291
|
+
);
|
|
292
|
+
} else {
|
|
293
|
+
warnings.push(
|
|
294
|
+
`allowedPaths contains an absolute (out-of-repo) path: ${entry} — honoured by hooks/enforce-scope.mjs Gate 5b; verify this grant is intentional`,
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
167
298
|
}
|
|
168
|
-
// Reject path traversal: any `../` segment
|
|
299
|
+
// Reject path traversal: any `../` segment. INDEPENDENT of the absolute checks
|
|
300
|
+
// above — an absolute entry that ALSO contains `../` must still be rejected
|
|
301
|
+
// here, unconditionally, even when it was already rejected above for a
|
|
302
|
+
// different reason. (Such an entry could never actually match Gate 5b's
|
|
303
|
+
// realpath-canonicalised candidate anyway, since realpath strips `..`
|
|
304
|
+
// segments before Gate 5b ever compares it — see enforce-scope.mjs REQ-09 —
|
|
305
|
+
// but validate-time rejection gives the operator immediate, actionable
|
|
306
|
+
// feedback instead of leaving a silently-dead allowedPaths entry.)
|
|
169
307
|
if (entry.includes('../')) {
|
|
170
308
|
errors.push(`allowedPaths contains path traversal: ${entry}`);
|
|
171
309
|
}
|
|
@@ -224,8 +362,9 @@ function validateGates(obj, errors) {
|
|
|
224
362
|
*
|
|
225
363
|
* @param {Record<string, unknown>} obj - the already schema-validated wave-scope object
|
|
226
364
|
* @param {string} fileScopePath - path to the agent fileScope JSON file
|
|
365
|
+
* @param {boolean} [expandTestSiblings] - also assert #970 test-sibling coverage
|
|
227
366
|
*/
|
|
228
|
-
function assertSubsetOrDie(obj, fileScopePath) {
|
|
367
|
+
function assertSubsetOrDie(obj, fileScopePath, expandTestSiblings = false) {
|
|
229
368
|
if (!existsSync(fileScopePath) || !statSync(fileScopePath).isFile()) {
|
|
230
369
|
die(`Cannot read --assert-subset file: ${fileScopePath}`, 2);
|
|
231
370
|
}
|
|
@@ -248,14 +387,40 @@ function assertSubsetOrDie(obj, fileScopePath) {
|
|
|
248
387
|
if (!ok) {
|
|
249
388
|
die(`agent fileScope not ⊆ allowedPaths — missing: [${missing.join(', ')}]`, 1);
|
|
250
389
|
}
|
|
390
|
+
// #970 — opt-in, and deliberately AFTER the plain subset assertion so the
|
|
391
|
+
// pre-existing failure mode keeps its exact message. This only ever adds a
|
|
392
|
+
// requirement: the union must also grant each production file's test sibling,
|
|
393
|
+
// or the agent is mechanically unable to update the test it just broke.
|
|
394
|
+
//
|
|
395
|
+
// The manifest's OWN `role` (already schema-validated as a non-empty string
|
|
396
|
+
// above) gates it, through the same predicate the expander uses. That is what
|
|
397
|
+
// lets the pre-dispatch command carry the flag unconditionally: on a Quality
|
|
398
|
+
// phase-1 manifest — production files with tests deliberately excluded — an
|
|
399
|
+
// ungated assertion would block every dispatch of that phase.
|
|
400
|
+
if (expandTestSiblings) {
|
|
401
|
+
if (!testSiblingExpansionApplies({ role: obj.role })) {
|
|
402
|
+
warn(
|
|
403
|
+
`--expand-test-siblings: skipped for role "${obj.role}" — test-sibling expansion applies only to [${TEST_SIBLING_EXPANSION_ROLES.join(', ')}] (scripts/lib/scope-gate.mjs)`,
|
|
404
|
+
);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
const sib = assertTestSiblingCoverage(fileScope, obj.allowedPaths, { role: obj.role });
|
|
408
|
+
if (!sib.ok) {
|
|
409
|
+
die(
|
|
410
|
+
`allowedPaths does not grant the test sibling of every production file in the agent fileScope (#970) — missing: [${sib.missing.join(', ')}]. Re-run the Scope Manifest step: allowedPaths must be expandTestSiblings(union, { role }) — see skills/wave-executor/wave-loop.md § Scope Manifest #3`,
|
|
411
|
+
1,
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
251
415
|
}
|
|
252
416
|
|
|
253
417
|
/**
|
|
254
418
|
* Main validation entry point. Reads input, validates, exits with appropriate code.
|
|
255
419
|
* @param {string} input - raw JSON string
|
|
256
420
|
* @param {string|null} [assertSubsetPath] - optional agent fileScope file for the #796 subset assertion
|
|
421
|
+
* @param {boolean} [expandTestSiblings] - opt-in #970 test-sibling coverage assertion
|
|
257
422
|
*/
|
|
258
|
-
function validate(input, assertSubsetPath = null) {
|
|
423
|
+
function validate(input, assertSubsetPath = null, expandTestSiblings = false) {
|
|
259
424
|
const obj = parseJson(input);
|
|
260
425
|
const errors = [];
|
|
261
426
|
const warnings = [];
|
|
@@ -278,12 +443,12 @@ function validate(input, assertSubsetPath = null) {
|
|
|
278
443
|
|
|
279
444
|
// #796 — optional dispatch-time subset assertion (runs only after schema validation passes)
|
|
280
445
|
if (assertSubsetPath) {
|
|
281
|
-
assertSubsetOrDie(obj, assertSubsetPath);
|
|
446
|
+
assertSubsetOrDie(obj, assertSubsetPath, expandTestSiblings);
|
|
282
447
|
}
|
|
283
448
|
|
|
284
449
|
// Echo validated JSON to stdout (trailing newline normalised)
|
|
285
450
|
process.stdout.write(input.endsWith('\n') ? input : input + '\n');
|
|
286
451
|
}
|
|
287
452
|
|
|
288
|
-
const { assertSubset, positionals } = parseArgs(process.argv);
|
|
289
|
-
validate(readInput(positionals[0]), assertSubset);
|
|
453
|
+
const { assertSubset, expandTestSiblings, positionals } = parseArgs(process.argv);
|
|
454
|
+
validate(readInput(positionals[0]), assertSubset, expandTestSiblings);
|
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
* CLI usage:
|
|
10
10
|
* node scripts/vault-integration-watcher.mjs [--dry-run] [--issue <id>] [--verbose]
|
|
11
11
|
* [--glab-bin <path>] override glab binary (used by tests)
|
|
12
|
+
* [--repo <spec>] override the -R/--repo host-pinning spec (#872;
|
|
13
|
+
* used by tests — default: resolveRepoSpec() against
|
|
14
|
+
* the process cwd, degrading gracefully to no -R)
|
|
12
15
|
* [--issue <id>] override tracking issue (default: 305)
|
|
13
16
|
* [--dep-issues <id,id>] override dependency issues (default: 303,304)
|
|
14
17
|
* [--dry-run] compute + print, no glab posts
|
|
@@ -32,6 +35,7 @@ import { join } from 'node:path';
|
|
|
32
35
|
import { homedir } from 'node:os';
|
|
33
36
|
import { getCrossRepoProjects, getConfinementRoot } from './lib/config/cross-repo.mjs';
|
|
34
37
|
import { validatePathInsideProject } from './lib/path-utils.mjs';
|
|
38
|
+
import { resolveRepoSpec, redactUrlCredentials } from './lib/vcs-repo-spec.mjs';
|
|
35
39
|
|
|
36
40
|
// ── Argument parsing ──────────────────────────────────────────────────────────
|
|
37
41
|
|
|
@@ -49,6 +53,17 @@ const DEP_ISSUES_RAW = getArg('--dep-issues') ?? '303,304';
|
|
|
49
53
|
const DEP_ISSUES = DEP_ISSUES_RAW.split(',').map((s) => s.trim());
|
|
50
54
|
const GLAB_BIN = getArg('--glab-bin') ?? process.env.GLAB_BIN ?? 'glab';
|
|
51
55
|
|
|
56
|
+
// -R/--repo host-pinning spec (#872) — resolved ONCE at module load, reused
|
|
57
|
+
// by every glab call site below (glabJson + postComment cover all 4 call
|
|
58
|
+
// sites: fetchIssues, fetchComments, the tracking-issue view in main(), and
|
|
59
|
+
// the note-add write). `--repo <spec>` is an explicit override (analogous to
|
|
60
|
+
// `--glab-bin`, used by tests); otherwise falls back to the real
|
|
61
|
+
// `resolveRepoSpec` (shells out to `git remote get-url`). Graceful
|
|
62
|
+
// degradation: undefined ⇒ no -R appended anywhere, matching the ambient
|
|
63
|
+
// glab/GITLAB_HOST fallback this watcher always used pre-#872 — never a hard
|
|
64
|
+
// fail, per #839's "never emit -R undefined" contract.
|
|
65
|
+
const REPO_SPEC = getArg('--repo') ?? resolveRepoSpec({ repoRoot: process.cwd(), vcs: 'gitlab' });
|
|
66
|
+
|
|
52
67
|
// ── Logging ───────────────────────────────────────────────────────────────────
|
|
53
68
|
|
|
54
69
|
function log(msg) {
|
|
@@ -56,7 +71,12 @@ function log(msg) {
|
|
|
56
71
|
}
|
|
57
72
|
|
|
58
73
|
function verbose(msg) {
|
|
59
|
-
|
|
74
|
+
// Defense-in-depth (#907, CWE-214): the resolved REPO_SPEC is already
|
|
75
|
+
// credential-stripped at the source (resolveRepoSpec), but a `--repo <spec>`
|
|
76
|
+
// override bypasses that strip and flows verbatim into the `glab … -R <spec>`
|
|
77
|
+
// line logged here. Redact any embedded userinfo credential before it reaches
|
|
78
|
+
// the CI job's stderr.
|
|
79
|
+
if (VERBOSE) log(redactUrlCredentials(msg));
|
|
60
80
|
}
|
|
61
81
|
|
|
62
82
|
function emit(action) {
|
|
@@ -66,13 +86,17 @@ function emit(action) {
|
|
|
66
86
|
// ── glab helpers ──────────────────────────────────────────────────────────────
|
|
67
87
|
|
|
68
88
|
/**
|
|
69
|
-
* Run a glab command and return parsed JSON output.
|
|
89
|
+
* Run a glab command and return parsed JSON output. Every call is pinned to
|
|
90
|
+
* -R <REPO_SPEC> (#872) when a spec resolved — appended centrally here so
|
|
91
|
+
* all glabJson call sites (fetchIssues, fetchComments, main()'s tracking-issue
|
|
92
|
+
* view) inherit the pin without each needing to build it themselves.
|
|
70
93
|
* @param {string[]} args
|
|
71
94
|
* @returns {unknown}
|
|
72
95
|
*/
|
|
73
96
|
function glabJson(args) {
|
|
74
|
-
|
|
75
|
-
|
|
97
|
+
const fullArgs = REPO_SPEC ? [...args, '-R', REPO_SPEC] : args;
|
|
98
|
+
verbose(`glab ${fullArgs.join(' ')}`);
|
|
99
|
+
const result = spawnSync(GLAB_BIN, fullArgs, { encoding: 'utf8', timeout: 30_000 });
|
|
76
100
|
if (result.error) {
|
|
77
101
|
log(`glab spawn error: ${result.error.message}`);
|
|
78
102
|
process.exit(2);
|
|
@@ -90,7 +114,7 @@ function glabJson(args) {
|
|
|
90
114
|
}
|
|
91
115
|
|
|
92
116
|
/**
|
|
93
|
-
* Post a comment to an issue.
|
|
117
|
+
* Post a comment to an issue. Pinned to -R <REPO_SPEC> (#872) — see glabJson.
|
|
94
118
|
* @param {string} issueId
|
|
95
119
|
* @param {string} body
|
|
96
120
|
*/
|
|
@@ -101,11 +125,9 @@ function postComment(issueId, body) {
|
|
|
101
125
|
return;
|
|
102
126
|
}
|
|
103
127
|
verbose(`posting comment to #${issueId}`);
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
{ encoding: 'utf8', timeout: 30_000 }
|
|
108
|
-
);
|
|
128
|
+
const args = ['issue', 'note', 'add', issueId, '--message', body];
|
|
129
|
+
if (REPO_SPEC) args.push('-R', REPO_SPEC);
|
|
130
|
+
const result = spawnSync(GLAB_BIN, args, { encoding: 'utf8', timeout: 30_000 });
|
|
109
131
|
if (result.error) {
|
|
110
132
|
log(`glab note add spawn error: ${result.error.message}`);
|
|
111
133
|
process.exit(2);
|
|
@@ -195,7 +195,7 @@ Flow-style arrays are also accepted: `globs: ["src/**", "tests/**"]`.
|
|
|
195
195
|
In the live path the coordinator does NOT call the loader directly — it runs the CLI `scripts/print-applicable-rules.mjs` (which calls the loader) once per wave and captures stdout as the `<APPLICABLE-RULES>` block:
|
|
196
196
|
|
|
197
197
|
```sh
|
|
198
|
-
RULES_BLOCK="$(node "$PLUGIN_ROOT/scripts/print-applicable-rules.mjs" 2>/dev/null)"
|
|
198
|
+
RULES_BLOCK="$(node "$PLUGIN_ROOT/scripts/print-applicable-rules.mjs" --context wave 2>/dev/null)"
|
|
199
199
|
# Empty stdout (no .claude/rules/, no matches, or any failure) → inject nothing.
|
|
200
200
|
```
|
|
201
201
|
|
|
@@ -219,7 +219,7 @@ const rules = loadApplicableRules({
|
|
|
219
219
|
### Backward compatibility
|
|
220
220
|
|
|
221
221
|
- Rule files without any frontmatter continue to load as always-on. No migration required.
|
|
222
|
-
-
|
|
222
|
+
- `paths:` is a full alias for `globs:` (issue #795) — files using the old `paths:` frontmatter key load correctly SCOPED, not always-on. When BOTH `globs:` and `paths:` are present on the same rule, `globs:` wins silently (no merge, no warning) and `paths:` is discarded.
|
|
223
223
|
|
|
224
224
|
### Failure mode
|
|
225
225
|
|
|
@@ -90,7 +90,7 @@ node "$PLUGIN_ROOT/scripts/lib/claude-md-budget-lint.mjs" --repo-root "$REPO_ROO
|
|
|
90
90
|
```
|
|
91
91
|
|
|
92
92
|
`--mode warn` is deliberate at Anlage-time — the lint informs, the operator decides; it never blocks the scaffold. Interpret the JSON `violations[]`:
|
|
93
|
-
- `max-lines` — the instruction file is already over the lean-root ceiling (
|
|
93
|
+
- `max-lines` — the instruction file is already over the lean-root ceiling. The ceiling's SSOT is the exported constant `DEFAULT_MAX_LINES` in `scripts/lib/claude-md-budget-lint.mjs` (read it, do not memorise it; `= 80` as of 2026-08-03), overridable per run with `--max-lines`. The budget counts **non-exempt effective lines**, not the raw `wc -l`: the runtime-critical `## Session Config` block is excluded, because it is machine-parsed configuration rather than trimmable prose and applying a raw-line ceiling to it was structurally unreachable (#959 — the ceiling was re-derived in these units at the same time). Recommend trimming to pointers per the lean-root convention (delegate detail to `README.md` / `.orchestrator/steering/` / `.claude/rules/*.md` — see this plugin's own `CLAUDE.md` for a worked example of the pointer pattern; it **passes** this lint, 61 non-exempt of 209 raw lines against the ceiling of 80, measured 2026-08-03 at HEAD `730ee9d` — re-run the command above rather than trusting that number).
|
|
94
94
|
- `max-line-chars` — a single line exceeds the char ceiling (400 default); surface the line number for a quick manual wrap.
|
|
95
95
|
- `provenance-header` — line 1 lacks a `<!-- source: ...` attribution. On the `claude init` path (Public Fast Tier, Claude Code) this is a WARN only, never a hard failure — `claude init` output is not plugin-authored and has no reason to carry the plugin's provenance convention.
|
|
96
96
|
|