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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { matchBlockHeader } from './block-header.mjs';
|
|
2
|
+
import { isSessionConfigHeading } from './section-extractor.mjs';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* config-protection.mjs — Parser for the top-level `config-protection:` YAML
|
|
@@ -109,7 +110,7 @@ export function _isConfigWeakeningAllowed(content) {
|
|
|
109
110
|
const lines = content.split(/\r?\n/);
|
|
110
111
|
let inConfig = false;
|
|
111
112
|
for (const line of lines) {
|
|
112
|
-
if (line
|
|
113
|
+
if (isSessionConfigHeading(line)) { inConfig = true; continue; }
|
|
113
114
|
if (inConfig && /^## /.test(line)) break;
|
|
114
115
|
if (inConfig) {
|
|
115
116
|
const m = line.match(/^\s*(?:-\s+\*\*)?allow-config-weakening(?::\*\*)?\s*:\s*(\S+)/);
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { matchBlockHeader } from './block-header.mjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* context-coverage.mjs — Parser for the top-level `context-coverage:` YAML block.
|
|
5
|
+
*
|
|
6
|
+
* Config block shape (see docs/session-config-template.md):
|
|
7
|
+
* context-coverage:
|
|
8
|
+
* enabled: false
|
|
9
|
+
* mode: warn
|
|
10
|
+
*
|
|
11
|
+
* Modelled on `./docs-staleness.mjs`'s parser design (issue #781, Epic #774) —
|
|
12
|
+
* the block is scoped from the raw file content, independent of the
|
|
13
|
+
* `## Session Config` section boundary, so a baseline using either the plain
|
|
14
|
+
* `key:` header or the bold-bullet markdown rendering
|
|
15
|
+
* (`- **context-coverage:**`, tolerated via `matchBlockHeader()`) parses
|
|
16
|
+
* identically.
|
|
17
|
+
*
|
|
18
|
+
* ZERO imports other than `./block-header.mjs` — `tests/lib/config/cycle-guard.test.mjs`
|
|
19
|
+
* forbids importing `../config.mjs` from this directory; this module keeps a
|
|
20
|
+
* clean leaf with no other dependencies at all.
|
|
21
|
+
*
|
|
22
|
+
* Not registered in `scripts/lib/config.mjs` here — the coordinator wires
|
|
23
|
+
* `_parseContextCoverage` into the orchestrator's config-object assembly
|
|
24
|
+
* separately (see `context-coverage-banner.mjs`'s header for the exact
|
|
25
|
+
* import + call lines to add there).
|
|
26
|
+
*
|
|
27
|
+
* Shipped default: `{ enabled: false, mode: 'warn' }` — opt-in (issue #831).
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Parse the top-level `context-coverage:` YAML block from markdown content.
|
|
32
|
+
* Defaults: enabled=false, mode="warn". Malformed values fall back per key.
|
|
33
|
+
*
|
|
34
|
+
* @param {string} content — full file contents
|
|
35
|
+
* @returns {{enabled: boolean, mode: string}}
|
|
36
|
+
*/
|
|
37
|
+
export function _parseContextCoverage(content) {
|
|
38
|
+
const defaults = { enabled: false, mode: 'warn' };
|
|
39
|
+
|
|
40
|
+
const lines = content.split(/\r?\n/);
|
|
41
|
+
let inBlock = false;
|
|
42
|
+
const blockLines = [];
|
|
43
|
+
|
|
44
|
+
for (const rawLine of lines) {
|
|
45
|
+
const line = rawLine.replace(/\r$/, '');
|
|
46
|
+
if (!inBlock) {
|
|
47
|
+
if (matchBlockHeader(line, 'context-coverage')) inBlock = true;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (line.length > 0 && !/^\s/.test(line)) break;
|
|
51
|
+
blockLines.push(line);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (blockLines.length === 0) return defaults;
|
|
55
|
+
|
|
56
|
+
let ccEnabled = false;
|
|
57
|
+
let ccMode = 'warn';
|
|
58
|
+
|
|
59
|
+
for (const rawLine of blockLines) {
|
|
60
|
+
const clean = rawLine.replace(/\s*#.*$/, '').replace(/\s+$/, '');
|
|
61
|
+
if (!clean.trim()) continue;
|
|
62
|
+
|
|
63
|
+
const kvMatch = clean.match(/^\s+([a-zA-Z_-]+):\s*(.*)/);
|
|
64
|
+
if (!kvMatch) continue;
|
|
65
|
+
|
|
66
|
+
const k = kvMatch[1];
|
|
67
|
+
let v = kvMatch[2].trim();
|
|
68
|
+
if (v.startsWith('"') && v.endsWith('"') && v.length >= 2) v = v.slice(1, -1);
|
|
69
|
+
else if (v.startsWith("'") && v.endsWith("'") && v.length >= 2) v = v.slice(1, -1);
|
|
70
|
+
|
|
71
|
+
switch (k) {
|
|
72
|
+
case 'enabled':
|
|
73
|
+
ccEnabled = v.toLowerCase() === 'true';
|
|
74
|
+
break;
|
|
75
|
+
case 'mode':
|
|
76
|
+
if (['strict', 'warn', 'off'].includes(v)) ccMode = v;
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return { enabled: ccEnabled, mode: ccMode };
|
|
82
|
+
}
|
|
@@ -99,7 +99,15 @@ export function _parseDriftCheck(content) {
|
|
|
99
99
|
dcEnabled = v.toLowerCase() === 'true';
|
|
100
100
|
break;
|
|
101
101
|
case 'mode':
|
|
102
|
-
|
|
102
|
+
// Enum: strict|warn|off. `hard` is a legacy alias for `strict`,
|
|
103
|
+
// normalized here. This is the drift-check half of the #217 enum
|
|
104
|
+
// migration (vault-sync moved hard→strict on 2026-04-21; drift-check
|
|
105
|
+
// was forgotten, so a committed `mode: strict` silently fell back to
|
|
106
|
+
// the `warn` default and the narrative-drift gate ran unenforced).
|
|
107
|
+
// Invalid values still fall back to the 'warn' default below.
|
|
108
|
+
if (['strict', 'hard', 'warn', 'off'].includes(v)) {
|
|
109
|
+
dcMode = v === 'hard' ? 'strict' : v;
|
|
110
|
+
}
|
|
103
111
|
break;
|
|
104
112
|
case 'include-paths':
|
|
105
113
|
if (!v) inIncludeList = true;
|
|
@@ -9,7 +9,7 @@ export const GITLAB_PORTFOLIO_DEFAULTS = {
|
|
|
9
9
|
enabled: false,
|
|
10
10
|
mode: 'warn',
|
|
11
11
|
'stale-days': 30,
|
|
12
|
-
'critical-labels': ['priority
|
|
12
|
+
'critical-labels': ['priority::critical', 'priority::high'],
|
|
13
13
|
};
|
|
14
14
|
|
|
15
15
|
const VALID_MODES = new Set(['warn', 'strict', 'off']);
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { matchBlockHeader } from './block-header.mjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* issue-budget.mjs — Parser for the `issue-budget:` block-style Session Config key.
|
|
5
|
+
*
|
|
6
|
+
* Bounds how many issues ONE session may create. Motivation (measured on the
|
|
7
|
+
* private instance over four weeks): 1784 issues created against 1285 closed
|
|
8
|
+
* (net +499), median inter-creation gap 2.6 s, 62 % of creations inside bursts
|
|
9
|
+
* of >= 5 per minute. The pre-existing `discovery-confidence-threshold` /
|
|
10
|
+
* `discovery-severity-threshold` knobs do NOT bound this: they are per-finding
|
|
11
|
+
* QUALITY filters (and the `low` default filters nothing), they are only read
|
|
12
|
+
* in skill prose, and the largest producers never consult them at all.
|
|
13
|
+
*
|
|
14
|
+
* This key is a QUANTITY cap, enforced mechanically by
|
|
15
|
+
* `hooks/pre-bash-issue-budget.mjs` (shell path) and
|
|
16
|
+
* `scripts/lib/spiral-carryover.mjs` `runCli()` (programmatic path), both via
|
|
17
|
+
* `scripts/lib/issue-budget.mjs`.
|
|
18
|
+
*
|
|
19
|
+
* Returns `{ "max-per-session", mode, overflow }`.
|
|
20
|
+
* Tolerant parser: malformed values silently fall back to defaults (the
|
|
21
|
+
* `reconcile.min-rule-days` posture, NOT the noisier handover-gate WARN) —
|
|
22
|
+
* a hook must never spam stderr on every single Bash call.
|
|
23
|
+
*
|
|
24
|
+
* Consumers: `scripts/lib/config.mjs`, `scripts/lib/issue-budget.mjs`,
|
|
25
|
+
* `hooks/pre-bash-issue-budget.mjs`, `skills/session-end/SKILL.md` Phase 5.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/** Valid `mode` values. */
|
|
29
|
+
const MODES = ['strict', 'warn', 'off'];
|
|
30
|
+
|
|
31
|
+
/** Valid `overflow` values. */
|
|
32
|
+
const OVERFLOW_SINKS = ['collect-issue', 'vault-note'];
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Parse the `issue-budget:` block from markdown content.
|
|
36
|
+
*
|
|
37
|
+
* Like every sibling block parser this scans the FULL content rather than the
|
|
38
|
+
* `## Session Config` fence, so the block resolves identically whether it is
|
|
39
|
+
* nested inside `## Session Config` (the documented home) or lifted to a
|
|
40
|
+
* top-level block. Bold-bullet headers (`- **issue-budget:**`) are tolerated
|
|
41
|
+
* via the shared `matchBlockHeader` (#830).
|
|
42
|
+
*
|
|
43
|
+
* Defaults:
|
|
44
|
+
* max-per-session: 12 integer >= 0; 0 means "no issue may be created"
|
|
45
|
+
* (a valid, deliberately harsh setting). Malformed
|
|
46
|
+
* or negative input falls back to 12.
|
|
47
|
+
* mode: strict strict | warn | off
|
|
48
|
+
* overflow: collect-issue collect-issue | vault-note
|
|
49
|
+
*
|
|
50
|
+
* YAML shape:
|
|
51
|
+
* issue-budget:
|
|
52
|
+
* max-per-session: 12
|
|
53
|
+
* mode: strict
|
|
54
|
+
* overflow: collect-issue
|
|
55
|
+
*
|
|
56
|
+
* @param {string} content — full file contents
|
|
57
|
+
* @returns {{ "max-per-session": number, mode: string, overflow: string }}
|
|
58
|
+
*/
|
|
59
|
+
export function _parseIssueBudget(content) {
|
|
60
|
+
const defaults = {
|
|
61
|
+
'max-per-session': 12,
|
|
62
|
+
mode: 'strict',
|
|
63
|
+
overflow: 'collect-issue',
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
if (typeof content !== 'string' || content === '') return { ...defaults };
|
|
67
|
+
|
|
68
|
+
const lines = content.split(/\r?\n/);
|
|
69
|
+
let inBlock = false;
|
|
70
|
+
const blockLines = [];
|
|
71
|
+
|
|
72
|
+
for (const rawLine of lines) {
|
|
73
|
+
const line = rawLine.replace(/\r$/, '');
|
|
74
|
+
if (!inBlock) {
|
|
75
|
+
if (matchBlockHeader(line, 'issue-budget')) inBlock = true;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
// Stop at next column-0 non-empty line (sibling top-level key or H2 heading)
|
|
79
|
+
if (line.length > 0 && !/^\s/.test(line)) break;
|
|
80
|
+
blockLines.push(line);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (blockLines.length === 0) return { ...defaults };
|
|
84
|
+
|
|
85
|
+
let maxPerSession = defaults['max-per-session'];
|
|
86
|
+
let mode = defaults.mode;
|
|
87
|
+
let overflow = defaults.overflow;
|
|
88
|
+
|
|
89
|
+
for (const rawLine of blockLines) {
|
|
90
|
+
// Strip inline comments and trailing whitespace
|
|
91
|
+
const clean = rawLine.replace(/\s*#.*$/, '').replace(/\s+$/, '');
|
|
92
|
+
if (!clean.trim()) continue;
|
|
93
|
+
|
|
94
|
+
const kvMatch = clean.match(/^\s+([a-zA-Z_-]+):\s*(.*)/);
|
|
95
|
+
if (!kvMatch) continue;
|
|
96
|
+
|
|
97
|
+
const k = kvMatch[1];
|
|
98
|
+
let v = kvMatch[2].trim();
|
|
99
|
+
if (v.startsWith('"') && v.endsWith('"') && v.length >= 2) v = v.slice(1, -1);
|
|
100
|
+
else if (v.startsWith("'") && v.endsWith("'") && v.length >= 2) v = v.slice(1, -1);
|
|
101
|
+
|
|
102
|
+
switch (k) {
|
|
103
|
+
case 'max-per-session':
|
|
104
|
+
// Non-negative integer only; a leading '-' fails \d+ → default.
|
|
105
|
+
if (/^\d+$/.test(v)) maxPerSession = parseInt(v, 10);
|
|
106
|
+
break;
|
|
107
|
+
|
|
108
|
+
case 'mode':
|
|
109
|
+
if (MODES.includes(v.toLowerCase())) mode = v.toLowerCase();
|
|
110
|
+
break;
|
|
111
|
+
|
|
112
|
+
case 'overflow':
|
|
113
|
+
if (OVERFLOW_SINKS.includes(v.toLowerCase())) overflow = v.toLowerCase();
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
'max-per-session': maxPerSession,
|
|
120
|
+
mode,
|
|
121
|
+
overflow,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { matchBlockHeader } from './block-header.mjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* moc-staleness.mjs — Parser for the top-level `moc-staleness:` YAML block.
|
|
5
|
+
*
|
|
6
|
+
* Config block shape (see docs/session-config-template.md):
|
|
7
|
+
* moc-staleness:
|
|
8
|
+
* enabled: false
|
|
9
|
+
* thresholds:
|
|
10
|
+
* moc: 90
|
|
11
|
+
* mode: warn
|
|
12
|
+
*
|
|
13
|
+
* Mirrors the docs-staleness.mjs parser design (issue #831) — a single
|
|
14
|
+
* "moc" tier threshold (days), not a per-tier map like vault-staleness.mjs.
|
|
15
|
+
*
|
|
16
|
+
* ZERO IMPORTS other than ./block-header.mjs by design:
|
|
17
|
+
* tests/lib/config/cycle-guard.test.mjs forbids any scripts/lib/config/*.mjs
|
|
18
|
+
* from importing ../config.mjs.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Parse the top-level `moc-staleness:` YAML block from markdown content.
|
|
23
|
+
* Defaults: enabled=false, thresholds={moc:90}, mode="warn".
|
|
24
|
+
* @param {string} content — full file contents
|
|
25
|
+
* @returns {{enabled: boolean, thresholds: {moc: number}, mode: string}}
|
|
26
|
+
*/
|
|
27
|
+
export function _parseMocStaleness(content) {
|
|
28
|
+
const defaults = {
|
|
29
|
+
enabled: false,
|
|
30
|
+
thresholds: { moc: 90 },
|
|
31
|
+
mode: 'warn',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const lines = content.split(/\r?\n/);
|
|
35
|
+
let inBlock = false;
|
|
36
|
+
const blockLines = [];
|
|
37
|
+
|
|
38
|
+
for (const rawLine of lines) {
|
|
39
|
+
const line = rawLine.replace(/\r$/, '');
|
|
40
|
+
if (!inBlock) {
|
|
41
|
+
if (matchBlockHeader(line, 'moc-staleness')) inBlock = true;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (line.length > 0 && !/^\s/.test(line)) break;
|
|
45
|
+
blockLines.push(line);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (blockLines.length === 0) return defaults;
|
|
49
|
+
|
|
50
|
+
let msEnabled = false;
|
|
51
|
+
let msMode = 'warn';
|
|
52
|
+
const msThresholds = { moc: 90 };
|
|
53
|
+
let inThresholdsBlock = false;
|
|
54
|
+
|
|
55
|
+
for (const rawLine of blockLines) {
|
|
56
|
+
const clean = rawLine.replace(/\s*#.*$/, '').replace(/\s+$/, '');
|
|
57
|
+
if (!clean.trim()) continue;
|
|
58
|
+
|
|
59
|
+
// Deeper indented key (thresholds sub-keys)
|
|
60
|
+
const deepMatch = clean.match(/^\s{4,}([a-zA-Z_-]+):\s*(.*)/);
|
|
61
|
+
if (deepMatch && inThresholdsBlock) {
|
|
62
|
+
const k = deepMatch[1];
|
|
63
|
+
let v = deepMatch[2].trim();
|
|
64
|
+
if (v.startsWith('"') && v.endsWith('"') && v.length >= 2) v = v.slice(1, -1);
|
|
65
|
+
else if (v.startsWith("'") && v.endsWith("'") && v.length >= 2) v = v.slice(1, -1);
|
|
66
|
+
if (k === 'moc') {
|
|
67
|
+
const n = parseFloat(v);
|
|
68
|
+
if (Number.isFinite(n) && n > 0) msThresholds[k] = n;
|
|
69
|
+
}
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Top-level key under moc-staleness (2-space indent)
|
|
74
|
+
const kvMatch = clean.match(/^\s+([a-zA-Z_-]+):\s*(.*)/);
|
|
75
|
+
if (!kvMatch) continue;
|
|
76
|
+
|
|
77
|
+
inThresholdsBlock = false;
|
|
78
|
+
|
|
79
|
+
const k = kvMatch[1];
|
|
80
|
+
let v = kvMatch[2].trim();
|
|
81
|
+
if (v.startsWith('"') && v.endsWith('"') && v.length >= 2) v = v.slice(1, -1);
|
|
82
|
+
else if (v.startsWith("'") && v.endsWith("'") && v.length >= 2) v = v.slice(1, -1);
|
|
83
|
+
|
|
84
|
+
switch (k) {
|
|
85
|
+
case 'enabled':
|
|
86
|
+
msEnabled = v.toLowerCase() === 'true';
|
|
87
|
+
break;
|
|
88
|
+
case 'mode':
|
|
89
|
+
if (['strict', 'warn', 'off'].includes(v)) msMode = v;
|
|
90
|
+
break;
|
|
91
|
+
case 'thresholds':
|
|
92
|
+
inThresholdsBlock = true;
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return { enabled: msEnabled, thresholds: msThresholds, mode: msMode };
|
|
98
|
+
}
|
|
@@ -16,6 +16,7 @@ import { matchBlockHeader } from './block-header.mjs';
|
|
|
16
16
|
* 'confidence-floor': number,
|
|
17
17
|
* 'min-rule-days': number,
|
|
18
18
|
* 'min-insight-chars': number,
|
|
19
|
+
* 'max-proposals-per-run': number,
|
|
19
20
|
* }
|
|
20
21
|
*
|
|
21
22
|
* Tolerant parser: malformed values silently fall back to defaults.
|
|
@@ -51,6 +52,12 @@ import { matchBlockHeader } from './block-header.mjs';
|
|
|
51
52
|
* gating the eligibility placeholder-insight check
|
|
52
53
|
* (issue #741.2). Malformed, absent, or negative
|
|
53
54
|
* values fall back to 24.
|
|
55
|
+
* reconcile.max-proposals-per-run: 10 — positive integer (min 1); volume brake (issue
|
|
56
|
+
* #900 D) — the reconcile engine sorts eligible
|
|
57
|
+
* learnings by confidence DESC and proposes at
|
|
58
|
+
* most this many per run, recording the cut count
|
|
59
|
+
* in the run summary (`summary.capped`). Malformed,
|
|
60
|
+
* absent, or non-positive values fall back to 10.
|
|
54
61
|
*
|
|
55
62
|
* YAML shape:
|
|
56
63
|
* reconcile:
|
|
@@ -61,6 +68,7 @@ import { matchBlockHeader } from './block-header.mjs';
|
|
|
61
68
|
* confidence-floor: 0.5
|
|
62
69
|
* min-rule-days: 7
|
|
63
70
|
* min-insight-chars: 24
|
|
71
|
+
* max-proposals-per-run: 10
|
|
64
72
|
*
|
|
65
73
|
* @param {string} content — full file contents
|
|
66
74
|
* @returns {{
|
|
@@ -71,6 +79,7 @@ import { matchBlockHeader } from './block-header.mjs';
|
|
|
71
79
|
* 'confidence-floor': number,
|
|
72
80
|
* 'min-rule-days': number,
|
|
73
81
|
* 'min-insight-chars': number,
|
|
82
|
+
* 'max-proposals-per-run': number,
|
|
74
83
|
* }}
|
|
75
84
|
*/
|
|
76
85
|
export function _parseReconcile(content) {
|
|
@@ -82,6 +91,7 @@ export function _parseReconcile(content) {
|
|
|
82
91
|
'confidence-floor': 0.5,
|
|
83
92
|
'min-rule-days': 7,
|
|
84
93
|
'min-insight-chars': 24,
|
|
94
|
+
'max-proposals-per-run': 10,
|
|
85
95
|
};
|
|
86
96
|
|
|
87
97
|
const lines = content.split(/\r?\n/);
|
|
@@ -108,6 +118,7 @@ export function _parseReconcile(content) {
|
|
|
108
118
|
let confidenceFloor = 0.5;
|
|
109
119
|
let minRuleDays = 7;
|
|
110
120
|
let minInsightChars = 24;
|
|
121
|
+
let maxProposalsPerRun = 10;
|
|
111
122
|
|
|
112
123
|
for (const rawLine of blockLines) {
|
|
113
124
|
// Strip inline comments and trailing whitespace
|
|
@@ -183,6 +194,15 @@ export function _parseReconcile(content) {
|
|
|
183
194
|
break;
|
|
184
195
|
}
|
|
185
196
|
|
|
197
|
+
case 'max-proposals-per-run': {
|
|
198
|
+
if (/^\d+$/.test(v)) {
|
|
199
|
+
const n = parseInt(v, 10);
|
|
200
|
+
if (n >= 1) maxProposalsPerRun = n;
|
|
201
|
+
// 0 or malformed: silently ignore, keep default 10
|
|
202
|
+
}
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
|
|
186
206
|
// targets inline-list with no brackets (e.g. targets: repo-local) — single value
|
|
187
207
|
case 'targets': {
|
|
188
208
|
if (v && !v.startsWith('[')) {
|
|
@@ -201,5 +221,6 @@ export function _parseReconcile(content) {
|
|
|
201
221
|
'confidence-floor': confidenceFloor,
|
|
202
222
|
'min-rule-days': minRuleDays,
|
|
203
223
|
'min-insight-chars': minInsightChars,
|
|
224
|
+
'max-proposals-per-run': maxProposalsPerRun,
|
|
204
225
|
};
|
|
205
226
|
}
|
|
@@ -8,6 +8,126 @@
|
|
|
8
8
|
// Section extraction (ported from config-yaml-parser.sh)
|
|
9
9
|
// ---------------------------------------------------------------------------
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* THE canonical predicate for "this line opens the `## Session Config` block".
|
|
13
|
+
*
|
|
14
|
+
* Exported (#959 follow-up) because this repo previously carried FIVE
|
|
15
|
+
* independent copies of this fact, and the loosest of them —
|
|
16
|
+
* `scripts/lib/claude-md-budget-lint.mjs` — was the module whose whole job is
|
|
17
|
+
* to MEASURE the block. That asymmetry was not academic: the lint accepted a
|
|
18
|
+
* heading decorated with a trailing HTML comment (`## Session Config <!-- … -->`,
|
|
19
|
+
* a decoration this repo's own convention encourages on sibling headings),
|
|
20
|
+
* while `_extractConfigSection` below rejected it. An author following the
|
|
21
|
+
* cited convention therefore got a CLAUDE.md (or AGENTS.md on Codex CLI —
|
|
22
|
+
* transparent aliases, see skills/_shared/instruction-file-resolution.md)
|
|
23
|
+
* whose every runtime config key
|
|
24
|
+
* silently fell back to its default, while the lint simultaneously reported
|
|
25
|
+
* "148 exempt: '## Session Config'" — the instrument affirming the block is
|
|
26
|
+
* present and runtime-critical at the exact moment the runtime cannot see it.
|
|
27
|
+
*
|
|
28
|
+
* The invariant this export exists to hold: **no consumer's predicate may
|
|
29
|
+
* accept a heading this one rejects.** Importing beats re-deriving; a copy
|
|
30
|
+
* that drifts loose is worse than no check at all.
|
|
31
|
+
*
|
|
32
|
+
* Intentionally EXACT (not a fuzzy regex) apart from CRLF tolerance — it is
|
|
33
|
+
* the literal comparison `_extractConfigSection` has always used, merely
|
|
34
|
+
* given a name. `## Session Config` (two spaces), a trailing HTML comment,
|
|
35
|
+
* and `## Session Config Convention` are all correctly rejected.
|
|
36
|
+
*
|
|
37
|
+
* Dependency note: this module imports NOTHING. Keep it that way — the budget
|
|
38
|
+
* lint consumes this predicate at bootstrap-scaffold time, before
|
|
39
|
+
* `.claude/rules/` (or most of the repo) exists.
|
|
40
|
+
*
|
|
41
|
+
* @param {string} line - a single line, with or without a trailing '\r'.
|
|
42
|
+
* @returns {boolean}
|
|
43
|
+
*/
|
|
44
|
+
export function isSessionConfigHeading(line) {
|
|
45
|
+
return typeof line === 'string' && line.replace(/\r$/, '') === SESSION_CONFIG_HEADING;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The literal heading text, for the one site that legitimately WRITES it
|
|
50
|
+
* (`scripts/lib/ecosystem-wizard/config-writer.mjs` appends a fresh
|
|
51
|
+
* `## Session Config` section when none exists).
|
|
52
|
+
*
|
|
53
|
+
* A producer and a comparator agreeing by coincidence is the same disease
|
|
54
|
+
* `isSessionConfigHeading` exists to cure, one level down: a writer that
|
|
55
|
+
* emits a heading the reader rejects manufactures exactly the silent
|
|
56
|
+
* everything-falls-back-to-defaults failure described above. Writers import
|
|
57
|
+
* this; comparators import the predicate. Neither re-types the string.
|
|
58
|
+
*
|
|
59
|
+
* @type {string}
|
|
60
|
+
*/
|
|
61
|
+
export const SESSION_CONFIG_HEADING = '## Session Config';
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Locate the `## Session Config` block and return its body span.
|
|
65
|
+
*
|
|
66
|
+
* THE canonical block-extractor, and the companion to
|
|
67
|
+
* `isSessionConfigHeading` above. Four consumers previously re-derived this
|
|
68
|
+
* span with four different regexes, and the divergence was not academic:
|
|
69
|
+
* `harness-audit/categories/category4.mjs` used
|
|
70
|
+
* `/^## Session Config\s*\n([\s\S]*?)(?=^## |\s*$)/m`, whose `\s*$` lookahead
|
|
71
|
+
* (with `/m`, `$` matches at every line end) makes the lazy body quantifier
|
|
72
|
+
* stop at the FIRST line of the block. On this repo's own CLAUDE.md that
|
|
73
|
+
* captured 17 characters — `persistence: true` — instead of 2877, so the
|
|
74
|
+
* `vault-integration:` block three lines below was structurally invisible and
|
|
75
|
+
* the c4.4 check scored 2/2 with "vault-integration not enabled — skip" in a
|
|
76
|
+
* repo where it is enabled. A capture bug in a scoring instrument reads as a
|
|
77
|
+
* pass, never as an error.
|
|
78
|
+
*
|
|
79
|
+
* The body runs from the line AFTER the heading up to (not including) the
|
|
80
|
+
* next `## ` heading, or EOF. Line-based over `isSessionConfigHeading`, so
|
|
81
|
+
* the heading predicate cannot drift from the span extractor.
|
|
82
|
+
*
|
|
83
|
+
* Offsets are indices into the ORIGINAL `content` string, so callers that
|
|
84
|
+
* need to splice (insert/replace) can do so without re-finding the block.
|
|
85
|
+
*
|
|
86
|
+
* @param {string} content - full markdown document
|
|
87
|
+
* @param {{ occurrence?: 'first'|'last' }} [options] - which heading to match
|
|
88
|
+
* when a document carries several (docs/session-config-template.md carries
|
|
89
|
+
* two, both inside fenced examples).
|
|
90
|
+
* @returns {{ body: string, bodyStart: number, bodyEnd: number,
|
|
91
|
+
* headingLine: number } | null} null when no heading matches.
|
|
92
|
+
*/
|
|
93
|
+
export function findSessionConfigBlock(content, { occurrence = 'first' } = {}) {
|
|
94
|
+
if (typeof content !== 'string' || content.length === 0) return null;
|
|
95
|
+
|
|
96
|
+
const lines = content.split('\n');
|
|
97
|
+
|
|
98
|
+
// Byte offset of the start of each line in `content`.
|
|
99
|
+
const lineStarts = [];
|
|
100
|
+
let offset = 0;
|
|
101
|
+
for (const line of lines) {
|
|
102
|
+
lineStarts.push(offset);
|
|
103
|
+
offset += line.length + 1; // +1 for the '\n' consumed by split
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const headingIdxs = [];
|
|
107
|
+
for (let i = 0; i < lines.length; i++) {
|
|
108
|
+
if (isSessionConfigHeading(lines[i])) headingIdxs.push(i);
|
|
109
|
+
}
|
|
110
|
+
if (headingIdxs.length === 0) return null;
|
|
111
|
+
|
|
112
|
+
const targetIdx = occurrence === 'last' ? headingIdxs[headingIdxs.length - 1] : headingIdxs[0];
|
|
113
|
+
|
|
114
|
+
// Body ends at the next `## ` heading (any H2 closes the block), or EOF.
|
|
115
|
+
let endIdx = lines.length;
|
|
116
|
+
for (let i = targetIdx + 1; i < lines.length; i++) {
|
|
117
|
+
if (/^## /.test(lines[i].replace(/\r$/, ''))) { endIdx = i; break; }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const bodyStart = targetIdx + 1 < lines.length ? lineStarts[targetIdx + 1] : content.length;
|
|
121
|
+
const bodyEnd = endIdx < lines.length ? lineStarts[endIdx] : content.length;
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
body: lines.slice(targetIdx + 1, endIdx).join('\n'),
|
|
125
|
+
bodyStart,
|
|
126
|
+
bodyEnd,
|
|
127
|
+
headingLine: targetIdx + 1, // 1-based, for error reporting
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
11
131
|
/**
|
|
12
132
|
* Extract the raw ## Session Config block lines from markdown content.
|
|
13
133
|
* - CRLF-tolerant
|
|
@@ -24,7 +144,7 @@ export function _extractConfigSection(content) {
|
|
|
24
144
|
for (const rawLine of lines) {
|
|
25
145
|
const line = rawLine.replace(/\r$/, '');
|
|
26
146
|
|
|
27
|
-
if (line
|
|
147
|
+
if (isSessionConfigHeading(line)) {
|
|
28
148
|
inSection = true;
|
|
29
149
|
continue;
|
|
30
150
|
}
|