Skip to content
Merged
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
4 changes: 4 additions & 0 deletions plugins/dev-team/knowledge/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
108 changes: 106 additions & 2 deletions plugins/dev-team/skills/setup/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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="<!-- dev-team: concise-response preference v1 -->"
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="<!-- dev-team: concise-response preference v1 -->"
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

Expand Down Expand Up @@ -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
Expand Down
145 changes: 145 additions & 0 deletions tests/skills/test_setup_concise_preference.py
Original file line number Diff line number Diff line change
@@ -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 = "<!-- dev-team: concise-response preference v1 -->"
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 = "<!-- dev-team: concise-response preference v1 -->"
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"
28 changes: 27 additions & 1 deletion tests/skills/test_setup_yes_flag.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 -----------------------------------------------


Expand Down Expand Up @@ -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)
Expand Down
Loading