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(close-task): **`close-task.sh` resolves `checkpoint.sh` instead of printing it bare.** The script ships at `claude-hooks/bin/checkpoint.sh` and reaches `~/.claude/bin` only via the opt-in `bin/install-claude-hooks.sh --basic`, so a project without the hooks got a `checkpoint.sh --write` step it could not run. `close-task.sh` now looks in PATH, then `~/.claude/bin`, then `$MXTK_ROOT/claude-hooks/bin`, and prints the full path it found, single-quoted so a path with spaces survives copy-paste. If none is found it prints `checkpoint.sh not installed — run <toolkit>/bin/install-claude-hooks.sh --basic, or skip: RESUME.md is the resume source` and carries on; the exit code still means only "writes owed". `close-the-loop.md` step 4 says the same. The `~/.claude/bin` copy (`claude-hooks/bin/close-task.sh`) now prints its sibling `checkpoint.sh` by full quoted path, because `~/.claude/bin` is usually not on PATH. Fixture `tests/wave2/test-close-task-checkpoint.sh` (14 cases, including a space and `'` in HOME and the toolkit path; 11 fail on master). Field evidence: the reporter's existing-app run, where the closing agent had to skip the step with no pointer. — joshuamoesa (#358)
- process(build): **Stage 5 drafts the ready set in parallel and applies serially.** The runbook dispatch row said "sequential within a module", which serialised drafting even for independent scripts; it now points to `iterative-build-loop.md` → Gate cost, the single statement of the rule. The unit is the build-plan row: every `BUILD` row whose "Depends on" rows are all applied, minus overlapping writers (two rows that replace, alter or drop+create the same document never share a ready set, and no row is drafted while it overlaps a queued unapplied one; `brd-to-build-plan.md` has Produces name every document a row writes), runs at once, its scripts in order, one `mdl-agent` per script. Applies stay one at a time through `bin/exec.sh`, and the ready set is recomputed after each. A queued draft made before a sibling apply landed is re-checked with `mxcli diff --format struct` (preflight STOP rule 23); any removal line (checked against `describe` per BUG-146) or STOP rule 21 target means re-draft, never patch, because a stale restatement of a shared document is exactly rule 21's silent seed-call loss and passes `check --references`. Report `ready set N, drafted N, applied k`; the `mdl-agent` template says a drafter never applies and never depends on an unapplied draft. Field evidence: none yet. — MendixMau
- fix(platform-link): **CLI commits now show their changed documents in Studio Pro History.** Studio Pro's History → Model changes pane reads a `ModelChanges` array from the commit's `refs/notes/mx_metadata` note. Trap 6c's hand-written template set it to `[]`, so every mxcli/agent commit showed "Model changes 0" (field project: 270 of 270 notes empty). New `project-bin/mx-metadata-note.py` (stdlib only) computes the array from the commit's changed `.mxunit` files, in Studio Pro's entry shape: Status, UnitID, UnitType, UnitName and Module (walked up the `.mpr` Unit tree). It has `changes` / `write` / `backfill` / `sync <remote>` subcommands. `install-project-hooks.sh` now also installs post-commit and post-rewrite hooks that write the note and never block, and `--check` covers them, so `sync-project.sh` rolls them out to wired projects. Notes are published with `mx-metadata-note.py sync <remote>` after the branch push. Push config is never touched. With mxcli v0.26+ in the project root the hooks call `mxcli git note --write` instead (mendixlabs/mxcli#1337); the Python script is the fallback for older pins, and on 160 field commits both wrote the same 3,594 entries. Trap 6c rewritten. Measured against Studio Pro's own notes, 51 commits in 5 apps: 3387/3389 entries exact on all five fields. Field run on a 357-commit Mendix 11.15.0 app: 158 notes backfilled and pushed, and Studio Pro History lists the documents per revision. Review fixes: an earlier revision added `HEAD` + the notes ref to `remote.<r>.push`. It was dropped because any push refspec replaces `push.default`. A branch tracking a differently named upstream got a new same-named branch on Team Server where `simple` refuses, a missing local notes ref failed the whole push, and nobody knows whether Studio Pro's own push reads that config. The installer now only fetches the remote's notes (non-forced). It removes that exact `HEAD` + notes pair if present, and leaves and reports any other entry. `sync` handles a remote with no notes ref (it used to exit 1) and retries a rejected push once after re-merging. A note that already lists changes (Studio Pro's) is never overwritten by `write`/`backfill` without `--force`, and `sync` keeps a colleague's non-empty note over ours. The mxcli probe greps `git note --help` for `--write`. Hooks and docs call the script through `resolve_py`. Fixture `tests/wave2/test-mx-metadata-note.sh` (40 cases, over real captured template units; single- and two-tree layouts, amend, backfill and `--force`, push exit codes identical to a no-hooks clone, leftover-refspec cleanup, sync to an empty remote and its retry, notes fetch at install, a diverged notes merge; green under `init.defaultBranch` master and main). — field project (Mendix 11.15 app), MendixMau
- learn(fixture-seeding): **explore the data structure first, choose rows by use case, scan actions for resets before any run.** Two seeding runs on public template apps: seeding raised journey passes 33 -> 58 and 20 -> 51 of the same sets (FAIL 93 -> 68, 163 -> 132; INVALID unchanged), and a "Reset all demodata" journey wiped accounts so 104 of 127 journeys reported `The username or password you entered is incorrect`, identical on empty and seeded databases. Skill gains three questions a seed must answer, a use-case checklist, a destructive-action scan, a generic traps table with symptoms, measured expectations and a denominator checklist; routing trigger now fires on seed/test/demo data and empty pages. Independent review fixes: object ids follow the Mendix bigint pattern with a business-key guard (no uuids), associations are seeded where the page's path stores them, four new traps (enum names, inheritance chain, calculated attributes, view/external entities), and the port-offset and container-label traps moved to `harness-architecture.md`. — MendixMau
Expand Down
12 changes: 10 additions & 2 deletions claude-hooks/bin/close-task.sh
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,16 @@ cat <<'TABLE'
Rules: verify before filing · one place only, link don't copy · a committed script is not
an executed script.

### Then
checkpoint.sh --write # and fill the Narrative (append-only history)
TABLE

# checkpoint.sh is installed beside this script in ~/.claude/bin, which is usually not on PATH:
# print the sibling's full path, single-quoted so a path with spaces survives copy-paste (#358).
shq() { printf "'%s'" "$(printf '%s' "$1" | sed "s/'/'\\\\''/g")"; }
_ck="$(cd "$(dirname "$0")" 2>/dev/null && pwd)/checkpoint.sh"
if [ -f "$_ck" ]; then _ck=$(shq "$_ck"); else _ck="checkpoint.sh"; fi
echo "### Then"
echo " $_ck --write # and fill the Narrative (append-only history)"
cat <<'TABLE'
OVERWRITE docs/progress/RESUME.md # <= 60 lines: where we are, next actions,
# do-not-lose, and which file to read for what.
# This is the ONLY file read after /clear.
Expand Down
27 changes: 25 additions & 2 deletions project-bin/close-task.sh
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,31 @@ cat <<'TABLE'
Rules: verify before filing · one place only, link don't copy · a committed script is not
an executed script.

### Then
checkpoint.sh --write # and fill the Narrative (append-only history)
TABLE

# ---- checkpoint.sh: resolve it, never call it bare ---------------------
# checkpoint.sh ships in the toolkit at claude-hooks/bin/ and reaches ~/.claude/bin only via the
# opt-in `bin/install-claude-hooks.sh --basic`; init-project does not install it. Printing it bare
# sent agents to a command that did not exist on their machine (#358). Look in PATH, then
# ~/.claude/bin, then the toolkit clone; print the full path found, or say how to get it. A
# missing checkpoint.sh is not an owed write: it never changes the exit code.
CKPT_BIN=$(command -v checkpoint.sh 2>/dev/null)
for _c in "${HOME:-/nonexistent}/.claude/bin/checkpoint.sh" "$MXTK_ROOT/claude-hooks/bin/checkpoint.sh"; do
[ -n "$CKPT_BIN" ] && break
[ -f "$_c" ] && CKPT_BIN="$_c"
done
# Single-quote a path so the printed command survives copy-paste with spaces (e.g. a HOME such as
# "John Doe"); an embedded ' becomes '\''. sed, not ${x//}, for bash 3.2.
shq() { printf "'%s'" "$(printf '%s' "$1" | sed "s/'/'\\\\''/g")"; }
echo "### Then"
if [ -n "$CKPT_BIN" ]; then
echo " $(shq "$CKPT_BIN") --write # and fill the Narrative (append-only history)"
else
if [ -d "$MXTK_ROOT" ]; then _inst=$(shq "$MXTK_ROOT/bin/install-claude-hooks.sh")
else _inst="<toolkit>/bin/install-claude-hooks.sh"; fi
echo " # checkpoint.sh not installed — run $_inst --basic, or skip: RESUME.md is the resume source"
fi
cat <<'TABLE'
OVERWRITE docs/progress/RESUME.md # <= 60 lines: where we are, next actions,
# do-not-lose, and which file to read for what.
# This is the ONLY file read after /clear.
Expand Down
3 changes: 3 additions & 0 deletions skills/close-the-loop.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,9 @@ forgotten* — they are on disk, not in working memory.
3. **Drain the `UNFILED:` lines** from the previous checkpoint — route or delete.
4. **Checkpoint**: `checkpoint.sh --write`, fill the Narrative, including any new
`UNFILED:` lines. This file is append-only history.
`close-task.sh` prints the full path it found (PATH, then `~/.claude/bin`, then the
toolkit's `claude-hooks/bin/`). If it says `checkpoint.sh not installed`: run
`<toolkit>/bin/install-claude-hooks.sh --basic`, or skip this step: `RESUME.md` is the resume source.
5. **`mxcli brain check -p <app.mpr>`** — `capture` and `promote` do not validate anchors;
only `check` does. Exit 1 means a promoted entry points at something the model no longer
has: fix the anchor or `brain drop` the entry before anything is committed.
Expand Down
118 changes: 118 additions & 0 deletions tests/wave2/test-close-task-checkpoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
#!/usr/bin/env bash
# Fixture for #358: close-task.sh must resolve checkpoint.sh, never print it bare.
#
# usage: test-close-task-checkpoint.sh /path/to/project-bin/close-task.sh
#
# checkpoint.sh lives in the toolkit at claude-hooks/bin/ and reaches ~/.claude/bin only via
# the opt-in `bin/install-claude-hooks.sh --basic`. The old "### Then" block printed
# `checkpoint.sh --write` unconditionally, so a project without the hooks got a command that
# did not exist. Lookup order: PATH, ~/.claude/bin, $MXTK_ROOT/claude-hooks/bin.
#
# T1 found on PATH -> prints that full path
# T2 found in ~/.claude/bin -> prints that full path (fake HOME)
# T3 found in toolkit -> prints $MXTK_ROOT/claude-hooks/bin/checkpoint.sh
# T4 PATH beats ~/.claude/bin and toolkit
# T5 not found -> install pointer, rest of the block still printed,
# exit code unchanged (0 on a clean project)
# T6 not found, writes owed -> still exit 1 (owed-write semantics kept)
# T7 paths with a space (and a ') in HOME / toolkit -> printed single-quoted, and the printed
# command actually runs when pasted into a shell
# T8 the claude-hooks/bin/close-task.sh copy prints its sibling checkpoint.sh full path, quoted

set -uo pipefail

SUT="${1:?usage: test-close-task-checkpoint.sh /path/to/project-bin/close-task.sh}"
case "$SUT" in /*) ;; *) SUT="$PWD/$SUT" ;; esac
[ -f "$SUT" ] || { echo "no such file: $SUT" >&2; exit 2; }

TMP=$(mktemp -d)
trap 'rm -rf "$TMP"' EXIT

pass=0; fail=0
ok() { pass=$((pass+1)); echo " PASS $1"; }
bad() { fail=$((fail+1)); echo " FAIL $1"; [ -n "${2:-}" ] && printf '%s\n' "$2" | sed 's/^/ | /'; }

# A project that is not a git repo, with the script installed as bin/close-task.sh (no
# toolkit next to it, so the "installed-from" fallback cannot resolve a toolkit root).
PROJ="$TMP/proj"; mkdir -p "$PROJ/bin"
cp "$SUT" "$PROJ/bin/close-task.sh"; chmod +x "$PROJ/bin/close-task.sh"

# A clean PATH: system tools only, no checkpoint.sh.
SYSPATH="/usr/bin:/bin"
mk_ckpt() { mkdir -p "$1"; printf '#!/bin/sh\nexit 0\n' > "$1/checkpoint.sh"; chmod +x "$1/checkpoint.sh"; }

EMPTYHOME="$TMP/home-empty"; mkdir -p "$EMPTYHOME"
NOTK="$TMP/no-toolkit"; mkdir -p "$NOTK"

run() { # $1=HOME $2=PATH $3=MXTK_ROOT [args...]
local h="$1" p="$2" m="$3"; shift 3
( cd "$PROJ" && env -i HOME="$h" PATH="$p" MXTK_ROOT="$m" GIT_CEILING_DIRECTORIES="$TMP" \
bash "$PROJ/bin/close-task.sh" "$@" 2>&1 )
}

echo "T1 found on PATH"
mk_ckpt "$TMP/pathbin"
out=$(run "$EMPTYHOME" "$TMP/pathbin:$SYSPATH" "$NOTK")
grep -qF " '$TMP/pathbin/checkpoint.sh' --write" <<<"$out" && ok "T1 prints PATH hit" || bad "T1 prints PATH hit" "$(grep -n -A2 '### Then' <<<"$out")"

echo "T2 found in ~/.claude/bin"
H2="$TMP/home2"; mk_ckpt "$H2/.claude/bin"
out=$(run "$H2" "$SYSPATH" "$NOTK")
grep -qF " '$H2/.claude/bin/checkpoint.sh' --write" <<<"$out" && ok "T2 prints ~/.claude/bin hit" || bad "T2 prints ~/.claude/bin hit" "$(grep -n -A2 '### Then' <<<"$out")"

echo "T3 found in toolkit claude-hooks/bin"
TK="$TMP/toolkit"; mk_ckpt "$TK/claude-hooks/bin"
out=$(run "$EMPTYHOME" "$SYSPATH" "$TK")
grep -qF " '$TK/claude-hooks/bin/checkpoint.sh' --write" <<<"$out" && ok "T3 prints toolkit hit" || bad "T3 prints toolkit hit" "$(grep -n -A2 '### Then' <<<"$out")"

echo "T4 lookup order"
out=$(run "$H2" "$TMP/pathbin:$SYSPATH" "$TK")
grep -qF " '$TMP/pathbin/checkpoint.sh' --write" <<<"$out" && ok "T4a PATH wins" || bad "T4a PATH wins" "$(grep -n -A2 '### Then' <<<"$out")"
out=$(run "$H2" "$SYSPATH" "$TK")
grep -qF " '$H2/.claude/bin/checkpoint.sh' --write" <<<"$out" && ok "T4b ~/.claude/bin beats toolkit" || bad "T4b ~/.claude/bin beats toolkit" "$(grep -n -A2 '### Then' <<<"$out")"

echo "T5 not found"
out=$(run "$EMPTYHOME" "$SYSPATH" "$NOTK"); rc=$?
grep -qF "checkpoint.sh not installed — run '$NOTK/bin/install-claude-hooks.sh' --basic, or skip: RESUME.md is the resume source" <<<"$out" \
&& ok "T5a install pointer printed" || bad "T5a install pointer printed" "$(grep -n -A2 '### Then' <<<"$out")"
grep -qE '^ +checkpoint\.sh --write' <<<"$out" && bad "T5b no bare checkpoint.sh line" "$(grep -n 'checkpoint.sh --write' <<<"$out")" || ok "T5b no bare checkpoint.sh line"
grep -qF 'OVERWRITE docs/progress/RESUME.md' <<<"$out" && grep -qF 'Shared toolkit:' <<<"$out" \
&& ok "T5c rest of the block still printed" || bad "T5c rest of the block still printed"
[ "$rc" = 0 ] && ok "T5d exit 0 on a clean project" || bad "T5d exit 0 on a clean project (got $rc)"

echo "T6 not found, writes owed"
run "$EMPTYHOME" "$SYSPATH" "$NOTK" --add "exec 010-foo.mdl" >/dev/null
out=$(run "$EMPTYHOME" "$SYSPATH" "$NOTK"); rc=$?
[ "$rc" = 1 ] && ok "T6 exit 1 when writes owed" || bad "T6 exit 1 when writes owed (got $rc)"

echo "T7 paths with spaces"
# Run the printed line through a shell: the path must resolve to the stub we made.
runs_line() { local line; line=$(grep -m1 -E "^ '.*' --write" <<<"$1") || return 1
( eval "${line%%#*}" ) >/dev/null 2>&1; }
H7="$TMP/John Doe"; mk_ckpt "$H7/.claude/bin"
out=$(run "$H7" "$SYSPATH" "$NOTK")
grep -qF " '$H7/.claude/bin/checkpoint.sh' --write" <<<"$out" && runs_line "$out" \
&& ok "T7a space in HOME: quoted and runnable" || bad "T7a space in HOME: quoted and runnable" "$(grep -n -A1 '### Then' <<<"$out")"
TK7="$TMP/my tool's kit"; mk_ckpt "$TK7/claude-hooks/bin"
out=$(run "$EMPTYHOME" "$SYSPATH" "$TK7")
grep -qF " '$TMP/my tool'\\''s kit/claude-hooks/bin/checkpoint.sh' --write" <<<"$out" && runs_line "$out" \
&& ok "T7b space and ' in toolkit path: quoted and runnable" || bad "T7b space and ' in toolkit path: quoted and runnable" "$(grep -n -A1 '### Then' <<<"$out")"
TK7N="$TMP/no kit here"; mkdir -p "$TK7N"
out=$(run "$EMPTYHOME" "$SYSPATH" "$TK7N")
grep -qF "run '$TK7N/bin/install-claude-hooks.sh' --basic" <<<"$out" \
&& ok "T7c install hint quoted" || bad "T7c install hint quoted" "$(grep -n -A1 '### Then' <<<"$out")"

echo "T8 claude-hooks copy"
HOOKS="$(cd "$(dirname "$SUT")/.." && pwd)/claude-hooks/bin/close-task.sh"
if [ -f "$HOOKS" ]; then
B8="$H7/.claude/bin"; cp "$HOOKS" "$B8/close-task.sh"
out=$( cd "$PROJ" && env -i HOME="$H7" PATH="$SYSPATH" GIT_CEILING_DIRECTORIES="$TMP" bash "$B8/close-task.sh" 2>&1 )
grep -qF " '$B8/checkpoint.sh' --write" <<<"$out" && runs_line "$out" \
&& ok "T8 sibling path printed, quoted" || bad "T8 sibling path printed, quoted" "$(grep -n -A1 '### Then' <<<"$out")"
else
bad "T8 claude-hooks/bin/close-task.sh not found next to $SUT"
fi

echo
echo "test-close-task-checkpoint: $pass passed, $fail failed"
[ "$fail" = 0 ]
Loading