aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.ai/protocols.org2
-rw-r--r--claude-rules/working-files.md22
-rw-r--r--claude-templates/.ai/protocols.org2
-rwxr-xr-xscripts/install-ai.sh18
-rwxr-xr-xscripts/sweep-gitignore-tooling.sh32
-rw-r--r--scripts/tests/install-ai.bats36
-rw-r--r--scripts/tests/sweep-gitignore-tooling.bats41
-rw-r--r--todo.org20
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 ]
+}
diff --git a/todo.org b/todo.org
index 5bfa77f..cf9e878 100644
--- a/todo.org
+++ b/todo.org
@@ -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>