claude-dev-env 8.98.11 → 8.99.1
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/.agents/agents/reference/pr-description-illustrative-voice.md +2 -2
- package/.agents/agents/reference/pr-description-verification.md +1 -1
- package/.agents/agents/test_pr_description_writer_style.py +2 -2
- package/.agents/skills/pull-request/SKILL.md +4 -4
- package/.agents/skills/pull-request/reference/body-sections.md +17 -0
- package/.agents/skills/pull-request/scripts/github_pr_command_constants/config/constants.py +5 -2
- package/.agents/skills/pull-request/scripts/pull_request.py +8 -8
- package/.agents/skills/pull-request/scripts/test_pull_request.py +17 -0
- package/bin/install-pstack-plugin.mjs +55 -27
- package/bin/install-pstack-plugin.test.mjs +27 -2
- package/bin/install.settings-defaults.test.mjs +6 -1
- package/package.json +1 -1
- package/scripts/durable_post_lint.py +1 -3
- package/scripts/durable_post_lint_config/config/constants.py +3 -5
- package/scripts/durable_post_lint_config/config/test_constants.py +2 -2
- package/scripts/test_durable_post_lint.py +9 -5
- package/settings.json +5 -0
|
@@ -45,6 +45,6 @@ The person running the loop asked for a full look, so every pass gets a full loo
|
|
|
45
45
|
- Prefer Before / After when the change is about how something is asked, found, checked, saved, retried, or stopped.
|
|
46
46
|
- Use picture words: hand it, open it, find it, check it, save it, skip it, stop, call by name.
|
|
47
47
|
- Prefer spoken results: “found it,” “missing,” “duplicated,” “couldn’t.”
|
|
48
|
-
- Keep `
|
|
49
|
-
- Put proof only in `
|
|
48
|
+
- Keep `What this does` short and concrete.
|
|
49
|
+
- Put proof only in `Proof in practice`.
|
|
50
50
|
- Explain jargon immediately or replace it.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Use this only when the PR writer needs a concrete verification sample.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Proof in practice should lead with something a reviewer can open, see, click, compare, or try.
|
|
6
6
|
|
|
7
7
|
Good:
|
|
8
8
|
|
|
@@ -92,10 +92,9 @@ Check each behavior claim against the final diff and verification evidence.
|
|
|
92
92
|
Preserve whether a rule is added, removed, or narrowed, and distinguish tests
|
|
93
93
|
added from tests run. Refresh these claims after a rebase or correction.
|
|
94
94
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
results, and material limits under `## Verification`.
|
|
95
|
+
Write the body in the section order `reference/body-sections.md` gives: What
|
|
96
|
+
this does, What changed, Existing work, Proof in practice, Eval for a `feat`
|
|
97
|
+
title, Risk, How to revert. Include only claims supported by the final diff.
|
|
99
98
|
|
|
100
99
|
Keep substantive review and test evidence. Put extensive logs, raw diffs, token
|
|
101
100
|
counts, and agent transcripts in an existing linked artifact or a closed
|
|
@@ -189,6 +188,7 @@ Read `reference/examples.md` for a create, a rejected comment, and a recovery.
|
|
|
189
188
|
| `reference/publication-tasks.md` | Ordered tasks for every publication gate |
|
|
190
189
|
| `reference/legacy-author-recovery.md` | Recovery of one selected legacy author record |
|
|
191
190
|
| `reference/examples.md` | Worked create, rejected comment, and recovery examples |
|
|
191
|
+
| `reference/body-sections.md` | Pull request body sections, their order, and what each holds |
|
|
192
192
|
| `scripts/pull_request.py` | Command for author selection and create, edit, comment, and review actions |
|
|
193
193
|
| `scripts/github_pr_command_constants/` | Constants for pull request actions, recovery, exit codes, and generic messages |
|
|
194
194
|
| `scripts/test_pull_request.py` | Tests for `pull_request.py` |
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Pull request body sections
|
|
2
|
+
|
|
3
|
+
A reviewer reads the body in place of the code. The body says what the change does, how it was tested, what could break, and how to undo it. The order follows the Claude Code team's [code review article](https://claude.dev/blog/solving-code-review-at-anthropic/), whose template "asks how the change was tested and how to undo it". The opening sections follow the team's public pull requests, such as [claude-code #69470](https://github.com/anthropics/claude-code/pull/69470), which opens with What this does, Why now and What changed.
|
|
4
|
+
|
|
5
|
+
| Section | Holds |
|
|
6
|
+
|---|---|
|
|
7
|
+
| `## What this does` | Two or three plain sentences: the behavior before, the behavior after, and who notices. Written for a reviewer who has not read the worker's conversation. |
|
|
8
|
+
| `## What changed` | One bullet per area of the diff, in plain words. |
|
|
9
|
+
| `## Existing work` | What already did part of the job, with a link or file and line, and what this change adds. Or the places searched. |
|
|
10
|
+
| `## Proof in practice` | Each command in backticks, the output without the change and with it, and anything still unproven. |
|
|
11
|
+
| `## Eval` | Only for a `feat` title. The eval's cases and grader, the command, and its result. |
|
|
12
|
+
| `## Risk` | What could break, for whom, and how anyone would notice. `Low` and one reason for a doc or test change. |
|
|
13
|
+
| `## How to revert` | The exact undo step, and any data or state a plain revert leaves behind. |
|
|
14
|
+
|
|
15
|
+
A repository's own sections, such as a request record or a session link, follow these. Optional `## Why now` and `## After merge` sections go after What this does and after How to revert.
|
|
16
|
+
|
|
17
|
+
`pull_request.py template` writes the headings in this order into a new body, and appends each missing heading to the end of a body that already has some. `durable_post_lint.py` reports a create body missing What this does, Risk, or How to revert, and the proof gate holds one missing Existing work, Proof in practice, or Eval for a `feat` title.
|
|
@@ -9,11 +9,14 @@ ACTION_REVIEW = "review"
|
|
|
9
9
|
ACTION_TEMPLATE = "template"
|
|
10
10
|
|
|
11
11
|
ALL_BODY_TEMPLATE_HEADINGS: tuple[str, ...] = (
|
|
12
|
-
"
|
|
12
|
+
"What this does",
|
|
13
|
+
"What changed",
|
|
13
14
|
"Existing work",
|
|
14
15
|
"Proof in practice",
|
|
15
|
-
"
|
|
16
|
+
"Risk",
|
|
17
|
+
"How to revert",
|
|
16
18
|
)
|
|
19
|
+
FEATURE_BODY_TEMPLATE_HEADING_FOLLOWS = "Proof in practice"
|
|
17
20
|
FEATURE_BODY_TEMPLATE_HEADING = "Eval"
|
|
18
21
|
FEATURE_TITLE_PATTERN: re.Pattern[str] = re.compile(r"^\s*feat(\([^)]*\))?!?:")
|
|
19
22
|
BODY_TEMPLATE_HEADING_FORMAT = "## {heading}"
|
|
@@ -26,6 +26,7 @@ from github_pr_command_constants.config.constants import (
|
|
|
26
26
|
BODY_TEMPLATE_HEADING_FORMAT,
|
|
27
27
|
BODY_TEMPLATE_LINE_END,
|
|
28
28
|
FEATURE_BODY_TEMPLATE_HEADING,
|
|
29
|
+
FEATURE_BODY_TEMPLATE_HEADING_FOLLOWS,
|
|
29
30
|
FEATURE_TITLE_PATTERN,
|
|
30
31
|
ALL_GITHUB_AUTHORIZATION_ENVIRONMENT_KEYS,
|
|
31
32
|
ALL_LINTER_ACTIONS_BY_COMMAND,
|
|
@@ -77,13 +78,11 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
77
78
|
|
|
78
79
|
|
|
79
80
|
def _template_headings(title: str) -> list[str]:
|
|
80
|
-
|
|
81
|
-
BODY_TEMPLATE_HEADING_FORMAT.format(heading=each_heading)
|
|
82
|
-
for each_heading in ALL_BODY_TEMPLATE_HEADINGS
|
|
83
|
-
]
|
|
81
|
+
all_heading_names = list(ALL_BODY_TEMPLATE_HEADINGS)
|
|
84
82
|
if FEATURE_TITLE_PATTERN.match(title):
|
|
85
|
-
|
|
86
|
-
|
|
83
|
+
eval_position = all_heading_names.index(FEATURE_BODY_TEMPLATE_HEADING_FOLLOWS) + 1
|
|
84
|
+
all_heading_names.insert(eval_position, FEATURE_BODY_TEMPLATE_HEADING)
|
|
85
|
+
return [BODY_TEMPLATE_HEADING_FORMAT.format(heading=each_heading) for each_heading in all_heading_names]
|
|
87
86
|
|
|
88
87
|
|
|
89
88
|
def write_body_template(body_file: Path, title: str) -> list[str]:
|
|
@@ -91,8 +90,9 @@ def write_body_template(body_file: Path, title: str) -> list[str]:
|
|
|
91
90
|
|
|
92
91
|
::
|
|
93
92
|
|
|
94
|
-
new file, title "fix: x" -> ["##
|
|
95
|
-
|
|
93
|
+
new file, title "fix: x" -> ["## What this does", "## What changed", "## Existing work",
|
|
94
|
+
"## Proof in practice", "## Risk", "## How to revert"]
|
|
95
|
+
new file, title "feat: x" -> the same with "## Eval" after "## Proof in practice"
|
|
96
96
|
file already has them all -> [] and the file is unchanged
|
|
97
97
|
|
|
98
98
|
Args:
|
|
@@ -544,3 +544,20 @@ def should_keep_written_text_and_add_only_missing_headings(tmp_path: Path) -> No
|
|
|
544
544
|
assert first_text.startswith("## Why\nThe gate bounced twice.\n")
|
|
545
545
|
assert first_text.count("## Why") == 1
|
|
546
546
|
assert body_path.read_text(encoding="utf-8") == first_text
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def should_write_the_reader_headings_in_order_with_eval_after_the_proof(tmp_path: Path) -> None:
|
|
550
|
+
body_path = tmp_path / BODY_FILENAME
|
|
551
|
+
_run_template(body_path, "feat(hooks): add the gate")
|
|
552
|
+
all_heading_lines = [
|
|
553
|
+
each_line for each_line in body_path.read_text(encoding="utf-8").splitlines() if each_line.startswith("## ")
|
|
554
|
+
]
|
|
555
|
+
assert all_heading_lines == [
|
|
556
|
+
"## What this does",
|
|
557
|
+
"## What changed",
|
|
558
|
+
"## Existing work",
|
|
559
|
+
"## Proof in practice",
|
|
560
|
+
"## Eval",
|
|
561
|
+
"## Risk",
|
|
562
|
+
"## How to revert",
|
|
563
|
+
]
|
|
@@ -91,12 +91,13 @@ const HOST_DEFINITIONS = Object.freeze({
|
|
|
91
91
|
* verb gets refresh commands: the marketplace add and plugin install leave an
|
|
92
92
|
* existing entry in place, and the update commands then pull the newest
|
|
93
93
|
* published version over it, so an installed plugin stays installed through
|
|
94
|
-
* the whole run. When
|
|
95
|
-
* verb, the removal commands run and take
|
|
96
|
-
* cached versions, and the marketplace clone,
|
|
97
|
-
* follow fetch the newest published version
|
|
98
|
-
*
|
|
99
|
-
* that marketplace once for the whole
|
|
94
|
+
* the whole run. When a refresh command fails for a reason only a rebuild
|
|
95
|
+
* fixes, or the host has no update verb, the removal commands run and take
|
|
96
|
+
* out each installed plugin, its cached versions, and the marketplace clone,
|
|
97
|
+
* and the install commands that follow fetch the newest published version
|
|
98
|
+
* into an empty slot. Removing a marketplace uninstalls each plugin it
|
|
99
|
+
* supplied, so the plan removes and adds that marketplace once for the whole
|
|
100
|
+
* set.
|
|
100
101
|
*
|
|
101
102
|
* Claude Code and Codex publish a marketplace under different plugin verbs, so
|
|
102
103
|
* the install verb belongs to the host and the marketplace and identifier
|
|
@@ -219,7 +220,12 @@ function runHostCommand(executable, commandArguments, options) {
|
|
|
219
220
|
env: { ...process.env, ...options.environment },
|
|
220
221
|
windowsVerbatimArguments: invocation.windowsVerbatimArguments,
|
|
221
222
|
});
|
|
222
|
-
return {
|
|
223
|
+
return {
|
|
224
|
+
status: spawned.status,
|
|
225
|
+
stdout: spawned.stdout ?? '',
|
|
226
|
+
stderr: spawned.stderr ?? '',
|
|
227
|
+
error: spawned.error,
|
|
228
|
+
};
|
|
223
229
|
}
|
|
224
230
|
|
|
225
231
|
const COMMAND_NOT_FOUND_EXIT_CODE = 9009;
|
|
@@ -297,15 +303,33 @@ function lockedFolderDetail(allSpecs, host, homeDirectory, errorText, findLocker
|
|
|
297
303
|
}).join('');
|
|
298
304
|
}
|
|
299
305
|
|
|
306
|
+
const REBUILD_REQUIRED_PATTERN = /doesn't match its extraKnownMarketplaces entry|Source path does not exist/;
|
|
307
|
+
|
|
308
|
+
function commandFailureDetail(outcome) {
|
|
309
|
+
return singleLineDetail(outcome.stderr) || singleLineDetail(outcome.stdout)
|
|
310
|
+
|| outcome.error?.message || `exit ${outcome.status}`;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function failedCommandOutcome(allSpecs, host, homeDirectory, executable, commandArguments, outcome, findLockers) {
|
|
314
|
+
const detail = commandFailureDetail(outcome);
|
|
315
|
+
const lockAdvice = lockedFolderDetail(allSpecs, host, homeDirectory, detail, findLockers);
|
|
316
|
+
return {
|
|
317
|
+
host,
|
|
318
|
+
executable,
|
|
319
|
+
status: 'failed',
|
|
320
|
+
warning: `${executable} ${commandArguments.join(' ')} failed: ${detail}${lockAdvice}`,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
300
324
|
function refreshInPlace(plan, executable, commandEnvironment, runCommand) {
|
|
301
325
|
for (const commandArguments of plan.refreshCommands) {
|
|
302
326
|
const outcome = runCommand(executable, [...commandArguments], {
|
|
303
327
|
environment: commandEnvironment,
|
|
304
328
|
});
|
|
305
|
-
if (namesAnAbsentCommand(outcome)) return 'absent';
|
|
306
|
-
if (outcome.status !== 0 || outcome.error) return 'failed';
|
|
329
|
+
if (namesAnAbsentCommand(outcome)) return { state: 'absent' };
|
|
330
|
+
if (outcome.status !== 0 || outcome.error) return { state: 'failed', commandArguments, outcome };
|
|
307
331
|
}
|
|
308
|
-
return 'refreshed';
|
|
332
|
+
return { state: 'refreshed' };
|
|
309
333
|
}
|
|
310
334
|
|
|
311
335
|
function removeOrphanedVersionsForHost(allSpecs, host, homeDirectory) {
|
|
@@ -326,11 +350,18 @@ function installForHost(allSpecs, host, homeDirectory, environment, runCommand,
|
|
|
326
350
|
}
|
|
327
351
|
if (plan.refreshCommands.length > 0) {
|
|
328
352
|
const refreshOutcome = refreshInPlace(plan, executable, commandEnvironment, runCommand);
|
|
329
|
-
if (refreshOutcome === 'absent') return absentHostOutcome(allSpecs, host, executable);
|
|
330
|
-
if (refreshOutcome === 'refreshed') {
|
|
353
|
+
if (refreshOutcome.state === 'absent') return absentHostOutcome(allSpecs, host, executable);
|
|
354
|
+
if (refreshOutcome.state === 'refreshed') {
|
|
331
355
|
removeOrphanedVersionsForHost(allSpecs, host, homeDirectory);
|
|
332
356
|
return { host, executable, status: 'installed', warning: null };
|
|
333
357
|
}
|
|
358
|
+
const refreshOutput = `${refreshOutcome.outcome.stderr ?? ''}\n${refreshOutcome.outcome.stdout ?? ''}`;
|
|
359
|
+
if (!REBUILD_REQUIRED_PATTERN.test(refreshOutput)) {
|
|
360
|
+
return failedCommandOutcome(
|
|
361
|
+
allSpecs, host, homeDirectory, executable,
|
|
362
|
+
refreshOutcome.commandArguments, refreshOutcome.outcome, findLockers,
|
|
363
|
+
);
|
|
364
|
+
}
|
|
334
365
|
}
|
|
335
366
|
for (const commandArguments of plan.removalCommands) {
|
|
336
367
|
const outcome = runCommand(executable, [...commandArguments], {
|
|
@@ -347,14 +378,9 @@ function installForHost(allSpecs, host, homeDirectory, environment, runCommand,
|
|
|
347
378
|
});
|
|
348
379
|
if (namesAnAbsentCommand(outcome)) return absentHostOutcome(allSpecs, host, executable);
|
|
349
380
|
if (outcome.status !== 0 || outcome.error) {
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
host,
|
|
354
|
-
executable,
|
|
355
|
-
status: 'failed',
|
|
356
|
-
warning: `${executable} ${commandArguments.join(' ')} failed: ${detail}${lockAdvice}`,
|
|
357
|
-
};
|
|
381
|
+
return failedCommandOutcome(
|
|
382
|
+
allSpecs, host, homeDirectory, executable, commandArguments, outcome, findLockers,
|
|
383
|
+
);
|
|
358
384
|
}
|
|
359
385
|
}
|
|
360
386
|
removeOrphanedVersionsForHost(allSpecs, host, homeDirectory);
|
|
@@ -385,12 +411,14 @@ export function restartNotice(allSpecs, allInstalledHosts) {
|
|
|
385
411
|
* plugin, both of which leave an existing entry alone, then updates the
|
|
386
412
|
* marketplace and each plugin. An installed plugin stays installed and enabled
|
|
387
413
|
* through the run, so a session that starts mid-run still loads it, and a
|
|
388
|
-
* failed
|
|
389
|
-
*
|
|
390
|
-
*
|
|
391
|
-
*
|
|
392
|
-
*
|
|
393
|
-
*
|
|
414
|
+
* failed refresh command reports the host failed and leaves the prior version
|
|
415
|
+
* in place. Two refresh errors need a rebuild: a source that no longer
|
|
416
|
+
* matches the settings entry, and a source path the sparse clone lacks. The
|
|
417
|
+
* host reads either from the failing refresh command's output, then falls
|
|
418
|
+
* back to the clean install Codex always runs: it removes every plugin in the
|
|
419
|
+
* set and then the marketplace, so an older version never survives beside the
|
|
420
|
+
* new one, and adds them again. A removal that finds nothing to remove exits
|
|
421
|
+
* non-zero on a fresh home, and the install goes on.
|
|
394
422
|
* Before each Claude marketplace add, the install deletes the half-made clone
|
|
395
423
|
* folder an interrupted earlier add left, so the add starts in an empty slot.
|
|
396
424
|
*
|
|
@@ -482,7 +510,7 @@ export function removeRetiredMarketplacePlugins(allRetiredSpecs, options, depend
|
|
|
482
510
|
);
|
|
483
511
|
if (namesAnAbsentCommand(outcome)) break;
|
|
484
512
|
if (outcome.status !== 0 || outcome.error) {
|
|
485
|
-
const detail =
|
|
513
|
+
const detail = commandFailureDetail(outcome);
|
|
486
514
|
failedRemovals.push({ pluginIdentifier: eachSpec.pluginIdentifier, detail });
|
|
487
515
|
continue;
|
|
488
516
|
}
|
|
@@ -117,6 +117,30 @@ test('a failed Claude refresh falls back to removing and adding the marketplace
|
|
|
117
117
|
]);
|
|
118
118
|
});
|
|
119
119
|
|
|
120
|
+
test('a Claude refresh error printed on standard output still triggers the rebuild', () => {
|
|
121
|
+
const runner = recordingRunner((executable, commandArguments) => (
|
|
122
|
+
commandArguments[2] === 'add' && runner.calls.length === 1
|
|
123
|
+
? { status: 1, stdout: "its source doesn't match its extraKnownMarketplaces entry", stderr: '' }
|
|
124
|
+
: { status: 0, stderr: '' }));
|
|
125
|
+
const outcome = installMarketplacePlugins([USAGE_WRAPUP_PLUGIN_SPEC], { ...ROOTS, hosts: ['claude'] }, runner);
|
|
126
|
+
assert.equal(outcome.status, 'installed');
|
|
127
|
+
assert.ok(runner.calls.some(call => call.commandArguments.includes('uninstall')));
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('a Claude refresh that fails on the network keeps the installed plugin and reports the failure', () => {
|
|
131
|
+
const runner = recordingRunner((executable, commandArguments) => (
|
|
132
|
+
commandArguments[1] === 'update'
|
|
133
|
+
? { status: 1, stderr: 'Failed to update plugin: getaddrinfo ENOTFOUND github.com' }
|
|
134
|
+
: { status: 0, stderr: '' }));
|
|
135
|
+
const outcome = installMarketplacePlugins([PSTACK_PLUGIN_SPEC], { ...ROOTS, hosts: ['claude'] }, runner);
|
|
136
|
+
assert.equal(outcome.status, 'failed');
|
|
137
|
+
assert.match(outcome.hosts[0].warning, /claude plugin update pstack@pstack-claude failed: .*ENOTFOUND/);
|
|
138
|
+
const allRemovalCalls = runner.calls.filter(call => (
|
|
139
|
+
call.commandArguments.includes('uninstall') || call.commandArguments.includes('remove')
|
|
140
|
+
));
|
|
141
|
+
assert.deepEqual(allRemovalCalls, []);
|
|
142
|
+
});
|
|
143
|
+
|
|
120
144
|
test('each host removes the installed plugin and marketplace before a clean install', () => {
|
|
121
145
|
assert.deepEqual(marketplacePluginsPlan([PSTACK_PLUGIN_SPEC], 'claude').removalCommands, [
|
|
122
146
|
['plugin', 'uninstall', PSTACK_PLUGIN_IDENTIFIER],
|
|
@@ -131,9 +155,10 @@ test('each host removes the installed plugin and marketplace before a clean inst
|
|
|
131
155
|
test('a removal that finds nothing installed still lets the install run', () => {
|
|
132
156
|
const runner = recordingRunner((executable, commandArguments) => (
|
|
133
157
|
commandArguments.includes('uninstall') || commandArguments.includes('remove')
|
|
134
|
-
|| commandArguments.includes('update')
|
|
135
158
|
? { status: 1, stderr: 'not installed' }
|
|
136
|
-
:
|
|
159
|
+
: commandArguments.includes('update')
|
|
160
|
+
? { status: 1, stderr: 'Source path does not exist: plugins/pstack' }
|
|
161
|
+
: { status: 0, stderr: '' }));
|
|
137
162
|
const outcome = installMarketplacePlugins([PSTACK_PLUGIN_SPEC], { ...ROOTS, hosts: ['claude'] }, runner);
|
|
138
163
|
assert.equal(outcome.status, 'installed');
|
|
139
164
|
assert.deepEqual(runner.calls.at(-1).commandArguments, ['plugin', 'install', PSTACK_PLUGIN_IDENTIFIER]);
|
|
@@ -35,6 +35,11 @@ const INSTALL_ENTRY = join(PACKAGE_ROOT, 'bin', 'install.mjs');
|
|
|
35
35
|
const EXPECTED_DENY_ENTRIES = [];
|
|
36
36
|
const EXPECTED_ALLOW_ENTRIES = [
|
|
37
37
|
'WebFetch(domain:docs.github.com)',
|
|
38
|
+
'WebFetch(domain:claude.com)',
|
|
39
|
+
'WebFetch(domain:*.claude.com)',
|
|
40
|
+
'WebFetch(domain:claude.dev)',
|
|
41
|
+
'WebFetch(domain:*.claude.dev)',
|
|
42
|
+
'WebFetch(domain:www.anthropic.com)',
|
|
38
43
|
'Read(~/.claude/docs/**)',
|
|
39
44
|
];
|
|
40
45
|
const PROFILE_ALLOW_ENTRIES = DECLARED_PROFILE_SETTINGS
|
|
@@ -76,7 +81,7 @@ function runInstallerInSandbox(sandboxHome, installerArguments = []) {
|
|
|
76
81
|
});
|
|
77
82
|
}
|
|
78
83
|
|
|
79
|
-
test('package settings.json publishes the
|
|
84
|
+
test('package settings.json publishes the docs fetch allows and no managed deny entries', () => {
|
|
80
85
|
assert.deepEqual(packageManagedPermissions(), {
|
|
81
86
|
allow: EXPECTED_ALLOW_ENTRIES,
|
|
82
87
|
deny: EXPECTED_DENY_ENTRIES,
|
package/package.json
CHANGED
|
@@ -25,7 +25,6 @@ from durable_post_lint_config.config.constants import (
|
|
|
25
25
|
ALL_BODY_REQUIRED_ACTIONS,
|
|
26
26
|
ALL_PATH_ANCHORED_VOLATILE_PATH_MARKERS,
|
|
27
27
|
ALL_POST_ACTIONS,
|
|
28
|
-
ALL_PR_DESCRIPTION_ACTIONS,
|
|
29
28
|
ALL_RELEASE_BODY_MARKERS,
|
|
30
29
|
ALL_REQUIRED_PR_DESCRIPTION_HEADINGS,
|
|
31
30
|
ALL_TITLE_ACTIONS,
|
|
@@ -266,9 +265,8 @@ def _body_findings(
|
|
|
266
265
|
if _is_release_automation_branch(head_branch):
|
|
267
266
|
all_findings.extend(_release_body_findings(body_text))
|
|
268
267
|
else:
|
|
269
|
-
if action in ALL_PR_DESCRIPTION_ACTIONS:
|
|
270
|
-
all_findings.extend(_description_heading_findings(body_text))
|
|
271
268
|
if action == ACTION_PR_CREATE:
|
|
269
|
+
all_findings.extend(_description_heading_findings(body_text))
|
|
272
270
|
all_findings.extend(_pull_request_proof_findings(title, body_text))
|
|
273
271
|
if action == ACTION_GITHUB_MCP_COMMENT:
|
|
274
272
|
all_findings.extend(_comment_length_findings(body_text))
|
|
@@ -37,9 +37,6 @@ ALL_BODY_REQUIRED_ACTIONS: frozenset[str] = frozenset(
|
|
|
37
37
|
ACTION_PR_REVIEW,
|
|
38
38
|
}
|
|
39
39
|
)
|
|
40
|
-
ALL_PR_DESCRIPTION_ACTIONS: frozenset[str] = frozenset(
|
|
41
|
-
{ACTION_PR_CREATE, ACTION_PR_EDIT}
|
|
42
|
-
)
|
|
43
40
|
ALL_TITLE_ACTIONS: frozenset[str] = frozenset({ACTION_PR_CREATE, ACTION_PR_EDIT})
|
|
44
41
|
ALL_CONVENTIONAL_TITLE_TYPES: tuple[str, ...] = (
|
|
45
42
|
"build",
|
|
@@ -58,8 +55,9 @@ CONVENTIONAL_TITLE_PATTERN: re.Pattern[str] = re.compile(
|
|
|
58
55
|
rf"^(?:{'|'.join(ALL_CONVENTIONAL_TITLE_TYPES)})(?:\([^)\r\n]+\))?!?: [^\s].*$"
|
|
59
56
|
)
|
|
60
57
|
ALL_REQUIRED_PR_DESCRIPTION_HEADINGS: tuple[str, ...] = (
|
|
61
|
-
"
|
|
62
|
-
"
|
|
58
|
+
"What this does",
|
|
59
|
+
"Risk",
|
|
60
|
+
"How to revert",
|
|
63
61
|
)
|
|
64
62
|
ALL_PATH_ANCHORED_VOLATILE_PATH_MARKERS: tuple[str, ...] = (
|
|
65
63
|
".claude-profile-a/jobs/",
|
|
@@ -40,5 +40,5 @@ def should_name_the_branch_prefix_release_automation_pushes() -> None:
|
|
|
40
40
|
assert RELEASE_BRANCH_PREFIX == "release-please--branches--"
|
|
41
41
|
|
|
42
42
|
|
|
43
|
-
def
|
|
44
|
-
assert ALL_REQUIRED_PR_DESCRIPTION_HEADINGS == ("
|
|
43
|
+
def should_require_the_reader_headings_a_reviewer_reads_in_place_of_the_code() -> None:
|
|
44
|
+
assert ALL_REQUIRED_PR_DESCRIPTION_HEADINGS == ("What this does", "Risk", "How to revert")
|
|
@@ -18,9 +18,9 @@ from hooks_constants.pull_request_proof_constants import (
|
|
|
18
18
|
)
|
|
19
19
|
|
|
20
20
|
SCRIPT_PATH = Path(__file__).resolve().parent / "durable_post_lint.py"
|
|
21
|
-
VALID_PR_BODY = """##
|
|
21
|
+
VALID_PR_BODY = """## What this does
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
What this does text.
|
|
24
24
|
|
|
25
25
|
## Scope
|
|
26
26
|
|
|
@@ -34,9 +34,13 @@ Tradeoffs text.
|
|
|
34
34
|
|
|
35
35
|
Blast radius text.
|
|
36
36
|
|
|
37
|
-
##
|
|
37
|
+
## Risk
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
Risk text.
|
|
40
|
+
|
|
41
|
+
## How to revert
|
|
42
|
+
|
|
43
|
+
Revert text.
|
|
40
44
|
|
|
41
45
|
## Existing work
|
|
42
46
|
|
|
@@ -201,7 +205,7 @@ def test_pr_body_reports_each_missing_heading(missing_heading: str) -> None:
|
|
|
201
205
|
]
|
|
202
206
|
|
|
203
207
|
|
|
204
|
-
def
|
|
208
|
+
def should_accept_an_edit_body_without_the_reader_headings() -> None:
|
|
205
209
|
all_findings = durable_post_lint.lint_durable_post(
|
|
206
210
|
action="pr-edit",
|
|
207
211
|
title="fix(cli): tighten input handling",
|
package/settings.json
CHANGED
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
"permissions": {
|
|
4
4
|
"allow": [
|
|
5
5
|
"WebFetch(domain:docs.github.com)",
|
|
6
|
+
"WebFetch(domain:claude.com)",
|
|
7
|
+
"WebFetch(domain:*.claude.com)",
|
|
8
|
+
"WebFetch(domain:claude.dev)",
|
|
9
|
+
"WebFetch(domain:*.claude.dev)",
|
|
10
|
+
"WebFetch(domain:www.anthropic.com)",
|
|
6
11
|
"Read(~/.claude/docs/**)"
|
|
7
12
|
]
|
|
8
13
|
},
|