claude-dev-env 8.40.1 → 8.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/.agents/agents/test_agent_frontmatter.py +17 -14
  2. package/.agents/agents-archived/clean-coder.md +4 -4
  3. package/.agents/agents-archived/pr-description-writer.md +1 -1
  4. package/.agents/skills/pr-lifecycle/SKILL.md +470 -0
  5. package/.agents/skills/privacy-hygiene/reference/sweep-procedure.md +1 -1
  6. package/bin/ever-shipped-skills.mjs +1 -0
  7. package/bin/ever-shipped-skills.test.mjs +4 -0
  8. package/bin/install.cursor-rules.test.mjs +2 -1
  9. package/docs/rule-guides/code-standards.md +1 -1
  10. package/docs/rule-guides/destructive-commands.md +1 -1
  11. package/hooks/blocking/pr_lifecycle_skill_gate.py +190 -0
  12. package/hooks/blocking/test_pr_lifecycle_skill_gate.py +163 -0
  13. package/hooks/hooks.json +25 -0
  14. package/hooks/hooks_constants/pr_lifecycle_skill_gate_constants.py +33 -0
  15. package/hooks/hooks_constants/skill_loaded_reminder_constants.py +0 -10
  16. package/hooks/hooks_constants/spawn_readiness_hook_constants.py +30 -12
  17. package/hooks/hooks_constants/test_pr_lifecycle_skill_gate_constants.py +12 -0
  18. package/hooks/hooks_constants/test_transcript_skill_scan_constants.py +7 -0
  19. package/hooks/hooks_constants/transcript_skill_scan_constants.py +7 -0
  20. package/hooks/routing/spawn_readiness_hook.py +118 -46
  21. package/hooks/routing/test_spawn_readiness_hook.py +113 -21
  22. package/hooks/routing/test_spawn_readiness_steps.py +2 -4
  23. package/hooks/session/skill_loaded_reminder.py +4 -51
  24. package/hooks/session/test_skill_loaded_reminder.py +4 -0
  25. package/hooks/test_transcript_skill_scan.py +58 -0
  26. package/hooks/transcript_skill_scan.py +93 -0
  27. package/package.json +1 -1
  28. package/rules/flag-non-breaking-findings.md +2 -2
  29. package/rules/shell-invocation.md +2 -14
  30. package/rules/skill-pointers.md +3 -0
  31. package/scripts/policy_lint/adapter_configuration.py +5 -0
  32. package/scripts/policy_lint/config/constants.py +1 -0
  33. package/scripts/tests/test_adapter_configuration.py +8 -0
  34. package/scripts/tests/test_banned_prose_words.py +1 -1
  35. package/scripts/tests/test_rule_load_scopes.py +1 -7
  36. package/rules/agent-merges-its-own-green-pull-request.md +0 -55
  37. package/rules/ci-owns-the-gate.md +0 -101
  38. package/rules/durable-post-artifacts.md +0 -76
  39. package/rules/gh-cli-conventions.md +0 -36
  40. package/rules/git-workflow.md +0 -121
  41. package/rules/re-stage-before-commit.md +0 -14
  42. package/rules/review-closure-is-a-check.md +0 -54
