diff --git a/plugins/dev-team/knowledge/index.json b/plugins/dev-team/knowledge/index.json index 8ea1662d1..384953a55 100644 --- a/plugins/dev-team/knowledge/index.json +++ b/plugins/dev-team/knowledge/index.json @@ -5766,6 +5766,10 @@ "summary": "If `.claude/CLAUDE.md` does not already exist in the target project, generate one containing:", "anchor": "8-generate-project-level-claudemd" }, + "8a. Ask about concise-response preference": { + "summary": "**Check first, before prompting anyone.** If the marker is already present", + "anchor": "8a-ask-about-concise-response-preference" + }, "9. Generate PostToolUse formatting hook": { "summary": "Wire a PostToolUse hook entry for the project's `.claude/settings.json` that", "anchor": "9-generate-posttooluse-formatting-hook" diff --git a/plugins/dev-team/skills/setup/SKILL.md b/plugins/dev-team/skills/setup/SKILL.md index 9f380de88..90057284d 100644 --- a/plugins/dev-team/skills/setup/SKILL.md +++ b/plugins/dev-team/skills/setup/SKILL.md @@ -72,6 +72,13 @@ repo in a surprising way; take the skip default and print a one-line note: that skill); it writes to the repo, so it stays opt-in even under `--yes`. - **Unrecognized / ambiguous stack** — `--yes` never guesses a toolchain. Do the language-neutral steps and report what could not be set up. +- **Concise-response preference** (Step 8a) — `--yes` never guesses a + communication-style preference. Skip the prompt and the `CLAUDE.md` + append entirely; note `concise-preference-skipped-under-yes`. This is + the outcome only when Step 8a's own pre-check found the block absent — + when the block is already present, the pre-check reports + `concise-preference-already-covered` before `--yes` is ever consulted, + regardless of whether `--yes` was passed. **Precedence.** If both `--yes` and `--dry-run` are passed, `--dry-run` wins: report only, write and install nothing, and add "`--yes` ignored under @@ -766,7 +773,104 @@ If `.claude/CLAUDE.md` does not already exist in the target project, generate on If `.claude/CLAUDE.md` already exists, ask whether to merge or skip. **Under `--yes`, take the skip branch** — never overwrite existing project config (orchestrator constraint 3) — and note `CLAUDE.md exists — left unchanged` in -the Step 12 report. +the Step 12 report. This note describes only Step 8's own generate-or-skip +decision — Step 8a runs independently right after and may still append to +this same file, so "left unchanged" here and a `concise-preference-added` +outcome from Step 8a are not a contradiction in the Step 12 report. + +### 8a. Ask about concise-response preference + +**Check first, before prompting anyone.** If the marker is already present +in `.claude/CLAUDE.md`, the preference is already in place — do not ask +the operator at all, under any flag combination: + +```bash +MARKER="" +grep -qF "$MARKER" .claude/CLAUDE.md 2>/dev/null && echo "concise-preference-already-covered" || echo "concise-preference-not-yet-covered" +``` + +If this reports `concise-preference-already-covered`, record that outcome +for the Step 12 report and skip the rest of this step entirely — no +prompt, no `--yes`/`--dry-run` branching, nothing else to do. Only when it +reports `concise-preference-not-yet-covered` does the rest of this step +run. + +Ask the operator: "Would you like Claude to give concise responses by default in this repo? This appends a block to the project's committed `.claude/CLAUDE.md`, so it becomes a shared convention for every contributor, not just you." **Under `--yes`, skip this prompt entirely** — never +guess a communication-style preference — and note +`concise-preference-skipped-under-yes` (run /setup without --yes to choose) +in the Step 12 report. **Under `--dry-run`, report what would be appended +without writing** (same convention as Step 11's two idempotent blocks). + +If the operator declines, note `concise-preference-declined` and move on — +do not write anything. + +If the operator confirms, idempotently append the block below to the +project's `CLAUDE.md` (the same file Step 8 generated or left in place — +`.claude/CLAUDE.md`; this step creates `.claude/` and the file itself if +Step 8 somehow left them absent). The marker is a dedicated, content-free +comment — not a substring of the prose — so a future wording tweak to the +block below can never desync it from the idempotency check (the hazard +Step 11's own `.mcp.json` marker note, below, already calls out for its own +pair of blocks). Both `.claude` and `.claude/CLAUDE.md` are checked for +symlinks before any write, since a symlinked parent directory is an +equally valid escape route as a symlinked file: + +```bash +MARKER="" +if [ -L .claude ] || [ -L .claude/CLAUDE.md ]; then + echo "concise-preference-skipped-symlink" +else + mkdir -p .claude + touch .claude/CLAUDE.md + if ! grep -qF "$MARKER" .claude/CLAUDE.md; then + { printf '\n%s\n' "$MARKER"; cat <<'CONCISE_BLOCK' +## General +Unless asked to behave otherwise, always give concise responses and sacrifice grammar for the sake of concision. Ask clarifying questions when needed and offer your best guess at available interpretations/answers for those questions when possible. + +### Be RUTHLESSLY concise — this is the rule I break most often +Default to a few sentences. If the answer is "yes", say "yes" and stop. + +- **Answer the question asked. Nothing else.** No adjacent findings, no "while I was looking I noticed", no caveats I didn't ask for. Sit on it until I ask. +- **One thing at a time.** Never hand me a numbered list of 3+ considerations, options, or trade-offs unless I asked for options. Pick one, recommend it, move on. +- **No teaching.** Skip the mechanism, the background, the "why this matters". State the conclusion. I'll ask why if I care. +- **No tables, no headers, no bold-label paragraphs** for a simple answer. Prose or a couple of lines. +- **Cut every parenthetical, every "worth noting", every "the real finding is".** +- Corrections: one sentence, no post-mortem. + +Length is the tell: if a reply is over ~10 lines and I didn't ask for depth, it's wrong. Detail I have to skim to find the answer is worse than no answer. +CONCISE_BLOCK + } >> .claude/CLAUDE.md && echo "concise-preference-added" || echo "concise-preference-write-failed" + else + echo "concise-preference-already-covered" + fi +fi +``` + +This always appends a fresh, self-contained `## General` heading rather +than trying to detect and merge into a pre-existing one — a prior version +of this step attempted that merge and introduced two bugs of its own (an +orphaned sub-heading when the file's last `##` section wasn't `General`, +and a `^## General$` anchor that silently missed CRLF line endings on +Windows). A file ending up with two `## General` headings is a cosmetic, +rare edge case; a script that silently corrupts document structure on a +supported platform is not an acceptable trade for avoiding it. + +Record the outcome — `concise-preference-declined` / +`concise-preference-added` / `concise-preference-already-covered` / +`concise-preference-skipped-under-yes` / `concise-preference-skipped-symlink` +/ `concise-preference-write-failed` — for the Step 12 report, using these +exact tokens. `already-covered` has two possible sources — usually the +pre-check above (the normal path, before any prompt), or, on the rare +race where the file changes between the pre-check and confirmation, the +append script's own internal re-check — either way the token is the +same. The other three the append script can reach (`added` / +`skipped-symlink` / `write-failed`) are echoed verbatim from its own +output — `write-failed` covers a `mkdir`/`touch`/append that fails (e.g. +`.claude` exists as a non-directory, or the tree is read-only), so a +failed write is never misreported as `added`; the remaining two +(`declined` / `skipped-under-yes`) are recorded directly from the prose +branches below the pre-check, +since the script never runs on those paths. ### 9. Generate PostToolUse formatting hook @@ -957,7 +1061,7 @@ Repowise's own install/decline state for that run. ### Created - `.claude/project-stack.json` — stack detection results -- `.claude/CLAUDE.md` — project conventions +- `.claude/CLAUDE.md` — project conventions [Step 8a concise-response preference, independent of this line's created/left-unchanged status: concise-preference-added | concise-preference-already-covered | concise-preference-declined | concise-preference-skipped-under-yes | concise-preference-skipped-symlink | concise-preference-write-failed] - `.claude/settings.json` — PostToolUse formatting hook (prettier + eslint) - `.gitignore` — dev-team runtime artifacts (.claude/memory/, .claude/metrics/, .claude/plans/, .dev-team-reports/, memory/, reports/, metrics/, plans/, .pr-review-passed) [downstream only; omit if already covered] plus `.mcp.json` machine-specific-path hygiene (#1376, #1416) [runs in-repo too, via project-init's Repowise standing check; omit if already covered] - Activated templates: ts-enforcer, esm-enforcer, react-testing diff --git a/tests/skills/test_setup_concise_preference.py b/tests/skills/test_setup_concise_preference.py new file mode 100644 index 000000000..3c2e51d96 --- /dev/null +++ b/tests/skills/test_setup_concise_preference.py @@ -0,0 +1,145 @@ +"""Content-guard: setup/SKILL.md's Step 8a concise-response preference. + +Mirrors test_setup_skill_artifact_paths.py's Step 11 marker-bump test: the +idempotency marker must be a dedicated, content-free sentinel (not a +substring of the prose it guards), so a future wording tweak to the +appended block can never desync the `grep -qF` check from what it's +supposed to detect. Also pins the outcome-token vocabulary so the bash +script's `echo` output, the "Record the outcome" instruction, and the +Step 12 report line never drift from one another (a review finding on the +PR that introduced this step, #2103), and pins that an already-covered +repo is never prompted at all (#2112 follow-up). +""" + +from __future__ import annotations + +from skill_doc_helpers import PLUGIN_ROOT, collapsed + +SKILL = (PLUGIN_ROOT / "skills" / "setup" / "SKILL.md").read_text(encoding="utf-8") + +# Tokens some bash snippet in this step can echo. `already-covered` has two +# sources: usually the pre-check (before any prompt), or, on a rare race, +# the append script's own internal re-check — either way, same token. +SCRIPT_ECHOED_TOKENS = ( + "concise-preference-added", + "concise-preference-already-covered", + "concise-preference-skipped-symlink", + "concise-preference-write-failed", +) +# Tokens recorded directly from prose branches — never echoed by any script. +PROSE_ONLY_TOKENS = ( + "concise-preference-declined", + "concise-preference-skipped-under-yes", +) + + +def _step_8a_body() -> str: + """Just Step 8a's own section, from its heading up to Step 9's.""" + start = SKILL.index("### 8a. Ask about concise-response preference") + end = SKILL.index("### 9. Generate PostToolUse formatting hook", start) + return SKILL[start:end] + + +def _nth_bash_fence(body: str, n: int) -> str: + """The literal ```bash ... ``` code fence content, 0-indexed by + appearance order within `body`.""" + pos = 0 + for _ in range(n + 1): + start = body.index("```bash\n", pos) + len("```bash\n") + pos = start + end = body.index("\n```", start) + return body[start:end] + + +def _step_8a_precheck_script() -> str: + """The first ```bash fence — the already-covered gate that must run + before any prompt.""" + return _nth_bash_fence(_step_8a_body(), 0) + + +def _step_8a_append_script() -> str: + """The second ```bash fence — the actual idempotent-append logic, + reached only once the pre-check found the marker absent.""" + return _nth_bash_fence(_step_8a_body(), 1) + + +def _step_8a_record_outcome_sentence() -> str: + body = _step_8a_body() + return body[body.index("Record the outcome") :] + + +def test_step_8a_marker_is_not_a_substring_of_the_appended_prose(): + marker = "" + assert marker in SKILL + # The marker must not itself be inside the single-quoted heredoc body — + # if it were, a rewording of the *marker* would also change the prose, + # defeating the purpose of decoupling them. + heredoc_start = SKILL.index("<<'CONCISE_BLOCK'") + len("<<'CONCISE_BLOCK'") + heredoc_end = SKILL.index("\nCONCISE_BLOCK", heredoc_start) + assert marker not in SKILL[heredoc_start:heredoc_end] + + +def test_step_8a_precheck_runs_before_the_prompt(): + # The already-covered gate must be textually positioned before the + # operator is ever asked — an already-covered repo must never see the + # prompt at all, under any flag combination. + body = _step_8a_body() + assert body.index("```bash") < body.index("Ask the operator") + + +def test_step_8a_precheck_uses_the_same_marker_as_the_append_script(): + marker = "" + precheck = _step_8a_precheck_script() + append = _step_8a_append_script() + assert marker in precheck + assert marker in append + + +def test_step_8a_precheck_short_circuits_the_whole_step(): + body = collapsed(_step_8a_body()) + assert "skip the rest of this step entirely" in body + assert "no prompt" in body + + +def test_step_8a_creates_claude_dir_before_appending(): + script = _step_8a_append_script() + assert "mkdir -p .claude" in script + # Order matters: mkdir must run before the append redirect, or the + # append silently no-ops when .claude/ doesn't exist yet. + assert script.index("mkdir -p .claude") < script.index(">> .claude/CLAUDE.md") + + +def test_step_8a_symlink_guard_covers_both_the_dir_and_the_file(): + script = _step_8a_append_script() + assert "[ -L .claude ]" in script + assert "[ -L .claude/CLAUDE.md ]" in script + assert "concise-preference-skipped-symlink" in script + # The symlink check must run before mkdir/touch, not after. + assert script.index("[ -L .claude ]") < script.index("mkdir -p .claude") + + +def test_step_8a_script_echoes_its_own_outcome_tokens(): + script = _step_8a_append_script() + for token in SCRIPT_ECHOED_TOKENS: + assert token in script, f"{token!r} should be echoed by the append script" + + +def test_step_8a_append_outcome_is_gated_on_write_success(): + # A prior version echoed "added" unconditionally after the redirect, + # so a failed mkdir/touch/append (e.g. .claude exists as a regular + # file, or the tree is read-only) was silently misreported as success. + script = _step_8a_append_script() + assert ">> .claude/CLAUDE.md && echo" in script + assert "|| echo \"concise-preference-write-failed\"" in script + + +def test_step_8a_outcome_tokens_all_reach_the_report_instruction(): + sentence = _step_8a_record_outcome_sentence() + for token in SCRIPT_ECHOED_TOKENS + PROSE_ONLY_TOKENS: + assert token in sentence, f"{token!r} should be named in the 'Record the outcome' instruction" + + +def test_step_8a_outcome_tokens_all_reach_the_step_12_report_line(): + line = next(line for line in SKILL.splitlines() if "Step 8a concise-response preference" in line) + for token in SCRIPT_ECHOED_TOKENS + PROSE_ONLY_TOKENS: + assert token in line, f"{token!r} should be listed in the Step 12 report line" diff --git a/tests/skills/test_setup_yes_flag.py b/tests/skills/test_setup_yes_flag.py index 19fa305a6..63be01595 100644 --- a/tests/skills/test_setup_yes_flag.py +++ b/tests/skills/test_setup_yes_flag.py @@ -18,7 +18,12 @@ from __future__ import annotations -from skill_doc_helpers import PLUGIN_ROOT, collapsed, grep, section_outside_code +from skill_doc_helpers import ( + PLUGIN_ROOT, + collapsed, + grep, + section_outside_code, +) SETUP = PLUGIN_ROOT / "skills" / "setup" / "SKILL.md" PROJECT_INIT = PLUGIN_ROOT / "skills" / "project-init" / "SKILL.md" @@ -48,6 +53,21 @@ def _yes_semantics_conservative_repowise_bullet() -> str: ) +def _yes_semantics_conservative_concise_preference_bullet() -> str: + """Bound to just the Conservative bucket's `Concise-response preference` + bullet (the last one in that bucket) — not the whole document — so this + can't be satisfied by Step 8a's own, unrelated "never guess a + communication-style preference" sentence in its own prose section + (a real gap a prior version of this test had: it matched the whole + document, so deleting this bullet entirely would still pass).""" + return section_outside_code( + _setup(), + r"^- \*\*Concise-response preference", + boundary_pattern=r"^\*\*Precedence\.", + include_start_line=True, + ) + + # --- argument-hint frontmatter ----------------------------------------------- @@ -86,6 +106,12 @@ def test_setup_never_guesses_stack_under_yes(): assert grep(r"never guess a stack", body) +def test_setup_never_guesses_communication_style_under_yes(): + bullet = collapsed(_yes_semantics_conservative_concise_preference_bullet()) + assert grep(r"never guesses a communication-style preference", bullet) + assert "concise-preference-skipped-under-yes" in bullet + + def test_setup_dry_run_beats_yes(): body = collapsed(_setup()) assert grep(r"`--dry-run` wins", body)