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
|
@@ -20,7 +20,7 @@ import { probe } from './resource-probe.mjs';
|
|
|
20
20
|
* "proceed" decision.
|
|
21
21
|
*
|
|
22
22
|
* @param {object} opts - Same opts shape as evaluateWaveResourceGate
|
|
23
|
-
* @returns {Promise<{ramFreeGb: number, ramAvailableGb: number|null, cpuLoadPct: number, concurrentSessions: number} | {probeFailed: true}>}
|
|
23
|
+
* @returns {Promise<{ramFreeGb: number, ramAvailableGb: number|null, cpuLoadPct: number, cpuLoad5mPct: number|null, concurrentSessions: number} | {probeFailed: true}>}
|
|
24
24
|
*/
|
|
25
25
|
async function extractMeasurements(opts) {
|
|
26
26
|
const { probeOverride } = opts;
|
|
@@ -31,6 +31,8 @@ async function extractMeasurements(opts) {
|
|
|
31
31
|
// Tests may supply ramAvailableGb to exercise the macOS path; absent → null.
|
|
32
32
|
ramAvailableGb: probeOverride.ramAvailableGb ?? null,
|
|
33
33
|
cpuLoadPct: probeOverride.cpuLoadPct,
|
|
34
|
+
// 5m-average CPU pct (#943); absent → null (legacy overrides → 1m-only judging).
|
|
35
|
+
cpuLoad5mPct: probeOverride.cpuLoad5mPct ?? null,
|
|
34
36
|
concurrentSessions: probeOverride.concurrentSessions,
|
|
35
37
|
};
|
|
36
38
|
}
|
|
@@ -47,6 +49,9 @@ async function extractMeasurements(opts) {
|
|
|
47
49
|
// os.freemem() is already accurate. (#667)
|
|
48
50
|
ramAvailableGb: snapshot.ram_available_gb ?? null,
|
|
49
51
|
cpuLoadPct: snapshot.cpu_load_pct,
|
|
52
|
+
// 5m load-average as pct-of-cores (#943). null on Windows/zero-load, where
|
|
53
|
+
// the gate falls back to judging the 1m-derived cpu_load_pct alone.
|
|
54
|
+
cpuLoad5mPct: snapshot.cpu_load_5m_pct ?? null,
|
|
50
55
|
// concurrent sessions: number of claude processes found by the probe.
|
|
51
56
|
concurrentSessions: snapshot.claude_processes_count ?? 0,
|
|
52
57
|
};
|
|
@@ -54,15 +59,89 @@ async function extractMeasurements(opts) {
|
|
|
54
59
|
|
|
55
60
|
/**
|
|
56
61
|
* Apply the gate decision rule sequence (rules 3-8) given measurements and
|
|
57
|
-
* config
|
|
62
|
+
* config, then apply the HR-004 heavy-repo preflight ceiling on top. Returns
|
|
63
|
+
* the full gate result.
|
|
58
64
|
*
|
|
59
|
-
* @param {{ramFreeGb: number, cpuLoadPct: number, concurrentSessions: number}} measurements
|
|
65
|
+
* @param {{ramFreeGb: number, ramAvailableGb?: number|null, cpuLoadPct: number, cpuLoad5mPct?: number|null, concurrentSessions: number}} measurements
|
|
60
66
|
* @param {object} opts - Same opts shape as evaluateWaveResourceGate
|
|
61
67
|
* @returns {{decision: string, agents: number, reasons: string[], measurements: object}}
|
|
62
68
|
*/
|
|
63
69
|
function applyDecisionRules(measurements, opts) {
|
|
70
|
+
const result = computeResourceDecision(measurements, opts);
|
|
71
|
+
return applyHeavyRepoCap(result, opts);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Resolve an `agents-per-wave` config value into a plain numeric cap, or
|
|
76
|
+
* `null` when no cap should apply.
|
|
77
|
+
*
|
|
78
|
+
* `_coerceInteger()` (scripts/lib/config/coercers.mjs) parses the documented
|
|
79
|
+
* HR-003 parenthetical-override syntax — `agents-per-wave: 4 (deep: 18)` —
|
|
80
|
+
* into an OBJECT `{ default: 4, deep: 18 }`, not a plain number. Feeding that
|
|
81
|
+
* object straight into a `typeof cap !== 'number'` guard makes the heavy-repo
|
|
82
|
+
* cap silently no-op for every repo using the override syntax, which defeats
|
|
83
|
+
* HR-004 exactly where it matters most (a heavy repo that also runs deep
|
|
84
|
+
* sessions).
|
|
85
|
+
*
|
|
86
|
+
* `evaluateWaveResourceGate()` has no session-mode input in scope — `waveRole`
|
|
87
|
+
* is a wave role (e.g. "Impl-Core"), not a session mode (e.g. "deep") — so the
|
|
88
|
+
* object shape resolves to `cap.default` here. That is the conservative
|
|
89
|
+
* choice: the documented HR-003 convention writes the override as
|
|
90
|
+
* `<default> (mode: <higher-ceiling>)`, i.e. `default` is the MORE
|
|
91
|
+
* restrictive of the pair. Falling back to it can only under-apply a looser
|
|
92
|
+
* mode-specific ceiling; it never lets a heavy repo exceed its base cap.
|
|
93
|
+
*
|
|
94
|
+
* @param {number|{default: number, [mode: string]: number}|*} cap
|
|
95
|
+
* @returns {number|null}
|
|
96
|
+
*/
|
|
97
|
+
function resolveApwCap(cap) {
|
|
98
|
+
if (typeof cap === 'number') return Number.isFinite(cap) ? cap : null;
|
|
99
|
+
if (cap !== null && typeof cap === 'object' && !Array.isArray(cap)) {
|
|
100
|
+
const def = cap.default;
|
|
101
|
+
return typeof def === 'number' && Number.isFinite(def) ? def : null;
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* HR-003/HR-004 heavy-repo preflight ceiling (baseline #60). A STATIC cap
|
|
108
|
+
* independent of the live resource-probe verdict: when `config['heavy-repo']`
|
|
109
|
+
* is `true`, `agents` is clamped to at most `config['agents-per-wave']`
|
|
110
|
+
* (resolved via {@link resolveApwCap} to handle the object-override shape).
|
|
111
|
+
* More-restrictive-wins — this only ever LOWERS `agents`, never raises it
|
|
112
|
+
* above what the resource-driven rules already decided (e.g. a
|
|
113
|
+
* coordinator-direct 0 stays 0).
|
|
114
|
+
*
|
|
115
|
+
* @param {{decision: string, agents: number, reasons: string[], measurements: object}} result
|
|
116
|
+
* @param {object} opts - Same opts shape as evaluateWaveResourceGate
|
|
117
|
+
* @returns {{decision: string, agents: number, reasons: string[], measurements: object}}
|
|
118
|
+
*/
|
|
119
|
+
function applyHeavyRepoCap(result, opts) {
|
|
120
|
+
const { config } = opts;
|
|
121
|
+
if (!config || config['heavy-repo'] !== true) return result;
|
|
122
|
+
const cap = resolveApwCap(config['agents-per-wave']);
|
|
123
|
+
if (cap === null) return result;
|
|
124
|
+
if (result.agents <= cap) return result; // already within the ceiling — never raise
|
|
125
|
+
return {
|
|
126
|
+
...result,
|
|
127
|
+
decision: result.decision === 'coordinator-direct' ? 'coordinator-direct' : 'reduce',
|
|
128
|
+
agents: cap,
|
|
129
|
+
reasons: [...result.reasons, `heavy-repo: true caps agents-per-wave to ${cap} (HR-004)`],
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Rules 3-8: resource-driven decision sequence (RAM/CPU/concurrent-sessions).
|
|
135
|
+
* Extracted so `applyDecisionRules` can layer the HR-004 heavy-repo cap on
|
|
136
|
+
* top without duplicating this sequence.
|
|
137
|
+
*
|
|
138
|
+
* @param {{ramFreeGb: number, ramAvailableGb?: number|null, cpuLoadPct: number, cpuLoad5mPct?: number|null, concurrentSessions: number}} measurements
|
|
139
|
+
* @param {object} opts - Same opts shape as evaluateWaveResourceGate
|
|
140
|
+
* @returns {{decision: string, agents: number, reasons: string[], measurements: object}}
|
|
141
|
+
*/
|
|
142
|
+
function computeResourceDecision(measurements, opts) {
|
|
64
143
|
const { config, plannedAgents } = opts;
|
|
65
|
-
const { ramFreeGb, ramAvailableGb, cpuLoadPct, concurrentSessions } = measurements;
|
|
144
|
+
const { ramFreeGb, ramAvailableGb, cpuLoadPct, cpuLoad5mPct, concurrentSessions } = measurements;
|
|
66
145
|
const T = config['resource-thresholds'];
|
|
67
146
|
|
|
68
147
|
// macOS fix (#667): os.freemem() reports only `Pages free`, which reads
|
|
@@ -111,24 +190,41 @@ function applyDecisionRules(measurements, opts) {
|
|
|
111
190
|
};
|
|
112
191
|
}
|
|
113
192
|
|
|
114
|
-
// Rule 6: CPU overloaded → reduce.
|
|
115
|
-
|
|
193
|
+
// Rule 6: CPU overloaded → reduce. #943: this gate runs, by construction,
|
|
194
|
+
// right after the coordinator's own CPU-saturating quality-gate run — the 1m
|
|
195
|
+
// load average still carries that decaying tail (observed 2026-07-30:
|
|
196
|
+
// 96% → 91% → 78% → 75% within 36s), so a 1m-only reading systematically
|
|
197
|
+
// over-reports and halves waves without a real bottleneck. When the probe
|
|
198
|
+
// supplied a numeric 5m percentage, judge on min(1m, 5m): only-1m-high is a
|
|
199
|
+
// decaying transient (informational, no reduce), both-high is genuine
|
|
200
|
+
// sustained load. `cpuLoad5mPct` null (legacy overrides, Windows) → 1m-only.
|
|
201
|
+
const has5mCpu = typeof cpuLoad5mPct === 'number' && Number.isFinite(cpuLoad5mPct);
|
|
202
|
+
const effectiveCpuLoadPct = has5mCpu ? Math.min(cpuLoadPct, cpuLoad5mPct) : cpuLoadPct;
|
|
203
|
+
if (effectiveCpuLoadPct > T['cpu-load-max-pct']) {
|
|
204
|
+
const detail = has5mCpu ? ` (min of 1m ${cpuLoadPct}% / 5m ${cpuLoad5mPct}%)` : '';
|
|
116
205
|
return {
|
|
117
206
|
decision: 'reduce',
|
|
118
207
|
agents: Math.max(1, Math.floor(plannedAgents / 2)),
|
|
119
208
|
reasons: [
|
|
120
|
-
`CPU load ${
|
|
209
|
+
`CPU load ${effectiveCpuLoadPct}%${detail} > max ${T['cpu-load-max-pct']}% — reducing agent count`,
|
|
121
210
|
],
|
|
122
211
|
measurements,
|
|
123
212
|
};
|
|
124
213
|
}
|
|
214
|
+
const cpuTransientNote =
|
|
215
|
+
has5mCpu && cpuLoadPct > T['cpu-load-max-pct']
|
|
216
|
+
? `info: CPU 1m load ${cpuLoadPct}% > max ${T['cpu-load-max-pct']}% but 5m load ${cpuLoad5mPct}% is below — decaying transient (typically the coordinator's own just-finished gate run), not reducing (#943)`
|
|
217
|
+
: null;
|
|
125
218
|
|
|
126
219
|
// Rule 7: concurrent sessions above warn → proceed with warning.
|
|
127
220
|
if (concurrentSessions > T['concurrent-sessions-warn']) {
|
|
128
221
|
return {
|
|
129
222
|
decision: 'proceed',
|
|
130
223
|
agents: plannedAgents,
|
|
131
|
-
reasons: [
|
|
224
|
+
reasons: [
|
|
225
|
+
...(cpuTransientNote ? [cpuTransientNote] : []),
|
|
226
|
+
`warn: ${concurrentSessions} concurrent sessions`,
|
|
227
|
+
],
|
|
132
228
|
measurements,
|
|
133
229
|
};
|
|
134
230
|
}
|
|
@@ -137,7 +233,10 @@ function applyDecisionRules(measurements, opts) {
|
|
|
137
233
|
return {
|
|
138
234
|
decision: 'proceed',
|
|
139
235
|
agents: plannedAgents,
|
|
140
|
-
reasons: [
|
|
236
|
+
reasons: [
|
|
237
|
+
...(cpuTransientNote ? [cpuTransientNote] : []),
|
|
238
|
+
'all thresholds within bounds',
|
|
239
|
+
],
|
|
141
240
|
measurements,
|
|
142
241
|
};
|
|
143
242
|
}
|
|
@@ -148,14 +247,19 @@ function applyDecisionRules(measurements, opts) {
|
|
|
148
247
|
* @param {object} opts.config - Parsed Session Config (from parse-config.sh output)
|
|
149
248
|
* @param {number} opts.plannedAgents - Number of agents the session-plan wants to dispatch
|
|
150
249
|
* @param {string} opts.waveRole - e.g. "Impl-Core", "Quality"
|
|
151
|
-
* @param {object} [opts.probeOverride] - {ramFreeGb, cpuLoadPct, concurrentSessions}
|
|
152
|
-
* testing; when omitted, calls resource-probe
|
|
250
|
+
* @param {object} [opts.probeOverride] - {ramFreeGb, cpuLoadPct, cpuLoad5mPct?, concurrentSessions}
|
|
251
|
+
* for testing; when omitted, calls resource-probe
|
|
153
252
|
* @returns {Promise<{decision: "proceed"|"reduce"|"coordinator-direct", agents: number, reasons: string[], measurements: object}>}
|
|
154
253
|
*/
|
|
155
254
|
export async function evaluateWaveResourceGate(opts) {
|
|
156
255
|
const { config, plannedAgents } = opts;
|
|
157
256
|
|
|
158
257
|
// Rule 1: resource-awareness disabled — skip all probing.
|
|
258
|
+
// `resource-awareness: false` is a FULL opt-out, INCLUDING the HR-004
|
|
259
|
+
// heavy-repo static cap below (applyHeavyRepoCap runs only inside
|
|
260
|
+
// applyDecisionRules, which this early return bypasses entirely). The
|
|
261
|
+
// static cap only applies on the resource-aware path — deliberate,
|
|
262
|
+
// reviewed 2026-07-23, baseline #60.
|
|
159
263
|
if (config['resource-awareness'] === false) {
|
|
160
264
|
return {
|
|
161
265
|
decision: 'proceed',
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* worktree/listing.mjs — list and filter worktrees.
|
|
3
3
|
*
|
|
4
4
|
* Exports:
|
|
5
|
-
* listWorktrees() — list all git worktrees
|
|
5
|
+
* listWorktrees() — list all git worktrees (bare array; swallows git failure)
|
|
6
|
+
* listWorktreesChecked() — same listing WITH a "git actually ran" signal (#919.3)
|
|
6
7
|
* applyWorktreeExcludes(wtPath, patterns) — remove top-level dirs from worktree
|
|
7
8
|
*
|
|
8
9
|
* No imports from lifecycle.mjs — this module is intentionally a leaf of the
|
|
@@ -18,23 +19,39 @@ import path from 'node:path';
|
|
|
18
19
|
// ---------------------------------------------------------------------------
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
|
-
* List all git worktrees
|
|
22
|
+
* List all git worktrees WITH an explicit "git actually ran" signal (#919.3).
|
|
23
|
+
*
|
|
24
|
+
* The bare `listWorktrees()` below swallows a failing `git worktree list` into
|
|
25
|
+
* an empty array — indistinguishable from "git ran, repo has no extra
|
|
26
|
+
* worktrees". That collapse is the last fail-open gap in the
|
|
27
|
+
* `checkLiveForeignSession` full path (peer-discovery.mjs, #906/#908 residual):
|
|
28
|
+
* a total surface failure read as "nobody home". This variant keeps the listing
|
|
29
|
+
* contract but makes the failure DISTINGUISHABLE, so callers that need the
|
|
30
|
+
* distinction can make the fail-safe call themselves.
|
|
22
31
|
*
|
|
23
32
|
* @param {object} [opts]
|
|
24
33
|
* @param {Function} [opts.$] Optional zx-compatible executor. Defaults to real zx.$.
|
|
25
34
|
* Tests pass a mock here to avoid vi.mock('zx') under fork pool.
|
|
26
|
-
* @returns {Promise<Array<{path: string, branch: string, head: string}
|
|
27
|
-
*
|
|
35
|
+
* @returns {Promise<{ok: boolean, worktrees: Array<{path: string, branch: string, head: string}>, error?: string}>}
|
|
36
|
+
* `ok: true` — `git worktree list` ran; `worktrees` is the (possibly empty)
|
|
37
|
+
* parsed listing. An empty list here is a MEASUREMENT.
|
|
38
|
+
* `ok: false` — the git invocation itself failed (spawn error, non-zero
|
|
39
|
+
* exit, not a repo, …); `worktrees` is `[]` and `error` carries
|
|
40
|
+
* the failure message. An empty list here is NOT a measurement.
|
|
28
41
|
*/
|
|
29
|
-
export async function
|
|
42
|
+
export async function listWorktreesChecked(opts = {}) {
|
|
30
43
|
const dollar = opts.$ ?? defaultDollar;
|
|
31
44
|
const git = dollar({ cwd: process.cwd() });
|
|
32
45
|
let output;
|
|
33
46
|
try {
|
|
34
47
|
const result = await git`git worktree list --porcelain`;
|
|
35
48
|
output = result.stdout;
|
|
36
|
-
} catch {
|
|
37
|
-
return
|
|
49
|
+
} catch (err) {
|
|
50
|
+
return {
|
|
51
|
+
ok: false,
|
|
52
|
+
worktrees: [],
|
|
53
|
+
error: err instanceof Error ? err.message : String(err),
|
|
54
|
+
};
|
|
38
55
|
}
|
|
39
56
|
|
|
40
57
|
const worktrees = [];
|
|
@@ -67,6 +84,26 @@ export async function listWorktrees(opts = {}) {
|
|
|
67
84
|
worktrees.push(current);
|
|
68
85
|
}
|
|
69
86
|
|
|
87
|
+
return { ok: true, worktrees };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* List all git worktrees in the current repository.
|
|
92
|
+
*
|
|
93
|
+
* Thin backward-compatible wrapper over `listWorktreesChecked()`: existing
|
|
94
|
+
* callers get the bare array they always got, INCLUDING the historical
|
|
95
|
+
* swallow-to-`[]` on git failure. Callers that must distinguish "git ran,
|
|
96
|
+
* empty" from "git failed" use `listWorktreesChecked()` instead (#919.3).
|
|
97
|
+
*
|
|
98
|
+
* @param {object} [opts]
|
|
99
|
+
* @param {Function} [opts.$] Optional zx-compatible executor. Defaults to real zx.$.
|
|
100
|
+
* Tests pass a mock here to avoid vi.mock('zx') under fork pool.
|
|
101
|
+
* @returns {Promise<Array<{path: string, branch: string, head: string}>>}
|
|
102
|
+
* Array of worktree descriptors; empty array if none OR on git failure
|
|
103
|
+
* (the two are indistinguishable here — by design, see wrapper note).
|
|
104
|
+
*/
|
|
105
|
+
export async function listWorktrees(opts = {}) {
|
|
106
|
+
const { worktrees } = await listWorktreesChecked(opts);
|
|
70
107
|
return worktrees;
|
|
71
108
|
}
|
|
72
109
|
|
package/scripts/mcp-server.sh
CHANGED
|
@@ -8,7 +8,8 @@ set -euo pipefail
|
|
|
8
8
|
# - session_config — reads Session Config from the project instruction file
|
|
9
9
|
# (CLAUDE.md, or AGENTS.md alias on Codex CLI — see
|
|
10
10
|
# skills/_shared/instruction-file-resolution.md)
|
|
11
|
-
# - session_metrics — reads last 5 session metrics entries
|
|
11
|
+
# - session_metrics — reads last 5 REAL session metrics entries (#834:
|
|
12
|
+
# abandoned phantom stubs are filtered out first)
|
|
12
13
|
|
|
13
14
|
# ---------------------------------------------------------------------------
|
|
14
15
|
# Helpers
|
|
@@ -75,7 +76,7 @@ handle_tools_list() {
|
|
|
75
76
|
},
|
|
76
77
|
{
|
|
77
78
|
"name": "session_metrics",
|
|
78
|
-
"description": "Reads the last 5 session metrics entries from .orchestrator/metrics/sessions.jsonl",
|
|
79
|
+
"description": "Reads the last 5 REAL session metrics entries from .orchestrator/metrics/sessions.jsonl (abandoned phantom stubs excluded)",
|
|
79
80
|
"inputSchema": {"type": "object", "properties": {}, "required": []}
|
|
80
81
|
}
|
|
81
82
|
]
|
|
@@ -127,9 +128,23 @@ tool_session_config() {
|
|
|
127
128
|
return
|
|
128
129
|
fi
|
|
129
130
|
|
|
130
|
-
# Extract everything from "## Session Config" to the next heading or EOF
|
|
131
|
-
|
|
132
|
-
|
|
131
|
+
# Extract everything from "## Session Config" to the next heading or EOF.
|
|
132
|
+
#
|
|
133
|
+
# The address matches isSessionConfigHeading() in
|
|
134
|
+
# scripts/lib/config/section-extractor.mjs (the SSOT) exactly: the literal
|
|
135
|
+
# heading, plus an optional CR so a CRLF checkout is read the same way. Shell
|
|
136
|
+
# cannot import the JS predicate, so the alignment is by hand and this
|
|
137
|
+
# comment names the authority. Before #968 this was the only STRICTER site in
|
|
138
|
+
# the repo — a bare `$` anchors before the CR in a BRE, so on a CRLF checkout
|
|
139
|
+
# (this plugin ships to Windows-side Codex/Cursor users) the tool reported
|
|
140
|
+
# "no Session Config section" for a file the runtime parses fine.
|
|
141
|
+
#
|
|
142
|
+
# `\r` is not portable inside a BRE (GNU sed only), so the CR is injected as
|
|
143
|
+
# a literal byte via printf.
|
|
144
|
+
local cr config sc_re
|
|
145
|
+
cr=$(printf '\r')
|
|
146
|
+
sc_re="^## Session Config${cr}\{0,1\}\$"
|
|
147
|
+
config=$(sed -n "/$sc_re/,/^## /{/$sc_re/d;/^## /d;p;}" "$instr_file" 2>/dev/null) || true
|
|
133
148
|
|
|
134
149
|
if [[ -z "$config" ]]; then
|
|
135
150
|
respond "$id" "$(text_content "No '## Session Config' section found in $instr_file")"
|
|
@@ -156,8 +171,19 @@ tool_session_metrics() {
|
|
|
156
171
|
return
|
|
157
172
|
fi
|
|
158
173
|
|
|
174
|
+
# Filter out phantom `status: 'abandoned'` stubs (#834, session-close-backfill
|
|
175
|
+
# — 0 waves, seconds of runtime) BEFORE taking the tail, so a recent phantom
|
|
176
|
+
# cannot displace real session records out of the last-5 window.
|
|
177
|
+
#
|
|
178
|
+
# `-R` (raw-input) + `fromjson?` parses each line individually and SKIPS
|
|
179
|
+
# unparseable ones instead of aborting the whole stream — plain
|
|
180
|
+
# `jq -c 'select(...)'` aborts at the FIRST malformed line (jq: parse error,
|
|
181
|
+
# exit 5), which is fatal here because sessions.jsonl is append-only from
|
|
182
|
+
# multiple writers and a torn write is exactly the case that matters. This
|
|
183
|
+
# mirrors the per-line try/catch behaviour of the .mjs path
|
|
184
|
+
# (scripts/lib/session-schema/filters.mjs).
|
|
159
185
|
local entries
|
|
160
|
-
entries=$(
|
|
186
|
+
entries=$(jq -R -c 'fromjson? | select(.status != "abandoned")' "$metrics_file" 2>/dev/null | tail -n 5) || true
|
|
161
187
|
|
|
162
188
|
if [[ -z "$entries" ]]; then
|
|
163
189
|
respond "$id" "$(text_content "No metrics found (file is empty)")"
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# measure-context-overhead.sh — measure the REAL instruction overhead of a directory.
|
|
3
|
+
#
|
|
4
|
+
# Why this exists (and why token-audit.sh does not replace it):
|
|
5
|
+
# token-audit.sh measures instruction bytes ON DISK. That is a proxy, and a poor
|
|
6
|
+
# one: it cannot see which rule files the loader classifies as always-on, it
|
|
7
|
+
# cannot see plugin skill descriptions, and it cannot see the tool definitions.
|
|
8
|
+
# This script measures what actually reaches the model, by sending a trivial
|
|
9
|
+
# prompt and reading the token accounting back out of the API response. Every
|
|
10
|
+
# token it reports is overhead, because the prompt itself does no work.
|
|
11
|
+
#
|
|
12
|
+
# Method: `claude -p "<trivial prompt>" --output-format json`, then sum
|
|
13
|
+
# input_tokens + cache_creation_input_tokens + cache_read_input_tokens.
|
|
14
|
+
# The cache split varies between runs; the SUM is the stable quantity and is
|
|
15
|
+
# what the model is charged for reading.
|
|
16
|
+
#
|
|
17
|
+
# COST WARNING: each measurement is a real API call against the configured
|
|
18
|
+
# model. Measured 2026-07-30 on claude-opus-5[1m]: USD 0.27 (empty directory)
|
|
19
|
+
# to USD 1.06 (this repo). Budget accordingly before running a large matrix.
|
|
20
|
+
#
|
|
21
|
+
# Usage:
|
|
22
|
+
# bash scripts/measure-context-overhead.sh <dir> [<dir> ...]
|
|
23
|
+
# bash scripts/measure-context-overhead.sh --ablate <repo-root>
|
|
24
|
+
#
|
|
25
|
+
# --ablate builds throwaway copies of <repo-root>'s instruction surface
|
|
26
|
+
# (CLAUDE.md (or AGENTS.md on Codex CLI) + .claude/rules/) under $TMPDIR and
|
|
27
|
+
# measures the full / reduced / stripped variants, so the cost of each layer can
|
|
28
|
+
# be attributed. It never writes to, and never deletes from, the source repository.
|
|
29
|
+
#
|
|
30
|
+
# Instruction-file resolution follows skills/_shared/instruction-file-resolution.md:
|
|
31
|
+
# CLAUDE.md wins, AGENTS.md is the Codex CLI alias, empty counts as absent, and
|
|
32
|
+
# exactly ONE of the two is ever read. The resolved file is copied into every
|
|
33
|
+
# variant under the name the measuring harness reads (`claude -p` -> CLAUDE.md),
|
|
34
|
+
# so an AGENTS.md-only repo gets its instruction layer measured instead of
|
|
35
|
+
# silently reported as 0 bytes.
|
|
36
|
+
#
|
|
37
|
+
# Baseline recorded 2026-07-30 (claude-opus-5[1m], CLI 2.1.220) for THIS repo,
|
|
38
|
+
# which resolves to CLAUDE.md — a repo resolving to AGENTS.md has its own:
|
|
39
|
+
# full 110687 tok | no top-3 rules 83138 | no rules 43291 | nothing 42001
|
|
40
|
+
# Reading: the 26 rule files account for 67396 tokens (61% of total); the
|
|
41
|
+
# instruction file itself accounts for 1290. Optimising the instruction file is
|
|
42
|
+
# optimising the wrong file.
|
|
43
|
+
|
|
44
|
+
set -uo pipefail
|
|
45
|
+
|
|
46
|
+
PROMPT='Antworte nur mit dem Wort: OK'
|
|
47
|
+
|
|
48
|
+
measure_one() {
|
|
49
|
+
local dir="$1" label="$2"
|
|
50
|
+
if [ ! -d "$dir" ]; then
|
|
51
|
+
printf '%s\tNO-SUCH-DIR\n' "$label"
|
|
52
|
+
return
|
|
53
|
+
fi
|
|
54
|
+
( cd "$dir" && claude -p "$PROMPT" --output-format json 2>/dev/null ) | node -e "
|
|
55
|
+
let s = '';
|
|
56
|
+
process.stdin.on('data', (d) => (s += d)).on('end', () => {
|
|
57
|
+
const label = process.argv[1];
|
|
58
|
+
try {
|
|
59
|
+
const j = JSON.parse(s);
|
|
60
|
+
const u = j.usage || {};
|
|
61
|
+
const ctx =
|
|
62
|
+
(u.input_tokens || 0) +
|
|
63
|
+
(u.cache_creation_input_tokens || 0) +
|
|
64
|
+
(u.cache_read_input_tokens || 0);
|
|
65
|
+
process.stdout.write(
|
|
66
|
+
[label, ctx, u.cache_creation_input_tokens || 0, u.cache_read_input_tokens || 0,
|
|
67
|
+
u.output_tokens || 0, (j.total_cost_usd || 0).toFixed(4)].join('\t') + '\n',
|
|
68
|
+
);
|
|
69
|
+
} catch {
|
|
70
|
+
process.stdout.write(label + '\tPARSE-ERROR\n');
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
" "$label"
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
header() { printf 'LABEL\tCONTEXT_TOK\tcache_create\tcache_read\toutput\tUSD\n'; }
|
|
77
|
+
|
|
78
|
+
# ── --ablate mode ───────────────────────────────────────────────────
|
|
79
|
+
if [ "${1:-}" = "--ablate" ]; then
|
|
80
|
+
SRC="${2:-$(pwd)}"
|
|
81
|
+
[ -d "$SRC" ] || { echo "not a directory: $SRC" >&2; exit 1; }
|
|
82
|
+
|
|
83
|
+
# Resolve the project-instruction file. CLAUDE.md wins; AGENTS.md is the
|
|
84
|
+
# Codex CLI alias; an empty file counts as absent; never both.
|
|
85
|
+
if [ -s "$SRC/CLAUDE.md" ]; then INSTR_SRC="$SRC/CLAUDE.md"
|
|
86
|
+
elif [ -s "$SRC/AGENTS.md" ]; then INSTR_SRC="$SRC/AGENTS.md"
|
|
87
|
+
else
|
|
88
|
+
echo "no project-instruction file in $SRC (looked for CLAUDE.md, then AGENTS.md)." >&2
|
|
89
|
+
echo "Refusing to ablate: v2-no-rules and v3-bare differ ONLY by that file, so" >&2
|
|
90
|
+
echo "the table would render as 'the instruction layer costs 0 tokens' — the" >&2
|
|
91
|
+
echo "one reading this tool must never produce without evidence." >&2
|
|
92
|
+
exit 1
|
|
93
|
+
fi
|
|
94
|
+
# The measuring harness is `claude -p`, whose instruction file is CLAUDE.md.
|
|
95
|
+
# Copy the resolved source under THAT name so the content is actually loaded,
|
|
96
|
+
# whichever of the two aliases the source repo happens to use.
|
|
97
|
+
INSTR_DEST="CLAUDE.md"
|
|
98
|
+
echo "instruction file: $INSTR_SRC -> <variant>/$INSTR_DEST" >&2
|
|
99
|
+
src_rules=("$SRC"/.claude/rules/*.md)
|
|
100
|
+
[ -e "${src_rules[0]}" ] || \
|
|
101
|
+
echo "note: no .claude/rules/*.md in $SRC — the rule layers measure as zero." >&2
|
|
102
|
+
|
|
103
|
+
BASE="${TMPDIR:-/tmp}/so-ablation-$$"
|
|
104
|
+
mkdir -p "$BASE"
|
|
105
|
+
# Copy ONLY the instruction surface. No git, no source, no side effects.
|
|
106
|
+
build() {
|
|
107
|
+
local v="$BASE/$1"
|
|
108
|
+
mkdir -p "$v/.claude/rules"
|
|
109
|
+
cp "$INSTR_SRC" "$v/$INSTR_DEST" || {
|
|
110
|
+
echo "failed to copy $INSTR_SRC -> $v/$INSTR_DEST" >&2
|
|
111
|
+
exit 1
|
|
112
|
+
}
|
|
113
|
+
cp "$SRC"/.claude/rules/*.md "$v/.claude/rules/" 2>/dev/null || true
|
|
114
|
+
}
|
|
115
|
+
build v0-full
|
|
116
|
+
build v1-no-top3
|
|
117
|
+
rm -f "$BASE/v1-no-top3/.claude/rules/loop-and-monitor.md" \
|
|
118
|
+
"$BASE/v1-no-top3/.claude/rules/parallel-sessions.md" \
|
|
119
|
+
"$BASE/v1-no-top3/.claude/rules/security.md"
|
|
120
|
+
build v2-no-rules
|
|
121
|
+
rm -f "$BASE"/v2-no-rules/.claude/rules/*.md
|
|
122
|
+
build v3-bare
|
|
123
|
+
rm -f "$BASE"/v3-bare/.claude/rules/*.md "$BASE/v3-bare/$INSTR_DEST"
|
|
124
|
+
|
|
125
|
+
echo "=== instruction bytes on disk ==="
|
|
126
|
+
for v in v0-full v1-no-top3 v2-no-rules v3-bare; do
|
|
127
|
+
b=$(cat "$BASE/$v/$INSTR_DEST" "$BASE/$v"/.claude/rules/*.md 2>/dev/null | wc -c | tr -d ' ')
|
|
128
|
+
n=$(ls "$BASE/$v"/.claude/rules/*.md 2>/dev/null | wc -l | tr -d ' ')
|
|
129
|
+
printf ' %-14s %8s B rules=%s\n' "$v" "${b:-0}" "${n:-0}"
|
|
130
|
+
done
|
|
131
|
+
echo
|
|
132
|
+
header
|
|
133
|
+
for v in v0-full v1-no-top3 v2-no-rules v3-bare; do
|
|
134
|
+
measure_one "$BASE/$v" "$v"
|
|
135
|
+
done
|
|
136
|
+
echo
|
|
137
|
+
echo "variants left in $BASE (throwaway; remove when done)"
|
|
138
|
+
exit 0
|
|
139
|
+
fi
|
|
140
|
+
|
|
141
|
+
# ── direct mode ─────────────────────────────────────────────────────
|
|
142
|
+
if [ "$#" -eq 0 ]; then
|
|
143
|
+
echo "usage: bash scripts/measure-context-overhead.sh <dir> [<dir> ...]" >&2
|
|
144
|
+
echo " bash scripts/measure-context-overhead.sh --ablate <repo-root>" >&2
|
|
145
|
+
exit 1
|
|
146
|
+
fi
|
|
147
|
+
|
|
148
|
+
header
|
|
149
|
+
for d in "$@"; do
|
|
150
|
+
measure_one "$d" "$(basename "$d")"
|
|
151
|
+
done
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
import { parseArgs } from 'node:util';
|
|
43
43
|
import { readFileSync, existsSync } from 'node:fs';
|
|
44
44
|
import { spawnSync } from 'node:child_process';
|
|
45
|
-
import { dirname, join } from 'node:path';
|
|
45
|
+
import { dirname, join, isAbsolute } from 'node:path';
|
|
46
46
|
import { fileURLToPath } from 'node:url';
|
|
47
47
|
|
|
48
48
|
import { isWaveAgentContext, WAVE_AGENT_ENV_VAR, WAVE_AGENT_ENV_VALUE } from './lib/wave-context.mjs';
|
|
@@ -104,7 +104,8 @@ const rawArgv = process.argv.slice(2);
|
|
|
104
104
|
if (rawArgv.includes('--help') || rawArgv.includes('-h')) {
|
|
105
105
|
process.stdout.write(
|
|
106
106
|
'Usage: SO_WAVE_AGENT=1 memory-propose.mjs --type <type> --subject "..." ' +
|
|
107
|
-
'--insight "..." --evidence "..." --confidence <0-1>
|
|
107
|
+
'--insight "..." --evidence "..." --confidence <0-1> ' +
|
|
108
|
+
'[--file-paths "a.mjs,b.mjs"] [--dry-run]\n\n' +
|
|
108
109
|
'Environment:\n' +
|
|
109
110
|
' SO_WAVE_AGENT=1 — REQUIRED. The CLI returns exit 3 (rejected-wrong-context)\n' +
|
|
110
111
|
' when this env-var is absent or not exactly "1".\n' +
|
|
@@ -113,7 +114,14 @@ if (rawArgv.includes('--help') || rawArgv.includes('-h')) {
|
|
|
113
114
|
' --dry-run — Validate the proposal (argv + schema) but do NOT write to\n' +
|
|
114
115
|
' proposals.jsonl. Bypasses the STATE.md / SO_WAVE_AGENT /\n' +
|
|
115
116
|
' current-wave context gates so it can be run safely from\n' +
|
|
116
|
-
' coordinator context (issue #741.3).\n
|
|
117
|
+
' coordinator context (issue #741.3).\n' +
|
|
118
|
+
' --file-paths — Optional. Repo-relative path(s) this learning applies to.\n' +
|
|
119
|
+
' Repeatable AND/OR comma-separated (`--file-paths a.mjs\n' +
|
|
120
|
+
' --file-paths b.mjs,c.mjs`), deduped. Rejects absolute\n' +
|
|
121
|
+
' paths, ".." segments, embedded newlines, entries over 256\n' +
|
|
122
|
+
' chars, and more than 20 entries (exit 4). Without\n' +
|
|
123
|
+
' --file-paths this learning can never become /reconcile-\n' +
|
|
124
|
+
' eligible (issue #900).\n\n' +
|
|
117
125
|
'Exit codes / stdout status:\n' +
|
|
118
126
|
` 0 — ${STATUS.QUEUED} (or ${STATUS.DRY_RUN_OK} under --dry-run)\n` +
|
|
119
127
|
` 1 — ${STATUS.QUOTA_EXCEEDED}\n` +
|
|
@@ -133,12 +141,13 @@ try {
|
|
|
133
141
|
parsedArgs = parseArgs({
|
|
134
142
|
args: rawArgv,
|
|
135
143
|
options: {
|
|
136
|
-
type:
|
|
137
|
-
subject:
|
|
138
|
-
insight:
|
|
139
|
-
evidence:
|
|
140
|
-
confidence:
|
|
141
|
-
'dry-run':
|
|
144
|
+
type: { type: 'string' },
|
|
145
|
+
subject: { type: 'string' },
|
|
146
|
+
insight: { type: 'string' },
|
|
147
|
+
evidence: { type: 'string' },
|
|
148
|
+
confidence: { type: 'string' },
|
|
149
|
+
'dry-run': { type: 'boolean' },
|
|
150
|
+
'file-paths': { type: 'string', multiple: true },
|
|
142
151
|
},
|
|
143
152
|
strict: false, // emit unknown flags as positionals rather than throwing
|
|
144
153
|
});
|
|
@@ -173,6 +182,59 @@ if (confidenceRaw !== undefined) {
|
|
|
173
182
|
}
|
|
174
183
|
}
|
|
175
184
|
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
// Step 1b — Parse + validate --file-paths (issue #900 C)
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
//
|
|
189
|
+
// --file-paths is repeatable AND each occurrence may itself be comma-separated
|
|
190
|
+
// (`--file-paths a.mjs --file-paths b.mjs,c.mjs`). Flattened, trimmed,
|
|
191
|
+
// empty-filtered, and deduped BEFORE validation so callers see one clean
|
|
192
|
+
// error per genuinely-bad entry rather than noise from formatting.
|
|
193
|
+
//
|
|
194
|
+
// Validation runs here (Step 1, argv-level) — BEFORE createProposalRecord
|
|
195
|
+
// (Step 6) — so a malformed --file-paths value produces the same exit-4
|
|
196
|
+
// argv-error contract as every other required/optional flag, never a
|
|
197
|
+
// downstream schema-validation surprise.
|
|
198
|
+
|
|
199
|
+
const FILE_PATHS_MAX_COUNT = 20;
|
|
200
|
+
const FILE_PATH_MAX_CHARS = 256;
|
|
201
|
+
|
|
202
|
+
const filePathsRaw = parsedArgs.values['file-paths'];
|
|
203
|
+
/** @type {string[]|undefined} */
|
|
204
|
+
let filePaths;
|
|
205
|
+
if (Array.isArray(filePathsRaw) && filePathsRaw.length > 0) {
|
|
206
|
+
const flattened = filePathsRaw
|
|
207
|
+
.flatMap((entry) => String(entry).split(','))
|
|
208
|
+
.map((p) => p.trim())
|
|
209
|
+
.filter((p) => p.length > 0);
|
|
210
|
+
filePaths = [...new Set(flattened)];
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (filePaths !== undefined) {
|
|
214
|
+
if (filePaths.length > FILE_PATHS_MAX_COUNT) {
|
|
215
|
+
argErrors.push(
|
|
216
|
+
`--file-paths accepts at most ${FILE_PATHS_MAX_COUNT} paths (got ${filePaths.length})`,
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
for (const p of filePaths) {
|
|
220
|
+
if (isAbsolute(p)) {
|
|
221
|
+
argErrors.push(`--file-paths must be repo-relative — absolute path rejected: "${p}"`);
|
|
222
|
+
} else if (p.split(/[\\/]/).includes('..')) {
|
|
223
|
+
argErrors.push(`--file-paths must not contain ".." path segments: "${p}"`);
|
|
224
|
+
} else if (/[\r\n]/.test(p)) {
|
|
225
|
+
argErrors.push(`--file-paths entries must not contain newline characters: "${p}"`);
|
|
226
|
+
} else if (/[*?[\]{}]/.test(p)) {
|
|
227
|
+
argErrors.push(
|
|
228
|
+
`--file-paths must not contain glob metacharacters (* ? [ ] { }) — a literal repo-relative path is required: "${p}"`,
|
|
229
|
+
);
|
|
230
|
+
} else if (p.length > FILE_PATH_MAX_CHARS) {
|
|
231
|
+
argErrors.push(
|
|
232
|
+
`--file-paths entry exceeds ${FILE_PATH_MAX_CHARS} chars (got ${p.length}): "${p.slice(0, 40)}..."`,
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
176
238
|
if (argErrors.length > 0) {
|
|
177
239
|
exit({ status: STATUS.ERROR, validation: argErrors }, 4);
|
|
178
240
|
}
|
|
@@ -362,6 +424,7 @@ try {
|
|
|
362
424
|
evidence,
|
|
363
425
|
confidence,
|
|
364
426
|
waveId,
|
|
427
|
+
filePaths,
|
|
365
428
|
});
|
|
366
429
|
} catch (err) {
|
|
367
430
|
exit({ status: STATUS.ERROR, validation: [`Failed to create proposal record: ${err.message}`] }, 4);
|