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
|
@@ -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);
|
|
@@ -26,7 +26,9 @@
|
|
|
26
26
|
* - --json → `{ count, rules: [{path, alwaysOn, matchedGlobs}] }`
|
|
27
27
|
*
|
|
28
28
|
* Exit codes (per .claude/rules/cli-design.md):
|
|
29
|
-
* 0 — success
|
|
29
|
+
* 0 — success, INCLUDING EPIPE (the reader closed its end of the pipe
|
|
30
|
+
* early — `| head`, `| grep -q`, any truncating consumer — before the
|
|
31
|
+
* full payload drained; see the process.stdout 'error' handler below)
|
|
30
32
|
* 1 — user/input error (bad --wave-scope path, malformed wave-scope JSON)
|
|
31
33
|
* 2 — system error (unexpected internal failure)
|
|
32
34
|
* Data → stdout, diagnostics → stderr.
|
|
@@ -34,6 +36,11 @@
|
|
|
34
36
|
* Best-effort by design: a missing rules dir, missing STATE.md, or missing
|
|
35
37
|
* host.json each degrade to "no gating / no rules" rather than failing — the
|
|
36
38
|
* wave-executor caller treats any non-zero exit as "inject nothing, continue".
|
|
39
|
+
* EPIPE is deliberately EXCLUDED from that "non-zero = inject nothing"
|
|
40
|
+
* contract: the actual wave-executor caller drains stdout fully
|
|
41
|
+
* (execFileSync/spawnSync-style capture) and never closes the pipe early, so
|
|
42
|
+
* EPIPE can only be triggered by an exploratory or truncating reader — never
|
|
43
|
+
* by the real caller this CLI exists to serve.
|
|
37
44
|
*
|
|
38
45
|
* Related: issue #336 (glob-scoped rules), #694 (rule-activation / FA1),
|
|
39
46
|
* scripts/lib/rule-loader.mjs (loadApplicableRules),
|
|
@@ -49,6 +56,31 @@ import { findProjectRoot } from './lib/common.mjs';
|
|
|
49
56
|
import { loadApplicableRules } from './lib/rule-loader.mjs';
|
|
50
57
|
import { readHostClass } from './lib/autopilot/telemetry.mjs';
|
|
51
58
|
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// EPIPE hardening (regression follow-up on #876)
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
//
|
|
63
|
+
// process.stdout.write() to a pipe is ASYNCHRONOUS in Node. When the reader
|
|
64
|
+
// closes its end early — `| head`, `| grep -q`, any truncating consumer —
|
|
65
|
+
// before the writer has finished draining, the deferred write fails and
|
|
66
|
+
// process.stdout emits an 'error' event carrying err.code === 'EPIPE'. Left
|
|
67
|
+
// unhandled, that is an UNCAUGHT EXCEPTION: Node prints a stack trace to
|
|
68
|
+
// stderr ("Unhandled 'error' event") and exits 1 — even though nothing
|
|
69
|
+
// actually failed on the producer side; the reader simply chose to stop
|
|
70
|
+
// consuming. Registered before any stdout write below so it covers every
|
|
71
|
+
// output branch (--help, --json, Markdown, and the empty-match no-op).
|
|
72
|
+
//
|
|
73
|
+
// Exit 0 on EPIPE, matching conventional Unix CLI behaviour (`cat file |
|
|
74
|
+
// head` reports no error to its caller) and restoring the pre-#876 exit
|
|
75
|
+
// contract for this specific case. Any other stdout write error is
|
|
76
|
+
// unexpected and is re-thrown rather than swallowed.
|
|
77
|
+
process.stdout.on('error', (err) => {
|
|
78
|
+
if (err && err.code === 'EPIPE') {
|
|
79
|
+
process.exit(0);
|
|
80
|
+
}
|
|
81
|
+
throw err;
|
|
82
|
+
});
|
|
83
|
+
|
|
52
84
|
const HELP = `Usage: node scripts/print-applicable-rules.mjs [options]
|
|
53
85
|
|
|
54
86
|
Prints the glob-scoped + always-on rule set applicable to the current wave,
|
|
@@ -223,6 +255,18 @@ try {
|
|
|
223
255
|
fail(`Rule loading failed: ${err.message}`, 2);
|
|
224
256
|
}
|
|
225
257
|
|
|
258
|
+
// NOTE (#876): deliberately no `process.exit(0)` after these stdout writes.
|
|
259
|
+
// process.stdout.write() to a pipe is ASYNCHRONOUS in Node — an explicit
|
|
260
|
+
// process.exit() terminates the process before the kernel pipe buffer (64KiB
|
|
261
|
+
// on macOS) has been fully drained, silently truncating any payload beyond
|
|
262
|
+
// that threshold with exit code still 0. Letting the script fall off the end
|
|
263
|
+
// lets Node's event loop wait for the pending write to flush before the
|
|
264
|
+
// process exits naturally (default exit code 0) — the fix generalizes to any
|
|
265
|
+
// payload size, not just today's measured ~105KB. The three branches below
|
|
266
|
+
// are mutually exclusive (if/else-if/else) so only one ever writes to stdout.
|
|
267
|
+
// When the reader closes early instead of draining to completion, the write
|
|
268
|
+
// fails with EPIPE — handled asynchronously by the process.stdout 'error'
|
|
269
|
+
// listener registered near the top of this file, not by anything here.
|
|
226
270
|
if (opts.json) {
|
|
227
271
|
const out = {
|
|
228
272
|
count: rules.length,
|
|
@@ -233,15 +277,10 @@ if (opts.json) {
|
|
|
233
277
|
})),
|
|
234
278
|
};
|
|
235
279
|
process.stdout.write(JSON.stringify(out, null, 2) + '\n');
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
process.
|
|
280
|
+
} else if (rules.length === 0) {
|
|
281
|
+
// Empty match set → print nothing (caller injects nothing).
|
|
282
|
+
} else {
|
|
283
|
+
const header = '## Applicable Rules (scoped to this wave)';
|
|
284
|
+
const body = rules.map((r) => r.content.trimEnd()).join('\n\n---\n\n');
|
|
285
|
+
process.stdout.write(`${header}\n\n${body}\n`);
|
|
242
286
|
}
|
|
243
|
-
|
|
244
|
-
const header = '## Applicable Rules (scoped to this wave)';
|
|
245
|
-
const body = rules.map((r) => r.content.trimEnd()).join('\n\n---\n\n');
|
|
246
|
-
process.stdout.write(`${header}\n\n${body}\n`);
|
|
247
|
-
process.exit(0);
|