diff options
| -rw-r--r-- | .ai/protocols.org | 2 | ||||
| -rw-r--r-- | claude-rules/working-files.md | 22 | ||||
| -rw-r--r-- | claude-templates/.ai/protocols.org | 2 | ||||
| -rwxr-xr-x | scripts/install-ai.sh | 18 | ||||
| -rwxr-xr-x | scripts/sweep-gitignore-tooling.sh | 32 | ||||
| -rw-r--r-- | scripts/tests/install-ai.bats | 36 | ||||
| -rw-r--r-- | scripts/tests/sweep-gitignore-tooling.bats | 41 | ||||
| -rw-r--r-- | todo.org | 20 |
8 files changed, 164 insertions, 9 deletions
diff --git a/.ai/protocols.org b/.ai/protocols.org index 98cd934..d89e345 100644 --- a/.ai/protocols.org +++ b/.ai/protocols.org @@ -187,6 +187,8 @@ Canonical rule: =~/code/rulesets/claude-rules/cross-project.md=. Every in-progress task that produces files (drafts, source documents, diagrams, scripts, sub-deliverables) gets a dedicated subdirectory under =<project-root>/working/=, named after the task. All artifacts for that task live in that subdirectory until the task is marked done. +=working/= is version-controlled from creation — it's the tracked home of in-progress work, never gitignored. Filing on completion *reorganizes* durable artifacts into permanent homes; it doesn't mark when they became durable (they were durable, and tracked, from the start). Genuinely disposable artifacts go in a gitignored =temp/= (or =/tmp=), never =working/=; the install tooling ignores =temp/= in both track and gitignore modes. + When the task ships, files are **renamed individually** (standard form: =YYYY-MM-DD-<task-slug>-<descriptor>.<ext>=) and **moved flat** into the appropriate permanent home (typically =assets/= or an area-specific =<area>/assets/=). The working subdirectory is then empty and gets deleted. ***Never rename the directory itself as a substitute for filing.*** The point is to keep =assets/= flat-searchable — a nested =assets/old-tech-deck-2026/slide.png= is harder to find than =assets/2026-05-18-tech-deck-vol2-slide-04-diagram.png=. diff --git a/claude-rules/working-files.md b/claude-rules/working-files.md index 2432268..b915579 100644 --- a/claude-rules/working-files.md +++ b/claude-rules/working-files.md @@ -26,6 +26,28 @@ hits a nested path instead of a single canonical name. Always rename the files individually with a shared prefix so they sort together but live as flat siblings in `assets/`. +## `working/` Is Version-Controlled From Creation + +`working/` holds the project work currently being developed, so it is +tracked in git the moment a task subdirectory and its artifacts are created — +not staged locally and excluded until it graduates. `working/` is the tracked +home of in-progress work, and it is never added to `.gitignore` (the +install/sweep tooling deliberately leaves it out). + +Filing on completion **reorganizes** durable artifacts into their permanent +homes; it does not mark the point at which they first become durable. The +artifacts were durable — and tracked — from creation. Graduation is a move, +not a promotion from throwaway to keep. + +The corollary: genuinely disposable work does not belong in `working/`. +Ephemeral, single-use, or regenerable artifacts — scratch output, a +throwaway conversion, intermediate data you will delete — go in a project-root +`temp/` directory (gitignored) or system `/tmp`, never in `working/`. +`working/` is for work that will graduate; `temp/` is for work that will be +thrown away. The install tooling ignores `temp/` in both track and +gitignore-mode projects, since ephemerality is independent of whether a +project tracks its `.ai/` tooling. + ## Directory Layout <project-root>/ diff --git a/claude-templates/.ai/protocols.org b/claude-templates/.ai/protocols.org index 98cd934..d89e345 100644 --- a/claude-templates/.ai/protocols.org +++ b/claude-templates/.ai/protocols.org @@ -187,6 +187,8 @@ Canonical rule: =~/code/rulesets/claude-rules/cross-project.md=. Every in-progress task that produces files (drafts, source documents, diagrams, scripts, sub-deliverables) gets a dedicated subdirectory under =<project-root>/working/=, named after the task. All artifacts for that task live in that subdirectory until the task is marked done. +=working/= is version-controlled from creation — it's the tracked home of in-progress work, never gitignored. Filing on completion *reorganizes* durable artifacts into permanent homes; it doesn't mark when they became durable (they were durable, and tracked, from the start). Genuinely disposable artifacts go in a gitignored =temp/= (or =/tmp=), never =working/=; the install tooling ignores =temp/= in both track and gitignore modes. + When the task ships, files are **renamed individually** (standard form: =YYYY-MM-DD-<task-slug>-<descriptor>.<ext>=) and **moved flat** into the appropriate permanent home (typically =assets/= or an area-specific =<area>/assets/=). The working subdirectory is then empty and gets deleted. ***Never rename the directory itself as a substitute for filing.*** The point is to keep =assets/= flat-searchable — a nested =assets/old-tech-deck-2026/slide.png= is harder to find than =assets/2026-05-18-tech-deck-vol2-slide-04-diagram.png=. diff --git a/scripts/install-ai.sh b/scripts/install-ai.sh index 0c90f64..8c04e22 100755 --- a/scripts/install-ai.sh +++ b/scripts/install-ai.sh @@ -179,6 +179,24 @@ case "$track_mode" in ;; esac +# Ignore temp/ in BOTH track and gitignore modes (not the "not-a-git-repo" +# case). temp/ is the home for ephemeral, disposable, regenerable artifacts — +# throwaway regardless of whether the project tracks its .ai/ tooling, so it +# rides neither the track .gitkeep step nor the gitignore-only tooling block. +# working/ is deliberately NOT ignored: it's the tracked home of in-progress +# work, version-controlled from creation (see working-files.md). Idempotent: +# accepts either the unanchored `temp/` or anchored `/temp/` form. +if [ -n "$track_mode" ]; then + gi="$project/.gitignore" + if ! { [ -f "$gi" ] && { grep -qFx 'temp/' "$gi" || grep -qFx '/temp/' "$gi"; }; }; then + { + [ -s "$gi" ] && echo "" + echo "# Ephemeral working artifacts (throwaway; see working-files.md)" + echo "temp/" + } >> "$gi" + fi +fi + # Banner. echo echo "Done." diff --git a/scripts/sweep-gitignore-tooling.sh b/scripts/sweep-gitignore-tooling.sh index 68bfe2d..7194d6c 100755 --- a/scripts/sweep-gitignore-tooling.sh +++ b/scripts/sweep-gitignore-tooling.sh @@ -153,7 +153,37 @@ for project in "${projects[@]}"; do done done +# temp/ backfill — mode-independent. Unlike the personal-tooling set above +# (gitignore-mode only, track-mode deliberately skipped), temp/ holds ephemeral +# artifacts in every project regardless of whether it tracks its .ai/, so both +# track- and gitignore-mode projects get it. A separate pass, not an IGNORE_SET +# member — folding it into that set would miss every track-mode project, which +# is exactly the set that needs it. working/ is never ignored: it's the tracked +# home of in-progress work. +temp_swept=0 +for project in "${projects[@]}"; do + [ -d "$project/.git" ] || continue + name="$(basename "$project")" + gi="$project/.gitignore" + + if [ -f "$gi" ] && { grep -qFx 'temp/' "$gi" || grep -qFx '/temp/' "$gi"; }; then + continue + fi + + if [ "$dry_run" -eq 1 ]; then + echo "DRY $name — would add: temp/" + else + { + [ -s "$gi" ] && echo "" + echo "# Ephemeral working artifacts (throwaway; see working-files.md; swept $(date +%Y-%m-%d))" + echo "temp/" + } >> "$gi" + echo "temp $name — added: temp/" + fi + temp_swept=$((temp_swept + 1)) +done + echo -echo "Summary: $swept swept, $complete already complete, $skipped skipped (of ${#projects[@]} projects)." +echo "Summary: $swept swept, $complete already complete, $skipped skipped (of ${#projects[@]} projects); $temp_swept temp/ backfilled." [ "$dry_run" -eq 1 ] && echo "(dry-run — no files written)" exit 0 diff --git a/scripts/tests/install-ai.bats b/scripts/tests/install-ai.bats index 9c6040c..14f1152 100644 --- a/scripts/tests/install-ai.bats +++ b/scripts/tests/install-ai.bats @@ -200,3 +200,39 @@ LAUNCHER="$REAL_REPO/claude-templates/bin/install-ai" [ "$status" -eq 0 ] [[ "$output" == *"Bootstrap .ai/"* ]] } + +# --- temp/ ephemeral-artifacts ignore (working/ tracked-from-creation ruling) --- + +@test "install-ai --gitignore: ignores temp/ but never working/" { + mkdir -p "$TEST_HOME/code/fresh" + (cd "$TEST_HOME/code/fresh" && git init -q) + + run bash "$INSTALL_AI" --gitignore "$TEST_HOME/code/fresh" + + [ "$status" -eq 0 ] + grep -qFx "temp/" "$TEST_HOME/code/fresh/.gitignore" + # working/ is the tracked home of in-progress work — it must never be ignored. + ! grep -qEx "/?working/?" "$TEST_HOME/code/fresh/.gitignore" +} + +@test "install-ai --track: ignores temp/ even in track mode" { + mkdir -p "$TEST_HOME/code/tracked" + (cd "$TEST_HOME/code/tracked" && git init -q) + + run bash "$INSTALL_AI" --track "$TEST_HOME/code/tracked" + + [ "$status" -eq 0 ] + # temp/ is ephemeral regardless of whether the project tracks its .ai/ tooling. + grep -qFx "temp/" "$TEST_HOME/code/tracked/.gitignore" +} + +@test "install-ai: temp/ ignore is idempotent (not re-added)" { + mkdir -p "$TEST_HOME/code/fresh" + (cd "$TEST_HOME/code/fresh" && git init -q) + printf 'temp/\n' > "$TEST_HOME/code/fresh/.gitignore" + + run bash "$INSTALL_AI" --gitignore "$TEST_HOME/code/fresh" + + [ "$status" -eq 0 ] + [ "$(grep -cFx 'temp/' "$TEST_HOME/code/fresh/.gitignore")" -eq 1 ] +} diff --git a/scripts/tests/sweep-gitignore-tooling.bats b/scripts/tests/sweep-gitignore-tooling.bats index f18eac5..240c3be 100644 --- a/scripts/tests/sweep-gitignore-tooling.bats +++ b/scripts/tests/sweep-gitignore-tooling.bats @@ -190,3 +190,44 @@ make_project() { [ "$status" -eq 0 ] [[ "$output" != *"publicly reachable"* ]] } + +# --- temp/ ephemeral-artifacts backfill (mode-independent) --- + +@test "sweep: adds temp/ to a gitignore-mode project" { + make_project gimode $'.ai/\n' + + run bash "$SWEEP" "$ROOT" + + [ "$status" -eq 0 ] + grep -qFx "temp/" "$ROOT/gimode/.gitignore" +} + +@test "sweep: adds temp/ to a TRACK-mode project too (temp/ is mode-independent)" { + make_project trackmode $'# build\nout/\n' + + run bash "$SWEEP" "$ROOT" + + [ "$status" -eq 0 ] + # The tooling set is skipped in track mode, but temp/ is not. + grep -qFx "temp/" "$ROOT/trackmode/.gitignore" + ! grep -qFx ".ai/" "$ROOT/trackmode/.gitignore" +} + +@test "sweep: never adds working/" { + make_project gimode $'.ai/\n' + + run bash "$SWEEP" "$ROOT" + + [ "$status" -eq 0 ] + ! grep -qEx "/?working/?" "$ROOT/gimode/.gitignore" +} + +@test "sweep: temp/ backfill is idempotent" { + make_project gimode $'.ai/\ntemp/\n' + bash "$SWEEP" "$ROOT" >/dev/null + + run bash "$SWEEP" "$ROOT" + + [ "$status" -eq 0 ] + [ "$(grep -cFx 'temp/' "$ROOT/gimode/.gitignore")" -eq 1 ] +} @@ -75,14 +75,18 @@ What we're verifying: the morning teardown reviews and merges cleanly, nothing r - Confirm main was never pushed to during the night and the approval queue was walked. Expected: the night's work is reviewable as one branch, merges by choice, and main stayed clean throughout. -** VERIFY [#B] working/ tracked-from-creation + gitignored temp/ :feature: -Craig's ruling relayed from .emacs.d (2026-07-19): working/ is version-controlled from creation (not excluded until graduation); ephemeral artifacts go in a gitignored temp/ or /tmp; graduation reorganizes durable artifacts into permanent homes rather than marking when they become durable. Parked by sentry (shared-asset + convention change with a design decision), not applied unattended. - -Prepared proposal + findings: [[file:working/sentry-2026-07-19-working-files-ruling/proposed.org][working/sentry-2026-07-19-working-files-ruling/proposed.org]]. - -Two findings that shape the work: (1) the canonical install/sweep machinery already never gitignores working/ — the tooling set is only =.ai/ .claude/ CLAUDE.md AGENTS.md=, so "track working/ across projects" is already the default; .emacs.d's =/working/= ignore was a local deviation. (2) temp/ is a new pattern needing a design call — it must be gitignored in BOTH track and gitignore modes (ephemeral regardless of tooling-track choice), so it's orthogonal to the personal-tooling set. That call is Craig's; recommendation is in the proposal. - -Files to touch on approval: =claude-rules/working-files.md= (convention text), =scripts/install-ai.sh= + =scripts/sweep-gitignore-tooling.sh= (temp/ ignore in both modes), =.ai/protocols.org= Working-Files Convention section (one-line mirror). Not :solo: — carries the temp/-placement design decision. +** DONE [#B] working/ tracked-from-creation + gitignored temp/ :feature: +CLOSED: [2026-07-20 Mon] +Craig's ruling relayed from .emacs.d (2026-07-19): working/ is version-controlled from creation (not excluded until graduation); ephemeral artifacts go in a gitignored temp/ or /tmp; graduation reorganizes durable artifacts into permanent homes rather than marking when they become durable. + +Implemented (Shape A) during the morning sentry review, 2026-07-20: +- =claude-rules/working-files.md=: added "working/ Is Version-Controlled From Creation" section (tracked-from-creation, graduation-is-a-move, temp/ for ephemeral). +- =.ai/protocols.org= (canonical + mirror): one-paragraph mirror in the Working-Files Convention section. +- =scripts/install-ai.sh=: emits a =temp/= ignore block in BOTH track and gitignore modes; working/ never ignored. Idempotent. +- =scripts/sweep-gitignore-tooling.sh=: separate mode-independent temp/ backfill pass (a distinct loop, not an IGNORE_SET member — that would skip track-mode projects, the set that most needs it). +- Tests: 3 new install-ai bats + 4 new sweep bats (temp/ in both modes, never working/, idempotent). Suite green, 384 bats ok. + +Finding confirmed at implementation: the canonical machinery already never ignored working/ (tooling set is only =.ai/ .claude/ CLAUDE.md AGENTS.md=), so .emacs.d's =/working/= ignore was a purely local deviation. The staged proposal at =working/sentry-2026-07-19-working-files-ruling/= is now spent — a filing/cleanup candidate. ** TODO [#C] Polyglot projects — supported, or refused? :spec: SCHEDULED: <2026-07-20 Mon> |