@@ -0,0 +1,93 @@
1
+ """Find a skill invocation after the last transcript compaction."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from collections.abc import Collection, Iterable
7
+
8
+ from hooks_constants.transcript_skill_scan_constants import (
9
+ ASSISTANT_ENTRY_TYPE,
10
+ COMPACT_BOUNDARY_SUBTYPE,
11
+ SKILL_TOOL_NAME,
12
+ TOOL_USE_BLOCK_TYPE,
13
+ USER_ENTRY_TYPE,
14
+ )
15
+
16
+
17
+ def _invokes_skill(
18
+ all_entry_fields: dict[str, object],
19
+ skill_names: Collection[str],
20
+ slash_command_markers: Collection[str],
21
+ ) -> bool:
22
+ message = all_entry_fields.get("message")
23
+ if not isinstance(message, dict):
24
+ return False
25
+ content = message.get("content")
26
+ if all_entry_fields.get("type") == USER_ENTRY_TYPE and isinstance(content, str):
27
+ return any(marker in content for marker in slash_command_markers)
28
+ if all_entry_fields.get("type") != ASSISTANT_ENTRY_TYPE or not isinstance(content, list):
29
+ return False
30
+ for each_block in content:
31
+ if not isinstance(each_block, dict) or each_block.get("type") != TOOL_USE_BLOCK_TYPE:
32
+ continue
33
+ if each_block.get("name") != SKILL_TOOL_NAME or not isinstance(each_block.get("input"), dict):
34
+ continue
35
+ invoked_name = each_block["input"].get("skill")
36
+ if isinstance(invoked_name, str) and any(
37
+ invoked_name == name or invoked_name.endswith(":" + name) for name in skill_names
38
+ ):
39
+ return True
40
+ return False
41
+
42
+
43
+ def _relevant_entry(each_line: str, skill_names: Collection[str]) -> dict[str, object] | None:
44
+ if not any(name in each_line for name in skill_names) and COMPACT_BOUNDARY_SUBTYPE not in each_line:
45
+ return None
46
+ try:
47
+ all_entry_fields = json.loads(each_line)
48
+ except json.JSONDecodeError:
49
+ return None
50
+ return all_entry_fields if isinstance(all_entry_fields, dict) else None
51
+
52
+
53
+ def skill_invocation_status(
54
+ all_transcript_lines: Iterable[str],
55
+ skill_names: Collection[str],
56
+ slash_command_markers: Collection[str],
57
+ ) -> tuple[bool, bool]:
58
+ """Report whether the skill was ever invoked and whether it is loaded now.
59
+
60
+ Args:
61
+ all_transcript_lines: JSON transcript entries, one per line.
62
+ skill_names: Accepted skill names, with plugin prefixes accepted.
63
+ slash_command_markers: Accepted user command tags.
64
+
65
+ Returns:
66
+ Whether any entry invoked the skill, and whether an invocation follows
67
+ the last compact boundary.
68
+ """
69
+ was_invoked = False
70
+ loaded = False
71
+ for each_line in all_transcript_lines:
72
+ all_entry_fields = _relevant_entry(each_line, skill_names)
73
+ if all_entry_fields is not None:
74
+ loaded = all_entry_fields.get("subtype") != COMPACT_BOUNDARY_SUBTYPE and (
75
+ loaded or _invokes_skill(all_entry_fields, skill_names, slash_command_markers)
76
+ )
77
+ was_invoked = was_invoked or loaded
78
+ return was_invoked, loaded
79
+
80
+
81
+ def is_skill_loaded_after_last_compaction(
82
+ all_transcript_lines: Iterable[str],
83
+ skill_names: Collection[str],
84
+ slash_command_markers: Collection[str],
85
+ ) -> bool:
86
+ """Return whether a matching invocation follows the last compact boundary.
87
+
88
+ Args:
89
+ all_transcript_lines: JSON transcript entries, one per line.
90
+ skill_names: Accepted skill names, with plugin prefixes accepted.
91
+ slash_command_markers: Accepted user command tags.
92
+ """
93
+ return skill_invocation_status(all_transcript_lines, skill_names, slash_command_markers)[1]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "8.40.1",
3
+ "version": "8.42.0",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {
@@ -130,5 +130,5 @@ zero when no error remains.
130
130
 
131
131
  | Rule | Role |
132
132
  |---|---|
133
- | [`ci-owns-the-gate.md`](ci-owns-the-gate.md) | The full check suite runs once, on CI |
134
- | [`git-workflow.md`](git-workflow.md) | A red required check blocks the branch |
133
+ | [CI Owns the Gate](../.agents/skills/pr-lifecycle/SKILL.md#ci-owns-the-gate) | The full check suite runs once, on CI |
134
+ | [Git workflow](../.agents/skills/pr-lifecycle/SKILL.md#git-workflow) | A red required check blocks the branch |
@@ -18,18 +18,6 @@ When a script file's literal body needs `$(...)`, author it with the Write tool,
18
18
 
19
19
  ## Enforcement
20
20
 
21
- No PreToolUse hook denies a Bash command. The substitution constraint above is guidance a reader follows, and a permission prompt on a wrapped command is the signal that one slipped through.
21
+ No PreToolUse hook denies a Bash command for its shell form. The substitution constraint above is guidance a reader follows, and a permission prompt on a wrapped command is the signal that one slipped through.
22
22
 
23
- One PreToolUse hook does run on a Bash command, and it only rewrites. `blocking/msys_rev_path_rewriter.py` (PreToolUse on Bash, hosted by `bash_pre_tool_use_dispatcher`) reads a git command before it runs and keeps Git Bash from converting a `<rev>:<path>` argument. That roster holds this one hook, and a test asserts its whole content, so a blocking hook added beside it fails the suite.
24
-
25
- Git Bash rewrites that argument when the revision holds a slash and the path after the colon starts with a slash or a dot. It turns the colon into a semicolon and the slashes into backslashes, so `git show origin/main:.claude/settings.json` reaches git as `origin\main;.claude\settings.json` and git reports a revision that does not exist. A leading dot on a file is enough. `origin/main:.gitignore` reaches git as `origin\main;.gitignore`. `git show origin/main:packages/app.py` passes through untouched, and so does any path after the colon that starts with `./`, `../`, or `~/`. A revision without a slash, such as `HEAD:.claude/settings.json`, passes through too.
26
-
27
- On that shape the rewriter names only the arguments it found, so `git show origin/main:.claude/settings.json` runs as:
28
-
29
- ```
30
- export MSYS2_ARG_CONV_EXCL='origin/main:'; git show origin/main:.claude/settings.json
31
- ```
32
-
33
- `MSYS2_ARG_CONV_EXCL` takes a semicolon-separated list of argument prefixes. Naming one prefix per detected token leaves every other argument in the command converting as before. The blanket pair `MSYS_NO_PATHCONV=1 MSYS2_ARG_CONV_EXCL='*'` turns conversion off for the whole command, which changes a path the command meant to convert, so the rewriter does not emit it.
34
-
35
- A quoted token is not a revision. `git commit -m "fix a/b:.py"` carries a slash and a dot in one token, and a rewrite there would break the message, so the detection skips a token holding whitespace or a quote.
23
+ `blocking/msys_rev_path_rewriter.py` only rewrites a Bash command. It keeps Git Bash from converting a `<rev>:<path>` argument when the revision holds a slash. A test pins the dispatcher roster to this one hook.
@@ -0,0 +1,3 @@
1
+ # Pull request lifecycle skill
2
+
3
+ Before a commit, a push, a pull request action or a merge, invoke the `pr-lifecycle` skill; a PreToolUse hook denies those commands until it loads.
@@ -127,6 +127,7 @@ def _names_exempt_registration_path(registered_string: str) -> bool:
127
127
  ::
128
128
 
129
129
  hooks/blocking/bash_pre_tool_use_dispatcher.py -> exempt
130
+ hooks/blocking/pr_lifecycle_skill_gate.py -> exempt
130
131
  hooks/blocking/step_note_gate.py -> exempt
131
132
  hooks/blocking/reply_length_gate.py -> exempt
132
133
  hooks/blocking/edit_marker_gate.py -> exempt
@@ -137,6 +138,10 @@ def _names_exempt_registration_path(registered_string: str) -> bool:
137
138
  while its roster hosts one allow-and-rewrite hook. Its path segment reads as
138
139
  a policy boundary that the chain never carries.
139
140
 
141
+ The pull request lifecycle gate denies a commit, push, pull request, or
142
+ merge call once, until the session loads the ``pr-lifecycle`` skill. It
143
+ decides when a rule set loads and no code or safety policy.
144
+
140
145
  The step-note gate allows every call until the user runs ``/step-notes on``.
141
146
  It asks for a readable status line and decides no code or safety policy.
142
147
 
@@ -63,6 +63,7 @@ ALL_ACTION_BOUNDARY_SEGMENTS = frozenset(
63
63
  ALL_ACTION_BOUNDARY_PREFIXES = ("deny_", "block_", "ask_")
64
64
  ALL_ACTION_BOUNDARY_EXEMPT_REGISTRATION_PATHS = (
65
65
  "blocking/bash_pre_tool_use_dispatcher.py",
66
+ "blocking/pr_lifecycle_skill_gate.py",
66
67
  "blocking/step_note_gate.py",
67
68
  "blocking/reply_length_gate.py",
68
69
  "blocking/edit_marker_gate.py",
@@ -50,6 +50,14 @@ def test_new_bash_pre_tool_use_dispatcher_registration_is_clean(tmp_path: Path)
50
50
  assert adapters.hook_configuration_diagnostics(current_document, tmp_path) == ()
51
51
 
52
52
 
53
+ def test_pull_request_lifecycle_gate_registration_is_clean(tmp_path: Path) -> None:
54
+ current_document = _hook_document(
55
+ ["hooks/blocking/pr_lifecycle_skill_gate.py"],
56
+ '{"hooks": {}}',
57
+ )
58
+ assert adapters.hook_configuration_diagnostics(current_document, tmp_path) == ()
59
+
60
+
53
61
  def test_new_step_note_gate_registration_is_clean(tmp_path: Path) -> None:
54
62
  current_document = _hook_document(
55
63
  ["hooks/blocking/step_note_gate.py"],
@@ -84,7 +84,7 @@ def test_governed_paths_lists_the_shipped_surfaces(tmp_path: Path) -> None:
84
84
 
85
85
 
86
86
  def test_governed_paths_cover_instruction_surfaces_and_skip_archives() -> None:
87
- assert governs_path("rules/git-workflow.md")
87
+ assert governs_path(".agents/skills/pr-lifecycle/SKILL.md")
88
88
  assert governs_path(".agents/skills/eli5/SKILL.md")
89
89
  assert governs_path("audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md")
90
90
  assert governs_path("system-prompts/software-engineer.xml")
@@ -10,23 +10,17 @@ ALWAYS_ON_RULE_NAMES = frozenset(
10
10
  {
11
11
  "AGENTS.md",
12
12
  "CLAUDE.md",
13
- "agent-merges-its-own-green-pull-request.md",
14
13
  "asd-ste100-language.md",
15
- "ci-owns-the-gate.md",
16
14
  "cleanup-temp-files.md",
17
15
  "correction-lens.md",
18
16
  "destructive-commands.md",
19
- "durable-post-artifacts.md",
20
17
  "explore-thoroughly.md",
21
18
  "filesystem-search.md",
22
- "gh-cli-conventions.md",
23
- "git-workflow.md",
24
19
  "memory-stores-durable-facts.md",
25
20
  "no-contrast-framing.md",
26
21
  "question-presentation.md",
27
- "re-stage-before-commit.md",
28
22
  "research-mode.md",
29
- "review-closure-is-a-check.md",
23
+ "skill-pointers.md",
30
24
  "shell-invocation.md",
31
25
  "verify-before-asking.md",
32
26
  "verify-runtime-state.md",
@@ -1,55 +0,0 @@
1
- # The Agent Merges Its Own Green Pull Request
2
-
3
- **When this applies:** Any pull request an agent opened or was asked to drive, once its checks report.
4
-
5
- ## Rule
6
-
7
- The agent that drives a pull request merges it. A pull request that is green, carries no open review thread, and sits at a merge state of `clean` is merged in the same run that brought it there. A merge state of `unstable` counts as `clean` when every check's newest report on the head passes. GitHub also counts the older runs of a check that ran again, so a cancelled run followed by a passing re-run still reads `unstable`. Waiting for the owner to type "merge" parks finished work on the person the work was done for.
8
-
9
- Three things stay with the owner, and nothing else does:
10
-
11
- - A pull request the owner asked to hold.
12
- - A change the owner said they want to read first.
13
- - A repository whose branch rule requires an approving review the agent cannot give.
14
-
15
- Where the branch rule requires zero approvals and one status check, that check is the gate, and the agent merges on its verdict.
16
-
17
- Validation is the precondition, and green means every check reported on the exact head commit. A branch rule that requires one status check names the floor a merge needs; a pull request whose other checks are red or still running is held until they report.
18
-
19
- ## The precondition is mechanical
20
-
21
- One command prints the verdict:
22
-
23
- ```
24
- python packages/claude-dev-env/scripts/agent_merge_check.py <owner>/<name> <number>
25
- ```
26
-
27
- It prints `MERGE` and exits 0 when the pull request is ready. It prints `HOLD` with the reason and exits 1 for a draft, for a head behind or conflicting with the base, for a required check that is not passing, for a check whose newest report on the head is still running, cancelled, or red, for a head the merge queue ejected for failed checks, and for an open review thread. It exits 2 when the state could not be read.
28
-
29
- Each hold reason names its own repair, and each repair belongs to the agent:
30
-
31
- | Hold | What the agent does |
32
- |---|---|
33
- | Head behind the base | Merge the base branch in and push |
34
- | Head conflicts with the base | Merge the base branch in, resolve, push |
35
- | A required check is red | Read the failing check, fix it, push |
36
- | A check is red, cancelled, or still running | Fix a red one, re-run a cancelled one, or wait for a running one, then read the verdict again |
37
- | A review thread is open | Answer it, push the fix, resolve the thread |
38
- | Ejected from the merge queue for failed checks on this head | Read the merge_group run, fix the failure, push, then read the verdict again |
39
- | The pull request is a draft | Mark it ready once the checks pass |
40
-
41
- ## When a gate elsewhere holds the merge command
42
-
43
- A session working inside another repository can sit behind that repository's own pre-merge gate, which reads the checkout the session works in and refuses a merge command whatever repository the pull request belongs to. That session hands the merge to the session that owns this repository's pull requests, by message, naming the pull request. The receiving session reads the verdict above and merges. The hand-off carries the work; it never lands on the owner.
44
-
45
- ## After the merge
46
-
47
- Delete nothing by hand. The repository deletes the head branch on merge.
48
-
49
- ## Sibling rules
50
-
51
- | Rule | Role |
52
- |---|---|
53
- | [`git-workflow.md`](git-workflow.md) | Open ready for review, and confirm each required context fired after the push |
54
- | [`ci-owns-the-gate.md`](ci-owns-the-gate.md) | The gate runs once, and it runs on CI |
55
- | [`correction-lens.md`](correction-lens.md) | A correction becomes a control at the highest layer that can hold it |
@@ -1,101 +0,0 @@
1
- # CI Owns the Gate
2
-
3
- **When this applies:** Before pushing a branch, and any time a repository's full
4
- check suite or policy gate is about to run on your own machine.
5
-
6
- ## Rule
7
-
8
- The gate runs once, and it runs on CI. Push the branch, read the verdict, act on
9
- what it says.
10
-
11
- The inner development loop stays yours. Run the single test you are writing, as
12
- often as it helps. That is how the change gets built. This rule governs the
13
- second full pass, the one whose only product is a prediction of CI's answer.
14
- Push instead, and spend the wait on the next piece of work.
15
-
16
- ## Why
17
-
18
- **A pinned gate answers only from its pinned revision.** The workflow names an
19
- exact revision of the policy package, and that revision decides which rules run.
20
- The copy under your home directory is a separate artifact at its own revision. A
21
- run against the home copy reports on those rules, which are a different question
22
- from the one CI asks. Treat its exit code as information about the home copy
23
- alone.
24
-
25
- **Self-hosted runners often share your machine.** Where the runners execute on
26
- the same host as your shell, a local suite run draws its processor time from the
27
- runners, and it does so while they work on the branch you just pushed. Read
28
- where the runners live, and count a local run against the same budget.
29
-
30
- **One authoritative answer beats two.** Where both runs agree, the second one
31
- restated the first. Where they differ, the environments differ, and CI is the
32
- environment that decides.
33
-
34
- ## Running a gate locally
35
-
36
- Clone the revision the workflow pins, then point the gate at that clone. That
37
- run asks CI's question and its answer carries. Report a local result by naming
38
- the revision it used, so a reader can tell which question it answered.
39
-
40
- The selection flag decides which question the staged policy lint answers.
41
- `--staged` and `--base <revision>` carry each file's prior text, so the lint
42
- subtracts what the prior text already reported and only a breach the change
43
- introduced survives. `--files` and `--repository` carry no prior text, so every
44
- breach in the file reports and the command exits non-zero on debt the change
45
- never touched. CI runs the merge-base form, so reproduce a CI verdict with it:
46
-
47
- ```
48
- git merge-base HEAD origin/main
49
- python packages/claude-dev-env/scripts/cde_lint.py --base <the revision that printed>
50
- ```
51
-
52
- A `--files` run that comes back red on a file you touched has answered a
53
- different question. Read the reported line before you treat it as yours.
54
-
55
- ## The verdict belongs to CI
56
-
57
- CI decides whether a change passed, from evidence CI gathered. Keep that loop
58
- closed. A flag, trailer, receipt, or environment variable through which the
59
- change under test announces its own result hands the verdict to the subject.
60
- Where the runner and the agent share one host, a signature names the same party
61
- twice, so it carries the claim no further.
62
-
63
- A cache stays available on one condition. CI derives the key itself from the
64
- tree it is about to test, looks for a previous run under that key, and
65
- republishes that result. CI computes, CI verifies, CI decides.
66
-
67
- ## A repository that runs no CI
68
-
69
- An owner can rule that a repository spends no CI minutes. There the local gate
70
- is the gate, and the agent that drives the pull request runs it on every head
71
- it asks to merge. The repository carries the gate as a `cde verify` manifest at
72
- `.claude/local-gate.json`: tests with collection floors, and lint.
73
-
74
- 1. Check out the pull request head with a clean tree and fetch its base.
75
- 2. Run the manifest against the base commit the pull request names:
76
-
77
- ```
78
- python <cde>/scripts/local_verification/cli.py --manifest .claude/local-gate.json --repo . --base <base sha> --output <outside the repository>/report.json
79
- ```
80
-
81
- 3. Publish the report:
82
-
83
- ```
84
- python <cde>/scripts/local_report_publisher.py --token-environment GH_TOKEN --repository <owner>/<name> --pull-number <number> --local-repo . --manifest .claude/local-gate.json --report <outside the repository>/report.json
85
- ```
86
-
87
- The publisher checks the report against the manifest digest, the clean tree,
88
- and the live head and base, then posts the `local-checks` commit status. A
89
- report that fails any of those posts a failure or an error. The branch ruleset
90
- lists `local-checks` as a required status check, so the host refuses a merge
91
- until a passing report covers the head, and `agent_merge_check.py` prints
92
- `HOLD` on the same requirement. A push moves the head and leaves the status
93
- behind, so each new head runs the gate again.
94
-
95
- ## Sibling rules
96
-
97
- | Rule | Role |
98
- |---|---|
99
- | [`git-workflow.md`](git-workflow.md) | Confirm each required context fired after the push |
100
- | [`verify-runtime-state.md`](verify-runtime-state.md) | A status field is a report; read the thing the work was meant to make |
101
- | [`falsify-before-green.md`](falsify-before-green.md) | A green counts as evidence once the check has run red on a deliberate break |
@@ -1,76 +0,0 @@
1
- # GitHub post input rules
2
-
3
- ## When this applies
4
-
5
- Use this rule for a GitHub issue, pull request, comment, or review created
6
- through `gh` or a GitHub MCP post tool. The image section also covers a PNG a
7
- commit adds and a file a release upload sends.
8
-
9
- ## Rule
10
-
11
- A post remains after the job ends. Job scratch directories, worktrees, and
12
- system temp folders do not. Do not put a path from one of those directories in
13
- a post.
14
-
15
- Handle text and binary content differently:
16
-
17
- - **Text data** such as logs, tables, diffs, and stack traces belongs inline in
18
- the post body. Do not link a scratch file that holds text data.
19
- - **Binary artifacts** such as images, screenshots, and archives belong in the
20
- repository's durable `artifacts` release. Use the helper:
21
-
22
- ```
23
- python3 ~/.claude/scripts/gh_artifact_upload.py <file-path> <owner/repo>
24
- ```
25
-
26
- The helper creates the `artifacts` prerelease when needed, uploads the file
27
- under a `YYYYMMDD_HHMMSS_<name>` asset name, and prints a permanent download
28
- URL. Put that URL in the post.
29
-
30
- ## Optimize every image before it reaches GitHub
31
-
32
- The helper shrinks a PNG with [oxipng](https://github.com/oxipng/oxipng) before
33
- it uploads, and prints the size before and after. Oxipng is lossless.
34
- `--strip none` keeps every chunk, and `--nb --nc` keep the bit depth and the
35
- color type, so a reader that checks the image mode sees the same file shape.
36
-
37
- Any other route to GitHub runs the same pass first: a PNG a commit adds, and a
38
- file sent with `gh release upload`.
39
-
40
- ```
41
- oxipng --opt 4 --strip none --nb --nc <file> [<file> ...]
42
- ```
43
-
44
- The `Binary optimization` check fails a pull request whose changed PNG files
45
- still shrink under that pass. A fixture whose exact bytes a test pins takes
46
- `binary-optimizer=keep` in `.gitattributes`, and the check skips it.
47
-
48
- ## Volatile paths that must not appear in a post body
49
-
50
- - A job scratch directory: `.claude-profile-a/jobs/`
51
- - A worktree: `.claude/worktrees/`
52
- - A system temp location: `AppData\\Local\\Temp`, `%TEMP%`, `$env:TEMP`, or
53
- `/tmp/`
54
- - The job scratch environment variable: `$CLAUDE_JOB_DIR`
55
-
56
- Both slash directions count. The path rule applies when a slash or backslash
57
- precedes a marker, or when a path segment follows it. A standalone directory
58
- name does not form a path.
59
-
60
- ## Validation
61
-
62
- Resolve the active managed root (`CLAUDE_CONFIG_DIR` when set, `~/.claude`
63
- otherwise), then run `<managed-root>/scripts/durable_post_lint.py` before the
64
- server write. Pass the matching action and body file. Use `pr-create`,
65
- `pr-edit`, `pr-comment`, `pr-review`, `issue-create`, `issue-edit`,
66
- `issue-comment`, or `github-mcp-post`.
67
-
68
- Pass `--repository <owner>/<name>` for the repository the post targets. A
69
- post may name a private organization only inside a repository that
70
- organization owns. The same digests cover the owner's private repository, the
71
- owner's personal handle, and a private client. The linter holds the names as
72
- digests, so it reports the line that names one without printing the name.
73
- Describe the name in general terms and drop the link.
74
-
75
- The linter reads the body file and reports a volatile local path without
76
- printing the body. Fix the body and rerun the linter before posting.
@@ -1,36 +0,0 @@
1
- # gh CLI conventions
2
-
3
- Two `gh` call shapes need explicit handling.
4
-
5
- ## Put body content in a file
6
-
7
- Every `gh` command that carries markdown body content uses
8
- `--body-file <path>`. This applies to `gh pr create`, `gh pr edit`,
9
- `gh pr comment`, `gh pr review`, `gh issue create`, `gh issue edit`, and
10
- `gh issue comment`. Never pass a `--body` or `-b` string. Write the file as
11
- BOM-free UTF-8:
12
-
13
- ```powershell
14
- [IO.File]::WriteAllText($bodyPath, $body, [Text.UTF8Encoding]::new($false))
15
- ```
16
-
17
- MCP GitHub tools take `body` as a structured parameter. Write the same body to
18
- a UTF-8 file and run the shared linter before sending that parameter.
19
-
20
- For pull requests, use
21
- `.agents/skills/pull-request/scripts/pull_request.py`. It passes
22
- `--body-file` to `gh` after the action-aware linter succeeds. For issues and
23
- GitHub MCP posts, run the linter directly with `issue-create`, `issue-edit`,
24
- `issue-comment`, or `github-mcp-post` as the action.
25
-
26
- ## Paginated reads slurp before they filter
27
-
28
- Every `gh api` read of a paginated GitHub list endpoint uses
29
- `--paginate --slurp` and pipes the result to external `jq`. This applies to PR
30
- reviews, comments, and files, plus issue comments, pulls, and issues. The
31
- built-in `--jq` runs once per page and can produce a wrong cross-page result.
32
-
33
- Single-object endpoints such as `pulls/<n>` and `issues/<n>` do not need
34
- pagination and may use `--jq` directly. For a newest-first walk, sort the
35
- slurped array and take the last element. For one page, cap the request with a
36
- `per_page` query parameter.
@@ -1,121 +0,0 @@
1
- # Git workflow
2
-
3
- User-level rule: applies to **every** git repo that uses GitHub with `gh`. Small or non-primary repos follow the same rule unless the user says otherwise in the session.
4
-
5
- ## Workflow decision tree
6
-
7
- **When to use stacked PRs:** Feature B depends on Feature A's implementation
8
-
9
- **When to extract shared infrastructure first:** Multiple features need same utilities/helpers
10
-
11
- **Extract Shared Infrastructure Pattern:**
12
- 1. Create infrastructure PR with only shared code
13
- 2. Get reviewed and MERGE infrastructure first
14
- 3. Launch parallel feature PRs that use merged infrastructure
15
-
16
- ## Pull request submission rules
17
-
18
- **Open every pull request ready for review.** Pass `--draft` only when the owner asks
19
- for a draft.
20
-
21
- **A release bot's PR body is machine input. Leave it alone.** Release automation reads
22
- back the body of its own merged pull request to decide it owns that merge. Rewriting the
23
- body, or trimming its header or footer, makes the bot treat the merge as somebody else's
24
- work: it cuts no tag, the publish job skips, and it opens one more release pull request on
25
- the next run. The merge stays in the repository. No tag is cut and the package never publishes.
26
-
27
- Spot one by its head branch, which starts `release-please--branches--`, or by a body that
28
- opens with the bot's own marker line. The description rules in this file, the
29
- `pstack:poteto-agent` writing brief, and the house wording style all step aside for it. The
30
- failure signature in the release job log reads
31
- `could not parse pull request body as a release PR`.
32
-
33
- `pstack:poteto-agent` writes a title and body from the diff when you want one.
34
- Publish the title and body file through
35
- `~/.agents/skills/pull-request/scripts/pull_request.py`. That path is under the
36
- agents home, not the repository. A worktree holds no `.agents/` copy.
37
-
38
- Resolve the active managed root (`CLAUDE_CONFIG_DIR` when set, `~/.claude`
39
- otherwise), then run `<managed-root>/scripts/durable_post_lint.py` before any
40
- pull request, issue, or GitHub MCP post. The linter checks the action-specific
41
- title, body, and volatile-path rules before credential lookup or network
42
- access.
43
-
44
- Use `.agents/skills/pull-request/scripts/recover_legacy_author.py
45
- <exact-state-file> --confirm-inactive` only for one explicitly selected legacy
46
- author record. Do not infer a record from age alone. Keep every other record
47
- untouched.
48
-
49
- ## Confirm the required checks fired, and let CI run them
50
-
51
- The gate runs once, and it runs on CI. Push the branch and read its verdict.
52
- [`ci-owns-the-gate.md`](ci-owns-the-gate.md) holds the reasoning and the shape
53
- a local run takes when one is warranted.
54
-
55
- Read the branch ruleset for the required check contexts before you push a
56
- branch, or any level of a stack: `gh api repos/<owner>/<repo>/rules/branches/<trunk>`.
57
- Read it to learn which checks must report. After the push, confirm each of those
58
- contexts appears on that level's head. A required check that never fired is
59
- invisible debt at every level, and it surfaces only after the whole stack is
60
- pushed, when the repair costs a second pass over every branch.
61
-
62
- A red required check blocks the branch, whoever owns the failing line. The
63
- staged policy lint grades a change against the file's prior text, so a finding
64
- that survives is one the change introduced or made worse. Fix that line in the
65
- next push or report the branch blocked. A finding the change did not introduce
66
- is a gate-scoping defect: report it against the lint and leave the file's shape
67
- alone. Restructuring a file to satisfy a mis-scoped check trades one finding for
68
- a set of new ones. Read the gate's own report rather than a narrower substitute. A
69
- single-file mypy call cannot see sibling modules and reports false import
70
- errors, so it neither clears nor convicts a change.
71
-
72
- A checks listing that reports nothing on the branch is a finding, not a neutral
73
- state. Find out whether the workflow's event filters exclude the branch, or whether
74
- the check simply never ran, before you treat that branch as clean.
75
-
76
- ## Each stack level stands on its own
77
-
78
- A symbol belongs at the level that first **uses** it, not the level that first
79
- mentions it. A bottom pull request that declares the imports its descendants will
80
- need fails the linter on unused imports. A test helper that calls a function three
81
- levels above it fails on an undefined name. Both defects stay invisible while you
82
- read the finished tip, and both are obvious the moment you check one level alone.
83
-
84
- Prove each level before you push it: import the modules that level changes, and run
85
- the required linter against that level's own base. To repair a level, rebuild its
86
- import header as the union of what that level references, let the linter's
87
- autofix strip the rest, and move a premature helper up to the level that defines
88
- what it calls.
89
-
90
- ## A force-push that moves content obliges a description refresh
91
-
92
- Force-with-lease protects the ref. It protects nobody's understanding of what the
93
- branch now holds. When a rewrite moves content between levels of a stack, or
94
- otherwise changes what a branch contains, refresh that pull request's description
95
- before you ask anyone to read or merge it.
96
-
97
- ## Never commit working documents or images
98
-
99
- **Keep these files out of the repository:**
100
-
101
- | Pattern | Reason |
102
- |---------|--------|
103
- | `docs/plans/*.md` | Working documents for planning, not repo content |
104
- | `*.plan.md` | Temporary planning files |
105
- | `SESSION_STATE.md` | Local session state |
106
- | `*.png *.jpg *.jpeg *.gif *.webp *.avif *.svg *.ico` | Images go to external storage, not GitHub |
107
-
108
- An image a PR needs as visual evidence is not an exception to that row. Upload it to the repository's durable `artifacts` release with `python3 ~/.claude/scripts/gh_artifact_upload.py <file> <owner/repo>` and embed the permanent URL in the PR comment. The image lives on GitHub without entering the repository tree.
109
-
110
- ## Responding to review feedback
111
-
112
- **When this applies:** GitHub PR review feedback on a branch you are fixing.
113
-
114
- 1. Fetch every reviewer comment before making any fix.
115
- 2. Create a checklist in the session's task tool with one item per comment.
116
- 3. Fix systematically, marking each todo complete.
117
- 4. Reply to each comment inline.
118
-
119
- Repair only the reported findings.
120
-
121
- Every `gh` post in this workflow uses `--body-file` per `gh-cli-conventions.md` and keeps volatile scratch paths out per `durable-post-artifacts.md`. Stage session edits per `re-stage-before-commit.md` before each commit.
@@ -1,14 +0,0 @@
1
- # Re-Stage Session Edits Before Commit
2
-
3
- Stage the files you edited this session right before you commit them. A plain `git commit` records only the staged snapshot; a tracked file this session changed but left unstaged stays behind in the working tree.
4
-
5
- No hook denies a commit that would drop tracked session edits. Run `git status` before you commit, then stage what you changed with `git add <paths>` or commit with `git commit -a`.
6
-
7
- Staging covers tracked files you edited. Do not commit untracked files unless the user explicitly instructs it. An untracked file in the working tree is outside the change until they say otherwise.
8
-
9
- ## Staging shapes
10
-
11
- - **A pathspec.** `git commit -- <paths>` or `git commit <paths>` commits only the named paths on purpose.
12
- - **A preceding `git add` or `git stage`.** `git add <paths> && git commit …` stages the files in its own segment before the commit runs.
13
-
14
- A `--amend` carries the same risk. An amend records the staged snapshot too, so an unstaged session edit is dropped the same way a plain commit drops it.