baldart 4.52.4 → 4.53.1
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/CHANGELOG.md +26 -0
- package/VERSION +1 -1
- package/framework/.claude/agents/api-perf-cost-auditor.md +4 -0
- package/framework/.claude/agents/code-reviewer.md +11 -0
- package/framework/.claude/agents/doc-reviewer.md +1 -0
- package/framework/.claude/agents/security-reviewer.md +7 -0
- package/framework/.claude/skills/new/SKILL.md +1 -1
- package/framework/.claude/skills/new/references/final-review.md +12 -3
- package/framework/.claude/skills/new/references/review-cycle.md +14 -1
- package/framework/.claude/skills/new/references/setup.md +24 -32
- package/framework/.claude/skills/worktree-manager/SKILL.md +55 -188
- package/framework/.claude/skills/worktree-manager/scripts/setup-worktree.sh +457 -0
- package/framework/.claude/workflows/new-card-review.js +35 -10
- package/framework/.claude/workflows/new-final-review.js +29 -8
- package/framework/.claude/workflows/new2.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# setup-worktree.sh — SSOT deterministic CODE-worktree creation + baseline.
|
|
4
|
+
#
|
|
5
|
+
# This is the single source of truth for the worktree build sequence consumed by
|
|
6
|
+
# three callers (no per-caller duplication — the sin CLAUDE.md forbids):
|
|
7
|
+
# • /nw (worktree-manager § "/nw") — interactive + slug-only + programmatic
|
|
8
|
+
# • /new (skill: framework/.claude/skills/new/references/setup.md step 4)
|
|
9
|
+
# • new2 (framework/.claude/workflows/new2.js Pre-flight)
|
|
10
|
+
#
|
|
11
|
+
# Why a deterministic script instead of a model-driven subagent: the worktree
|
|
12
|
+
# setup is PURE PLUMBING (git worktree add → npm install → env copy → port →
|
|
13
|
+
# registry → baseline). Driving it through a Skill-interpreting subagent put a
|
|
14
|
+
# model in the loop for mechanical work — and the model either FABRICATED a
|
|
15
|
+
# well-formed "baseline: pass" block with nothing on disk (haiku, 2/2), or went
|
|
16
|
+
# IDLE mid-execution leaving a half-built worktree (sonnet, node_modules missing).
|
|
17
|
+
# A deterministic script cannot fabricate or stall: it does the work or fails
|
|
18
|
+
# honestly with a log. Callers launch it as a BACKGROUND Bash, end the turn, and
|
|
19
|
+
# on resume read the structured manifest + run their own disk-verification gate.
|
|
20
|
+
#
|
|
21
|
+
# SCOPE: the FRESH-path only. The caller's git-authoritative idempotency pre-check
|
|
22
|
+
# (setup.md step 4a2) decides resume / reset / fresh BEFORE invoking this — this
|
|
23
|
+
# script assumes a clean slate and fails loud on a branch/path collision (a code
|
|
24
|
+
# worktree is not silently re-creatable). It does NOT repair a failing baseline
|
|
25
|
+
# (role boundary — the coder specialist does that downstream); it reports the
|
|
26
|
+
# failure with a log precise enough to act on.
|
|
27
|
+
#
|
|
28
|
+
# Usage:
|
|
29
|
+
# setup-worktree.sh --branch <branch> --slug <slug> --manifest <path> [opts]
|
|
30
|
+
#
|
|
31
|
+
# Required:
|
|
32
|
+
# --branch <branch> resolved branch name (caller derives it; e.g.
|
|
33
|
+
# feat/FEAT-0200-menu-ranking or feat/<slug>)
|
|
34
|
+
# --slug <slug> kebab-case slug (used for the log filename only;
|
|
35
|
+
# the worktree path derives from --branch)
|
|
36
|
+
# --manifest <path> file to write the structured result block to (the
|
|
37
|
+
# caller reads THIS at resume — never the bg stdout)
|
|
38
|
+
#
|
|
39
|
+
# Optional (each falls back to baldart.config.yml / autodetect when omitted, so
|
|
40
|
+
# /nw can call with just the three required flags; /new + new2 pass the values
|
|
41
|
+
# they already resolved in Phase 0 to avoid re-deriving / drift):
|
|
42
|
+
# --card <id> primary card id (default: none → null)
|
|
43
|
+
# --cards <a,b,c> all card ids sharing the wt (default: [])
|
|
44
|
+
# --group-parent <id> group.parent (default: none → null)
|
|
45
|
+
# --main <path> main repo root (default: resolve_main)
|
|
46
|
+
# --trunk <branch> git.trunk_branch (default: config/autodetect)
|
|
47
|
+
# --env-files <a,b> stack.env_files CSV (default: config / .env.local,.env)
|
|
48
|
+
# --backlog-dir <dir> paths.backlog_dir (default: config / backlog)
|
|
49
|
+
# --tc-typecheck <cmd> toolchain typecheck cmd (default: config / npx tsc --noEmit)
|
|
50
|
+
# --tc-lint <cmd> toolchain lint cmd (default: config / npx eslint --max-warnings=0 src/)
|
|
51
|
+
# --tc-build <cmd> toolchain build cmd (default: config / npm run build)
|
|
52
|
+
# --install-cmd <cmd> dependency install cmd (default: npm install)
|
|
53
|
+
# --log <path> install/build log sink (default: /tmp/wt-setup-<slug>.log)
|
|
54
|
+
# --port-min <n> port scan floor (default: 3001)
|
|
55
|
+
# --port-max <n> port scan ceiling (default: 3099)
|
|
56
|
+
# --build-timeout <s> hard build timeout seconds (default: 600)
|
|
57
|
+
#
|
|
58
|
+
# Manifest written (parsed by callers — STABLE contract):
|
|
59
|
+
# status: ok | error
|
|
60
|
+
# error: <message | ->
|
|
61
|
+
# worktree_path: <abs | ->
|
|
62
|
+
# branch: <branch>
|
|
63
|
+
# port: <n | ->
|
|
64
|
+
# created_at: <ISO-8601 | -> (stamped at worktree creation, NOT at finish)
|
|
65
|
+
# baseline: pass | fail | timeout | -
|
|
66
|
+
# baseline_log: <path | ->
|
|
67
|
+
#
|
|
68
|
+
# Exit: 0 on a VERIFIED worktree with baseline:pass; non-zero otherwise. The
|
|
69
|
+
# caller never trusts the exit code or the manifest as the sole evidence — it
|
|
70
|
+
# re-verifies the worktree on disk (setup.md step 6a). This script writes the
|
|
71
|
+
# manifest on EVERY exit path so a non-zero exit is always diagnosable.
|
|
72
|
+
|
|
73
|
+
set -u
|
|
74
|
+
|
|
75
|
+
err() { printf '%s\n' "$*" >&2; }
|
|
76
|
+
|
|
77
|
+
# --- args ------------------------------------------------------------------
|
|
78
|
+
BRANCH= SLUG= MANIFEST= CARD= CARDS= GROUP_PARENT= MAIN= TRUNK=
|
|
79
|
+
ENV_FILES_ARG= BACKLOG_DIR= TC_TC= TC_LINT= TC_BUILD= INSTALL_CMD=
|
|
80
|
+
LOG= PORT_MIN=3001 PORT_MAX=3099 BUILD_TIMEOUT=600
|
|
81
|
+
ENV_FILES_SET=0
|
|
82
|
+
while [ $# -gt 0 ]; do
|
|
83
|
+
case "$1" in
|
|
84
|
+
--branch) BRANCH="${2:-}"; shift 2 ;;
|
|
85
|
+
--slug) SLUG="${2:-}"; shift 2 ;;
|
|
86
|
+
--manifest) MANIFEST="${2:-}"; shift 2 ;;
|
|
87
|
+
--card) CARD="${2:-}"; shift 2 ;;
|
|
88
|
+
--cards) CARDS="${2:-}"; shift 2 ;;
|
|
89
|
+
--group-parent) GROUP_PARENT="${2:-}"; shift 2 ;;
|
|
90
|
+
--main) MAIN="${2:-}"; shift 2 ;;
|
|
91
|
+
--trunk) TRUNK="${2:-}"; shift 2 ;;
|
|
92
|
+
--env-files) ENV_FILES_ARG="${2:-}"; ENV_FILES_SET=1; shift 2 ;;
|
|
93
|
+
--backlog-dir) BACKLOG_DIR="${2:-}"; shift 2 ;;
|
|
94
|
+
--tc-typecheck) TC_TC="${2:-}"; shift 2 ;;
|
|
95
|
+
--tc-lint) TC_LINT="${2:-}"; shift 2 ;;
|
|
96
|
+
--tc-build) TC_BUILD="${2:-}"; shift 2 ;;
|
|
97
|
+
--install-cmd) INSTALL_CMD="${2:-}"; shift 2 ;;
|
|
98
|
+
--log) LOG="${2:-}"; shift 2 ;;
|
|
99
|
+
--port-min) PORT_MIN="${2:-}"; shift 2 ;;
|
|
100
|
+
--port-max) PORT_MAX="${2:-}"; shift 2 ;;
|
|
101
|
+
--build-timeout) BUILD_TIMEOUT="${2:-}"; shift 2 ;;
|
|
102
|
+
*) err "ERROR: unknown arg: $1"; exit 2 ;;
|
|
103
|
+
esac
|
|
104
|
+
done
|
|
105
|
+
|
|
106
|
+
# --- manifest writer (called on EVERY exit path) ---------------------------
|
|
107
|
+
M_STATUS="error" M_ERROR="-" M_WTPATH="-" M_PORT="-" M_CREATED="-" M_BASELINE="-" M_BLOG="-"
|
|
108
|
+
write_manifest() {
|
|
109
|
+
[ -n "$MANIFEST" ] || return 0
|
|
110
|
+
{
|
|
111
|
+
printf 'status: %s\n' "$M_STATUS"
|
|
112
|
+
printf 'error: %s\n' "$M_ERROR"
|
|
113
|
+
printf 'worktree_path: %s\n' "$M_WTPATH"
|
|
114
|
+
printf 'branch: %s\n' "${BRANCH:--}"
|
|
115
|
+
printf 'port: %s\n' "$M_PORT"
|
|
116
|
+
printf 'created_at: %s\n' "$M_CREATED"
|
|
117
|
+
printf 'baseline: %s\n' "$M_BASELINE"
|
|
118
|
+
printf 'baseline_log: %s\n' "$M_BLOG"
|
|
119
|
+
} > "$MANIFEST" 2>/dev/null || true
|
|
120
|
+
}
|
|
121
|
+
fail() { M_STATUS="error"; M_ERROR="$1"; write_manifest; err "ERROR: $1"; exit "${2:-1}"; }
|
|
122
|
+
|
|
123
|
+
[ -n "$BRANCH" ] || { err "ERROR: --branch is required"; exit 2; }
|
|
124
|
+
[ -n "$SLUG" ] || { err "ERROR: --slug is required"; exit 2; }
|
|
125
|
+
[ -n "$MANIFEST" ] || { err "ERROR: --manifest is required"; exit 2; }
|
|
126
|
+
|
|
127
|
+
# --- canonical main-repo-root resolver (mirrors allocate-id.sh resolve_main;
|
|
128
|
+
# inlined by the same rationale that script documents — it is not on disk
|
|
129
|
+
# inside a freshly-checked-out worktree, and sourcing across the freshly
|
|
130
|
+
# created worktree boundary is fragile). Built on --show-toplevel, NEVER
|
|
131
|
+
# --git-common-dir+parent (wrong under git init --separate-git-dir). -------
|
|
132
|
+
resolve_main() {
|
|
133
|
+
local top gd common
|
|
134
|
+
top="$(git rev-parse --show-toplevel 2>/dev/null)" || return 1
|
|
135
|
+
gd="$(git rev-parse --git-dir 2>/dev/null)" || return 1
|
|
136
|
+
common="$(git rev-parse --git-common-dir 2>/dev/null)" || return 1
|
|
137
|
+
if [ "$gd" = "$common" ]; then
|
|
138
|
+
printf '%s\n' "$top"
|
|
139
|
+
else
|
|
140
|
+
(cd "$top/.." 2>/dev/null && git rev-parse --show-toplevel 2>/dev/null) || return 1
|
|
141
|
+
fi
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
# --- read a paths.* / git.* / stack.* scalar from baldart.config.yml --------
|
|
145
|
+
config_scalar() {
|
|
146
|
+
local block="$1" key="$2" cfg="$MAIN/baldart.config.yml"
|
|
147
|
+
[ -f "$cfg" ] || return 0
|
|
148
|
+
grep -A60 "^${block}:" "$cfg" 2>/dev/null \
|
|
149
|
+
| grep -m1 "[[:space:]]*${key}:" \
|
|
150
|
+
| sed -E "s/.*${key}:[[:space:]]*\"?([^\"#]*)\"?.*/\1/" \
|
|
151
|
+
| sed -E 's/[[:space:]]+$//'
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
# --- resolve MAIN ----------------------------------------------------------
|
|
155
|
+
if [ -z "$MAIN" ]; then
|
|
156
|
+
MAIN="$(resolve_main)" || fail "cannot resolve main repo root (not inside a git repository)" 1
|
|
157
|
+
fi
|
|
158
|
+
[ -d "$MAIN/.git" ] || [ -e "$MAIN/.git" ] || fail "resolved main '$MAIN' is not a git repo" 1
|
|
159
|
+
|
|
160
|
+
# --- resolve TRUNK (config → origin/HEAD → develop/main/master) -------------
|
|
161
|
+
if [ -z "$TRUNK" ]; then
|
|
162
|
+
TRUNK="$(config_scalar git trunk_branch)"
|
|
163
|
+
fi
|
|
164
|
+
if [ -z "$TRUNK" ]; then
|
|
165
|
+
TRUNK="$(git -C "$MAIN" symbolic-ref --quiet refs/remotes/origin/HEAD 2>/dev/null | sed -E 's#^refs/remotes/origin/##' || true)"
|
|
166
|
+
fi
|
|
167
|
+
if [ -z "$TRUNK" ]; then
|
|
168
|
+
for cand in develop main master; do
|
|
169
|
+
if git -C "$MAIN" show-ref --verify --quiet "refs/heads/$cand"; then TRUNK="$cand"; break; fi
|
|
170
|
+
done
|
|
171
|
+
fi
|
|
172
|
+
[ -n "$TRUNK" ] || fail "git.trunk_branch unresolved (config + origin/HEAD + develop/main/master all empty)" 1
|
|
173
|
+
|
|
174
|
+
# --- resolve env_files (CSV → array; default .env.local,.env when unset) ----
|
|
175
|
+
ENV_FILES=()
|
|
176
|
+
if [ "$ENV_FILES_SET" = 1 ]; then
|
|
177
|
+
# explicit --env-files wins, even if empty (caller may intentionally pass none)
|
|
178
|
+
IFS=',' read -r -a ENV_FILES <<< "$ENV_FILES_ARG"
|
|
179
|
+
else
|
|
180
|
+
# not passed → try config stack.env_files (CSV/inline-list best-effort), else default
|
|
181
|
+
cfg_env="$(grep -A4 '^[[:space:]]*env_files:' "$MAIN/baldart.config.yml" 2>/dev/null | tr -d '[]"' | grep -oE "\.[A-Za-z0-9._/-]+" | tr '\n' ',' || true)"
|
|
182
|
+
if [ -n "$cfg_env" ]; then
|
|
183
|
+
IFS=',' read -r -a ENV_FILES <<< "$cfg_env"
|
|
184
|
+
else
|
|
185
|
+
ENV_FILES=( ".env.local" ".env" )
|
|
186
|
+
fi
|
|
187
|
+
fi
|
|
188
|
+
# strip whitespace from each entry
|
|
189
|
+
_clean=(); for ef in "${ENV_FILES[@]:-}"; do ef="$(printf '%s' "$ef" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//')"; [ -n "$ef" ] && _clean+=("$ef"); done
|
|
190
|
+
ENV_FILES=("${_clean[@]:-}")
|
|
191
|
+
|
|
192
|
+
# --- resolve backlog dir + toolchain commands ------------------------------
|
|
193
|
+
[ -n "$BACKLOG_DIR" ] || { BACKLOG_DIR="$(config_scalar paths backlog_dir)"; [ -n "$BACKLOG_DIR" ] || BACKLOG_DIR="backlog"; }
|
|
194
|
+
[ -n "$INSTALL_CMD" ] || INSTALL_CMD="npm install"
|
|
195
|
+
# toolchain.commands.* only when features.has_toolchain: true; else the defaults.
|
|
196
|
+
_tc() {
|
|
197
|
+
local cfg="$MAIN/baldart.config.yml"
|
|
198
|
+
grep -E '^[[:space:]]*has_toolchain:[[:space:]]*true' "$cfg" >/dev/null 2>&1 || return 0
|
|
199
|
+
grep -A20 '^toolchain:' "$cfg" 2>/dev/null | grep -A15 '^[[:space:]]*commands:' \
|
|
200
|
+
| grep -E "^[[:space:]]+$1:" | head -1 \
|
|
201
|
+
| sed -E "s/.*$1:[[:space:]]*\"?([^\"#]*)\"?.*/\1/" | sed -E 's/[[:space:]]+$//'
|
|
202
|
+
}
|
|
203
|
+
[ -n "$TC_TC" ] || TC_TC="$(_tc typecheck)"; [ -n "$TC_TC" ] || TC_TC="npx tsc --noEmit"
|
|
204
|
+
[ -n "$TC_LINT" ] || TC_LINT="$(_tc lint)"; [ -n "$TC_LINT" ] || TC_LINT="npx eslint --max-warnings=0 src/"
|
|
205
|
+
[ -n "$TC_BUILD" ] || TC_BUILD="$(_tc build)"; [ -n "$TC_BUILD" ] || TC_BUILD="npm run build"
|
|
206
|
+
|
|
207
|
+
# --- log sink --------------------------------------------------------------
|
|
208
|
+
[ -n "$LOG" ] || LOG="/tmp/wt-setup-${SLUG}.log"
|
|
209
|
+
: > "$LOG" 2>/dev/null || true
|
|
210
|
+
|
|
211
|
+
# --- worktree path is deterministic from the branch (R8) -------------------
|
|
212
|
+
WT_REL=".worktrees/$(printf '%s' "$BRANCH" | tr '/' '-')"
|
|
213
|
+
WT_ABS="$MAIN/$WT_REL"
|
|
214
|
+
M_WTPATH="$WT_ABS"
|
|
215
|
+
|
|
216
|
+
# --- lock helpers (mkdir is atomic on POSIX; same pattern as allocate-id.sh) -
|
|
217
|
+
WT_DIR="$MAIN/.worktrees"
|
|
218
|
+
LOCKDIR="$WT_DIR/.wt-setup.lock"
|
|
219
|
+
LOCK_STALE_SECONDS=30
|
|
220
|
+
LOCK_MAX_TRIES=150
|
|
221
|
+
file_mtime() { stat -f %m "$1" 2>/dev/null || stat -c %Y "$1" 2>/dev/null || echo 0; }
|
|
222
|
+
acquire_lock() {
|
|
223
|
+
local tries=0 now m age
|
|
224
|
+
mkdir -p "$WT_DIR" 2>/dev/null || true
|
|
225
|
+
while ! mkdir "$LOCKDIR" 2>/dev/null; do
|
|
226
|
+
if [ -d "$LOCKDIR" ]; then
|
|
227
|
+
now="$(date +%s)"; m="$(file_mtime "$LOCKDIR")"
|
|
228
|
+
case "$m" in ''|*[!0-9]*) m=0 ;; esac
|
|
229
|
+
age=$(( now - m ))
|
|
230
|
+
if [ "$m" -gt 0 ] && [ "$age" -gt "$LOCK_STALE_SECONDS" ]; then
|
|
231
|
+
err "WARN: stealing stale wt-setup lock (age ${age}s)"; rm -rf "$LOCKDIR" 2>/dev/null; continue
|
|
232
|
+
fi
|
|
233
|
+
fi
|
|
234
|
+
tries=$(( tries + 1 ))
|
|
235
|
+
[ "$tries" -gt "$LOCK_MAX_TRIES" ] && { err "WARN: could not acquire wt-setup lock — proceeding without it (port race possible)"; return 1; }
|
|
236
|
+
sleep 0.2
|
|
237
|
+
done
|
|
238
|
+
printf '%s\n' "$$" > "$LOCKDIR/pid" 2>/dev/null || true
|
|
239
|
+
return 0
|
|
240
|
+
}
|
|
241
|
+
release_lock() { rm -rf "$LOCKDIR" 2>/dev/null || true; }
|
|
242
|
+
|
|
243
|
+
# --- registry upsert via node (atomic tmp+rename; no jq dependency) ---------
|
|
244
|
+
# Fields are passed through the environment to avoid bash→JSON quoting hell.
|
|
245
|
+
REG="$WT_DIR/registry.json"
|
|
246
|
+
registry_upsert() {
|
|
247
|
+
REG_FILE="$REG" \
|
|
248
|
+
R_CARD="$CARD" R_CARDS="$CARDS" R_GROUP="$GROUP_PARENT" R_SLUG="$SLUG" \
|
|
249
|
+
R_BRANCH="$BRANCH" R_TRUNK="$TRUNK" R_MAIN="$MAIN" R_PATH="$WT_ABS" \
|
|
250
|
+
R_PORT="$1" R_CREATED="$M_CREATED" R_ENVSYNCED="$2" R_BUILDVERIFIED="$3" \
|
|
251
|
+
node -e '
|
|
252
|
+
const fs = require("fs");
|
|
253
|
+
const f = process.env.REG_FILE;
|
|
254
|
+
let reg = { worktrees: [] };
|
|
255
|
+
try { const j = JSON.parse(fs.readFileSync(f, "utf8")); if (j && Array.isArray(j.worktrees)) reg = j; } catch (_) {}
|
|
256
|
+
const csv = (s) => (s || "").split(",").map(x => x.trim()).filter(Boolean);
|
|
257
|
+
const num = (s) => { const n = parseInt(s, 10); return Number.isFinite(n) ? n : null; };
|
|
258
|
+
const bool = (s) => s === "true" ? true : s === "false" ? false : null;
|
|
259
|
+
const entry = {
|
|
260
|
+
card: process.env.R_CARD || null,
|
|
261
|
+
cards: csv(process.env.R_CARDS),
|
|
262
|
+
groupParent: process.env.R_GROUP || null,
|
|
263
|
+
slug: process.env.R_SLUG || null,
|
|
264
|
+
branch: process.env.R_BRANCH,
|
|
265
|
+
trunkBranch: process.env.R_TRUNK || null,
|
|
266
|
+
mainRoot: process.env.R_MAIN || null,
|
|
267
|
+
path: process.env.R_PATH,
|
|
268
|
+
port: num(process.env.R_PORT),
|
|
269
|
+
createdAt: process.env.R_CREATED && process.env.R_CREATED !== "-" ? process.env.R_CREATED : null,
|
|
270
|
+
envSyncedAt: process.env.R_ENVSYNCED && process.env.R_ENVSYNCED !== "-" ? process.env.R_ENVSYNCED : null,
|
|
271
|
+
buildVerified: bool(process.env.R_BUILDVERIFIED),
|
|
272
|
+
};
|
|
273
|
+
const i = reg.worktrees.findIndex(w => w && w.branch === entry.branch);
|
|
274
|
+
if (i >= 0) reg.worktrees[i] = { ...reg.worktrees[i], ...entry };
|
|
275
|
+
else reg.worktrees.push(entry);
|
|
276
|
+
const tmp = f + ".tmp";
|
|
277
|
+
fs.writeFileSync(tmp, JSON.stringify(reg, null, 2) + "\n");
|
|
278
|
+
fs.renameSync(tmp, f);
|
|
279
|
+
' 2>>"$LOG"
|
|
280
|
+
}
|
|
281
|
+
# ports currently held by registry entries (so concurrent setups never collide)
|
|
282
|
+
registry_used_ports() {
|
|
283
|
+
REG_FILE="$REG" node -e '
|
|
284
|
+
const fs = require("fs");
|
|
285
|
+
try {
|
|
286
|
+
const j = JSON.parse(fs.readFileSync(process.env.REG_FILE, "utf8"));
|
|
287
|
+
(j.worktrees || []).forEach(w => { if (w && Number.isFinite(w.port)) console.log(w.port); });
|
|
288
|
+
} catch (_) {}
|
|
289
|
+
' 2>/dev/null
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
# ===========================================================================
|
|
293
|
+
# 1. .gitignore safety (auto-heal, idempotent, non-blocking)
|
|
294
|
+
if [ ! -f "$MAIN/.gitignore" ] || ! grep -qE '^\.worktrees/?$' "$MAIN/.gitignore"; then
|
|
295
|
+
printf '\n.worktrees/\n' >> "$MAIN/.gitignore"
|
|
296
|
+
err "WARN: appended '.worktrees/' to $MAIN/.gitignore (was missing)."
|
|
297
|
+
fi
|
|
298
|
+
|
|
299
|
+
# 2. Fetch trunk (read-only; never touches the main repo HEAD)
|
|
300
|
+
git -C "$MAIN" fetch origin "$TRUNK" --quiet 2>>"$LOG" || err "WARN: git fetch origin $TRUNK failed — branching from possibly-stale local ref"
|
|
301
|
+
|
|
302
|
+
# 3. Collision guard — a code worktree is NOT silently re-creatable. The caller's
|
|
303
|
+
# 4a2 pre-check should have reset any orphan; if something is still here, fail
|
|
304
|
+
# loud rather than clobber.
|
|
305
|
+
EXISTING="$(git -C "$MAIN" worktree list --porcelain | awk -v b="refs/heads/$BRANCH" '$1=="worktree"{p=$2} $1=="branch"&&$2==b{print p}')"
|
|
306
|
+
if [ -n "$EXISTING" ]; then
|
|
307
|
+
fail "branch '$BRANCH' already has a worktree at '$EXISTING' (caller's idempotency pre-check should reset/resume before invoking this script)" 3
|
|
308
|
+
fi
|
|
309
|
+
if [ -e "$WT_ABS" ]; then
|
|
310
|
+
fail "worktree path '$WT_ABS' already exists on disk (stale orphan — caller must reset it first)" 3
|
|
311
|
+
fi
|
|
312
|
+
|
|
313
|
+
# 4. Create the worktree off origin/$TRUNK + stamp created_at AT creation
|
|
314
|
+
git -C "$MAIN" worktree add "$WT_REL" -b "$BRANCH" "origin/$TRUNK" >>"$LOG" 2>&1 \
|
|
315
|
+
|| fail "git worktree add failed (see $LOG)" 4
|
|
316
|
+
M_CREATED="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
317
|
+
# Provisional NON-terminal manifest — written the instant the worktree exists, so
|
|
318
|
+
# that if this process is KILLED mid-install/build (e.g. the caller's background
|
|
319
|
+
# timeout fires, or no `timeout` binary exists on this host so the build runs
|
|
320
|
+
# unbounded) the manifest on disk still reads status:error / baseline:- . The
|
|
321
|
+
# caller's disk gate (setup.md 6a) treats anything that is NOT a terminal
|
|
322
|
+
# `status: ok` + `baseline: pass` as NOT-verified → fallback, never a false pass
|
|
323
|
+
# on a half-built worktree whose node_modules merely happens to exist.
|
|
324
|
+
M_STATUS="error"; M_ERROR="setup incomplete (worktree created, install/build not finished — process killed or interrupted)"
|
|
325
|
+
write_manifest
|
|
326
|
+
|
|
327
|
+
# 4b. Sync untracked backlog cards from main (created during /prd, not yet on trunk)
|
|
328
|
+
if [ -d "$MAIN/$BACKLOG_DIR" ]; then
|
|
329
|
+
mkdir -p "$WT_ABS/$BACKLOG_DIR" 2>/dev/null || true
|
|
330
|
+
for CARD_FILE in "$MAIN/$BACKLOG_DIR"/*.yml; do
|
|
331
|
+
[ -e "$CARD_FILE" ] || continue
|
|
332
|
+
BN="$(basename "$CARD_FILE")"
|
|
333
|
+
[ -f "$WT_ABS/$BACKLOG_DIR/$BN" ] || cp "$CARD_FILE" "$WT_ABS/$BACKLOG_DIR/$BN" 2>/dev/null || true
|
|
334
|
+
done
|
|
335
|
+
fi
|
|
336
|
+
|
|
337
|
+
cd "$WT_ABS" || fail "cannot cd into worktree '$WT_ABS'" 4
|
|
338
|
+
|
|
339
|
+
# 5. Install dependencies — NON-INTERACTIVE (stdin closed, CI=1). An install that
|
|
340
|
+
# blocks on a prompt is the bash equivalent of the model "going idle" — the
|
|
341
|
+
# exact failure this script eliminates; never reintroduce it via a prompt.
|
|
342
|
+
err "… installing dependencies ($INSTALL_CMD)"
|
|
343
|
+
if ! CI=1 bash -c "$INSTALL_CMD" </dev/null >>"$LOG" 2>&1; then
|
|
344
|
+
M_BASELINE="fail"; M_BLOG="$LOG"
|
|
345
|
+
fail "dependency install failed ($INSTALL_CMD) — see $LOG" 5
|
|
346
|
+
fi
|
|
347
|
+
|
|
348
|
+
# 6. Copy gitignored env artifacts (stack.env_files). FILES via plain cp
|
|
349
|
+
# (dereferences symlinks; never cp -P), DIRS via cp -R. Missing FILE → WARN,
|
|
350
|
+
# never abort (the baseline build gate is the real enforcement).
|
|
351
|
+
primary_env=""
|
|
352
|
+
copied_any=0
|
|
353
|
+
for ef in "${ENV_FILES[@]:-}"; do
|
|
354
|
+
[ -n "$ef" ] || continue
|
|
355
|
+
src="$MAIN/$ef"; dest_dir="$(dirname "$ef")"
|
|
356
|
+
if [ -d "$src" ]; then
|
|
357
|
+
mkdir -p "$dest_dir"; rm -rf "$ef"
|
|
358
|
+
cp -R "$src" "$dest_dir"/ 2>/dev/null || true
|
|
359
|
+
[ -e "$ef" ] && copied_any=1 || err "WARN: env dir '$ef' present in main but copy failed."
|
|
360
|
+
elif [ -f "$src" ]; then
|
|
361
|
+
mkdir -p "$dest_dir"
|
|
362
|
+
if cp "$src" "$ef" 2>>"$LOG"; then copied_any=1; [ -z "$primary_env" ] && primary_env="$ef"; fi
|
|
363
|
+
else
|
|
364
|
+
err "WARN: env artifact '$ef' (stack.env_files) not found in $MAIN — the build may fail if it is required."
|
|
365
|
+
fi
|
|
366
|
+
done
|
|
367
|
+
ENVSYNCED="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
368
|
+
if [ "$copied_any" = 0 ] && [ "${#ENV_FILES[@]}" -gt 0 ]; then
|
|
369
|
+
err "WARN: NO env artifacts copied (none of: ${ENV_FILES[*]:-} exist in $MAIN)."
|
|
370
|
+
fi
|
|
371
|
+
|
|
372
|
+
# 7. Allocate a free port UNDER THE LOCK (registry-held ports + live listeners),
|
|
373
|
+
# then write a PROVISIONAL registry entry (buildVerified:false) so the port is
|
|
374
|
+
# reserved against concurrent /nw before the long build runs. The lock is held
|
|
375
|
+
# only across the cheap pick+write — never across npm install / build.
|
|
376
|
+
PORT=""
|
|
377
|
+
if acquire_lock; then trap release_lock EXIT; fi
|
|
378
|
+
USED="$(registry_used_ports | tr '\n' ' ')"
|
|
379
|
+
p="$PORT_MIN"
|
|
380
|
+
while [ "$p" -le "$PORT_MAX" ]; do
|
|
381
|
+
if printf ' %s ' "$USED" | grep -q " $p " ; then p=$(( p + 1 )); continue; fi
|
|
382
|
+
if lsof -i :"$p" -sTCP:LISTEN >/dev/null 2>&1; then p=$(( p + 1 )); continue; fi
|
|
383
|
+
PORT="$p"; break
|
|
384
|
+
done
|
|
385
|
+
if [ -z "$PORT" ]; then
|
|
386
|
+
release_lock; trap - EXIT
|
|
387
|
+
M_BASELINE="fail"; M_BLOG="$LOG"
|
|
388
|
+
fail "no free port in ${PORT_MIN}-${PORT_MAX} (deterministic — recreating cannot fix)" 6
|
|
389
|
+
fi
|
|
390
|
+
M_PORT="$PORT"
|
|
391
|
+
registry_upsert "$PORT" "$ENVSYNCED" "false"
|
|
392
|
+
release_lock; trap - EXIT
|
|
393
|
+
|
|
394
|
+
# 7b. Persist PORT into the primary copied env file (the dev server reads it).
|
|
395
|
+
PORT_ENV_FILE="${primary_env:-.env.local}"
|
|
396
|
+
if grep -q "^PORT=" "$PORT_ENV_FILE" 2>/dev/null; then
|
|
397
|
+
sed -i '' "s/^PORT=.*/PORT=$PORT/" "$PORT_ENV_FILE" 2>/dev/null \
|
|
398
|
+
|| sed -i "s/^PORT=.*/PORT=$PORT/" "$PORT_ENV_FILE" 2>/dev/null || true
|
|
399
|
+
else
|
|
400
|
+
echo "PORT=$PORT" >> "$PORT_ENV_FILE" 2>/dev/null || true
|
|
401
|
+
fi
|
|
402
|
+
|
|
403
|
+
# 8. Assert git hooks are ACTIVE (best-effort WARN, never blocks)
|
|
404
|
+
HOOK_SRC=""
|
|
405
|
+
for d in .husky .githooks scripts/git-hooks githooks; do
|
|
406
|
+
if [ -d "$d" ] && ls "$d" 2>/dev/null | grep -qE '^(pre-commit|pre-push|commit-msg)$'; then HOOK_SRC="$d"; break; fi
|
|
407
|
+
done
|
|
408
|
+
if [ -n "$HOOK_SRC" ]; then
|
|
409
|
+
ACTIVE="$(git config --get core.hooksPath || echo .git/hooks)"
|
|
410
|
+
case "$ACTIVE" in
|
|
411
|
+
"$HOOK_SRC"|"$HOOK_SRC"/*|.husky/_) : ;;
|
|
412
|
+
*) err "WARN: git hooks INACTIVE — versioned hooks in '$HOOK_SRC' but core.hooksPath='$ACTIVE'. Fix: git config core.hooksPath $HOOK_SRC" ;;
|
|
413
|
+
esac
|
|
414
|
+
fi
|
|
415
|
+
|
|
416
|
+
# 9. Baseline — tsc/lint are report-but-continue; build is STOP-on-fail.
|
|
417
|
+
# The build runs under a HARD timeout so a hung/interactive build cannot stall
|
|
418
|
+
# the caller's pre-flight barrier forever.
|
|
419
|
+
err "… baseline typecheck ($TC_TC)"
|
|
420
|
+
CI=1 bash -c "$TC_TC" </dev/null >>"$LOG" 2>&1 || err "WARN: typecheck failed (report-but-continue — trunk should be clean; see $LOG)"
|
|
421
|
+
err "… baseline lint ($TC_LINT)"
|
|
422
|
+
CI=1 bash -c "$TC_LINT" </dev/null >>"$LOG" 2>&1 || err "WARN: lint failed (report-but-continue; see $LOG)"
|
|
423
|
+
|
|
424
|
+
# Resolve a timeout binary (coreutils 'timeout', or gtimeout on macOS). If none,
|
|
425
|
+
# run without one (best-effort) and WARN — better than aborting setup.
|
|
426
|
+
TIMEOUT_BIN=""
|
|
427
|
+
command -v timeout >/dev/null 2>&1 && TIMEOUT_BIN="timeout"
|
|
428
|
+
[ -z "$TIMEOUT_BIN" ] && command -v gtimeout >/dev/null 2>&1 && TIMEOUT_BIN="gtimeout"
|
|
429
|
+
|
|
430
|
+
err "… baseline build ($TC_BUILD, timeout ${BUILD_TIMEOUT}s)"
|
|
431
|
+
if [ -n "$TIMEOUT_BIN" ]; then
|
|
432
|
+
CI=1 "$TIMEOUT_BIN" "$BUILD_TIMEOUT" bash -c "$TC_BUILD" </dev/null >>"$LOG" 2>&1
|
|
433
|
+
BUILD_RC=$?
|
|
434
|
+
else
|
|
435
|
+
err "WARN: no 'timeout'/'gtimeout' binary — running build WITHOUT a hard timeout."
|
|
436
|
+
CI=1 bash -c "$TC_BUILD" </dev/null >>"$LOG" 2>&1
|
|
437
|
+
BUILD_RC=$?
|
|
438
|
+
fi
|
|
439
|
+
|
|
440
|
+
if [ "$BUILD_RC" -eq 124 ] && [ -n "$TIMEOUT_BIN" ]; then
|
|
441
|
+
M_BASELINE="timeout"; M_BLOG="$LOG"
|
|
442
|
+
# leave buildVerified:false in the registry (already provisional)
|
|
443
|
+
fail "baseline build TIMED OUT after ${BUILD_TIMEOUT}s — partial log at $LOG" 7
|
|
444
|
+
elif [ "$BUILD_RC" -ne 0 ]; then
|
|
445
|
+
M_BASELINE="fail"; M_BLOG="$LOG"
|
|
446
|
+
fail "baseline build FAILED (do NOT fix here — the coder repairs it) — see $LOG" 7
|
|
447
|
+
fi
|
|
448
|
+
|
|
449
|
+
# 10. Build passed → flip buildVerified:true in the registry and finish clean.
|
|
450
|
+
if acquire_lock; then trap release_lock EXIT; fi
|
|
451
|
+
registry_upsert "$PORT" "$ENVSYNCED" "true"
|
|
452
|
+
release_lock; trap - EXIT
|
|
453
|
+
|
|
454
|
+
M_STATUS="ok"; M_ERROR="-"; M_BASELINE="pass"; M_BLOG="-"
|
|
455
|
+
write_manifest
|
|
456
|
+
err "✓ worktree ready: $WT_ABS (branch $BRANCH, port $PORT, baseline pass)"
|
|
457
|
+
exit 0
|
|
@@ -80,6 +80,7 @@ const FINDING = {
|
|
|
80
80
|
evidence: { type: 'string', description: 'exact file:line + code quote' },
|
|
81
81
|
minimal_fix_direction: { type: 'string' },
|
|
82
82
|
domain: { enum: ['doc', 'security', 'migration', 'code', 'perf', 'test'], description: 'Domain-Override routing bucket' },
|
|
83
|
+
requires_action: { type: 'boolean', description: 'false = a VERIFIED observation that needs NO change anywhere (concern cleared / safe-as-is / advisory) — recorded for audit, NEVER routed to a writer. Omit (⇒ actionable) when a concrete edit is needed. If a change is needed but not by you (out of scope), keep true and say so in minimal_fix_direction (it surfaces as residual).' },
|
|
83
84
|
},
|
|
84
85
|
}
|
|
85
86
|
const FINDINGS_SCHEMA = {
|
|
@@ -200,7 +201,8 @@ const codexPrompt =
|
|
|
200
201
|
`Return codexAvailable:false ONLY if $REVIEW_FILE ends up containing "CODEX_NOT_FOUND" or stays empty after the FULL 10-minute window — NEVER because a single Bash call returned slowly.\n\n` +
|
|
201
202
|
`${waveBrief}\n\n${baselineBrief}\n\n` +
|
|
202
203
|
`For each finding return: finding_id, title, severity (BLOCKER|HIGH|MEDIUM|LOW), confidence (0-100), evidence (exact file:line + code quote), minimal_fix_direction, and domain (doc|security|migration|code|perf|test). ` +
|
|
203
|
-
`Run the mandatory false-positive check on every finding and suppress the unconvincing ones (your findings are treated as already FP-validated).
|
|
204
|
+
`Run the mandatory false-positive check on every finding and suppress the unconvincing ones (your findings are treated as already FP-validated). ` +
|
|
205
|
+
`Then the actionability check: an observation you have VERIFIED needs NO change anywhere (the fix direction would be "no fix required" / "acceptable as-is" / "verified non-issue") is not work — set requires_action:false on it (it is recorded but never sent to a fixer), or simply do not emit it. Emit requires_action true/omitted ONLY when a concrete edit is needed. Set codexAvailable:true when the review ran.`
|
|
204
206
|
|
|
205
207
|
const tcGateLines = [
|
|
206
208
|
['lint', tcCmds.lint], ['typecheck', tcCmds.typecheck], ['test', tcCmds.test],
|
|
@@ -218,12 +220,14 @@ function simplifyPrompt(c) {
|
|
|
218
220
|
` • Quality — redundant state, parameter sprawl, copy-paste with slight variation, leaky abstractions, stringly-typed code where enums exist, unnecessary JSX nesting, WHAT-comments / narration.\n` +
|
|
219
221
|
` • Efficiency — redundant computation, duplicate API calls, N+1, missed concurrency, hot-path bloat, missing change-detection guards, unbounded structures.\n\n` +
|
|
220
222
|
`${cardScopeBrief(c)}\n\n${baselineBrief}\n\n` +
|
|
221
|
-
`Run a false-positive check on every finding and SUPPRESS the unconvincing ones (your surviving findings are treated as validated).
|
|
223
|
+
`Run a false-positive check on every finding and SUPPRESS the unconvincing ones (your surviving findings are treated as validated). ` +
|
|
224
|
+
`Then the actionability check: an observation you have VERIFIED needs NO change (the fix direction would be "no fix required" / "acceptable as-is" / "optional") is not work — set requires_action:false (recorded, never sent to a fixer), or do not emit it. Return findings with domain in {code, perf}. Flag only a finding you genuinely cannot resolve as confidence < 80.`
|
|
222
225
|
}
|
|
223
226
|
|
|
224
227
|
function securityPrompt(c) {
|
|
225
228
|
return `AppSec review (read-only) over ONE card's committed diff — auth, permissions, secrets, webhooks, file upload, infra, multi-tenant isolation, injection. Security-sensitive paths: ${highRisk.join(', ') || '(none configured)'}.\n\n${cardScopeBrief(c)}\n\n${baselineBrief}\n\n` +
|
|
226
|
-
`You OWN the security domain end-to-end: run the mandatory false-positive check on every finding yourself and SUPPRESS the unconvincing ones — your surviving findings are treated as validated and are NOT re-judged by another agent.
|
|
229
|
+
`You OWN the security domain end-to-end: run the mandatory false-positive check on every finding yourself and SUPPRESS the unconvincing ones — your surviving findings are treated as validated and are NOT re-judged by another agent. ` +
|
|
230
|
+
`Then the actionability check: a control you have VERIFIED as safe-as-is needs NO change (the fix direction would be "no fix required" / "verified safe") — set requires_action:false on it (recorded for the security audit trail, never sent to a fixer), or do not emit it. Flag only a finding you genuinely cannot resolve as confidence < 80. Return findings with domain in {security, migration}.`
|
|
227
231
|
}
|
|
228
232
|
|
|
229
233
|
// Build the finder fan-out. Per-card: simplify (if runSimplify), security (if hasSecurityFiles).
|
|
@@ -273,12 +277,19 @@ for (const item of findResults) {
|
|
|
273
277
|
if (!codexRan) {
|
|
274
278
|
codexEngine = 'code-reviewer (fallback)'
|
|
275
279
|
const fb = await agent(
|
|
276
|
-
`Codex was unavailable for this wave's code review. Run the FULL code review yourself over the wave diff, per ${protocolRef} (Phase 3.7).\n\n${waveBrief}\n\n${baselineBrief}\n\nReturn findings using the schema fields, with a self false-positive check applied (your findings are treated as validated).`,
|
|
280
|
+
`Codex was unavailable for this wave's code review. Run the FULL code review yourself over the wave diff, per ${protocolRef} (Phase 3.7).\n\n${waveBrief}\n\n${baselineBrief}\n\nReturn findings using the schema fields, with a self false-positive check applied (your findings are treated as validated). An observation you VERIFIED needs NO change (fix direction "no fix required" / "acceptable as-is") is not work — set requires_action:false (recorded, never sent to a fixer), or do not emit it.`,
|
|
277
281
|
{ label: 'code-reviewer (fallback)', phase: 'Discovery', agentType: 'code-reviewer', schema: FINDINGS_SCHEMA }
|
|
278
282
|
)
|
|
279
283
|
if (fb && Array.isArray(fb.findings)) raw.push(...fb.findings.map((f) => ({ ...f, source: 'code-reviewer', preValidated: true })))
|
|
280
284
|
}
|
|
281
285
|
|
|
286
|
+
// Globally-unique finding_id at the fan-in. Finders number F### INDEPENDENTLY, so a Codex F003 and
|
|
287
|
+
// a security F003 collide. The Fix-phase bookkeeping (appliedIds / unresolvedIds / codeResidual /
|
|
288
|
+
// bucket) keys on finding_id with FLAT Sets, so a collision cross-contaminates: one finder's
|
|
289
|
+
// `unresolved` id drags another finder's APPLIED same-id finding into residual (the empty-coder
|
|
290
|
+
// re-spawn bug). Prefix source + running index → uniqueness without losing the human-readable id.
|
|
291
|
+
raw.forEach((f, i) => { f.finding_id = `${f.source || 'src'}#${i}:${f.finding_id || ('F' + i)}` })
|
|
292
|
+
|
|
282
293
|
// ───────────────────────────────────────────────────────────────────────────
|
|
283
294
|
// Phase Verify — specialist-owned validation (parity with new-final-review.js F.4)
|
|
284
295
|
// ───────────────────────────────────────────────────────────────────────────
|
|
@@ -330,16 +341,29 @@ const isDoc = (f) => /doc|wiki|ssot|readme/.test(String(f.domain).toLowerCase())
|
|
|
330
341
|
// migration/test → coder), so match the exact 'security' domain, not the broader verifier regex.
|
|
331
342
|
const isSecurity = (f) => String(f.domain).toLowerCase() === 'security'
|
|
332
343
|
const isManual = (f) => f.classification === 'NEEDS_MANUAL_CONFIRMATION'
|
|
344
|
+
// Actionability gate: a VERIFIED finding that needs NO change must NOT reach a writer (it floods the
|
|
345
|
+
// fix pass with no-ops). Honoured ONLY for MED/LOW — a BLOCKER/HIGH is always actionable (the flag
|
|
346
|
+
// can never suppress a blocker; a cross-wave HIGH like "don't merge before the consumer cards" stays
|
|
347
|
+
// actionable → surfaces as residual). Signal: finder flag requires_action:false, OR a conservative
|
|
348
|
+
// regex backstop on the fix direction for when the model omitted the flag. Never a silent drop —
|
|
349
|
+
// no-action findings are logged + counted, just excluded from the writer partitions.
|
|
350
|
+
const NO_ACTION_RE = /(^|[.\s(])(no\s+(fix|change|action)\s+(is\s+)?(required|needed)|none\s+(needed|required)|not\s+a\s+(defect|bug|correctness\s+bug)|acceptable\s+as[-\s]is|verified\s+(safe|non[-\s]issue)|no\s+action\s+(needed|required))([.\s,;)]|$)/i
|
|
351
|
+
const isNoAction = (f) => (f.severity !== 'BLOCKER' && f.severity !== 'HIGH') &&
|
|
352
|
+
(f.requires_action === false || NO_ACTION_RE.test(String(f.minimal_fix_direction || '')))
|
|
333
353
|
// Partition `surviving` (= VERIFIED + NEEDS_MANUAL; FALSE_POSITIVE already dropped) with NO overlap:
|
|
334
354
|
// securityFix = VERIFIED security → security-reviewer applies (it owns the security invariants).
|
|
335
355
|
// actionable = VERIFIED non-doc non-security → the coder fixes these.
|
|
336
356
|
// docResidual = VERIFIED doc → the skill runs doc-reviewer post-E2E on final code.
|
|
337
357
|
// manualResidual= NEEDS_MANUAL any → human gate, owned by the skill (a doc-manual must NOT be
|
|
338
358
|
// silently auto-re-reviewed: it carries its needs-manual classification out).
|
|
339
|
-
|
|
340
|
-
const
|
|
341
|
-
const
|
|
359
|
+
// No-action VERIFIED findings: recorded + counted, excluded from every writer partition.
|
|
360
|
+
const noAction = surviving.filter((f) => f.classification === 'VERIFIED' && isNoAction(f))
|
|
361
|
+
const noActionSet = new Set(noAction)
|
|
362
|
+
const securityFix = surviving.filter((f) => f.classification === 'VERIFIED' && !isDoc(f) && isSecurity(f) && !noActionSet.has(f))
|
|
363
|
+
const actionable = surviving.filter((f) => f.classification === 'VERIFIED' && !isDoc(f) && !isSecurity(f) && !noActionSet.has(f))
|
|
364
|
+
const docResidual = surviving.filter((f) => f.classification === 'VERIFIED' && isDoc(f) && !noActionSet.has(f))
|
|
342
365
|
const manualResidual = surviving.filter(isManual)
|
|
366
|
+
for (const f of noAction) log(`Review: no-action finding recorded (not routed to a writer): [${f.finding_id}] ${f.severity} ${f.domain} — ${f.title} (${f.requires_action === false ? 'requires_action:false' : 'no-action-inferred'})`)
|
|
343
367
|
|
|
344
368
|
const SKIP_CHECKS = { lint: 'SKIP', tsc: 'SKIP', build: 'SKIP' }
|
|
345
369
|
|
|
@@ -360,7 +384,7 @@ async function applyFixPass(findings, writer, label, role) {
|
|
|
360
384
|
findings.map((f) => `- [${f.finding_id}] (${f.card || '?'} / ${f.domain} / ${f.severity}) ${f.title}\n evidence: ${f.evidence}\n direction: ${f.minimal_fix_direction}`).join('\n') +
|
|
361
385
|
`\n\nAfter applying: run \`${tc('lint', 'npm run lint')}\` and (when the project uses typescript) \`${tc('typecheck', 'npx tsc --noEmit')}\` and \`${tc('build', 'npm run build')}\` in the worktree. If a check fails because of an edit you made, fix the regression — at most 2 retries — staying within the allowed files. ` +
|
|
362
386
|
`Do NOT commit. Do NOT git stash (refs/stash is shared across worktrees). ` +
|
|
363
|
-
`Return: applied (finding_ids you fixed), unresolved (finding_ids you could NOT fix within the allowed files / 2 retries), and checks (PASS/FAIL/SKIP for lint, tsc, build).`
|
|
387
|
+
`Return: applied (finding_ids you fixed — list a finding here if you MADE the edit, even when the build stays red for reasons OUTSIDE this finding / your ownership, e.g. a known cross-wave cascade; the build status is reported separately in checks), unresolved (finding_ids you could NOT fix within the allowed files / 2 retries — i.e. the EDIT itself could not be made), and checks (PASS/FAIL/SKIP for lint, tsc, build).`
|
|
364
388
|
const r = await agent(fixBrief, { label, phase: 'Fix', agentType: writer, schema: FIX_SCHEMA })
|
|
365
389
|
// Normalize: the agent may die (null) or return a truthy object missing fields.
|
|
366
390
|
const res = (r && typeof r === 'object') ? r : { applied: [], unresolved: findings.map((f) => f.finding_id), checks: { ...SKIP_CHECKS } }
|
|
@@ -407,6 +431,7 @@ const summary = makeSummary({
|
|
|
407
431
|
cards: cards.length,
|
|
408
432
|
totalFindings: raw.length,
|
|
409
433
|
verified: surviving.filter((f) => f.classification === 'VERIFIED').length,
|
|
434
|
+
noAction: noAction.length, // VERIFIED-but-no-change findings: recorded, never sent to a writer
|
|
410
435
|
falsePositive: classified.filter((f) => f.classification === 'FALSE_POSITIVE').length,
|
|
411
436
|
needsManual: manualResidual.length,
|
|
412
437
|
fixesApplied: appliedIds.size,
|
|
@@ -418,7 +443,7 @@ const summary = makeSummary({
|
|
|
418
443
|
blockers: surviving.filter((f) => f.classification === 'VERIFIED' && f.severity === 'BLOCKER').length,
|
|
419
444
|
highs: surviving.filter((f) => f.classification === 'VERIFIED' && f.severity === 'HIGH').length,
|
|
420
445
|
})
|
|
421
|
-
log(`Wave review done: ${summary.fixesApplied} fixed, ${summary.codeResidual} code-residual, ${summary.docResidual} doc-residual, ${summary.needsManual} needs-manual, ${summary.failingGates.length} failing gate(s)${checksFailed ? ', post-fix checks FAILED' : ''}. Engine: ${codexEngine}.`)
|
|
446
|
+
log(`Wave review done: ${summary.fixesApplied} fixed, ${summary.codeResidual} code-residual, ${summary.docResidual} doc-residual, ${summary.noAction} no-action (recorded), ${summary.needsManual} needs-manual, ${summary.failingGates.length} failing gate(s)${checksFailed ? ', post-fix checks FAILED' : ''}. Engine: ${codexEngine}.`)
|
|
422
447
|
|
|
423
448
|
return { codexEngine, perCard, gateTable, summary }
|
|
424
449
|
|
|
@@ -426,7 +451,7 @@ return { codexEngine, perCard, gateTable, summary }
|
|
|
426
451
|
function asArr(x) { return Array.isArray(x) ? x.filter(Boolean) : [] }
|
|
427
452
|
function dedupe(xs) { return Array.from(new Set(asArr(xs))) }
|
|
428
453
|
function makeSummary(o) {
|
|
429
|
-
return Object.assign({ cards: 0, totalFindings: 0, verified: 0, falsePositive: 0, needsManual: 0, fixesApplied: 0, docResidual: 0, codeResidual: 0, qaRan: false, checksFailed: false, failingGates: [], blockers: 0, highs: 0 }, o || {})
|
|
454
|
+
return Object.assign({ cards: 0, totalFindings: 0, verified: 0, noAction: 0, falsePositive: 0, needsManual: 0, fixesApplied: 0, docResidual: 0, codeResidual: 0, qaRan: false, checksFailed: false, failingGates: [], blockers: 0, highs: 0 }, o || {})
|
|
430
455
|
}
|
|
431
456
|
function slimFinding(f) {
|
|
432
457
|
return { finding_id: f.finding_id, title: f.title, severity: f.severity, domain: f.domain, evidence: f.evidence, minimal_fix_direction: f.minimal_fix_direction, classification: f.classification, card: f.card }
|