session-orchestrator 3.17.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 +105 -412
- package/README.md +12 -9
- 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/USER-GUIDE.md +8 -3
- package/docs/ci-setup.md +121 -7
- package/docs/codex-setup.md +1 -1
- package/docs/components.md +6 -6
- package/docs/cursor-setup.md +22 -9
- package/docs/events-schema.md +5 -1
- package/docs/instruction-delivery.md +444 -0
- package/docs/rule-authoring.md +58 -9
- package/docs/session-config-reference.md +244 -9
- package/docs/session-config-template.md +39 -3
- 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/subagent-telemetry.mjs +527 -37
- package/package.json +5 -2
- 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/eval-session.mjs +50 -9
- package/scripts/fleet-instruction-scan.mjs +141 -0
- 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/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/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/reconcile.mjs +21 -0
- package/scripts/lib/config/section-extractor.mjs +121 -1
- package/scripts/lib/config-schema.mjs +23 -3
- package/scripts/lib/config.mjs +17 -0
- package/scripts/lib/convergence-monitor.mjs +49 -3
- package/scripts/lib/description-surface.mjs +535 -0
- package/scripts/lib/dispatcher/enumerate.mjs +26 -40
- 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/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 +9 -3
- 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/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-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/validator.mjs +16 -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/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/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/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 +17 -3
- 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/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 +17 -1
- 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 +34 -13
- 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 +73 -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/validator.mjs +108 -7
- 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+)/);
|
|
@@ -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
|
+
}
|
|
@@ -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
|
}
|
|
@@ -9,7 +9,11 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
const ENFORCEMENT_VALUES = new Set(['strict', 'warn', 'off']);
|
|
12
|
-
|
|
12
|
+
// Exported (issue #836) so cross-layer mode-vocabulary parity tests can use
|
|
13
|
+
// this Set as the single source of truth for "the modes a Session Config
|
|
14
|
+
// `*.mode` key accepts" — rather than re-deriving a duplicate literal list
|
|
15
|
+
// that could silently drift from this schema.
|
|
16
|
+
export const VAULT_MODE_VALUES = new Set(['strict', 'warn', 'off']);
|
|
13
17
|
|
|
14
18
|
const REQUIRED_STRING_FIELDS = [
|
|
15
19
|
'test-command',
|
|
@@ -25,11 +29,13 @@ const REQUIRED_STRING_FIELDS = [
|
|
|
25
29
|
*/
|
|
26
30
|
export function validateSessionConfig(config) {
|
|
27
31
|
const errors = [];
|
|
32
|
+
const warnings = [];
|
|
28
33
|
|
|
29
34
|
if (config === null || typeof config !== 'object' || Array.isArray(config)) {
|
|
30
35
|
return {
|
|
31
36
|
ok: false,
|
|
32
37
|
errors: [{ path: '$', rule: 'type', message: 'config must be a JSON object' }],
|
|
38
|
+
warnings,
|
|
33
39
|
};
|
|
34
40
|
}
|
|
35
41
|
|
|
@@ -90,10 +96,24 @@ export function validateSessionConfig(config) {
|
|
|
90
96
|
for (const msg of gpErrs) errors.push({ path: 'gitlab-portfolio', rule: 'object', message: msg });
|
|
91
97
|
}
|
|
92
98
|
|
|
99
|
+
// HR-003 cross-field anti-pattern (baseline #60): a heavy repo left on
|
|
100
|
+
// isolation: auto|none accumulates worktrees without the aggressive cleanup
|
|
101
|
+
// HR-003 recommends — this is advisory (warn-level), it never flips `ok`.
|
|
102
|
+
if (config['heavy-repo'] === true) {
|
|
103
|
+
const isolationVal = config['isolation'];
|
|
104
|
+
if (isolationVal === 'auto' || isolationVal === 'none') {
|
|
105
|
+
warnings.push({
|
|
106
|
+
path: 'isolation',
|
|
107
|
+
rule: 'heavy-repo-cross-field',
|
|
108
|
+
message: `heavy-repo: true but isolation is '${isolationVal}' — heavy repos should pin isolation: worktree (HR-003)`,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
93
113
|
if (errors.length > 0) {
|
|
94
|
-
return { ok: false, errors };
|
|
114
|
+
return { ok: false, errors, warnings };
|
|
95
115
|
}
|
|
96
|
-
return { ok: true, config };
|
|
116
|
+
return { ok: true, config, warnings };
|
|
97
117
|
}
|
|
98
118
|
|
|
99
119
|
function validateAgentsPerWave(value, errors) {
|
package/scripts/lib/config.mjs
CHANGED
|
@@ -56,6 +56,7 @@ import { _parseColdStart } from './config/cold-start.mjs';
|
|
|
56
56
|
import { _parseAutoDream } from './config/auto-dream.mjs';
|
|
57
57
|
import { _parseStateMdLock } from './config/state-md-lock.mjs';
|
|
58
58
|
import { _parseHandoverGate } from './config/handover-gate.mjs';
|
|
59
|
+
import { _parseIssueBudget } from './config/issue-budget.mjs';
|
|
59
60
|
import { _parseBrokenWindow } from './config/broken-window.mjs';
|
|
60
61
|
import { _parseSlopcheck } from './config/slopcheck.mjs';
|
|
61
62
|
import { _parseDiscoveryValidator } from './config/discovery-validator.mjs';
|
|
@@ -195,6 +196,9 @@ export function parseSessionConfig(mdContent, { hostPaths } = {}) {
|
|
|
195
196
|
const allowDestructiveOps = _coerceBoolean(kv, 'allow-destructive-ops', false);
|
|
196
197
|
const resourceAwareness = _coerceBoolean(kv, 'resource-awareness', true);
|
|
197
198
|
const enableHostBanner = _coerceBoolean(kv, 'enable-host-banner', true);
|
|
199
|
+
// heavy-repo / worktree-cleanup: HR-003 preflight fields (templates/shared/.claude/rules/heavy-repo.md),
|
|
200
|
+
// documented but previously unwired — silently dropped by the parser (baseline issue #60).
|
|
201
|
+
const heavyRepo = _coerceBoolean(kv, 'heavy-repo', false);
|
|
198
202
|
|
|
199
203
|
// List fields
|
|
200
204
|
const crossRepos = _coerceList(kv, 'cross-repos', undefined);
|
|
@@ -212,6 +216,10 @@ export function parseSessionConfig(mdContent, { hostPaths } = {}) {
|
|
|
212
216
|
const enforcement = _coerceEnum(kv, 'enforcement', 'warn', ['strict', 'warn', 'off']);
|
|
213
217
|
const isolation = _coerceEnum(kv, 'isolation', 'auto', ['worktree', 'none', 'auto']);
|
|
214
218
|
const discoverySeverityThreshold = _coerceEnum(kv, 'discovery-severity-threshold', 'low', ['critical', 'high', 'medium', 'low']);
|
|
219
|
+
// worktree-cleanup: HR-003 (baseline issue #60). NOTE: 'aggressive' currently behaves
|
|
220
|
+
// identically to 'default' at runtime — the per-wave aggressive sweep is a tracked
|
|
221
|
+
// follow-up (see docs/session-config-reference.md). This wires the parser + value only.
|
|
222
|
+
const worktreeCleanup = _coerceEnum(kv, 'worktree-cleanup', 'default', ['default', 'aggressive']);
|
|
215
223
|
|
|
216
224
|
// Object fields
|
|
217
225
|
const agentMapping = _coerceObject(kv, 'agent-mapping');
|
|
@@ -296,6 +304,12 @@ export function parseSessionConfig(mdContent, { hostPaths } = {}) {
|
|
|
296
304
|
// Handover-Alignment-Gate — Epic #724)
|
|
297
305
|
const handoverGate = _parseHandoverGate(mdContent);
|
|
298
306
|
|
|
307
|
+
// issue-budget: per-session issue-creation cap (QUANTITY gate). Distinct from
|
|
308
|
+
// discovery-severity-threshold / discovery-confidence-threshold above, which
|
|
309
|
+
// are per-finding QUALITY filters and cannot bound creation volume. Enforced
|
|
310
|
+
// by hooks/pre-bash-issue-budget.mjs + scripts/lib/spiral-carryover.mjs.
|
|
311
|
+
const issueBudget = _parseIssueBudget(mdContent);
|
|
312
|
+
|
|
299
313
|
// broken-window-budget: parsed from full content (#730/H5 — session-end Phase 2.6)
|
|
300
314
|
const brokenWindow = _parseBrokenWindow(mdContent);
|
|
301
315
|
|
|
@@ -437,6 +451,8 @@ export function parseSessionConfig(mdContent, { hostPaths } = {}) {
|
|
|
437
451
|
'allow-destructive-ops': allowDestructiveOps,
|
|
438
452
|
'resource-awareness': resourceAwareness,
|
|
439
453
|
'enable-host-banner': enableHostBanner,
|
|
454
|
+
'heavy-repo': heavyRepo,
|
|
455
|
+
'worktree-cleanup': worktreeCleanup,
|
|
440
456
|
'resource-thresholds': resourceThresholds,
|
|
441
457
|
'worktree-exclude': worktreeExclude,
|
|
442
458
|
'vault-integration': vaultIntegration,
|
|
@@ -445,6 +461,7 @@ export function parseSessionConfig(mdContent, { hostPaths } = {}) {
|
|
|
445
461
|
'auto-dream': autoDream,
|
|
446
462
|
'state-md-lock': stateMdLock,
|
|
447
463
|
'handover-gate': handoverGate,
|
|
464
|
+
'issue-budget': issueBudget,
|
|
448
465
|
'broken-window-budget': brokenWindow,
|
|
449
466
|
'slopcheck': slopcheck,
|
|
450
467
|
'skill-evolution': skillEvolution,
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
|
|
45
45
|
import { existsSync, statSync, openSync, readSync, closeSync } from 'node:fs';
|
|
46
46
|
import { join } from 'node:path';
|
|
47
|
+
import { pathToFileURL } from 'node:url';
|
|
47
48
|
|
|
48
49
|
const DEFAULT_INTERVAL_S = 2;
|
|
49
50
|
const EVENTS_FILE_REL = '.orchestrator/metrics/events.jsonl';
|
|
@@ -138,6 +139,27 @@ function tailRead(absPath, prevOffset) {
|
|
|
138
139
|
}
|
|
139
140
|
}
|
|
140
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Event types this monitor is meant to classify. Anything else is ignored even
|
|
144
|
+
* when it carries a wave number.
|
|
145
|
+
*
|
|
146
|
+
* The allowlist is a wave-LIFECYCLE prefix plus the agent-dispatch counters —
|
|
147
|
+
* i.e. exactly the records that can carry the three measurements
|
|
148
|
+
* `evaluateSignals` compares (`files_changed`, `test.passed`,
|
|
149
|
+
* `agents_dispatched`). It is deliberately fail-CLOSED: a new event type that
|
|
150
|
+
* gains a `wave_number` field must be added here consciously.
|
|
151
|
+
*/
|
|
152
|
+
const WAVE_EVENT_PREFIX = 'orchestrator.wave.';
|
|
153
|
+
const WAVE_EVENT_NAMES = new Set(['agent.dispatched', 'orchestrator.agent.dispatched']);
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* @param {string} evType
|
|
157
|
+
* @returns {boolean}
|
|
158
|
+
*/
|
|
159
|
+
function isWaveScopedEvent(evType) {
|
|
160
|
+
return evType.startsWith(WAVE_EVENT_PREFIX) || WAVE_EVENT_NAMES.has(evType);
|
|
161
|
+
}
|
|
162
|
+
|
|
141
163
|
/**
|
|
142
164
|
* Classify a raw events.jsonl record and update the in-memory wave summary
|
|
143
165
|
* map. Returns the wave number affected, or null if the record is not a
|
|
@@ -148,11 +170,30 @@ function tailRead(absPath, prevOffset) {
|
|
|
148
170
|
* - { event_type, wave, wave_number, files_changed, test.passed,
|
|
149
171
|
* agent.dispatched, agents_dispatched }
|
|
150
172
|
*
|
|
173
|
+
* ## Event-type gate (#966 step 1)
|
|
174
|
+
*
|
|
175
|
+
* A wave number alone is NOT sufficient to classify — the type gate runs first.
|
|
176
|
+
* `orchestrator.quality_gate.*` is by far the highest-volume record in
|
|
177
|
+
* `events.jsonl` and was ignored here only ACCIDENTALLY, because it happened to
|
|
178
|
+
* carry no wave number. The moment `run-quality-gate.mjs` started emitting
|
|
179
|
+
* `wave_number`, every gate run would have instantiated or refreshed a
|
|
180
|
+
* `WaveSummary`, advanced `latestWave`, and burnt the once-per-wave
|
|
181
|
+
* `alreadyEmitted` keys — suppressing the genuine signal when the real wave
|
|
182
|
+
* record arrived later. Several other high-volume types (`session.stopped`,
|
|
183
|
+
* `memory.propose_invoked`) already carry a wave and sat in the same trap.
|
|
184
|
+
*
|
|
185
|
+
* Note the measurement keys are read FLAT (`rec['test.passed']`) and are
|
|
186
|
+
* deliberately NOT reconciled with the gate event's nested `counts.passed` —
|
|
187
|
+
* folding one into the other would silently change what this monitor measures.
|
|
188
|
+
*
|
|
151
189
|
* @param {Record<string, unknown>} rec
|
|
152
190
|
* @param {Map<number, WaveSummary>} state
|
|
153
191
|
* @returns {number | null}
|
|
154
192
|
*/
|
|
155
193
|
function classify(rec, state) {
|
|
194
|
+
const evType = String(rec.event_type ?? rec.event ?? '');
|
|
195
|
+
if (!isWaveScopedEvent(evType)) return null;
|
|
196
|
+
|
|
156
197
|
const waveNumber = pickInt(rec.wave_number ?? rec.wave ?? rec.waveId);
|
|
157
198
|
if (waveNumber === null) return null;
|
|
158
199
|
let summary = state.get(waveNumber);
|
|
@@ -170,8 +211,7 @@ function classify(rec, state) {
|
|
|
170
211
|
const testPassed = pickInt(rec['test.passed'] ?? rec.test_passed ?? rec.testsPassed);
|
|
171
212
|
if (testPassed !== null) summary.testPassed = testPassed;
|
|
172
213
|
// Count one agent.dispatched event toward this wave's dispatch count.
|
|
173
|
-
|
|
174
|
-
if (evType === 'agent.dispatched') {
|
|
214
|
+
if (WAVE_EVENT_NAMES.has(evType)) {
|
|
175
215
|
summary.agentDispatchCount = (summary.agentDispatchCount ?? 0) + 1;
|
|
176
216
|
} else {
|
|
177
217
|
const dispatched = pickInt(rec.agents_dispatched ?? rec.agentsDispatched);
|
|
@@ -386,4 +426,10 @@ function main() {
|
|
|
386
426
|
});
|
|
387
427
|
}
|
|
388
428
|
|
|
389
|
-
|
|
429
|
+
// Run the tail loop only when executed as a script. Importing the module (for
|
|
430
|
+
// unit tests over `classify`) must not parse vitest's argv and exit 1.
|
|
431
|
+
if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
432
|
+
main();
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
export { classify, isWaveScopedEvent };
|