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
|
@@ -26,6 +26,8 @@
|
|
|
26
26
|
|
|
27
27
|
import { randomUUID } from 'node:crypto';
|
|
28
28
|
|
|
29
|
+
import { filterRealSessions } from '../session-schema.mjs';
|
|
30
|
+
|
|
29
31
|
// ---------------------------------------------------------------------------
|
|
30
32
|
// Constants
|
|
31
33
|
// ---------------------------------------------------------------------------
|
|
@@ -69,18 +71,40 @@ function isoPlusDays(nowIso, days) {
|
|
|
69
71
|
}
|
|
70
72
|
}
|
|
71
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Return the `effectiveness` sub-object of a session record, or an empty object
|
|
76
|
+
* when absent/malformed. Session records NEST their effectiveness metrics under
|
|
77
|
+
* this key — see `skills/session-end/metrics-collection.md` (the writer) and
|
|
78
|
+
* `scripts/lib/eval/session-resolve.mjs` (a sibling reader).
|
|
79
|
+
*
|
|
80
|
+
* @param {object} s
|
|
81
|
+
* @returns {object} the nested block, or `{}` when there is none
|
|
82
|
+
*/
|
|
83
|
+
function effectivenessOf(s) {
|
|
84
|
+
const e = s?.effectiveness;
|
|
85
|
+
return e && typeof e === 'object' && !Array.isArray(e) ? e : {};
|
|
86
|
+
}
|
|
87
|
+
|
|
72
88
|
/**
|
|
73
89
|
* Extract a completion ratio from a session record. Sessions encode this in a
|
|
74
90
|
* few historically-evolved shapes; we tolerate all of them and fall back to
|
|
75
91
|
* `null` (excluded from the average) when nothing usable is present.
|
|
76
92
|
*
|
|
93
|
+
* Shape precedence (#835): the NESTED `effectiveness.completion_rate` is the
|
|
94
|
+
* shape the session-end writer actually emits and is therefore read FIRST. The
|
|
95
|
+
* top-level read is retained as a fallback for legacy/hand-written records —
|
|
96
|
+
* dropping it would silently zero out any record predating the nesting.
|
|
97
|
+
*
|
|
77
98
|
* @param {object} s
|
|
78
99
|
* @returns {number|null} ratio in [0, 1], or null when unknown
|
|
79
100
|
*/
|
|
80
101
|
function completionOf(s) {
|
|
81
102
|
if (!s || typeof s !== 'object') return null;
|
|
82
|
-
|
|
83
|
-
|
|
103
|
+
const eff = effectivenessOf(s);
|
|
104
|
+
// Direct ratio fields — nested (canonical) first, then legacy top-level.
|
|
105
|
+
const direct = num(
|
|
106
|
+
eff.completion_rate ?? eff.completion_ratio ?? s.completion_rate ?? s.completion_ratio,
|
|
107
|
+
);
|
|
84
108
|
if (direct !== null && direct >= 0 && direct <= 1) return direct;
|
|
85
109
|
// Planned vs completed counts
|
|
86
110
|
const planned = num(s.planned_count ?? s.tasks_planned);
|
|
@@ -93,15 +117,18 @@ function completionOf(s) {
|
|
|
93
117
|
|
|
94
118
|
/**
|
|
95
119
|
* Extract a carryover ratio from a session record. Carryover = work not closed
|
|
96
|
-
* within the session that flowed to a follow-up. Same tolerance pattern
|
|
97
|
-
* `completionOf`.
|
|
120
|
+
* within the session that flowed to a follow-up. Same tolerance pattern —
|
|
121
|
+
* and the same nested-first precedence (#835) — as `completionOf`.
|
|
98
122
|
*
|
|
99
123
|
* @param {object} s
|
|
100
124
|
* @returns {number|null} ratio in [0, 1], or null when unknown
|
|
101
125
|
*/
|
|
102
126
|
function carryoverOf(s) {
|
|
103
127
|
if (!s || typeof s !== 'object') return null;
|
|
104
|
-
const
|
|
128
|
+
const eff = effectivenessOf(s);
|
|
129
|
+
const direct = num(
|
|
130
|
+
eff.carryover_ratio ?? eff.carryover_rate ?? s.carryover_ratio ?? s.carryover_rate,
|
|
131
|
+
);
|
|
105
132
|
if (direct !== null && direct >= 0 && direct <= 1) return direct;
|
|
106
133
|
const planned = num(s.planned_count ?? s.tasks_planned);
|
|
107
134
|
const carried = num(s.carryover_count ?? s.tasks_carried_over);
|
|
@@ -132,6 +159,18 @@ function mean(values) {
|
|
|
132
159
|
* whose autopilot_run_id matches a known run). For the skeleton it is used
|
|
133
160
|
* only as a non-emptiness signal.
|
|
134
161
|
*
|
|
162
|
+
* Abandoned-session filtering (#834): `sessions` is filtered to REAL (non-
|
|
163
|
+
* phantom) records via `filterRealSessions()` before bucketing. Phantom
|
|
164
|
+
* `status: 'abandoned'` stubs are legitimate DATA but not legitimate SIGNAL —
|
|
165
|
+
* they must not inflate `n_manual`/`n_autopilot` or dilute the
|
|
166
|
+
* completion/carryover averages. See `scripts/lib/session-schema/filters.mjs`.
|
|
167
|
+
*
|
|
168
|
+
* Mode resolution (#834): `mode` is a LEGACY alias for the canonical
|
|
169
|
+
* `session_type` field (`session-schema/constants.mjs` `SESSION_KEY_ALIASES`).
|
|
170
|
+
* `session_type` is read first; `mode` is a fallback for legacy records that
|
|
171
|
+
* predate the rename (production ledgers overwhelmingly carry `session_type`,
|
|
172
|
+
* not `mode` — reading `mode` alone left this analyzer largely inert).
|
|
173
|
+
*
|
|
135
174
|
* @param {Array} autopilotRuns
|
|
136
175
|
* @param {Array} sessions
|
|
137
176
|
* @returns {Map<string, {n_manual:number, n_autopilot:number,
|
|
@@ -142,6 +181,9 @@ export function groupByMode(autopilotRuns, sessions) {
|
|
|
142
181
|
const out = new Map();
|
|
143
182
|
if (!Array.isArray(sessions) || sessions.length === 0) return out;
|
|
144
183
|
|
|
184
|
+
const realSessions = filterRealSessions(sessions);
|
|
185
|
+
if (realSessions.length === 0) return out;
|
|
186
|
+
|
|
145
187
|
// Optional: known autopilot_run_id set for stricter pairing. Empty set means
|
|
146
188
|
// accept any session with a non-empty autopilot_run_id field.
|
|
147
189
|
const knownRunIds = new Set();
|
|
@@ -156,9 +198,14 @@ export function groupByMode(autopilotRuns, sessions) {
|
|
|
156
198
|
|
|
157
199
|
// Bucket: mode → {manual: [], autopilot: []}
|
|
158
200
|
const buckets = new Map();
|
|
159
|
-
for (const s of
|
|
201
|
+
for (const s of realSessions) {
|
|
160
202
|
if (!s || typeof s !== 'object') continue;
|
|
161
|
-
const mode =
|
|
203
|
+
const mode =
|
|
204
|
+
typeof s.session_type === 'string'
|
|
205
|
+
? s.session_type
|
|
206
|
+
: typeof s.mode === 'string'
|
|
207
|
+
? s.mode
|
|
208
|
+
: null;
|
|
162
209
|
if (!mode) continue;
|
|
163
210
|
const apId = typeof s.autopilot_run_id === 'string' ? s.autopilot_run_id : null;
|
|
164
211
|
const isAutopilot =
|
|
@@ -40,11 +40,14 @@ const tcErrorCount =
|
|
|
40
40
|
: 0;
|
|
41
41
|
|
|
42
42
|
// --- Test ---
|
|
43
|
+
// NOTE: `testFailed`, NOT `failed` — a local `failed` is already bound near the
|
|
44
|
+
// bottom of this file and drives `process.exit(failed ? 2 : 0)`. Shadowing it
|
|
45
|
+
// would corrupt the gate's exit code.
|
|
43
46
|
const testResult = runCheck(testCmd);
|
|
44
|
-
const { passed: testPassed, total: testTotal } =
|
|
47
|
+
const { passed: testPassed, failed: testFailed, total: testTotal } =
|
|
45
48
|
testResult.status !== 'skip'
|
|
46
49
|
? extractTestCounts(testResult.output)
|
|
47
|
-
: { passed: 0, total: 0 };
|
|
50
|
+
: { passed: 0, failed: 0, total: 0 };
|
|
48
51
|
|
|
49
52
|
// --- Lint ---
|
|
50
53
|
const lintResult = runCheck(lintCmd);
|
|
@@ -70,7 +73,16 @@ const output = {
|
|
|
70
73
|
variant: 'full-gate',
|
|
71
74
|
duration_seconds: durationSeconds,
|
|
72
75
|
typecheck: { status: tcResult.status, error_count: tcErrorCount },
|
|
73
|
-
|
|
76
|
+
// `failed` is published explicitly (#967 item 1) rather than left to be
|
|
77
|
+
// re-derived as `total - passed` downstream: the derivation and the parse can
|
|
78
|
+
// disagree, and only an explicit third number lets a consumer check
|
|
79
|
+
// `passed + failed === total` as a real producer/consumer drift guard.
|
|
80
|
+
test: {
|
|
81
|
+
status: testResult.status,
|
|
82
|
+
total: testTotal,
|
|
83
|
+
passed: testPassed,
|
|
84
|
+
failed: testFailed,
|
|
85
|
+
},
|
|
74
86
|
lint: { status: lintResult.status, warnings: lintWarnings },
|
|
75
87
|
debug_artifacts: debugArtifacts,
|
|
76
88
|
stubbed,
|
|
@@ -133,18 +133,61 @@ export function extractCount(output, pattern) {
|
|
|
133
133
|
}
|
|
134
134
|
|
|
135
135
|
/**
|
|
136
|
-
*
|
|
136
|
+
* Matches a runner summary line that reports TEST-CASE counts.
|
|
137
137
|
*
|
|
138
|
-
*
|
|
138
|
+
* Deliberately anchored on `Tests` + a word boundary so it matches vitest's
|
|
139
|
+
* ` Tests 12904 passed | 11 skipped (12915)` and jest's
|
|
140
|
+
* `Tests: 1 failed, 4 passed, 5 total`, but NOT vitest's preceding
|
|
141
|
+
* ` Test Files 550 passed (550)` line ("Test" is not followed by "s").
|
|
142
|
+
*/
|
|
143
|
+
const TEST_SUMMARY_LINE = /^\s*Tests\b/;
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Parse test-runner output for pass/fail/total TEST-CASE counts.
|
|
139
147
|
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
148
|
+
* ## Which line is parsed
|
|
149
|
+
*
|
|
150
|
+
* Real vitest prints TWO `<N> passed` summary lines, files first:
|
|
151
|
+
*
|
|
152
|
+
* ```
|
|
153
|
+
* Test Files 550 passed (550)
|
|
154
|
+
* Tests 12904 passed | 11 skipped (12915)
|
|
155
|
+
* ```
|
|
156
|
+
*
|
|
157
|
+
* A naive whole-output scan hits the FILE count (550) and publishes it as the
|
|
158
|
+
* test count — the number then rides `gate-full.mjs`'s `test.passed` into the
|
|
159
|
+
* `orchestrator.quality_gate.*` event stream looking authoritative. So: when a
|
|
160
|
+
* `Tests`-anchored summary line exists, ONLY that line is parsed (the LAST one,
|
|
161
|
+
* which is the final summary after any rerun). Terse or non-vitest output with
|
|
162
|
+
* no such line falls back to scanning the whole string, which preserves the
|
|
163
|
+
* bare `"42 passed"` / `"10 passed, 5 failed"` forms.
|
|
164
|
+
*
|
|
165
|
+
* ## What `total` means — passed + failed, NOT vitest's parenthesised number
|
|
166
|
+
*
|
|
167
|
+
* `total` is `passed + failed` and deliberately EXCLUDES skipped/todo tests.
|
|
168
|
+
* This is load-bearing, not an oversight: `scripts/run-quality-gate.mjs`
|
|
169
|
+
* reconstructs the third number from the published envelope as
|
|
170
|
+
* `failed = total - passed`. Adopting vitest's parenthesised total (12915,
|
|
171
|
+
* which counts the 11 skipped) would make that derivation report 11 phantom
|
|
172
|
+
* FAILURES. `total` therefore answers "how many test cases produced a verdict",
|
|
173
|
+
* and `total - passed === failed` holds by construction for every consumer.
|
|
174
|
+
*
|
|
175
|
+
* Skipped is not returned: neither call site (`gate-full.mjs`,
|
|
176
|
+
* `quality-gate.mjs`) consumes it, and its only use would be to rebuild the
|
|
177
|
+
* parenthesised total — precisely the number the downstream derivation must not
|
|
178
|
+
* see. Add it together with a consumer, never ahead of one.
|
|
179
|
+
*
|
|
180
|
+
* @param {string} output - Captured test-runner stdout/stderr (or a tail of it).
|
|
181
|
+
* @returns {{ passed: number, failed: number, total: number }} `total === passed + failed`.
|
|
142
182
|
*/
|
|
143
183
|
export function extractTestCounts(output) {
|
|
144
184
|
if (!output) return { passed: 0, failed: 0, total: 0 };
|
|
145
185
|
|
|
146
|
-
const
|
|
147
|
-
const
|
|
186
|
+
const summaryLines = output.split('\n').filter((line) => TEST_SUMMARY_LINE.test(line));
|
|
187
|
+
const scope = summaryLines.length > 0 ? summaryLines[summaryLines.length - 1] : output;
|
|
188
|
+
|
|
189
|
+
const passMatch = scope.match(/(\d+)\s+passed/);
|
|
190
|
+
const failMatch = scope.match(/(\d+)\s+failed/);
|
|
148
191
|
|
|
149
192
|
const passed = passMatch ? parseInt(passMatch[1], 10) : 0;
|
|
150
193
|
const failed = failMatch ? parseInt(failMatch[1], 10) : 0;
|
|
@@ -153,6 +196,89 @@ export function extractTestCounts(output) {
|
|
|
153
196
|
return { passed, failed, total };
|
|
154
197
|
}
|
|
155
198
|
|
|
199
|
+
/**
|
|
200
|
+
* Admit a suite-count triple, or refuse to claim a measurement.
|
|
201
|
+
*
|
|
202
|
+
* ## Why this exists (#967 item 2)
|
|
203
|
+
*
|
|
204
|
+
* Two functions used to write the `counts` field of the SAME
|
|
205
|
+
* `orchestrator.quality_gate.*` event under DIFFERENT admission policies:
|
|
206
|
+
* `suiteCountsFromGateStdout` (`scripts/run-quality-gate.mjs`) rejected
|
|
207
|
+
* inconsistent triples, while `suiteCountsFromOutput` (`scripts/lib/quality-gate.mjs`)
|
|
208
|
+
* admitted `passed > total` and a negative `passed`. A consumer therefore had to
|
|
209
|
+
* know two policies to read one field. This is the single shared policy; both
|
|
210
|
+
* callers keep only their own input adapter.
|
|
211
|
+
*
|
|
212
|
+
* ## Absent-not-null
|
|
213
|
+
*
|
|
214
|
+
* Returns `null` — NEVER a zero triple — for any unmeasured or inconsistent
|
|
215
|
+
* input. `{passed: 0, failed: 0, total: 0}` would be indistinguishable from a
|
|
216
|
+
* real all-skipped run and would publish a phantom measurement. Callers spread
|
|
217
|
+
* the result so the field is OMITTED rather than zero-filled:
|
|
218
|
+
*
|
|
219
|
+
* ```js
|
|
220
|
+
* const counts = admitSuiteCounts(raw);
|
|
221
|
+
* await emitEvent(name, { ...(counts ? { counts } : {}) });
|
|
222
|
+
* ```
|
|
223
|
+
*
|
|
224
|
+
* ## ONE channel for "the gate did not run" — a NULL `raw` (#969 MED-2)
|
|
225
|
+
*
|
|
226
|
+
* This function used to take a second `opts.measured` channel: `false` was an
|
|
227
|
+
* unconditional refusal, meant to carry the caller's positional evidence that no
|
|
228
|
+
* test gate ran. It was dead. Both adapters ALREADY convert that evidence to a
|
|
229
|
+
* null `raw` before the policy sees it (`suiteCountsFromOutput` returns
|
|
230
|
+
* `admitSuiteCounts(null)` for a null/empty output; the CLI's
|
|
231
|
+
* `suiteCountsFromGateStdout` returns `null` from its own envelope checks and
|
|
232
|
+
* never passed the opt at all), so no production path could ever reach here with
|
|
233
|
+
* a non-null triple AND `measured: false` — only test rows exercised the flag.
|
|
234
|
+
*
|
|
235
|
+
* Keeping both was the real cost: "the gate did not run" was expressible two
|
|
236
|
+
* ways and checked in two places, so a future caller could pass a real triple
|
|
237
|
+
* with `measured: false`, or a STALE triple with `measured: true`, and the two
|
|
238
|
+
* channels would disagree with the unverifiable boolean silently winning. A
|
|
239
|
+
* null `raw` is the channel that survives because the policy must reject
|
|
240
|
+
* non-objects anyway — it is structural, not an extra parameter, and it is
|
|
241
|
+
* expressible by every caller including the one that never opted in.
|
|
242
|
+
*
|
|
243
|
+
* ## What stays with the CALLER (deliberately not absorbed)
|
|
244
|
+
*
|
|
245
|
+
* This function sees a candidate triple and nothing else. It performs no I/O and
|
|
246
|
+
* reads no files. The following are per-caller INPUT ADAPTERS and must not
|
|
247
|
+
* migrate here: JSON-envelope parsing; the `test`-object-vs-status-string
|
|
248
|
+
* variant discrimination (non-full-gate variants emit a bare status string);
|
|
249
|
+
* `test.status ∈ {pass, fail}`; the `parsed.stubbed?.test` short-circuit; the
|
|
250
|
+
* raw-text tail parse ({@link extractTestCounts}); and the positional evidence
|
|
251
|
+
* that the test gate ran at all — which each caller expresses by handing over
|
|
252
|
+
* `null` rather than a triple.
|
|
253
|
+
*
|
|
254
|
+
* `failed` is accepted when the caller parsed one and DERIVED as `total - passed`
|
|
255
|
+
* when it did not, so both the parsed-`failed` path and the derived-`failed` path
|
|
256
|
+
* land on one consistency check (`passed + failed === total`).
|
|
257
|
+
*
|
|
258
|
+
* @param {{passed?: unknown, failed?: unknown, total?: unknown}|null|undefined} raw
|
|
259
|
+
* `null`/`undefined` is the caller's evidence that there is no measurement to
|
|
260
|
+
* admit (gate skipped, stub command, fail-fast before the test step, or no
|
|
261
|
+
* parseable count in the output).
|
|
262
|
+
* @returns {{passed: number, failed: number, total: number}|null}
|
|
263
|
+
*/
|
|
264
|
+
export function admitSuiteCounts(raw) {
|
|
265
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) return null;
|
|
266
|
+
|
|
267
|
+
const { passed, total } = raw;
|
|
268
|
+
if (!Number.isFinite(passed) || !Number.isFinite(total)) return null;
|
|
269
|
+
|
|
270
|
+
const failed = Number.isFinite(raw.failed) ? raw.failed : total - passed;
|
|
271
|
+
|
|
272
|
+
if (total <= 0) return null;
|
|
273
|
+
if (passed < 0 || failed < 0) return null;
|
|
274
|
+
// Redundant given the sum check below while `failed >= 0`, but kept explicit:
|
|
275
|
+
// it is the check the looser of the two former policies was missing.
|
|
276
|
+
if (passed > total) return null;
|
|
277
|
+
if (passed + failed !== total) return null;
|
|
278
|
+
|
|
279
|
+
return { passed, failed, total };
|
|
280
|
+
}
|
|
281
|
+
|
|
156
282
|
/**
|
|
157
283
|
* Scan changed files (since `ref`) for debug artifacts: `console.log`, `debugger`, `TODO`, `FIXME`.
|
|
158
284
|
*
|
|
@@ -23,6 +23,7 @@ import { execFile as execFileCb } from 'node:child_process';
|
|
|
23
23
|
import { promisify } from 'node:util';
|
|
24
24
|
|
|
25
25
|
import { detectVcsForRepo, discoverVaultRepos } from '../gitlab-portfolio/vcs-detect.mjs';
|
|
26
|
+
import { resolveRepoSpec } from '../vcs-repo-spec.mjs';
|
|
26
27
|
|
|
27
28
|
const realExecFile = promisify(execFileCb);
|
|
28
29
|
|
|
@@ -94,22 +95,31 @@ export function filterStaleMRs(mrs, opts = {}) {
|
|
|
94
95
|
* 2. `detectVcsForRepo({ repo })` if `opts.repo` is provided
|
|
95
96
|
* 3. 'gitlab' (cross-project default per CLAUDE.md / AGENTS.md Session Config `vcs: gitlab`) —
|
|
96
97
|
* only reached when neither vcs nor repo is provided, i.e. the caller
|
|
97
|
-
* intends "the repo at `repoRoot`"
|
|
98
|
-
*
|
|
98
|
+
* intends "the repo at `repoRoot`".
|
|
99
|
+
*
|
|
100
|
+
* `--repo` host-pinning (#872): a bare `glab`/`gh` spawn (no `--repo`) falls
|
|
101
|
+
* back to the ambient `GITLAB_HOST`/`GH_HOST` env var to pick a host, which
|
|
102
|
+
* can silently target the WRONG instance on a multi-instance host — `cwd:
|
|
103
|
+
* repoRoot` alone does not fix this (ambient env still wins over cwd). When
|
|
104
|
+
* `opts.repo` is not explicitly provided, this function auto-detects a
|
|
105
|
+
* `--repo` spec via `resolveRepoSpecFn` (default: the real `resolveRepoSpec`
|
|
106
|
+
* from `vcs-repo-spec.mjs`, which shells out to `git remote get-url`).
|
|
107
|
+
* Explicit `opts.repo` always wins over the auto-detected spec.
|
|
99
108
|
*
|
|
100
109
|
* Never throws: CLI failure (missing binary, non-zero exit, timeout) and
|
|
101
110
|
* malformed JSON output both resolve to a graceful `{ ok: false, error, stale: [], total: 0 }`
|
|
102
111
|
* result plus a diagnostic line on stderr — never an unhandled rejection.
|
|
103
112
|
*
|
|
104
113
|
* @param {object} [opts]
|
|
105
|
-
* @param {string} [opts.repoRoot=process.cwd()] - cwd for the CLI invocation (
|
|
106
|
-
* @param {string} [opts.repo] - explicit "owner/repo" or GitLab path identifier; when set, passed via `--repo`
|
|
114
|
+
* @param {string} [opts.repoRoot=process.cwd()] - cwd for the CLI invocation (also passed to resolveRepoSpecFn for remote auto-detection)
|
|
115
|
+
* @param {string} [opts.repo] - explicit "owner/repo" or GitLab path identifier; when set, passed via `--repo` and skips auto-detection
|
|
107
116
|
* @param {'gitlab'|'github'} [opts.vcs] - explicit VCS override (skips detectVcsForRepo)
|
|
108
117
|
* @param {number} [opts.thresholdDays=14]
|
|
109
118
|
* @param {'created'|'updated'} [opts.field='updated']
|
|
110
119
|
* @param {Function} [opts.exec] - injectable execFile-like function `(cmd, args, options) => Promise<{stdout, stderr}>`; defaults to the real promisified execFile
|
|
111
120
|
* @param {number} [opts.now=Date.now()]
|
|
112
121
|
* @param {number} [opts.timeoutMs=8000]
|
|
122
|
+
* @param {(o: { repoRoot: string, vcs: 'gitlab'|'github' }) => string|undefined} [opts.resolveRepoSpecFn] - injectable `--repo` auto-detector; defaults to the real `resolveRepoSpec`
|
|
113
123
|
* @returns {Promise<
|
|
114
124
|
* { ok: true, repo: string, vcs: 'gitlab'|'github', total: number, stale: Array<object> } |
|
|
115
125
|
* { ok: false, error: string, repo: string|null, vcs: 'gitlab'|'github'|null, total: 0, stale: [] }
|
|
@@ -125,6 +135,7 @@ export async function findStaleMRs(opts = {}) {
|
|
|
125
135
|
exec = realExecFile,
|
|
126
136
|
now = Date.now(),
|
|
127
137
|
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
138
|
+
resolveRepoSpecFn = resolveRepoSpec,
|
|
128
139
|
} = opts;
|
|
129
140
|
|
|
130
141
|
const resolvedVcs = vcsOverride ?? (repo ? detectVcsForRepo({ repo }) : 'gitlab');
|
|
@@ -144,11 +155,15 @@ export async function findStaleMRs(opts = {}) {
|
|
|
144
155
|
cmd = 'gh';
|
|
145
156
|
args = ['pr', 'list', '--state', 'open', '--json', GH_PR_JSON_FIELDS];
|
|
146
157
|
}
|
|
147
|
-
|
|
148
|
-
|
|
158
|
+
|
|
159
|
+
// #872: explicit repo always wins; otherwise auto-detect a --repo spec
|
|
160
|
+
// from the local git remotes (host-pinning — see docblock above).
|
|
161
|
+
const effectiveRepo = repo ?? resolveRepoSpecFn({ repoRoot, vcs: resolvedVcs });
|
|
162
|
+
if (effectiveRepo) {
|
|
163
|
+
args.push('--repo', effectiveRepo);
|
|
149
164
|
}
|
|
150
165
|
|
|
151
|
-
const repoLabel =
|
|
166
|
+
const repoLabel = effectiveRepo ?? repoRoot;
|
|
152
167
|
|
|
153
168
|
let stdout;
|
|
154
169
|
try {
|
|
@@ -209,6 +224,7 @@ export async function findStaleMRs(opts = {}) {
|
|
|
209
224
|
* @param {number} [opts.now=Date.now()]
|
|
210
225
|
* @param {number} [opts.timeoutMs=8000]
|
|
211
226
|
* @param {typeof discoverVaultRepos} [opts.discoverRepos] - injectable for tests
|
|
227
|
+
* @param {(o: { repoRoot: string, vcs: 'gitlab'|'github' }) => string|undefined} [opts.resolveRepoSpecFn] - passed through to each per-repo `findStaleMRs` call; unused in practice since discovered repos always carry an explicit `repo`
|
|
212
228
|
* @returns {Promise<Array<{ slug: string } & Awaited<ReturnType<typeof findStaleMRs>>>>}
|
|
213
229
|
*/
|
|
214
230
|
export async function findStaleMRsMultiRepo(opts = {}) {
|
|
@@ -220,6 +236,7 @@ export async function findStaleMRsMultiRepo(opts = {}) {
|
|
|
220
236
|
now = Date.now(),
|
|
221
237
|
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
222
238
|
discoverRepos = discoverVaultRepos,
|
|
239
|
+
resolveRepoSpecFn = resolveRepoSpec,
|
|
223
240
|
} = opts;
|
|
224
241
|
|
|
225
242
|
const repos = await discoverRepos({ vaultDir });
|
|
@@ -227,7 +244,7 @@ export async function findStaleMRsMultiRepo(opts = {}) {
|
|
|
227
244
|
|
|
228
245
|
const results = await Promise.all(
|
|
229
246
|
repos.map((r) =>
|
|
230
|
-
findStaleMRs({ repo: r.repo, vcs: r.vcs, thresholdDays, field, exec, now, timeoutMs }),
|
|
247
|
+
findStaleMRs({ repo: r.repo, vcs: r.vcs, thresholdDays, field, exec, now, timeoutMs, resolveRepoSpecFn }),
|
|
231
248
|
),
|
|
232
249
|
);
|
|
233
250
|
|
|
@@ -362,6 +379,7 @@ export async function main(argv, deps = {}) {
|
|
|
362
379
|
repoRoot = process.cwd(),
|
|
363
380
|
discoverRepos = discoverVaultRepos,
|
|
364
381
|
homedir = () => process.env.HOME ?? '',
|
|
382
|
+
resolveRepoSpecFn = resolveRepoSpec,
|
|
365
383
|
} = deps;
|
|
366
384
|
|
|
367
385
|
const flags = parseArgs(argv);
|
|
@@ -386,6 +404,7 @@ export async function main(argv, deps = {}) {
|
|
|
386
404
|
exec,
|
|
387
405
|
now,
|
|
388
406
|
discoverRepos,
|
|
407
|
+
resolveRepoSpecFn,
|
|
389
408
|
});
|
|
390
409
|
|
|
391
410
|
if (flags.json) {
|
|
@@ -410,6 +429,7 @@ export async function main(argv, deps = {}) {
|
|
|
410
429
|
field: flags.field,
|
|
411
430
|
exec,
|
|
412
431
|
now,
|
|
432
|
+
resolveRepoSpecFn,
|
|
413
433
|
});
|
|
414
434
|
|
|
415
435
|
if (!result.ok) {
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
|
|
13
13
|
import { spawn as _spawn } from 'node:child_process';
|
|
14
14
|
|
|
15
|
+
import { normalizeLabel, normalizedLabelSet } from '../label-scope.mjs';
|
|
16
|
+
|
|
15
17
|
/** Default timeout per repo CLI invocation (ms). */
|
|
16
18
|
export const DEFAULT_TIMEOUT_MS = 15_000;
|
|
17
19
|
|
|
@@ -332,7 +334,11 @@ export function summarizeRepo(issues, opts) {
|
|
|
332
334
|
|
|
333
335
|
const nowMs = now instanceof Date ? now.getTime() : Date.now();
|
|
334
336
|
const staleThresholdMs = staleDays * 86_400_000;
|
|
335
|
-
|
|
337
|
+
// Scope-tolerant matching: defaults use the canonical scoped spelling
|
|
338
|
+
// (`priority::critical`), but issues labelled before the migration still
|
|
339
|
+
// carry `priority:critical`. Normalising both sides keeps those counted —
|
|
340
|
+
// see scripts/lib/label-scope.mjs.
|
|
341
|
+
const criticalSet = normalizedLabelSet(criticalLabels);
|
|
336
342
|
|
|
337
343
|
let criticalCount = 0;
|
|
338
344
|
let staleCount = 0;
|
|
@@ -341,7 +347,7 @@ export function summarizeRepo(issues, opts) {
|
|
|
341
347
|
|
|
342
348
|
for (const issue of issues) {
|
|
343
349
|
// Critical
|
|
344
|
-
if (issue.labels.some((l) => criticalSet.has(l))) {
|
|
350
|
+
if (issue.labels.some((l) => criticalSet.has(normalizeLabel(l)))) {
|
|
345
351
|
criticalCount++;
|
|
346
352
|
}
|
|
347
353
|
|
|
@@ -289,7 +289,7 @@ EXIT CODES
|
|
|
289
289
|
|
|
290
290
|
// ── Fetch issues in parallel ───────────────────────────────────────────────
|
|
291
291
|
const staleDays = portfolioConfig['stale-days'] ?? 30;
|
|
292
|
-
const criticalLabels = portfolioConfig['critical-labels'] ?? ['priority
|
|
292
|
+
const criticalLabels = portfolioConfig['critical-labels'] ?? ['priority::critical', 'priority::high'];
|
|
293
293
|
const mode = portfolioConfig.mode ?? 'warn';
|
|
294
294
|
|
|
295
295
|
let resultsMap;
|
|
@@ -104,8 +104,12 @@ function coerceOriginIssue(v) {
|
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
/**
|
|
107
|
-
* Coerce a priority field: lowercase, trim, strip a leading `priority
|
|
108
|
-
*
|
|
107
|
+
* Coerce a priority field: lowercase, trim, strip a leading `priority::` (the
|
|
108
|
+
* canonical scoped form) or legacy `priority:` label prefix, validate against
|
|
109
|
+
* the closed enum. Anything invalid → null.
|
|
110
|
+
*
|
|
111
|
+
* Both spellings are accepted on INPUT because the label-data migration to the
|
|
112
|
+
* scoped form trails the producer migration — see `scripts/lib/label-scope.mjs`.
|
|
109
113
|
*
|
|
110
114
|
* @param {unknown} v
|
|
111
115
|
* @returns {'critical'|'high'|'medium'|'low'|null}
|
|
@@ -113,7 +117,7 @@ function coerceOriginIssue(v) {
|
|
|
113
117
|
function coercePriority(v) {
|
|
114
118
|
if (v === null || v === undefined) return null;
|
|
115
119
|
let s = String(v).trim().toLowerCase();
|
|
116
|
-
|
|
120
|
+
s = s.replace(/^priority::?/, '').trim();
|
|
117
121
|
return VALID_PRIORITIES.has(s) ? /** @type {any} */ (s) : null;
|
|
118
122
|
}
|
|
119
123
|
|
|
@@ -11,6 +11,8 @@ import { existsSync } from 'node:fs';
|
|
|
11
11
|
import { join, relative, sep } from 'node:path';
|
|
12
12
|
|
|
13
13
|
import { parseFrontmatter, safeRead, parseJsonl, pass, fail } from './helpers.mjs';
|
|
14
|
+
import { isRealSession } from '../../session-schema/filters.mjs';
|
|
15
|
+
import { findSessionConfigBlock } from '../../config/section-extractor.mjs';
|
|
14
16
|
|
|
15
17
|
// Default session-lock TTL in hours — mirrors DEFAULT_TTL_HOURS in
|
|
16
18
|
// scripts/lib/session-lock.mjs. Inlined to keep this category stdlib-only
|
|
@@ -93,9 +95,19 @@ export function runCategory4(root) {
|
|
|
93
95
|
{ latestCompletedAt: null, ageInDays: null },
|
|
94
96
|
'sessions.jsonl is empty'));
|
|
95
97
|
} else {
|
|
96
|
-
|
|
98
|
+
// Scan backward past any trailing `status: 'abandoned'` phantom stubs
|
|
99
|
+
// (#834, session-close-backfill) to the last REAL session — otherwise a
|
|
100
|
+
// recent phantom lets a dormant repo pass a check meant to certify
|
|
101
|
+
// recent REAL engagement.
|
|
97
102
|
let lastObj = null;
|
|
98
|
-
|
|
103
|
+
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
104
|
+
let parsed = null;
|
|
105
|
+
try { parsed = JSON.parse(lines[i]); } catch { /* ignore */ }
|
|
106
|
+
if (parsed && isRealSession(parsed)) {
|
|
107
|
+
lastObj = parsed;
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
99
111
|
const completedAt = lastObj ? lastObj.completed_at : null;
|
|
100
112
|
if (!completedAt) {
|
|
101
113
|
checks.push(fail('sessions-jsonl-recent', 3, relPath,
|
|
@@ -148,9 +160,14 @@ export function runCategory4(root) {
|
|
|
148
160
|
const relPath = 'skills/vault-sync/validator.mjs';
|
|
149
161
|
// Parse CLAUDE.md Session Config for vault-integration.enabled
|
|
150
162
|
const claudeMd = safeRead(join(root, 'CLAUDE.md')) || safeRead(join(root, 'AGENTS.md')) || '';
|
|
151
|
-
// Extract ## Session Config block
|
|
152
|
-
|
|
153
|
-
|
|
163
|
+
// Extract ## Session Config block via the SSOT (#968). The previous local
|
|
164
|
+
// regex `/^## Session Config\s*\n([\s\S]*?)(?=^## |\s*$)/m` was not merely
|
|
165
|
+
// a looser heading test — its `\s*$` lookahead (with /m, `$` matches at
|
|
166
|
+
// every line end) truncated the body to the block's FIRST line. On this
|
|
167
|
+
// repo's own CLAUDE.md it captured 17 chars ("persistence: true") of 2877,
|
|
168
|
+
// so `vault-integration.enabled: true` was invisible and this check
|
|
169
|
+
// reported "not enabled — skip" at full 2/2 credit.
|
|
170
|
+
const scBlock = findSessionConfigBlock(claudeMd)?.body ?? '';
|
|
154
171
|
const vaultEnabledMatch = /vault-integration\.enabled:\s*true/i.exec(scBlock) ||
|
|
155
172
|
/vault-integration:\s*\n\s+enabled:\s*true/im.exec(scBlock);
|
|
156
173
|
const vaultEnabled = Boolean(vaultEnabledMatch);
|