Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Sections dated before 2026-09-19 predate the cycle and stay as they are. Named m

## Unreleased

- fix(harvest-learnings): **a bug-log heading the toolkit log already carries is no longer drafted again when it has `~`, `*` or a backtick in its first 30 characters after the `BUG-nn:` tag.** Section 1 strips those characters from a project heading before it matches the heading's first 30 characters after the tag against `bug-logs/mxcli-bugs.md`, but searched the log as written, so the stripped text was never a substring of a heading like ``BUG-01: `alter entity drop attribute` causes …``. The log is now stripped the same way before the search, and the search takes `-e`, because stripping can leave a heading that starts with `--` (BUG-26, BUG-54), which `grep` read as an option. Reproduced with the script at 95e1b30 on a project whose bug log holds copies of all 223 headings in the toolkit log: it drafts 103 of them, and 0 after the fix. Pinned by `tests/wave2/test-harvest-learnings.sh` T6 (4 headings taken from the log itself; T6 fails on 95e1b30) — saingge
- process(design-artifacts, conversion-runbook): **Stage 3 runs in parallel by default, from one brief and a mechanical task list**: the lead fixes modules, canonical names and design tokens, writes `docs/progress/stage3-brief.md`, and takes the tasks from the stage-3 lines of `bin/lib/artifact-check.sh` (so no artifact is forgotten); one subagent per 1–2 screens or artifact group, a shared opening line so the start is cached, mechanical tasks on the cheap tier, 5-line replies, the lead the only writer of `PROJECT.md`. The runbook's Stage 3 fan-out row points there, and its Stage 2 row names `facts-lock.sh resolve`. Measured on one project (single runs, Sonnet, 250k auto-compact): serial 13 min / $3.60, parallel 10 min / $4.44, parallel with a brief 6 min / $4.32; 0 name mismatches in the parallel runs (serial dropped 3 entities); both parallel runs forgot the process map and swimlanes, which the task list now covers. Baseline words 64,004 before and after — Stage 3 experiment 2026-10-28
- fix(facts-lock, gate-check): **parallel BRDs keep one spelling per identifier**: `bin/facts-lock.sh <project> resolve` freezes each casing conflict (`WIPStatus`/`WipStatus`) whose majority spelling is used by strictly more BRD sources than any other and records the count in the lock (`resolvedBy`); a tie stays contested, and `build` still never picks on its own. Stage 2 of `gate-check.sh` now reads the lock once there are 2+ BRDs: a BRD that contradicts a frozen identifier FAILs and is quoted; no lock yet is a note, never a FAIL. Measured cause: 20 parallel BRDs on PROJECT-C carried 10 casing conflicts into Stage 3 as open questions, and facts-lock.sh, documented only as advice, reached 1 of 6 projects. Fixtures: `test-facts-lock.sh` resolve cases (4), `test-bug03-gates.sh` T19 (2 of 4 fail on the previous gate-check) — speed/parallel experiments 2026-10-28
- fix(row-groups): **a build plan whose number and Kind cells are formatted as code (`` `BUILD` ``, `` `12` ``) is read like a plain one**: before, it read 0 BUILD rows, so with `Row groups: on` grouping silently did nothing (the fast-lane agent in the 2026-10-28 speed experiment had to strip the backticks by hand). Fixture: `test-row-groups.sh` G5 (both cases fail on the previous row-groups.sh) — speed experiment 2026-10-28
Expand Down
10 changes: 8 additions & 2 deletions bin/harvest-learnings.sh
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ BUGS_OUT="$INBOX/$STAMP-$PROJECT_NAME-bugs.md"
FOUND_BUGS=0
if [ -d "$PROJECT_DIR/bug-logs" ]; then
TMP="$(mktemp)"
# A heading is stripped of ~ * ` before it is matched (below), so the log it is matched against is
# stripped the same way. Without that, a heading the toolkit log carries with a backtick in its
# first 30 characters never matched, and was drafted again.
BUGLOG_PLAIN="$(mktemp)"
tr -d '~*`' < "$TOOLKIT_BUGLOG" > "$BUGLOG_PLAIN" 2>/dev/null
find "$PROJECT_DIR/bug-logs" -name '*.md' -type f | sort | while IFS= read -r f; do
# Split the file on '## ' headings; test each heading's distinctive text against the
# toolkit log. awk emits candidate sections whole, delimited for the outer loop.
Expand All @@ -86,7 +91,8 @@ if [ -d "$PROJECT_DIR/bug-logs" ]; then
# measured: personal-toolkit and the toolkit disagree from 82 up), then take 30 chars.
slice="$(printf '%s' "$heading" | sed -E 's/^BUG-[0-9]+[: ]*//' | cut -c1-30)"
[ -n "$slice" ] || continue
if ! grep -qiF "$slice" "$TOOLKIT_BUGLOG" 2>/dev/null; then
# -e: stripping can leave a slice that starts with "-", which grep would read as an option.
if ! grep -qiF -e "$slice" "$BUGLOG_PLAIN" 2>/dev/null; then
{ printf '## [candidate — from %s] %s\n' "${f#$PROJECT_DIR/}" "$heading"
printf '%s' "$section" | tail -n +2
printf '\n'; } >> "$TMP"
Expand All @@ -99,7 +105,7 @@ if [ -d "$PROJECT_DIR/bug-logs" ]; then
FOUND_BUGS=1; WROTE=$((WROTE + 1))
echo " wrote ${BUGS_OUT#$TOOLKIT_ROOT/}"
fi
rm -f "$TMP"
rm -f "$TMP" "$BUGLOG_PLAIN"
fi
[ "$FOUND_BUGS" -eq 1 ] || echo " bug-logs: nothing the toolkit log doesn't already carry (or no bug-logs/)"

