session-orchestrator 5.4.0 → 5.6.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/060-evolve.mdc +5 -5
- package/.cursor-plugin/plugin.json +1 -1
- package/CHANGELOG.md +115 -3
- package/README.md +30 -15
- package/docs/ci-setup.md +120 -23
- package/docs/components.md +7 -7
- package/docs/events-schema.md +17 -14
- package/docs/install.md +14 -2
- package/docs/migration-v4.md +24 -17
- package/docs/plugin-architecture-v3.md +1 -1
- package/docs/scope-collision-guard.md +19 -15
- package/docs/session-config-reference.md +7 -7
- package/docs/vault-docs-architecture.md +8 -4
- package/hooks/_lib/hook-import-set.json +17 -8
- package/hooks/_lib/lock-bootstrap.mjs +118 -26
- package/hooks/config-protection.mjs +1 -1
- package/hooks/enforce-commands.mjs +28 -13
- package/hooks/enforce-scope.mjs +201 -23
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks.json +1 -1
- package/hooks/on-session-start.mjs +4 -2
- package/hooks/on-stop.mjs +77 -171
- package/hooks/post-bash-write-verify.mjs +219 -24
- package/hooks/post-edit-validate.mjs +15 -5
- package/hooks/post-tool-batch-wave-signal.mjs +41 -156
- package/hooks/pre-bash-memory-propose-audit.mjs +28 -11
- package/hooks/pre-bash-staging-fence.mjs +29 -8
- package/hooks/pre-task-scope-disjoint.mjs +603 -241
- package/hooks/wave-scope-commit-guard.mjs +235 -73
- package/package.json +9 -8
- package/scripts/apply-session-learnings.mjs +35 -14
- package/scripts/archive-closed-prds.mjs +38 -11
- package/scripts/autopilot.mjs +8 -1
- package/scripts/backfill-abandoned-sessions.mjs +80 -5
- package/scripts/backfill-learnings-from-vault.mjs +49 -22
- package/scripts/codex-install.mjs +3 -0
- package/scripts/cursor-install.mjs +46 -2
- package/scripts/emit-session.mjs +5 -3
- package/scripts/export-hw-learnings.mjs +18 -7
- package/scripts/fleet-checkin.mjs +26 -3
- package/scripts/lib/autopilot/loop.mjs +25 -10
- package/scripts/lib/autopilot/stall-sampler.mjs +13 -9
- package/scripts/lib/blocked-commands-policy.mjs +40 -0
- package/scripts/lib/common.mjs +41 -0
- package/scripts/lib/config/vault-integration.mjs +151 -16
- package/scripts/lib/config.mjs +5 -1
- package/scripts/lib/eval/engine.mjs +13 -0
- package/scripts/lib/events-retention-banner.mjs +393 -0
- package/scripts/lib/events-rotation.mjs +21 -6
- package/scripts/lib/events.mjs +51 -3
- package/scripts/lib/fleet-protocol.mjs +20 -2
- package/scripts/lib/hardening.mjs +1 -0
- package/scripts/lib/harness-audit/categories/category4.mjs +8 -6
- package/scripts/lib/instruction-budget-guard.mjs +8 -0
- package/scripts/lib/learnings/expiry-sweep.mjs +387 -20
- package/scripts/lib/learnings/io.mjs +125 -16
- package/scripts/lib/lock-reaper.mjs +38 -14
- package/scripts/lib/maintenance-due-banner.mjs +23 -8
- package/scripts/lib/named-vault-resolver.mjs +48 -5
- package/scripts/lib/orphan-reaper/no-follow-append.mjs +87 -0
- package/scripts/lib/orphan-reaper/reaper-audit.mjs +101 -69
- package/scripts/lib/orphan-reaper/scan-throttle.mjs +79 -27
- package/scripts/lib/orphan-reaper/scan.mjs +4 -0
- package/scripts/lib/orphan-reaper/trigger.mjs +173 -0
- package/scripts/lib/orphan-reaper.mjs +5 -1
- package/scripts/lib/platform.mjs +206 -2
- package/scripts/lib/plugin-update-banner.mjs +3 -1
- package/scripts/lib/process-group.mjs +83 -22
- package/scripts/lib/scope-baseline.mjs +116 -25
- package/scripts/lib/scope-echo.mjs +7 -0
- package/scripts/lib/scope-gate.mjs +201 -27
- package/scripts/lib/session-close-backfill.mjs +202 -4
- package/scripts/lib/session-end/tail-runner.mjs +10 -5
- package/scripts/lib/session-lock-shape.mjs +6 -3
- package/scripts/lib/session-lock.mjs +320 -67
- package/scripts/lib/session-registry.mjs +41 -16
- package/scripts/lib/session-start-probes.mjs +242 -68
- package/scripts/lib/session-token-rollup.mjs +39 -34
- package/scripts/lib/sessions-staleness-banner.mjs +3 -1
- package/scripts/lib/tail-window.mjs +27 -3
- package/scripts/lib/telemetry/pricing.mjs +5 -2
- package/scripts/lib/telemetry/sync.mjs +17 -0
- package/scripts/lib/tests-src-ratio.mjs +77 -2
- package/scripts/lib/tmux-layout/telemetry-stats.mjs +12 -0
- package/scripts/lib/validate/check-test-value-bans.mjs +320 -32
- package/scripts/lib/validate/check-vcs-repo-flag.mjs +1 -1
- package/scripts/lib/vault-mirror/auto-commit.mjs +5 -3
- package/scripts/lib/vault-mirror/namespace.mjs +47 -26
- package/scripts/lib/vault-mirror/process.mjs +350 -62
- package/scripts/lib/vault-mirror/pseudonym-map.mjs +14 -5
- package/scripts/lib/vault-mirror/render-learnings.mjs +66 -3
- package/scripts/lib/vault-mirror/render-sessions.mjs +28 -4
- package/scripts/lib/vault-mirror/telemetry.mjs +18 -11
- package/scripts/lib/vault-status/board-writer.mjs +5 -1
- package/scripts/lib/vcs-repo-spec.mjs +8 -0
- package/scripts/materialize-wave-scope.mjs +7 -4
- package/scripts/release.mjs +1 -0
- package/scripts/run-migrate-v2-cross-repo.mjs +14 -2
- package/scripts/run-quality-gate.mjs +8 -5
- package/scripts/self-update.mjs +229 -0
- package/scripts/sweep-expired-learnings.mjs +560 -28
- package/scripts/validate-wave-scope.mjs +60 -6
- package/scripts/vault-backfill.mjs +50 -37
- package/scripts/vault-mirror.mjs +170 -33
- package/skills/_shared/fleet-protocol.md +74 -71
- package/skills/_shared/parallel-aware-auq.md +1 -1
- package/skills/_shared/state-ownership.md +38 -14
- package/skills/autopilot/SKILL.md +1 -1
- package/skills/bootstrap/SKILL.md +5 -3
- package/skills/bootstrap/_shared-template.md +96 -1
- package/skills/bootstrap/fast-template.md +3 -0
- package/skills/bootstrap/references/bootstrap-refresh-lock-flow.md +5 -3
- package/skills/bootstrap/standard-template.md +2 -0
- package/skills/discovery/probes/ssot-code-diff.mjs +12 -6
- package/skills/discovery/probes/vault-narrative-staleness.mjs +6 -2
- package/skills/discovery/probes/vault-staleness.mjs +9 -3
- package/skills/docs-orchestrator/SKILL.md +12 -5
- package/skills/docs-orchestrator/audience-mapping.md +5 -3
- package/skills/evolve/SKILL.md +11 -2
- package/skills/evolve/references/evolve-analyze-mode.md +37 -17
- package/skills/session-end/learning-patterns.md +3 -3
- package/skills/session-end/metrics-collection.md +1 -1
- package/skills/session-end/phase-3-2-docs-verification.md +7 -6
- package/skills/session-end/plan-verification.md +25 -9
- package/skills/session-end/references/phase-3-documentation-updates.md +11 -1
- package/skills/session-end/session-metrics-write.md +45 -10
- package/skills/session-start/SKILL.md +3 -3
- package/skills/session-start/references/operations-contract.md +5 -3
- package/skills/session-start/references/phase-1-2-session-lock.md +32 -7
- package/skills/session-start/references/phase-4-ssot-environment-check.md +8 -1
- package/skills/vault-mirror/SKILL.md +21 -11
- package/skills/wave-executor/SKILL.md +1 -1
- package/skills/wave-executor/circuit-breaker.md +1 -1
- package/skills/wave-executor/references/wave-executor-state-init.md +1 -1
- package/skills/wave-executor/references/wave-loop-dispatch.md +8 -0
- package/skills/wave-executor/references/wave-loop-review.md +2 -2
- package/skills/wave-executor/references/wave-loop-scope-manifest.md +14 -6
- package/.agents/skills/navigator/SKILL.md +0 -22
- package/.agents/skills/navigator/agents/openai.yaml +0 -5
- package/.codex-plugin/skills/navigator/SKILL.md +0 -22
- package/.codex-plugin/skills/navigator/agents/openai.yaml +0 -5
- package/.cursor/commands/navigator.md +0 -15
- package/.cursor/skills/navigator/SKILL.md +0 -13
- package/pi/prompts/navigator.md +0 -12
- package/skills/navigator/SKILL.md +0 -193
- package/skills/navigator/references/account-check.sh +0 -87
- package/skills/navigator/references/fleet-decisions.mjs +0 -231
- package/skills/navigator/references/slot-check.sh +0 -114
|
@@ -198,6 +198,46 @@ export function mergePolicies(floor, overlay) {
|
|
|
198
198
|
return { rules: merged, warnings };
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
// Wave-manifest projection
|
|
203
|
+
// ---------------------------------------------------------------------------
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* The patterns a wave manifest's `blockedCommands` may carry (#1509): the
|
|
207
|
+
* `pattern` of every `severity: 'block'` rule that has NO `type`.
|
|
208
|
+
*
|
|
209
|
+
* A TYPED rule's `pattern` is not a command pattern. `redirect-truncate` rules
|
|
210
|
+
* carry `pattern: ">"` and `path-delete` rules a free-text label; both are
|
|
211
|
+
* decided by their TARGET, type-aware, in hooks/pre-bash-destructive-guard.mjs.
|
|
212
|
+
* `hooks/enforce-commands.mjs` reads every `blockedCommands` entry as a command
|
|
213
|
+
* pattern, so the flattened `">"` denied interpreter payloads such as
|
|
214
|
+
* `bash -c "echo hi > /tmp/x"` (measured 2026-10-03 @ 8292050e). An untyped
|
|
215
|
+
* rule is the one class whose pattern means the same thing to both hooks.
|
|
216
|
+
*
|
|
217
|
+
* An UNKNOWN `type` is excluded as well: its pattern's meaning is unknown, and
|
|
218
|
+
* the destructive guard still evaluates the rule on every Bash call, so leaving
|
|
219
|
+
* it out of the manifest drops no protection — admitting it repeats #1509 for
|
|
220
|
+
* the next rule type.
|
|
221
|
+
*
|
|
222
|
+
* Total: a non-array `rules` (e.g. `loadEffectivePolicy()`'s `null`) yields `[]`.
|
|
223
|
+
*
|
|
224
|
+
* @param {unknown} rules - the `rules` of {@link loadEffectivePolicy}
|
|
225
|
+
* @returns {string[]}
|
|
226
|
+
*/
|
|
227
|
+
export function blockedCommandPatterns(rules) {
|
|
228
|
+
if (!Array.isArray(rules)) return [];
|
|
229
|
+
return rules
|
|
230
|
+
.filter(
|
|
231
|
+
(r) =>
|
|
232
|
+
r !== null &&
|
|
233
|
+
typeof r === 'object' &&
|
|
234
|
+
r.severity === 'block' &&
|
|
235
|
+
(r.type ?? null) === null &&
|
|
236
|
+
typeof r.pattern === 'string',
|
|
237
|
+
)
|
|
238
|
+
.map((r) => r.pattern);
|
|
239
|
+
}
|
|
240
|
+
|
|
201
241
|
// ---------------------------------------------------------------------------
|
|
202
242
|
// Load (with per-path mtime cache)
|
|
203
243
|
// ---------------------------------------------------------------------------
|
package/scripts/lib/common.mjs
CHANGED
|
@@ -54,6 +54,47 @@ export function expandTilde(p) {
|
|
|
54
54
|
return p;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Replace the current user's home directory with `~` wherever it occurs in
|
|
59
|
+
* `text` — the inverse of {@link expandTilde}, for free text as well as a bare
|
|
60
|
+
* path: an `err.message` from `node:fs` (`ENOENT: …, open '/Users/bob/v/x.md'`),
|
|
61
|
+
* git stderr, a resolved vault dir. Every occurrence is replaced.
|
|
62
|
+
*
|
|
63
|
+
* Log lines of the vault writers and the session-end shell land in CI logs and
|
|
64
|
+
* session transcripts, where the home dir is an owner-privacy leak (#1490,
|
|
65
|
+
* `.claude/rules/security.md` § Owner-Privacy).
|
|
66
|
+
*
|
|
67
|
+
* Boundary-aware on both sides. On the right the home dir must end at a path
|
|
68
|
+
* separator, the end of the text, whitespace, a quote/bracket/list delimiter
|
|
69
|
+
* (`'"\`)]},;:`) or a sentence-final `.`, so `/Users/bob` never rewrites
|
|
70
|
+
* `/Users/bobby/x` or `/Users/bob.old`. On the left it must not continue a
|
|
71
|
+
* path segment, so `/data/Users/bob` stays. A home dir that is empty, the
|
|
72
|
+
* filesystem root or a bare drive root is a no-op — collapsing it would
|
|
73
|
+
* rewrite every absolute path. Non-strings pass through unchanged.
|
|
74
|
+
*
|
|
75
|
+
* @param {string} text
|
|
76
|
+
* @param {string} [home] — defaults to `os.homedir()`; an unresolvable home
|
|
77
|
+
* leaves `text` unchanged (a log redactor must never turn an error path into
|
|
78
|
+
* a crash)
|
|
79
|
+
* @returns {string}
|
|
80
|
+
*/
|
|
81
|
+
export function redactHomeDir(text, home) {
|
|
82
|
+
if (typeof text !== 'string' || text.length === 0) return text;
|
|
83
|
+
let h = home;
|
|
84
|
+
if (h === undefined) {
|
|
85
|
+
try {
|
|
86
|
+
h = os.homedir();
|
|
87
|
+
} catch {
|
|
88
|
+
return text;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
h = String(h ?? '').replace(/[/\\]+$/, '');
|
|
92
|
+
if (h.length === 0 || /^[a-z]:$/i.test(h)) return text;
|
|
93
|
+
const escaped = h.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
94
|
+
const re = new RegExp(`(?<![\\w.~-])${escaped}(?=$|[/\\\\\\s'"\`)\\]},;:]|\\.(?:$|\\s))`, 'g');
|
|
95
|
+
return text.replace(re, '~');
|
|
96
|
+
}
|
|
97
|
+
|
|
57
98
|
// ---------------------------------------------------------------------------
|
|
58
99
|
// Timestamp helpers
|
|
59
100
|
// ---------------------------------------------------------------------------
|
|
@@ -57,16 +57,31 @@ const MODE_ALLOWED = ['warn', 'strict', 'off'];
|
|
|
57
57
|
* value portion (including any literal `**` inside it) is untouched.
|
|
58
58
|
*
|
|
59
59
|
* Defaults:
|
|
60
|
-
* enabled:
|
|
61
|
-
* vault-dir:
|
|
62
|
-
* mode:
|
|
63
|
-
* vault-name:
|
|
60
|
+
* enabled: false
|
|
61
|
+
* vault-dir: null
|
|
62
|
+
* mode: "warn" (invalid values silently fall back to "warn")
|
|
63
|
+
* vault-name: null (absent = downstream callers use deriveRepo() default)
|
|
64
|
+
* gitlab-groups: null (#1094 — non-empty string[] or null; see _groupsFromScalar)
|
|
65
|
+
*
|
|
66
|
+
* `gitlab-groups` accepts a comma string (`a, b`), a flow array (`[a, b]`) and,
|
|
67
|
+
* in block form only, a YAML block list (`gitlab-groups:` + indented `- a`
|
|
68
|
+
* lines — the shape docs/session-config-template.md documents). Inside the
|
|
69
|
+
* inline-object form more than one group needs the `[a, b]` array, because the
|
|
70
|
+
* literal is split on commas. YAML semantics throughout: `null` / `~` / empty
|
|
71
|
+
* mean unset (also as a list element), and each element loses one pair of
|
|
72
|
+
* matching quotes.
|
|
64
73
|
*
|
|
65
74
|
* @param {string} content — full file contents
|
|
66
|
-
* @returns {{enabled: boolean, "vault-dir": string|null, mode: string, "vault-name": string|null}}
|
|
75
|
+
* @returns {{enabled: boolean, "vault-dir": string|null, mode: string, "vault-name": string|null, "gitlab-groups": string[]|null}}
|
|
67
76
|
*/
|
|
68
77
|
export function _parseVaultIntegration(content) {
|
|
69
|
-
const defaults = {
|
|
78
|
+
const defaults = {
|
|
79
|
+
enabled: false,
|
|
80
|
+
'vault-dir': null,
|
|
81
|
+
mode: 'warn',
|
|
82
|
+
'vault-name': null,
|
|
83
|
+
'gitlab-groups': null,
|
|
84
|
+
};
|
|
70
85
|
if (typeof content !== 'string' || content === '') return defaults;
|
|
71
86
|
|
|
72
87
|
// #1162: NOT a raw split. `preprocessBlockLines` drops HTML-commented lines
|
|
@@ -106,21 +121,36 @@ export function _parseVaultIntegration(content) {
|
|
|
106
121
|
let vaultDir = null;
|
|
107
122
|
let mode = 'warn';
|
|
108
123
|
let vaultName = null;
|
|
124
|
+
let gitlabGroups = null;
|
|
125
|
+
// Items of a YAML block list under a value-less `gitlab-groups:` line; null
|
|
126
|
+
// while no such list is open.
|
|
127
|
+
let groupItems = null;
|
|
109
128
|
|
|
110
129
|
for (const rawLine of blockLines) {
|
|
111
130
|
const clean = rawLine.replace(/\s*#.*$/, '').replace(/\s+$/, '');
|
|
112
131
|
if (!clean.trim()) continue;
|
|
113
132
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
133
|
+
if (groupItems !== null) {
|
|
134
|
+
const item = clean.match(/^\s+-(?:\s+(.*))?$/);
|
|
135
|
+
if (item) {
|
|
136
|
+
const value = _groupName((item[1] ?? '').trim());
|
|
137
|
+
if (value !== null) groupItems.push(value);
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Sub-keys at any indent. A list-item line (`- x`) never matches, so list
|
|
143
|
+
// items under a key this parser does not own are skipped here.
|
|
117
144
|
const kvMatch = clean.match(/^\s+([a-zA-Z_-]+):\s*(.*)$/);
|
|
118
145
|
if (!kvMatch) continue;
|
|
119
146
|
|
|
147
|
+
if (groupItems !== null) {
|
|
148
|
+
if (groupItems.length > 0) gitlabGroups = groupItems;
|
|
149
|
+
groupItems = null;
|
|
150
|
+
}
|
|
151
|
+
|
|
120
152
|
const k = kvMatch[1];
|
|
121
|
-
|
|
122
|
-
if (v.startsWith('"') && v.endsWith('"') && v.length >= 2) v = v.slice(1, -1);
|
|
123
|
-
else if (v.startsWith("'") && v.endsWith("'") && v.length >= 2) v = v.slice(1, -1);
|
|
153
|
+
const v = _stripQuotes(kvMatch[2].trim());
|
|
124
154
|
|
|
125
155
|
switch (k) {
|
|
126
156
|
case 'enabled':
|
|
@@ -140,21 +170,118 @@ export function _parseVaultIntegration(content) {
|
|
|
140
170
|
if (v === '' || v === 'none' || v === 'null') vaultName = null;
|
|
141
171
|
else vaultName = v;
|
|
142
172
|
break;
|
|
173
|
+
case 'gitlab-groups': {
|
|
174
|
+
// A value replaces any earlier one; an empty value opens a block list,
|
|
175
|
+
// which replaces it only once it has collected an item. The value goes
|
|
176
|
+
// to `_groupsFromScalar` WITH its quotes, as the inline form's does —
|
|
177
|
+
// stripping them here first read `"null"` as unset in this form only (#1496).
|
|
178
|
+
const raw = kvMatch[2].trim();
|
|
179
|
+
if (raw === '') groupItems = [];
|
|
180
|
+
else gitlabGroups = _groupsFromScalar(raw);
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
143
183
|
}
|
|
144
184
|
}
|
|
185
|
+
if (groupItems !== null && groupItems.length > 0) gitlabGroups = groupItems;
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
enabled,
|
|
189
|
+
'vault-dir': vaultDir,
|
|
190
|
+
mode,
|
|
191
|
+
'vault-name': vaultName,
|
|
192
|
+
'gitlab-groups': gitlabGroups,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
145
195
|
|
|
146
|
-
|
|
196
|
+
/**
|
|
197
|
+
* Strip ONE pair of matching surrounding quotes (`"x"` / `'x'`). A lone quote
|
|
198
|
+
* character is returned unchanged.
|
|
199
|
+
*
|
|
200
|
+
* @param {string} v — an already-trimmed value
|
|
201
|
+
* @returns {string}
|
|
202
|
+
*/
|
|
203
|
+
function _stripQuotes(v) {
|
|
204
|
+
if (v.length >= 2 && ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'")))) {
|
|
205
|
+
return v.slice(1, -1);
|
|
206
|
+
}
|
|
207
|
+
return v;
|
|
147
208
|
}
|
|
148
209
|
|
|
210
|
+
/**
|
|
211
|
+
* The YAML 1.2 core-schema null scalars, plus the empty value. `none` is NOT
|
|
212
|
+
* one: YAML reads it as a string, so it stays a literal group name.
|
|
213
|
+
*/
|
|
214
|
+
const YAML_NULL_RE = /^(?:|null|Null|NULL|~)$/;
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* One `gitlab-groups` element as YAML reads it: an unquoted null scalar (or an
|
|
218
|
+
* empty element) is no group at all → `null`; otherwise ONE pair of matching
|
|
219
|
+
* quotes is stripped, so `"null"` stays the string `null` exactly as in YAML.
|
|
220
|
+
*
|
|
221
|
+
* @param {string} raw — an already-trimmed element
|
|
222
|
+
* @returns {string|null}
|
|
223
|
+
*/
|
|
224
|
+
function _groupName(raw) {
|
|
225
|
+
if (YAML_NULL_RE.test(raw)) return null;
|
|
226
|
+
const name = _stripQuotes(raw).trim();
|
|
227
|
+
return name === '' ? null : name;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Normalise a scalar `gitlab-groups` value to a group list with YAML semantics:
|
|
232
|
+
* a null scalar (`null`, `~`, empty) is unset → `null`; otherwise drop one
|
|
233
|
+
* leading `[` and one trailing `]`, split on commas, trim, and read each element
|
|
234
|
+
* through `_groupName` (null elements dropped, one quote pair stripped). An
|
|
235
|
+
* empty result is `null`, the documented default. Before this, `null` scanned a
|
|
236
|
+
* GitLab group named "null" and `["a"]` one named `"a"` with the quotes.
|
|
237
|
+
*
|
|
238
|
+
* The ONE normalisation for both forms, so both pass the value with its quotes
|
|
239
|
+
* (#1496). A wholly quoted value is a YAML string, never a null: it loses that
|
|
240
|
+
* quote pair, is split on commas, and its elements are taken literally —
|
|
241
|
+
* `"null"` is the group "null", `"a, b"` the groups a and b.
|
|
242
|
+
*
|
|
243
|
+
* @param {string} v
|
|
244
|
+
* @returns {string[]|null}
|
|
245
|
+
*/
|
|
246
|
+
function _groupsFromScalar(v) {
|
|
247
|
+
const trimmed = v.trim();
|
|
248
|
+
if (YAML_NULL_RE.test(trimmed)) return null;
|
|
249
|
+
const unquoted = _stripQuotes(trimmed);
|
|
250
|
+
const readElement = unquoted === trimmed ? _groupName : (g) => (g === '' ? null : g);
|
|
251
|
+
const groups = unquoted
|
|
252
|
+
.replace(/^\[/, '')
|
|
253
|
+
.replace(/\]$/, '')
|
|
254
|
+
.split(',')
|
|
255
|
+
.map((g) => readElement(g.trim()))
|
|
256
|
+
.filter((g) => g !== null);
|
|
257
|
+
return groups.length > 0 ? groups : null;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* `gitlab-groups: [ ... ]` inside an inline object literal (braces already
|
|
262
|
+
* removed). Taken out BEFORE the comma split, which would otherwise cut the
|
|
263
|
+
* array apart; every other pair is split exactly as before.
|
|
264
|
+
*/
|
|
265
|
+
const INLINE_GROUPS_ARRAY_RE = /(^|,)\s*gitlab-groups\s*:\s*(\[[^\]]*\])\s*(?=,|$)/;
|
|
266
|
+
|
|
149
267
|
/**
|
|
150
268
|
* Parse an inline YAML object literal `{ key: val, key: val }` into the
|
|
151
269
|
* vault-integration return shape. Supports unquoted values including `~/` paths.
|
|
152
270
|
*
|
|
153
271
|
* @param {string} raw — full literal including braces
|
|
154
|
-
* @returns {{enabled: boolean, "vault-dir": string|null, mode: string}}
|
|
272
|
+
* @returns {{enabled: boolean, "vault-dir": string|null, mode: string, "vault-name": string|null, "gitlab-groups": string[]|null}}
|
|
155
273
|
*/
|
|
156
274
|
function _parseInlineObject(raw) {
|
|
157
|
-
|
|
275
|
+
let stripped = raw.replace(/^\s*\{/, '').replace(/\}\s*$/, '').trim();
|
|
276
|
+
let groupsArray = null;
|
|
277
|
+
const arrayMatch = stripped.match(INLINE_GROUPS_ARRAY_RE);
|
|
278
|
+
if (arrayMatch) {
|
|
279
|
+
groupsArray = arrayMatch[2];
|
|
280
|
+
stripped =
|
|
281
|
+
stripped.slice(0, arrayMatch.index) +
|
|
282
|
+
arrayMatch[1] +
|
|
283
|
+
stripped.slice(arrayMatch.index + arrayMatch[0].length);
|
|
284
|
+
}
|
|
158
285
|
const kv = new Map();
|
|
159
286
|
if (stripped !== '') {
|
|
160
287
|
for (const pair of stripped.split(',')) {
|
|
@@ -180,7 +307,15 @@ function _parseInlineObject(raw) {
|
|
|
180
307
|
vaultNameRaw === undefined || vaultNameRaw === '' || vaultNameRaw === 'none' || vaultNameRaw === 'null'
|
|
181
308
|
? null
|
|
182
309
|
: vaultNameRaw;
|
|
183
|
-
|
|
310
|
+
const groupsRaw = groupsArray ?? kv.get('gitlab-groups');
|
|
311
|
+
const gitlabGroups = groupsRaw === undefined ? null : _groupsFromScalar(groupsRaw);
|
|
312
|
+
return {
|
|
313
|
+
enabled,
|
|
314
|
+
'vault-dir': vaultDir,
|
|
315
|
+
mode,
|
|
316
|
+
'vault-name': vaultName,
|
|
317
|
+
'gitlab-groups': gitlabGroups,
|
|
318
|
+
};
|
|
184
319
|
}
|
|
185
320
|
|
|
186
321
|
// ---------------------------------------------------------------------------
|
package/scripts/lib/config.mjs
CHANGED
|
@@ -103,11 +103,15 @@ import { expandTilde } from './common.mjs';
|
|
|
103
103
|
*
|
|
104
104
|
* `cwd` in the ctx is a test-only DI seam; production reads `process.cwd()`.
|
|
105
105
|
*
|
|
106
|
+
* Exported for vault writers that read only the `vault-integration` block and
|
|
107
|
+
* cannot afford `parseSessionConfig`, which throws on an invalid value in any
|
|
108
|
+
* unrelated block (scripts/vault-backfill.mjs, #1094).
|
|
109
|
+
*
|
|
106
110
|
* @param {string|null|undefined} committed — value the committed Session Config produced
|
|
107
111
|
* @param {{ env?: Record<string, string|undefined>, ownerConfig?: object, cwd?: string }} [ctx] — from loadHostPaths()
|
|
108
112
|
* @returns {{ value: string|null|undefined, source: 'env'|'match'|'owner'|'committed' }}
|
|
109
113
|
*/
|
|
110
|
-
function resolveVaultDir(committed, { env = process.env, ownerConfig, cwd } = {}) {
|
|
114
|
+
export function resolveVaultDir(committed, { env = process.env, ownerConfig, cwd } = {}) {
|
|
111
115
|
const isNonBlank = (v) => typeof v === 'string' && v.trim() !== '';
|
|
112
116
|
|
|
113
117
|
const envVal = env?.SO_VAULT_DIR;
|
|
@@ -840,6 +840,19 @@ function resolveModel(model, env, resolveModelFromEnv) {
|
|
|
840
840
|
// Public API
|
|
841
841
|
// ---------------------------------------------------------------------------
|
|
842
842
|
|
|
843
|
+
/**
|
|
844
|
+
* Events window this reader needs retained (#1401 part 3): NONE FIXED, hence
|
|
845
|
+
* `null` rather than a made-up number. `evaluateSession` reads the whole
|
|
846
|
+
* ledger through `readEventsWithRotations` to score ONE named past session,
|
|
847
|
+
* arbitrarily old (`computeWindow`, #1407) — the window it needs is that
|
|
848
|
+
* session's own age, which no finite retention can be sized for. A session
|
|
849
|
+
* older than the retained ledger surfaces through `ledgerGapNote` instead.
|
|
850
|
+
* Read by `scripts/lib/events-retention-banner.mjs`.
|
|
851
|
+
*
|
|
852
|
+
* @type {number|null}
|
|
853
|
+
*/
|
|
854
|
+
export const REQUIRED_EVENTS_WINDOW_DAYS = null;
|
|
855
|
+
|
|
843
856
|
/**
|
|
844
857
|
* Evaluate one completed session against the rubric-v2 dimensions.
|
|
845
858
|
*
|