claude-dev-env 1.80.0 → 1.82.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.
- package/_shared/pr-loop/scripts/CLAUDE.md +2 -1
- package/_shared/pr-loop/scripts/code_rules_gate.py +116 -30
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +13 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +113 -0
- package/_shared/pr-loop/scripts/terminology_sweep.py +467 -0
- package/_shared/pr-loop/scripts/tests/CLAUDE.md +1 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +339 -0
- package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +297 -0
- package/audit-rubrics/CLAUDE.md +3 -2
- package/audit-rubrics/category_rubrics/CLAUDE.md +2 -1
- package/audit-rubrics/category_rubrics/category-a-api-contracts.md +2 -1
- package/audit-rubrics/category_rubrics/category-j-code-rules-compliance.md +13 -1
- package/audit-rubrics/category_rubrics/category-p-name-vs-behavior-contract.md +19 -0
- package/audit-rubrics/category_rubrics/category-q-cross-surface-claims.md +46 -0
- package/audit-rubrics/prompts/CLAUDE.md +2 -1
- package/audit-rubrics/prompts/category-a-api-contracts.md +1 -0
- package/audit-rubrics/prompts/category-j-code-rules-compliance.md +19 -7
- package/audit-rubrics/prompts/category-p-name-vs-behavior-contract.md +14 -0
- package/audit-rubrics/prompts/category-q-cross-surface-claims.md +51 -0
- package/docs/CODE_RULES.md +2 -2
- package/hooks/blocking/CLAUDE.md +3 -0
- package/hooks/blocking/code_rules_annotations_length.py +59 -11
- package/hooks/blocking/code_rules_banned_identifiers.py +48 -9
- package/hooks/blocking/code_rules_command_dispatch.py +140 -0
- package/hooks/blocking/code_rules_docstrings.py +93 -0
- package/hooks/blocking/code_rules_enforcer.py +54 -4
- package/hooks/blocking/code_rules_js_conventions.py +246 -0
- package/hooks/blocking/code_rules_naming_collection.py +5 -0
- package/hooks/blocking/code_rules_test_assertions.py +3 -0
- package/hooks/blocking/code_rules_unused_imports.py +0 -3
- package/hooks/blocking/conventional_pr_title_gate.py +444 -0
- package/hooks/blocking/duplicate_rmtree_helper_blocker.py +7 -0
- package/hooks/blocking/test_code_rules_command_dispatch.py +95 -0
- package/hooks/blocking/test_code_rules_enforcer_annotations.py +20 -2
- package/hooks/blocking/test_code_rules_enforcer_banned_identifier.py +10 -2
- package/hooks/blocking/test_code_rules_enforcer_banned_import_alias.py +8 -4
- package/hooks/blocking/test_code_rules_enforcer_dispatch_wiring.py +9 -3
- package/hooks/blocking/test_code_rules_enforcer_docstring_type_checking_gate.py +164 -0
- package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +16 -3
- package/hooks/blocking/test_code_rules_js_conventions.py +167 -0
- package/hooks/blocking/test_conventional_pr_title_gate.py +640 -0
- package/hooks/hooks.json +5 -0
- package/hooks/hooks_constants/CLAUDE.md +3 -0
- package/hooks/hooks_constants/blocking_check_limits.py +9 -0
- package/hooks/hooks_constants/command_dispatch_constants.py +28 -0
- package/hooks/hooks_constants/conventional_pr_title_gate_constants.py +58 -0
- package/hooks/hooks_constants/js_conventions_constants.py +54 -0
- package/package.json +1 -1
- package/rules/docstring-prose-matches-implementation.md +2 -1
- package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +43 -1
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/CLAUDE.md +2 -2
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +1 -4
- package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +100 -4
- package/skills/autoconverge/SKILL.md +92 -7
- package/skills/autoconverge/reference/convergence.md +3 -3
- package/skills/autoconverge/reference/gotchas.md +5 -5
- package/skills/autoconverge/reference/stop-conditions.md +1 -1
- package/skills/autoconverge/workflow/converge.contract.test.mjs +161 -29
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +342 -1
- package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +2 -2
- package/skills/autoconverge/workflow/converge.mjs +178 -32
- package/skills/autoconverge/workflow/converge_multi.mjs +6 -2
- package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +26 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Constants for the unanchored command-dispatch meta-gate."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
COMMAND_DISPATCH_PATH_MARKER: str = "hooks/blocking"
|
|
6
|
+
|
|
7
|
+
COMMAND_DISPATCH_LITERAL_PATTERN: re.Pattern[str] = re.compile(
|
|
8
|
+
r"\b(?:gh|git|npm|npx|node|python3?|pwsh|powershell|docker|kubectl|pip|cargo"
|
|
9
|
+
r"|yarn|pnpm)\\s[+*]"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
COMMAND_KEY_ACCESS_PATTERN: re.Pattern[str] = re.compile(
|
|
13
|
+
r"""(?:\[\s*|\.get\(\s*)["']command["']"""
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
FIRST_TOKEN_TOKENIZATION_PATTERN: re.Pattern[str] = re.compile(
|
|
17
|
+
r"shlex\.split|\.split\("
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
ALL_REGEX_START_ANCHOR_TOKENS: tuple[str, ...] = ("^", "\\A")
|
|
21
|
+
|
|
22
|
+
COMMAND_DISPATCH_MESSAGE_SUFFIX: str = (
|
|
23
|
+
"matches a multi-word command as a substring - anchor the pattern to the "
|
|
24
|
+
"start of the command (^) or tokenize the first word (shlex.split) so a "
|
|
25
|
+
"command like 'echo gh pr create' is not matched"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
MAX_COMMAND_DISPATCH_ISSUES: int = 20
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Configuration constants for the conventional_pr_title_gate PreToolUse hook."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
ALL_GH_EXECUTABLE_BASENAMES: frozenset[str] = frozenset({"gh", "gh.exe"})
|
|
6
|
+
PR_SUBCOMMAND_TOKEN: str = "pr"
|
|
7
|
+
ALL_PR_TITLE_SUBCOMMAND_VERBS: frozenset[str] = frozenset({"create", "edit"})
|
|
8
|
+
GH_PR_SUBCOMMAND_MINIMUM_TOKEN_COUNT: int = 3
|
|
9
|
+
|
|
10
|
+
BASH_TOOL_NAME: str = "Bash"
|
|
11
|
+
|
|
12
|
+
TITLE_LONG_FLAG: str = "--title"
|
|
13
|
+
TITLE_SHORT_FLAG: str = "-t"
|
|
14
|
+
REPO_LONG_FLAG: str = "--repo"
|
|
15
|
+
REPO_SHORT_FLAG: str = "-R"
|
|
16
|
+
|
|
17
|
+
WORKFLOWS_DIRECTORY_RELATIVE_PATH: str = ".github/workflows"
|
|
18
|
+
ALL_WORKFLOW_FILE_GLOB_PATTERNS: tuple[str, ...] = ("*.yml", "*.yaml")
|
|
19
|
+
|
|
20
|
+
ALL_SEMANTIC_TITLE_CI_MARKERS: tuple[str, ...] = (
|
|
21
|
+
"semantic-pull-request",
|
|
22
|
+
"action-semantic-pull-request",
|
|
23
|
+
"semantic_pull_request",
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
ALL_CONVENTIONAL_COMMIT_TYPES: tuple[str, ...] = (
|
|
27
|
+
"feat",
|
|
28
|
+
"fix",
|
|
29
|
+
"chore",
|
|
30
|
+
"docs",
|
|
31
|
+
"refactor",
|
|
32
|
+
"perf",
|
|
33
|
+
"ci",
|
|
34
|
+
"style",
|
|
35
|
+
"test",
|
|
36
|
+
"build",
|
|
37
|
+
"revert",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
CONVENTIONAL_COMMIT_TITLE_PATTERN: re.Pattern[str] = re.compile(
|
|
41
|
+
r"^(?:" + "|".join(ALL_CONVENTIONAL_COMMIT_TYPES) + r")(?:\([^)]+\))?!?: .+"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
SEMANTIC_ACTION_TYPES_INPUT_PATTERN: re.Pattern[str] = re.compile(r"^\s*types\s*:")
|
|
45
|
+
|
|
46
|
+
SEMANTIC_ACTION_FLOW_TYPES_INPUT_PATTERN: re.Pattern[str] = re.compile(r"[{,]\s*types\s*:")
|
|
47
|
+
|
|
48
|
+
YAML_LIST_ITEM_PREFIX: str = "- "
|
|
49
|
+
|
|
50
|
+
CORRECTIVE_MESSAGE: str = (
|
|
51
|
+
"BLOCKED [conventional-pr-title]: this repository's CI validates PR titles "
|
|
52
|
+
"against Conventional Commits, and the --title value here does not match. "
|
|
53
|
+
"Required shape: type(scope)!: description, where the scope and the "
|
|
54
|
+
"breaking-change marker (!) are optional. Allowed types: "
|
|
55
|
+
f"{', '.join(ALL_CONVENTIONAL_COMMIT_TYPES)}.\n\n"
|
|
56
|
+
"Example: feat(hooks): add the conventional PR title gate\n\n"
|
|
57
|
+
"Fix the --title value and retry."
|
|
58
|
+
)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Configuration constants for the JavaScript convention checks in code_rules_enforcer."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
ALL_JAVASCRIPT_BANNED_IDENTIFIERS: frozenset[str] = frozenset(
|
|
6
|
+
{
|
|
7
|
+
"result",
|
|
8
|
+
"data",
|
|
9
|
+
"output",
|
|
10
|
+
"response",
|
|
11
|
+
"value",
|
|
12
|
+
"item",
|
|
13
|
+
"temp",
|
|
14
|
+
"ctx",
|
|
15
|
+
"cfg",
|
|
16
|
+
"msg",
|
|
17
|
+
"btn",
|
|
18
|
+
"idx",
|
|
19
|
+
"cnt",
|
|
20
|
+
"tmp",
|
|
21
|
+
"elem",
|
|
22
|
+
"val",
|
|
23
|
+
}
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
_JAVASCRIPT_NEGATION_OPERAND: str = (
|
|
27
|
+
r"[A-Za-z_$][\w$]*"
|
|
28
|
+
r"(?:\s*\.\s*[A-Za-z_$][\w$]*|\s*\((?:[^()]|\([^()]*\))*\))*"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
JAVASCRIPT_BOOLEAN_DECLARATION_PATTERN: re.Pattern[str] = re.compile(
|
|
32
|
+
r"\b(?:const|let|var)\s+(?P<name>[A-Za-z_$][\w$]*)\s*=\s*"
|
|
33
|
+
r"(?:(?:true|false)\b|!\s*" + _JAVASCRIPT_NEGATION_OPERAND + r"\s*(?:;|$))"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
JAVASCRIPT_BOOLEAN_JSDOC_PARAMETER_PATTERN: re.Pattern[str] = re.compile(
|
|
37
|
+
r"@param\s*\{\s*boolean\s*\}\s+(?P<name>[A-Za-z_$][\w$]*)"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
JAVASCRIPT_DECLARATION_NAME_PATTERN: re.Pattern[str] = re.compile(
|
|
41
|
+
r"\b(?:const|let|var)\s+(?P<name>[A-Za-z_$][\w$]*)\s*="
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
JAVASCRIPT_BOOLEAN_PREFIX_PATTERN: re.Pattern[str] = re.compile(
|
|
45
|
+
r"^(?:is|has|should|can|was|did)(?:[A-Z0-9_]|$)"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
BOOLEAN_PREFIX_GUIDANCE: str = "prefix with is/has/should/can/was/did"
|
|
49
|
+
|
|
50
|
+
SINGLE_CHARACTER_NAME_LENGTH: int = 1
|
|
51
|
+
|
|
52
|
+
MAX_JAVASCRIPT_BOOLEAN_NAMING_ISSUES: int = 20
|
|
53
|
+
|
|
54
|
+
MAX_JAVASCRIPT_BANNED_IDENTIFIER_ISSUES: int = 20
|
package/package.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
When a docstring enumerates the behaviors a body applies, the enumeration covers every behavior the body applies. A reader trusts the list to be complete: an item the code applies but the prose omits is a silent gap that misleads every future reader and reviewer.
|
|
8
8
|
|
|
9
|
-
The gate validator `check_docstring_args_match_signature` covers the `Args:` section parameter names.
|
|
9
|
+
The gate validator `check_docstring_args_match_signature` covers the `Args:` section parameter names. Ten more gate validators each cover one deterministic slice of the free-form prose. `check_docstring_names_absent_type_checking_gate` covers a module or function docstring that names a `TYPE_CHECKING` gate-detection step, or a `type-checking-gate` helper family, while no identifier in the module's code carries the `type_checking` marker — the drift where the prose points a reader at a gate the body never performs; drop the `TYPE_CHECKING` gate wording, or add the detection the prose describes. `check_docstring_length_constant_superlative_vs_exact_gate` covers a length-constant module whose docstring describes an integer `*_LENGTH` constant with a superlative or range word (`the longest color string the swatch accepts`) while the only code consuming the constant compares `len(...)` against it with `==`/`!=` — an exact-length gate that rejects every other length — and never with an ordered operator; the check scans the constant module's package tree (its own directory, or the parent package when the module sits in a `config/` subdirectory), so the mismatching consumer may sit in a sibling module. `check_docstring_fallback_branch_coverage` covers a summary that scopes a fallback to a single condition (`only when`, `falls back to ... when`) while the body routes to that same fallback call from two or more distinct early-return guards. `check_class_docstring_names_public_methods` covers a class whose docstring is a single summary line while the class exposes two or more public methods whose names the summary never spells out — the drift where a one-line class summary keeps naming its first feature after the class grows a second public entry point. `check_docstring_no_consumer_claim` covers a producer docstring asserting that no consumer reads its output yet (`producer-only artifact`, `no submission-run consumer reads it yet`) — a transitional claim that drifts the moment a reader lands and contradicts any companion `SKILL.md` that documents the consumer; this is the deterministic slice of the O8 companion-doc producer/consumer drift below. `check_docstring_returns_plural_cardinality` covers a `Returns:` clause that names a dict-key prefix family with a plural noun (`the sheen stops`) while the returned dict literal holds exactly one key in that family (`sheen_mid`) — the drift where a single-key family carries a plural noun, so the prose claims a cardinality of two or more that the dict does not hold. `check_docstring_args_single_line_scope_vs_span` covers an `Args:` entry whose prose scopes a finding to a single named line (`only when its block-anchor line is among the changed lines`) while the body builds a `range(...)` span over the finding's source lines and scopes it through a span-intersection scoper — the drift where the Args entry claims a narrower single-line scope than the span-intersection body applies, so an edit touching any non-anchor line of the span still blocks. `check_docstring_cardinal_count_matches_constant_family` covers a docstring that states a cardinal count of an outcome family (`Covers the four outcome branches: ...`) and lists those members, while the module references more members of the same `UPPER_SNAKE` constant family than the count names (`OUTCOME_OFFENDER_UNREADABLE` is imported and exercised, yet the summary stops at four) — the drift where a summary keeps the old count after the code grows another branch; this gate runs on test modules as well as production modules. `check_docstring_raises_unraisable_largezipfile` covers a `Raises:` clause that names `zipfile.LargeZipFile` while the function opens its `zipfile.ZipFile` writer in a write mode (`w`/`a`/`x`) with `allowZip64` left at its default of True — `zipfile` raises `LargeZipFile` only when an entry needs ZIP64 and `allowZip64` is False, so a writer that allows ZIP64 documents an exception the body cannot produce. `check_docstring_no_network_claim_with_metadata_access` covers a function docstring promising a code path returns `without touching the network` (or a sibling no-network phrase) while the body calls a path-metadata method (`is_file`, `is_dir`, `exists`, `stat`, `lstat`) — on a network share each metadata call is a round-trip over the wire, so a cache-hit path the docstring swore avoids the network still pays a stat on every call; reword the claim to state the path is stat-checked on every call, or short-circuit to the cached path before the share is touched. The remaining free-form prose — `"a field counts as read when ..."`, `"resolves to shared temp only"`, `"strip ceremony, then drop blockquotes"`, and the broader module-responsibility paragraph outside the user-facing-text-scope slice the checklist below names — has no signature, method roster, or single structural shape to compare against, so the gate cannot catch its drift. This rule is the judgment standard for that prose; the audit lane below is the enforcement for everything outside the eleven gated slices.
|
|
10
10
|
|
|
11
11
|
## What to check before you write the docstring
|
|
12
12
|
|
|
@@ -28,6 +28,7 @@ Read the body and the docstring side by side:
|
|
|
28
28
|
- **Predicate breadth.** A boolean helper whose prose promises a narrow check accepts only the inputs the prose names — no broader input class the name and prose do not mention.
|
|
29
29
|
- **Exclusion-clause distinguisher.** A docstring sentence that says a named category of input "are not" / "is not" the thing the function flags (`plain logging, screenshot, or method-on-local calls inside a branch are not dispatch steps`) keys the exclusion to the same axis the body's classification keys on. When the body decides on one axis (a call sits in an `If.test` guard versus a plain statement) but the prose excludes on a different axis (the call's receiver shape — a method on a local), the exclusion clause names a category the body still flags: a guarded method-on-local call is flagged even though the prose lists method-on-local calls as excluded. Read the body's actual branch condition, then state the exclusion on that same axis (`plain (unguarded) calls inside a branch body are not dispatch steps`), so every member the prose excludes is a member the body also excludes.
|
|
30
30
|
- **Companion-doc ordering and content claims.** A `SKILL.md` (or sibling `.md`) sentence that names a produced artifact and claims its order (`sorted`, `alphabetical`, `in sorted order`) or its content (`the at-risk names`, `just the current set`) matches the producer function's docstring and body for that same artifact. A producer that builds the artifact by merging stored names with new names and appending — preserving file order, not re-sorting the union — leaves a doc that still says `sorted` drifted on both counts: the order claim is wrong, and the content claim hides the merged-in prior entries. When the producer's ordering or union changes, the same change updates the companion doc. The two move together in one commit, even when the producer edit does not touch the `.md` file.
|
|
31
|
+
- **TYPE_CHECKING gate claim vs code.** A docstring that names a `TYPE_CHECKING` gate-detection step, or a `type-checking-gate` helper family, matches a module whose code handles TYPE_CHECKING. When no identifier in the body carries the `type_checking` marker — no `TYPE_CHECKING` load, import alias, attribute, or helper name — the prose points a reader at a gate the module never performs. State what the module does, or add the detection the prose describes. The `check_docstring_names_absent_type_checking_gate` gate blocks this drift at Write/Edit time, and covers hook infrastructure, where the import-scan gates that carry this drift class live.
|
|
31
32
|
|
|
32
33
|
When the body changes the set of behaviors it applies, the same edit updates the prose enumeration. The two move together in one commit.
|
|
33
34
|
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
Builds <context> and <scope> from CLI args; <bug_categories>,
|
|
4
4
|
<rubric_reference>, and <constraints> come from the shared constants in
|
|
5
5
|
skills_pr_loop_constants; <comment_posting> and <output_format> are built
|
|
6
|
-
inline.
|
|
6
|
+
inline. <pr_description> carries the PR body text read from --pr-body-file,
|
|
7
|
+
and stays empty when that argument is absent or the file cannot be read.
|
|
7
8
|
|
|
8
9
|
Usage:
|
|
9
10
|
python scripts/build_audit_prompt.py --owner jl-cmd --repo claude-code-config --pr-number 422 --loop 1 --head-ref feat/branch --base-ref main --worktree-path <PATH> --run-temp-dir <PATH>
|
|
@@ -28,6 +29,38 @@ from skills_pr_loop_constants.path_resolver_constants import (
|
|
|
28
29
|
)
|
|
29
30
|
|
|
30
31
|
|
|
32
|
+
def read_pr_body_text(pr_body_file: Path | None) -> str | None:
|
|
33
|
+
"""Read the PR description body from a file when given and readable.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
pr_body_file: Path to a file holding the PR description body, or None.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
The file's text when the path is given and readable, otherwise None.
|
|
40
|
+
"""
|
|
41
|
+
if pr_body_file is None:
|
|
42
|
+
return None
|
|
43
|
+
try:
|
|
44
|
+
return pr_body_file.read_text(encoding="utf-8")
|
|
45
|
+
except OSError:
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _append_pr_description(root: Element, pr_body_text: str | None) -> None:
|
|
50
|
+
"""Append a <pr_description> element carrying the PR body text.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
root: Root element the <pr_description> child is appended to.
|
|
54
|
+
pr_body_text: PR description body text, or None when unavailable.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
None.
|
|
58
|
+
"""
|
|
59
|
+
pr_description = SubElement(root, "pr_description")
|
|
60
|
+
if pr_body_text:
|
|
61
|
+
pr_description.text = pr_body_text
|
|
62
|
+
|
|
63
|
+
|
|
31
64
|
def build_audit_prompt_xml(
|
|
32
65
|
*,
|
|
33
66
|
owner: str,
|
|
@@ -38,6 +71,7 @@ def build_audit_prompt_xml(
|
|
|
38
71
|
base_ref: str,
|
|
39
72
|
worktree_path: Path,
|
|
40
73
|
run_temp_dir: Path,
|
|
74
|
+
pr_body_text: str | None = None,
|
|
41
75
|
) -> Element:
|
|
42
76
|
"""Build the complete AUDIT spawn prompt XML.
|
|
43
77
|
|
|
@@ -50,6 +84,7 @@ def build_audit_prompt_xml(
|
|
|
50
84
|
base_ref: Base branch ref.
|
|
51
85
|
worktree_path: Path to the git worktree.
|
|
52
86
|
run_temp_dir: Path to the run temp directory.
|
|
87
|
+
pr_body_text: PR description body text, or None when unavailable.
|
|
53
88
|
|
|
54
89
|
Returns:
|
|
55
90
|
Root <spawn_prompt> element.
|
|
@@ -72,6 +107,8 @@ def build_audit_prompt_xml(
|
|
|
72
107
|
f"bugs, and anti-patterns. Work in {worktree_path.as_posix()}."
|
|
73
108
|
)
|
|
74
109
|
|
|
110
|
+
_append_pr_description(root, pr_body_text)
|
|
111
|
+
|
|
75
112
|
bug_categories = SubElement(root, "bug_categories")
|
|
76
113
|
for each_category_id, each_category_label in ALL_AUDIT_CATEGORY_ENTRIES:
|
|
77
114
|
cat_elem = SubElement(bug_categories, "category", {"id": each_category_id})
|
|
@@ -110,6 +147,7 @@ def emit_audit_prompt(
|
|
|
110
147
|
base_ref: str,
|
|
111
148
|
worktree_path: Path,
|
|
112
149
|
run_temp_dir: Path,
|
|
150
|
+
pr_body_text: str | None = None,
|
|
113
151
|
) -> str:
|
|
114
152
|
"""Build and serialize the AUDIT spawn prompt to a pretty-printed XML string.
|
|
115
153
|
|
|
@@ -122,6 +160,7 @@ def emit_audit_prompt(
|
|
|
122
160
|
base_ref: Base branch ref.
|
|
123
161
|
worktree_path: Path to the git worktree.
|
|
124
162
|
run_temp_dir: Path to the run temp directory.
|
|
163
|
+
pr_body_text: PR description body text, or None when unavailable.
|
|
125
164
|
|
|
126
165
|
Returns:
|
|
127
166
|
Pretty-printed XML string.
|
|
@@ -135,6 +174,7 @@ def emit_audit_prompt(
|
|
|
135
174
|
base_ref=base_ref,
|
|
136
175
|
worktree_path=worktree_path,
|
|
137
176
|
run_temp_dir=run_temp_dir,
|
|
177
|
+
pr_body_text=pr_body_text,
|
|
138
178
|
)
|
|
139
179
|
return emit_pretty_xml(root)
|
|
140
180
|
|
|
@@ -157,6 +197,7 @@ def parse_arguments(all_argv: list[str]) -> argparse.Namespace:
|
|
|
157
197
|
parser.add_argument("--base-ref", required=True)
|
|
158
198
|
parser.add_argument("--worktree-path", type=Path, required=True)
|
|
159
199
|
parser.add_argument("--run-temp-dir", type=Path, required=True)
|
|
200
|
+
parser.add_argument("--pr-body-file", type=Path, default=None)
|
|
160
201
|
return parser.parse_args(all_argv)
|
|
161
202
|
|
|
162
203
|
|
|
@@ -179,6 +220,7 @@ def main(all_arguments: list[str]) -> int:
|
|
|
179
220
|
base_ref=arguments.base_ref,
|
|
180
221
|
worktree_path=arguments.worktree_path,
|
|
181
222
|
run_temp_dir=arguments.run_temp_dir,
|
|
223
|
+
pr_body_text=read_pr_body_text(arguments.pr_body_file),
|
|
182
224
|
)
|
|
183
225
|
sys.stdout.write(xml_output)
|
|
184
226
|
return 0
|
|
@@ -7,7 +7,7 @@ Python package of named constants for the `pr-loop` shared scripts. All constant
|
|
|
7
7
|
| File | Role |
|
|
8
8
|
|---|---|
|
|
9
9
|
| `__init__.py` | Package marker. |
|
|
10
|
-
| `path_resolver_constants.py` | Path template strings and format constants: workspace directory naming, worktree directory name, diff patch filename pattern, outcome XML filename patterns,
|
|
10
|
+
| `path_resolver_constants.py` | Path template strings and format constants: workspace directory naming, worktree directory name, diff patch filename pattern, outcome XML filename patterns, fix status values, audit constraint texts, audit category entries, fix execution steps, and fix constraint texts. |
|
|
11
11
|
| `preflight_constants.py` | Constants used by `preflight_worktree.py` for exit codes and output marker strings. |
|
|
12
12
|
|
|
13
13
|
## Usage
|
|
@@ -15,5 +15,5 @@ Python package of named constants for the `pr-loop` shared scripts. All constant
|
|
|
15
15
|
Scripts import directly from the package:
|
|
16
16
|
|
|
17
17
|
```python
|
|
18
|
-
from skills_pr_loop_constants.path_resolver_constants import
|
|
18
|
+
from skills_pr_loop_constants.path_resolver_constants import PER_PR_WORKSPACE_TEMPLATE
|
|
19
19
|
```
|
|
@@ -9,7 +9,6 @@ WORKTREE_DIRNAME = "worktree"
|
|
|
9
9
|
DIFF_PATCH_TEMPLATE = "loop-{loop}.patch"
|
|
10
10
|
OUTCOME_XML_TEMPLATE = ".bugteam-pr{number}-loop{loop}.outcomes.xml"
|
|
11
11
|
FIX_OUTCOME_XML_TEMPLATE = ".bugteam-pr{number}-loop{loop}.fix-outcomes.xml"
|
|
12
|
-
LOOP_STATE_FILENAME = "loop-state.json"
|
|
13
12
|
SLUGIFY_SAFE_CHARS = re.compile(r"[^A-Za-z0-9._-]")
|
|
14
13
|
SLUGIFY_REPLACEMENT = "-"
|
|
15
14
|
MULTI_PR_SLUG_TEMPLATE = "{owner}-{repo}-pr-{number}"
|
|
@@ -47,6 +46,7 @@ ALL_AUDIT_CATEGORY_ENTRIES = [
|
|
|
47
46
|
("N", "Test-name scenario verifier"),
|
|
48
47
|
("O", "Docstring / fixture-prose vs implementation drift"),
|
|
49
48
|
("P", "Name / regex / word-list vs behavior-contract precision"),
|
|
49
|
+
("Q", "Cross-surface claim consistency (terminology, PR-description claims, message-vs-guard)"),
|
|
50
50
|
]
|
|
51
51
|
|
|
52
52
|
AUDIT_RUBRIC_REFERENCE_TEXT = (
|
|
@@ -77,9 +77,6 @@ ALL_FIX_CONSTRAINT_TEXTS = [
|
|
|
77
77
|
"forward slashes (e.g. C:/Users/...), even on Windows.",
|
|
78
78
|
]
|
|
79
79
|
|
|
80
|
-
XML_PRETTY_INDENT = " "
|
|
81
|
-
XML_SERIALIZE_ENCODING = "unicode"
|
|
82
|
-
XML_OUTPUT_ENCODING = "utf-8"
|
|
83
80
|
ALL_PYTHON_ONEXC_VERSION = (3, 12)
|
|
84
81
|
|
|
85
82
|
ALL_FINDING_BODY_ELEMENT_KEYS: tuple[str, ...] = (
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Tests pinning build_audit_prompt's emitted A-
|
|
1
|
+
"""Tests pinning build_audit_prompt's emitted A-Q category taxonomy."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -18,7 +18,7 @@ from skills_pr_loop_constants.path_resolver_constants import (
|
|
|
18
18
|
)
|
|
19
19
|
|
|
20
20
|
_CATEGORY_RUBRICS_DIR = _SCRIPTS_DIR.parents[3] / "audit-rubrics" / "category_rubrics"
|
|
21
|
-
_HEADING_PATTERN = re.compile(r"^# Category ([A-
|
|
21
|
+
_HEADING_PATTERN = re.compile(r"^# Category ([A-Q]) — (.+)$")
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
def _load_build_audit_prompt() -> ModuleType:
|
|
@@ -84,12 +84,12 @@ def test_context_and_scope_render_paths_with_forward_slashes() -> None:
|
|
|
84
84
|
assert "C:/Users/jon/AppData/Local/Temp/bugteam-pr-376/worktree" in scope.text
|
|
85
85
|
|
|
86
86
|
|
|
87
|
-
def
|
|
87
|
+
def test_bug_categories_carry_ids_a_through_q_in_order() -> None:
|
|
88
88
|
root = _build_audit_root()
|
|
89
89
|
bug_categories = root.find("bug_categories")
|
|
90
90
|
assert bug_categories is not None
|
|
91
91
|
all_emitted_ids = [each_category.get("id") for each_category in bug_categories]
|
|
92
|
-
all_expected_ids = list("
|
|
92
|
+
all_expected_ids = list("ABCDEFGHIJKLMNOPQ")
|
|
93
93
|
assert all_emitted_ids == all_expected_ids
|
|
94
94
|
|
|
95
95
|
|
|
@@ -161,3 +161,99 @@ def test_prompt_skeleton_sub_bucket_counts_match_rubric_rows() -> None:
|
|
|
161
161
|
f"{each_letter}{each_walk_match.group(1)} but rubric has "
|
|
162
162
|
f"{sub_bucket_row_count} rows"
|
|
163
163
|
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def test_bug_categories_include_category_q() -> None:
|
|
167
|
+
root = _build_audit_root()
|
|
168
|
+
bug_categories = root.find("bug_categories")
|
|
169
|
+
assert bug_categories is not None
|
|
170
|
+
label_by_id = {
|
|
171
|
+
each_category.get("id"): each_category.text for each_category in bug_categories
|
|
172
|
+
}
|
|
173
|
+
assert label_by_id["Q"] == (
|
|
174
|
+
"Cross-surface claim consistency "
|
|
175
|
+
"(terminology, PR-description claims, message-vs-guard)"
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def test_pr_description_carries_body_text_when_supplied() -> None:
|
|
180
|
+
root = build_audit_prompt.build_audit_prompt_xml(
|
|
181
|
+
owner="jl-cmd",
|
|
182
|
+
repo="claude-code-config",
|
|
183
|
+
pr_number=422,
|
|
184
|
+
loop=1,
|
|
185
|
+
head_ref="feat/branch",
|
|
186
|
+
base_ref="main",
|
|
187
|
+
worktree_path=Path("/tmp/bugteam-pr-422/worktree"),
|
|
188
|
+
run_temp_dir=Path("/tmp/bugteam-pr-422"),
|
|
189
|
+
pr_body_text="## Summary\nCloses the gate.",
|
|
190
|
+
)
|
|
191
|
+
pr_description = root.find("pr_description")
|
|
192
|
+
assert pr_description is not None
|
|
193
|
+
assert pr_description.text == "## Summary\nCloses the gate."
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def test_pr_description_empty_when_body_absent() -> None:
|
|
197
|
+
root = _build_audit_root()
|
|
198
|
+
pr_description = root.find("pr_description")
|
|
199
|
+
assert pr_description is not None
|
|
200
|
+
assert pr_description.text is None
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def test_read_pr_body_text_returns_file_contents(tmp_path: Path) -> None:
|
|
204
|
+
body_file = tmp_path / "pr-body.md"
|
|
205
|
+
body_file.write_text("body from disk", encoding="utf-8")
|
|
206
|
+
assert build_audit_prompt.read_pr_body_text(body_file) == "body from disk"
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def test_read_pr_body_text_returns_none_when_path_absent() -> None:
|
|
210
|
+
assert build_audit_prompt.read_pr_body_text(None) is None
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def test_read_pr_body_text_returns_none_when_file_missing(tmp_path: Path) -> None:
|
|
214
|
+
missing_file = tmp_path / "does-not-exist.md"
|
|
215
|
+
assert build_audit_prompt.read_pr_body_text(missing_file) is None
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def test_emit_audit_prompt_embeds_pr_body_text_in_pr_description() -> None:
|
|
219
|
+
xml_text = build_audit_prompt.emit_audit_prompt(
|
|
220
|
+
owner="jl-cmd",
|
|
221
|
+
repo="claude-code-config",
|
|
222
|
+
pr_number=422,
|
|
223
|
+
loop=1,
|
|
224
|
+
head_ref="feat/branch",
|
|
225
|
+
base_ref="main",
|
|
226
|
+
worktree_path=Path("/tmp/bugteam-pr-422/worktree"),
|
|
227
|
+
run_temp_dir=Path("/tmp/bugteam-pr-422"),
|
|
228
|
+
pr_body_text="body via emit",
|
|
229
|
+
)
|
|
230
|
+
assert "body via emit" in xml_text
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def test_parse_arguments_defaults_pr_body_file_to_none() -> None:
|
|
234
|
+
arguments = build_audit_prompt.parse_arguments([
|
|
235
|
+
"--owner", "jl-cmd",
|
|
236
|
+
"--repo", "claude-code-config",
|
|
237
|
+
"--pr-number", "422",
|
|
238
|
+
"--loop", "1",
|
|
239
|
+
"--head-ref", "feat/branch",
|
|
240
|
+
"--base-ref", "main",
|
|
241
|
+
"--worktree-path", "/tmp/wt",
|
|
242
|
+
"--run-temp-dir", "/tmp/rt",
|
|
243
|
+
])
|
|
244
|
+
assert arguments.pr_body_file is None
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def test_parse_arguments_reads_pr_body_file_path() -> None:
|
|
248
|
+
arguments = build_audit_prompt.parse_arguments([
|
|
249
|
+
"--owner", "jl-cmd",
|
|
250
|
+
"--repo", "claude-code-config",
|
|
251
|
+
"--pr-number", "422",
|
|
252
|
+
"--loop", "1",
|
|
253
|
+
"--head-ref", "feat/branch",
|
|
254
|
+
"--base-ref", "main",
|
|
255
|
+
"--worktree-path", "/tmp/wt",
|
|
256
|
+
"--run-temp-dir", "/tmp/rt",
|
|
257
|
+
"--pr-body-file", "/tmp/body.md",
|
|
258
|
+
])
|
|
259
|
+
assert arguments.pr_body_file == Path("/tmp/body.md")
|
|
@@ -131,7 +131,10 @@ no agent spawned. The workflow runs in the background and notifies this session
|
|
|
131
131
|
on completion. Watch live progress with `/workflows`.
|
|
132
132
|
|
|
133
133
|
The workflow returns
|
|
134
|
-
`{ converged, rounds, finalSha, blocker, standardsNote, copilotNote, reuseNote }`.
|
|
134
|
+
`{ converged, rounds, finalSha, blocker, standardsNote, copilotNote, reuseNote, deferredPrs }`.
|
|
135
|
+
`deferredPrs` is the list of draft environment-hardening PRs the standards-deferral
|
|
136
|
+
path opened this run, each as `{ owner, repo, prNumber }` — the seed the
|
|
137
|
+
[self-closing loop](#self-closing-loop-converge-the-deferred-prs) converges next.
|
|
135
138
|
|
|
136
139
|
## Budget-aware round boundaries
|
|
137
140
|
|
|
@@ -312,10 +315,10 @@ matches.
|
|
|
312
315
|
(hooks/rules that block those violation classes at Write/Edit time), resolves
|
|
313
316
|
any bot threads with a deferral note, and reports the deferral in
|
|
314
317
|
`standardsNote`.
|
|
315
|
-
- **Copilot gate:** request a Copilot review, poll up to
|
|
318
|
+
- **Copilot gate:** request a Copilot review, poll up to the configured cap; findings
|
|
316
319
|
route back into Converge. When Copilot is down or out of quota — it posts an
|
|
317
320
|
out-of-usage notice (the requester hit their quota) on the HEAD, or surfaces no
|
|
318
|
-
review at all after the cap — the gate logs a notice and the run marks the PR
|
|
321
|
+
review at all after the configured cap — the gate logs a notice and the run marks the PR
|
|
319
322
|
ready with the Copilot gate bypassed. `copilotNote` records the bypass.
|
|
320
323
|
- **Convergence check:** `check_convergence.py` is the authoritative gate; on a
|
|
321
324
|
full pass the workflow marks `draft=false`.
|
|
@@ -387,10 +390,13 @@ checked out in. The workflow runs in the background and notifies this session on
|
|
|
387
390
|
completion; watch live progress with `/workflows`, where each PR's child run
|
|
388
391
|
appears under its own group.
|
|
389
392
|
|
|
390
|
-
The workflow returns
|
|
391
|
-
|
|
392
|
-
`
|
|
393
|
-
`
|
|
393
|
+
The workflow returns
|
|
394
|
+
`{ converged, prCount, convergedCount, results, allDeferredPrs, blocker }`, where
|
|
395
|
+
`results` is one record per PR carrying
|
|
396
|
+
`{ owner, repo, prNumber, converged, rounds, finalSha, blocker, deferredPrs }`.
|
|
397
|
+
Each record's `deferredPrs` is that PR's own list of draft hardening PRs, and
|
|
398
|
+
`allDeferredPrs` is every record's `deferredPrs` flattened into one list. The
|
|
399
|
+
top-level `converged` is true only when every PR converged.
|
|
394
400
|
|
|
395
401
|
### Multi-PR teardown (on workflow completion)
|
|
396
402
|
|
|
@@ -403,6 +409,85 @@ permissions once per repository after every PR's teardown. Then print one summar
|
|
|
403
409
|
report — a line per PR as
|
|
404
410
|
`#<prNumber>: <converged | blocked> — rounds <N>, final <finalSha>[, blocker <blocker>]`.
|
|
405
411
|
|
|
412
|
+
## Self-closing loop: converge the deferred PRs
|
|
413
|
+
|
|
414
|
+
Every run leaves work behind. When a round holds only code-standard findings, the
|
|
415
|
+
standards-deferral path opens a draft environment-hardening PR and reports it in
|
|
416
|
+
`deferredPrs`. That PR is itself a draft that needs to reach ready, and its own
|
|
417
|
+
convergence can defer more standard findings, opening more hardening PRs. The
|
|
418
|
+
self-closing loop drives that chain to the ground: after teardown, the
|
|
419
|
+
orchestrator (this session, which launched the workflow) converges the deferred
|
|
420
|
+
PRs, then the PRs their runs defer, and so on until a generation opens none.
|
|
421
|
+
|
|
422
|
+
This loop runs by default at the end of every autoconverge run — single-PR and
|
|
423
|
+
multi-PR alike. It stops only when a generation's deferred-PR list comes back
|
|
424
|
+
empty.
|
|
425
|
+
|
|
426
|
+
### Seed the loop
|
|
427
|
+
|
|
428
|
+
Collect the first generation of deferred PRs from the run that just finished:
|
|
429
|
+
|
|
430
|
+
- A single-PR run seeds from its `deferredPrs`.
|
|
431
|
+
- A multi-PR run seeds from its `allDeferredPrs`.
|
|
432
|
+
|
|
433
|
+
When the seed list is empty, the loop is already done — the run deferred nothing,
|
|
434
|
+
so there is nothing to converge. Report that and stop.
|
|
435
|
+
|
|
436
|
+
### Each generation
|
|
437
|
+
|
|
438
|
+
Given a non-empty list of deferred PRs `{ owner, repo, prNumber }` (a generation
|
|
439
|
+
may span more than one repository — a hardening PR lands in whichever repo owns
|
|
440
|
+
the surface that blocks the deferred class, so `JonEcho/llm-settings` for hooks
|
|
441
|
+
and `jl-cmd/claude-code-config` for rules and skills both appear):
|
|
442
|
+
|
|
443
|
+
1. **Check out each deferred PR.** Run the
|
|
444
|
+
[Multi-PR pre-flight](#multi-pr-pre-flight-main-session) once per deferred PR.
|
|
445
|
+
Because a deferred PR can live in a repo the first run never touched, first
|
|
446
|
+
find a local checkout of that PR's repo; when none exists, clone it under the
|
|
447
|
+
session temp dir, then add the per-PR worktree on the PR's head ref. Drop any
|
|
448
|
+
PR whose strict pre-flight fails rather than stopping the whole generation.
|
|
449
|
+
Grant project permissions once per repository the generation spans.
|
|
450
|
+
2. **Converge the generation.** Launch `workflow/converge_multi.mjs` with one
|
|
451
|
+
entry per checked-out deferred PR, exactly as the
|
|
452
|
+
[multi-PR launch](#launch-the-multi-pr-workflow) describes.
|
|
453
|
+
3. **Tear down.** Run the [multi-PR teardown](#multi-pr-teardown-on-workflow-completion)
|
|
454
|
+
over the generation's `results`, and revoke project permissions once per
|
|
455
|
+
repository.
|
|
456
|
+
4. **Take the next seed.** The generation's `allDeferredPrs` is the next
|
|
457
|
+
generation's seed list. When it is empty, the loop is done.
|
|
458
|
+
|
|
459
|
+
Repeat from step 1 with each new seed. The depth is unbounded: the loop keeps
|
|
460
|
+
opening generations until one converges every deferred PR without deferring
|
|
461
|
+
anything new.
|
|
462
|
+
|
|
463
|
+
### Report each generation
|
|
464
|
+
|
|
465
|
+
Log one line as each generation finishes, so a watcher sees the chain close:
|
|
466
|
+
|
|
467
|
+
```
|
|
468
|
+
Self-closing generation <k>: <converged>/<total> deferred PR(s) converged, <new> new deferred PR(s) opened
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
When the loop ends, print a final line naming the generation count and the total
|
|
472
|
+
deferred PRs converged across every generation.
|
|
473
|
+
|
|
474
|
+
### Conventional titles on deferred PRs
|
|
475
|
+
|
|
476
|
+
Each hardening PR the loop opens targets a repo whose CI validates the PR title
|
|
477
|
+
as a Conventional Commit. The commit step's prompt directs the agent to title
|
|
478
|
+
the hardening PR as a Conventional Commit — a type prefix, an optional scope,
|
|
479
|
+
then a colon and a short summary — so a deferred PR carries a conforming title
|
|
480
|
+
(`feat(hooks): …`, `chore(rules): …`) before it exists. That prompt is where the
|
|
481
|
+
conforming title is enforced.
|
|
482
|
+
|
|
483
|
+
The `conventional_pr_title_gate` hook is a best-effort backstop on that title,
|
|
484
|
+
not the guarantee. It blocks a `gh pr create` with a non-conforming `--title`
|
|
485
|
+
only for a repo whose semantic-pull-request workflow leaves the action's
|
|
486
|
+
`types:` input at the default Conventional Commits list. For a repo that pins
|
|
487
|
+
its own explicit `types:` list — which the main target repo does in
|
|
488
|
+
`.github/workflows/pr-check.yml` — the hook fails open and lets the title
|
|
489
|
+
through, and the CI title check on GitHub has the final say.
|
|
490
|
+
|
|
406
491
|
## Folder map
|
|
407
492
|
|
|
408
493
|
- `SKILL.md` — this hub.
|
|
@@ -79,11 +79,11 @@ tracks CONVERGE passes only and is never the cap.
|
|
|
79
79
|
**COPILOT** gate:
|
|
80
80
|
|
|
81
81
|
- Request a Copilot review on HEAD (skipping a duplicate request), then poll up
|
|
82
|
-
to
|
|
82
|
+
to the configured cap, 360 seconds apart.
|
|
83
83
|
- Copilot findings → fix them and return to CONVERGE on the new HEAD.
|
|
84
84
|
- Copilot clean or approved → move to the convergence check.
|
|
85
|
-
- Copilot down or out of quota (an out-of-usage notice, or no review after
|
|
86
|
-
|
|
85
|
+
- Copilot down or out of quota (an out-of-usage notice, or no review after the
|
|
86
|
+
configured cap) → log a notice and move to the convergence check with the Copilot gate
|
|
87
87
|
bypassed.
|
|
88
88
|
|
|
89
89
|
**Convergence check**:
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
Hard-won lessons for the autoconverge workflow. Append a bullet each time a run
|
|
4
4
|
fails in a new way.
|
|
5
5
|
|
|
6
|
-
- **The workflow script cannot sleep.** `Date.now`, `Math.random`, and timers
|
|
7
|
-
are unavailable in the script body, and a foreground sleep is blocked. Every
|
|
8
|
-
reviewer wait lives inside an `agent`'s own poll loop
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
- **The workflow script cannot sleep, and neither can an agent's foreground shell.** `Date.now`, `Math.random`, and timers
|
|
7
|
+
are unavailable in the script body, and a foreground `sleep` / `Start-Sleep` is blocked in the headless harness. Every
|
|
8
|
+
reviewer wait lives inside an `agent`'s own poll loop, and that loop waits with the Monitor tool inside the same turn —
|
|
9
|
+
never a foreground sleep, and never backgrounding a wait and ending the turn to await it, which
|
|
10
|
+
leaves a schema-bearing agent with no `StructuredOutput` call. Never try to wait in the script itself.
|
|
11
11
|
|
|
12
12
|
- **Workflow agents start blank.** Spawned agents do not inherit CLAUDE.md,
|
|
13
13
|
rules, or this skill's context. Each agent prompt is self-contained: it names
|
|
@@ -47,7 +47,7 @@ skill still runs teardown (revoke permissions, final report).
|
|
|
47
47
|
Bugbot gate is bypassed.
|
|
48
48
|
- **Copilot down or out of quota** — when Copilot posts an out-of-usage notice on
|
|
49
49
|
the current HEAD (the user who requested the review reached their quota limit)
|
|
50
|
-
rather than a code review, or surfaces no review at all after
|
|
50
|
+
rather than a code review, or surfaces no review at all after the configured cap, the
|
|
51
51
|
Copilot gate returns `down: true`. The run logs a notice, runs the convergence
|
|
52
52
|
check with `--copilot-down` (the Copilot review gate and the
|
|
53
53
|
pending-requested-reviews gate bypassed), and marks the PR ready. `copilotNote`
|