Expand Down
40 changes: 38 additions & 2 deletions tests/wave2/test-harvest-learnings.sh
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
# T5 section 1 drafts every '## ' section of every bug-log file, not only the first of each,
# and still skips a later section whose heading the toolkit log carries. Fixture:
# fixtures/harvest-learnings/ (see its CAPTURE.md).
# T6 a heading the toolkit log carries is not drafted again when it has ~ * or ` in its first
# 30 characters, or starts with a dash once those are stripped.
#
# Usage: bash tests/wave2/test-harvest-learnings.sh /path/to/harvest-learnings.sh

Expand Down Expand Up @@ -196,8 +198,7 @@ else
EXPECTED5="$(cat "$FIX5"/bug-logs/*.md | grep '^## ' | sed 's/^## *//; s/[~*`]//g')"
N5="$(printf '%s\n' "$EXPECTED5" | grep -c .)"
# A later section whose heading the toolkit log already carries must still be filtered out.
# Taken with no ~ * or ` in it: the harvester strips those from a heading before matching it
# against the log, so a heading that has them never matches and is drafted every time.
# A plain heading is enough here; T6 covers the ones with ~ * or `.
KNOWN5="$(grep -m1 -E '^## BUG-[0-9]+: [^~*`]+$' "$CLONE/bug-logs/mxcli-bugs.md")"
if [ -n "$KNOWN5" ]; then
printf '\n%s\n\nA section the toolkit log already carries; never drafted.\n' "$KNOWN5" \
Expand Down Expand Up @@ -235,6 +236,41 @@ else
fi
fi

# ---------------------------------------------------------------------------------------------
# T6: a heading the toolkit log carries is not drafted again when it has ~ * or ` (or starts with
# "-" once stripped). The harvester strips ~ * ` from a project heading before matching it, but searched the raw
# log, which keeps them, so the stripped slice never matched: 103 of the log's own 223 headings were
# drafted again from a project that held nothing but copies of them. A heading that starts with a
# backtick and then "--" starts with "--" once stripped; grep read the slice as an option, which
# also counted as "not found". The headings come from the
# clone's own log, so the check follows the log instead of a copy of it.
# ---------------------------------------------------------------------------------------------
echo "== T6: headings with ~ * \` or a dash left first are recognised as known =="

if [ ! -f "$CLONE/bin/harvest-learnings.sh" ] || [ ! -f "$CLONE/bug-logs/mxcli-bugs.md" ]; then
bad "control: no isolated clone with bug-logs/mxcli-bugs.md (see T1) — fixture cannot run"
else
P6="$WORK/knownproj"; mkdir -p "$P6/bug-logs"
{ grep -m3 -E '^## BUG-[0-9]+: .{0,26}[~*`]' "$CLONE/bug-logs/mxcli-bugs.md"
grep -m1 -E '^## BUG-[0-9]+: [~*`]-' "$CLONE/bug-logs/mxcli-bugs.md"; } \
| awk '{print; print ""; print "Copy of a heading the toolkit log carries."; print ""}' > "$P6/bug-logs/known.md"
N6="$(grep -c '^## ' "$P6/bug-logs/known.md")"
if [ "$N6" -lt 4 ]; then
bad "control: the clone's log gave $N6 of the 4 headings this check needs (3 with ~ * \` in their first 30 characters, 1 that starts with a dash once stripped)"
else
printf '\n## BUG-900: a heading no toolkit log has\n\nGenuinely new.\n' >> "$P6/bug-logs/known.md"
OUT6="$(bash "$CLONE/bin/harvest-learnings.sh" "$P6" --to "$WORK/inbox6" 2>&1)"; RC6=$?
DRAFT6="$WORK/inbox6/$STAMP-knownproj-bugs.md"
[ "$RC6" -eq 0 ] && ok "exit 0" || bad "exit $RC6, expected 0" "$OUT6"
GOT6="$(grep '^## \[candidate' "$DRAFT6" 2>/dev/null)"
if [ "$(printf '%s\n' "$GOT6" | grep -c .)" -eq 1 ] && printf '%s' "$GOT6" | grep -qF 'BUG-900: a heading no toolkit log has'; then
ok "$N6 known headings (with ~ * \` in them, or a dash left first) not drafted; the new one is"
else
bad "expected only BUG-900 drafted; got:" "$GOT6"
fi
fi
fi

echo ""
echo "PASS=$PASS FAIL=$FAIL ($WORK)"
# Every draft this fixture produced lives under $WORK (the clone's contrib/inbox, or the no-git
Expand Down