claude-dev-env 7.7.1 → 8.1.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/.agents/agents/clean-coder.md +5 -6
- package/.agents/agents/issue-tracker.md +1 -1
- package/.agents/agents/pr-description-writer.md +1 -1
- package/.agents/agents/skill-writer-agent.md +1 -12
- package/.agents/agents/test_agent_frontmatter.py +2 -19
- package/.agents/skills/issue-tracker/SKILL.md +1 -1
- package/.agents/skills/orchestrator/SKILL.md +4 -5
- package/.agents/skills/pull-request/SKILL.md +8 -7
- package/.agents/skills/pull-request/reference/publication-tasks.md +1 -1
- package/.agents/skills/team-advisor/SKILL.md +2 -3
- package/_shared/advisor/advisor-protocol.md +0 -1
- package/_shared/advisor/reference/lifecycle.md +0 -1
- package/_shared/advisor/reference/warm-up.md +1 -2
- package/audit-rubrics/audit-categories.json +0 -4
- package/audit-rubrics/category_rubrics/category-e-dead-code.md +1 -1
- package/audit-rubrics/category_rubrics/category-j-code-rules-compliance.md +1 -2
- package/audit-rubrics/prompts/category-e-dead-code.md +5 -5
- package/audit-rubrics/prompts/category-j-code-rules-compliance.md +1 -24
- package/bin/install-constants.mjs +0 -5
- package/bin/install-pstack-plugin.mjs +192 -0
- package/bin/install-pstack-plugin.test.mjs +181 -0
- package/bin/install.agents-home.test.mjs +0 -4
- package/bin/install.codex-rules.test.mjs +6 -0
- package/bin/install.cursor-rules.test.mjs +3 -253
- package/bin/install.mjs +55 -160
- package/bin/install.test.mjs +110 -149
- package/bin/prune-retired-pstack-store.mjs +83 -0
- package/bin/prune-retired-pstack-store.test.mjs +105 -0
- package/bin/prune-session-continuity.mjs +57 -0
- package/bin/prune-session-continuity.test.mjs +70 -0
- package/bin/resolve-install-root.mjs +2 -0
- package/bin/resolve-install-root.test.mjs +69 -0
- package/docs/CODE_RULES.md +1 -1
- package/docs/agent-spawn-protocol.md +1 -1
- package/docs/codex-compatibility.md +4 -16
- package/docs/nas-ssh-invocation.md +1 -1
- package/docs/references/prose-style-enforcement.md +2 -2
- package/hooks/HOOK_SPECS_PROMPT_WORKFLOW.md +1 -3
- package/hooks/blocking/code_rules_constants_config.py +1 -108
- package/hooks/blocking/code_rules_enforcer.py +9 -5
- package/hooks/blocking/code_rules_test_layout.py +6 -7
- package/hooks/blocking/test_code_rules_constants_config.py +13 -0
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +0 -1
- package/hooks/blocking/test_code_rules_enforcer_dispatch_wiring.py +15 -0
- package/hooks/blocking/test_code_rules_enforcer_split_constants_config.py +0 -60
- package/hooks/blocking/test_context_control_policy_files.py +0 -11
- package/hooks/hooks_constants/blocking_check_limits.py +1 -1
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +0 -1
- package/hooks/hooks_constants/setup_project_paths_constants.py +1 -2
- package/hooks/session/test_untracked_repo_detector.py +1 -1
- package/hooks/validators/code_quality_checks.py +15 -2
- package/hooks/validators/test_code_quality_checks.py +22 -0
- package/package.json +1 -2
- package/rules/anti-corollary-tests.md +3 -1
- package/rules/archiving-agent-config.md +70 -0
- package/rules/asd-ste100-language.md +1 -1
- package/rules/ci-owns-the-gate.md +73 -0
- package/rules/cleanup-temp-files.md +1 -1
- package/rules/code-standards.md +3 -4
- package/rules/docstring-prose-matches-implementation.md +3 -3
- package/rules/explore-thoroughly.md +2 -2
- package/rules/failure-blast-radius.md +10 -9
- package/rules/git-workflow.md +27 -12
- package/rules/long-horizon-autonomy.md +3 -1
- package/rules/nas-ssh-invocation.md +1 -1
- package/rules/orphan-css-class.md +1 -1
- package/rules/paired-test-coverage.md +1 -1
- package/rules/research-mode.md +3 -1
- package/rules/retired-hook-prose.md +10 -2
- package/rules/testing.md +3 -3
- package/rules/verify-before-asking.md +4 -1
- package/rules/verify-runtime-state.md +2 -2
- package/rules/windows-filesystem-safe.md +1 -1
- package/rules/workflow-substitution-slots.md +1 -1
- package/scripts/Sync-RepoMain.ps1 +71 -6
- package/scripts/policy_lint/adapter_detectors.py +116 -10
- package/scripts/policy_lint/config/approved_test_pairs.py +12 -0
- package/scripts/policy_lint/config/constants.py +3 -0
- package/scripts/tests/Sync-RepoMain.Tests.ps1 +108 -0
- package/scripts/tests/test_codex_compat_materializer.py +3 -3
- package/scripts/tests/test_policy_lint_rules_validator_baseline.py +126 -0
- package/.agents/skills/session-continuity/AGENTS.md +0 -1
- package/.agents/skills/session-continuity/CLAUDE.md +0 -1
- package/.agents/skills/session-continuity/README.md +0 -223
- package/.agents/skills/session-continuity/SKILL.md +0 -125
- package/.agents/skills/session-continuity/continuity.mjs +0 -445
- package/.agents/skills/session-continuity/continuity.test.mjs +0 -381
- package/bin/install-session-continuity.mjs +0 -131
- package/bin/install-session-continuity.test.mjs +0 -106
- package/bin/install.pstack-plugin.test.mjs +0 -115
- package/bin/pstack-codex-discovery.test.mjs +0 -79
- package/bin/pstack-model-preferences.claude.json +0 -23
- package/bin/pstack-model-preferences.codex.json +0 -24
- package/bin/pstack.mjs +0 -392
- package/bin/pstack.test.mjs +0 -347
- package/bin/refresh-pstack-plugin-skills.test.mjs +0 -211
- package/bin/select-pstack-models.test.mjs +0 -776
- package/hooks/blocking/test_code_rules_enforcer_file_global_constants.py +0 -231
- package/rules/agent-spawn-protocol.md +0 -9
- package/rules/ask-user-question-required.md +0 -31
- package/rules/claims-as-quotes.md +0 -55
- package/rules/confirm-implementation-forks.md +0 -11
- package/rules/file-global-constants.md +0 -81
- package/rules/hedging-claims.md +0 -7
- package/rules/measurement-denominators.md +0 -39
- package/rules/prompt-workflow-context-controls.md +0 -19
- package/rules/pstack-host-mapping.md +0 -48
- package/rules/pstack-models.md +0 -142
- package/rules/vault-context.md +0 -5
- package/scripts/pstack-adapters/common.md +0 -17
- package/scripts/pstack-adapters/create-skill.md +0 -14
- package/scripts/pstack-adapters/host-claude.md +0 -7
- package/scripts/pstack-adapters/host-codex.md +0 -11
- package/scripts/pstack-adapters/host-cursor.md +0 -7
- package/scripts/pstack.lock.json +0 -11
- package/scripts/refresh_pstack_plugin_skills.mjs +0 -200
- package/scripts/select_pstack_models.mjs +0 -469
- /package/.agents/{skills → skills-archived}/source-command-logifix/SKILL.md +0 -0
|
@@ -11,7 +11,10 @@
|
|
|
11
11
|
- Only runs when the checked-out branch is the named branch
|
|
12
12
|
- Only runs when there are no tracked changes (untracked files are fine),
|
|
13
13
|
unless -StashDirty is passed
|
|
14
|
-
- Uses merge --ff-only
|
|
14
|
+
- Uses merge --ff-only (never force, never hard reset, never clean)
|
|
15
|
+
- When the fast-forward fails because the files were already copied in,
|
|
16
|
+
moves HEAD with a mixed reset, which writes no files. It keeps that move
|
|
17
|
+
only when every file the remote changed now matches the remote
|
|
15
18
|
- Logs and exits non-zero on skip, diverge, or failure so a Task Scheduler
|
|
16
19
|
history row can surface a problem instead of a silent no-op
|
|
17
20
|
|
|
@@ -128,6 +131,64 @@ function Invoke-GitOrExit {
|
|
|
128
131
|
return $result.Output
|
|
129
132
|
}
|
|
130
133
|
|
|
134
|
+
function Get-NulSeparatedPaths {
|
|
135
|
+
param(
|
|
136
|
+
[Parameter(Mandatory)]
|
|
137
|
+
[AllowEmptyString()]
|
|
138
|
+
[string]$Output
|
|
139
|
+
)
|
|
140
|
+
return @($Output -split "`0" | Where-Object { $_ })
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function Invoke-AdoptCopiedTree {
|
|
144
|
+
<#
|
|
145
|
+
Handles a mirror whose files are refreshed by copy while its HEAD stays
|
|
146
|
+
behind. The fast-forward then refuses to overwrite the copied files,
|
|
147
|
+
even though they already hold the remote content.
|
|
148
|
+
|
|
149
|
+
A mixed reset moves HEAD and the index to the remote tip and writes no
|
|
150
|
+
files. If any file the remote changed still differs afterwards, the
|
|
151
|
+
reset goes back to the old tip, so the tree ends as it started.
|
|
152
|
+
#>
|
|
153
|
+
param(
|
|
154
|
+
[Parameter(Mandatory)]
|
|
155
|
+
[string]$LocalTip,
|
|
156
|
+
[Parameter(Mandatory)]
|
|
157
|
+
[string]$RemoteRef
|
|
158
|
+
)
|
|
159
|
+
$stagedResult = Invoke-Git -Arguments @('diff', '--cached', '--quiet')
|
|
160
|
+
if ($stagedResult.ExitCode -ne 0) {
|
|
161
|
+
Write-SyncLog -Level 'ERROR' -Message 'adopt skipped: the index holds staged changes'
|
|
162
|
+
return $false
|
|
163
|
+
}
|
|
164
|
+
$changedResult = Invoke-Git -Arguments @('diff', '--name-only', '--no-renames', '-z', $LocalTip, $RemoteRef)
|
|
165
|
+
if ($changedResult.ExitCode -ne 0) {
|
|
166
|
+
Write-SyncLog -Level 'ERROR' -Message "adopt skipped: cannot list remote changes: $($changedResult.Output)"
|
|
167
|
+
return $false
|
|
168
|
+
}
|
|
169
|
+
$allRemoteChangedPaths = [System.Collections.Generic.HashSet[string]]::new([string[]](Get-NulSeparatedPaths -Output $changedResult.Output))
|
|
170
|
+
|
|
171
|
+
$resetResult = Invoke-Git -Arguments @('reset', '-q', $RemoteRef)
|
|
172
|
+
if ($resetResult.ExitCode -ne 0) {
|
|
173
|
+
Write-SyncLog -Level 'ERROR' -Message "adopt skipped: mixed reset failed: $($resetResult.Output)"
|
|
174
|
+
return $false
|
|
175
|
+
}
|
|
176
|
+
$differResult = Invoke-Git -Arguments @('diff', '--name-only', '--no-renames', '-z')
|
|
177
|
+
$allMismatchedPaths = @(Get-NulSeparatedPaths -Output $differResult.Output | Where-Object { $allRemoteChangedPaths.Contains($_) })
|
|
178
|
+
if ($differResult.ExitCode -eq 0 -and $allMismatchedPaths.Count -eq 0) {
|
|
179
|
+
return $true
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
$revertResult = Invoke-Git -Arguments @('reset', '-q', $LocalTip)
|
|
183
|
+
if ($revertResult.ExitCode -ne 0) {
|
|
184
|
+
Write-SyncLog -Level 'ERROR' -Message "adopt revert to $LocalTip failed; HEAD is at $RemoteRef with no file changed: $($revertResult.Output)"
|
|
185
|
+
return $false
|
|
186
|
+
}
|
|
187
|
+
$shownPaths = ($allMismatchedPaths | Select-Object -First 20) -join '; '
|
|
188
|
+
Write-SyncLog -Level 'ERROR' -Message "adopt refused: $($allMismatchedPaths.Count) file(s) the remote changed differ from it; HEAD kept at ${LocalTip}: $shownPaths"
|
|
189
|
+
return $false
|
|
190
|
+
}
|
|
191
|
+
|
|
131
192
|
try {
|
|
132
193
|
if (-not (Test-Path -LiteralPath $RepoPath)) {
|
|
133
194
|
Write-SyncLog -Level 'ERROR' -Message "repo path missing: $RepoPath"
|
|
@@ -194,14 +255,18 @@ try {
|
|
|
194
255
|
Write-SyncLog -Level 'ERROR' -Message "merge --ff-only failed: $($mergeResult.Output)"
|
|
195
256
|
if ($didStash) {
|
|
196
257
|
$popResult = Invoke-Git -Arguments @('stash', 'pop')
|
|
197
|
-
if ($popResult.ExitCode -
|
|
198
|
-
Write-SyncLog -Level 'OK' -Message "restored the stashed edits to the working tree after the failed fast-forward"
|
|
199
|
-
}
|
|
200
|
-
else {
|
|
258
|
+
if ($popResult.ExitCode -ne 0) {
|
|
201
259
|
Write-SyncLog -Level 'ERROR' -Message ("stash pop failed after the failed fast-forward; the edits are still stashed as '" + $stashMessage + "' — recover them with git stash list and git stash pop: " + $popResult.Output)
|
|
260
|
+
exit 1
|
|
202
261
|
}
|
|
262
|
+
Write-SyncLog -Level 'OK' -Message "restored the stashed edits to the working tree after the failed fast-forward"
|
|
203
263
|
}
|
|
204
|
-
|
|
264
|
+
if (-not (Invoke-AdoptCopiedTree -LocalTip $localTip -RemoteRef "$Remote/$Branch")) {
|
|
265
|
+
exit 1
|
|
266
|
+
}
|
|
267
|
+
$subject = Get-GitDecoration -Arguments @('log', '-1', '--format=%h %s') -Placeholder '<subject unavailable>'
|
|
268
|
+
Write-SyncLog -Level 'OK' -Message "$Branch $localTip -> $remoteTip adopted (files already matched) | $subject"
|
|
269
|
+
exit 0
|
|
205
270
|
}
|
|
206
271
|
|
|
207
272
|
$subject = Get-GitDecoration -Arguments @('log', '-1', '--format=%h %s') -Placeholder '<subject unavailable>'
|
|
@@ -91,31 +91,137 @@ def _messages_for_validator_outcomes(
|
|
|
91
91
|
)
|
|
92
92
|
|
|
93
93
|
|
|
94
|
+
def _validator_messages_for_text(
|
|
95
|
+
document: Document,
|
|
96
|
+
absolute_path: Path,
|
|
97
|
+
text: str,
|
|
98
|
+
validator_module: ModuleType,
|
|
99
|
+
) -> tuple[str, ...]:
|
|
100
|
+
all_validator_outcomes = validator_module.validate_proposed_file(
|
|
101
|
+
absolute_path.as_posix(),
|
|
102
|
+
text,
|
|
103
|
+
config_source_path=absolute_path,
|
|
104
|
+
include_ruff=False,
|
|
105
|
+
excluded_validator_names=constants.ALL_OVERLAPPING_VALIDATOR_NAMES,
|
|
106
|
+
)
|
|
107
|
+
return _messages_for_validator_outcomes(document, all_validator_outcomes)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _message_body(message: str) -> str:
|
|
111
|
+
_head, separator, body = message.partition(
|
|
112
|
+
constants.VALIDATOR_MESSAGE_PREFIX_SEPARATOR
|
|
113
|
+
)
|
|
114
|
+
return body if separator and body else message
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _message_identity(message: str) -> str:
|
|
118
|
+
return constants.VALIDATOR_MESSAGE_DIGIT_PATTERN.sub(
|
|
119
|
+
constants.VALIDATOR_MESSAGE_DIGIT_PLACEHOLDER, _message_body(message)
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _all_measured_sizes(message: str) -> tuple[int, ...]:
|
|
124
|
+
return tuple(
|
|
125
|
+
int(each_digit_run)
|
|
126
|
+
for each_digit_run in constants.VALIDATOR_MESSAGE_DIGIT_PATTERN.findall(
|
|
127
|
+
_message_body(message)
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _has_grown_since(message: str, prior_message: str) -> bool:
|
|
133
|
+
return any(
|
|
134
|
+
each_size > each_prior_size
|
|
135
|
+
for each_size, each_prior_size in zip(
|
|
136
|
+
_all_measured_sizes(message), _all_measured_sizes(prior_message)
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _prior_messages_by_identity(
|
|
142
|
+
all_prior_messages: tuple[str, ...],
|
|
143
|
+
) -> dict[str, list[str]]:
|
|
144
|
+
all_prior_by_identity: dict[str, list[str]] = {}
|
|
145
|
+
for each_prior_message in all_prior_messages:
|
|
146
|
+
all_prior_by_identity.setdefault(
|
|
147
|
+
_message_identity(each_prior_message), []
|
|
148
|
+
).append(each_prior_message)
|
|
149
|
+
return all_prior_by_identity
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _is_introduced_message(
|
|
153
|
+
message: str, all_prior_by_identity: dict[str, list[str]]
|
|
154
|
+
) -> bool:
|
|
155
|
+
all_matches = all_prior_by_identity.get(_message_identity(message))
|
|
156
|
+
if not all_matches:
|
|
157
|
+
return True
|
|
158
|
+
return _has_grown_since(message, all_matches.pop())
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _messages_introduced_by_the_change(
|
|
162
|
+
all_candidate_messages: tuple[str, ...],
|
|
163
|
+
all_prior_messages: tuple[str, ...],
|
|
164
|
+
) -> tuple[str, ...]:
|
|
165
|
+
"""Keep the messages the prior text did not already carry.
|
|
166
|
+
|
|
167
|
+
::
|
|
168
|
+
|
|
169
|
+
prior: Function 'main' is 47 lines (max 30)
|
|
170
|
+
now: Function 'main' is 47 lines (max 30) -> dropped, untouched debt
|
|
171
|
+
now: Function 'main' is 61 lines (max 30) -> kept, the change grew it
|
|
172
|
+
now: Function 'added' is 47 lines (max 30) -> kept, a new breach
|
|
173
|
+
|
|
174
|
+
A message keeps its identity when it moves down the file or its measured
|
|
175
|
+
size falls, so only a new breach or a worse one survives. Each prior message
|
|
176
|
+
covers one candidate.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
all_candidate_messages: Messages the post-edit text produced.
|
|
180
|
+
all_prior_messages: Messages the prior text produced.
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
The messages this change is answerable for.
|
|
184
|
+
"""
|
|
185
|
+
all_prior_by_identity = _prior_messages_by_identity(all_prior_messages)
|
|
186
|
+
return tuple(
|
|
187
|
+
each_message
|
|
188
|
+
for each_message in all_candidate_messages
|
|
189
|
+
if _is_introduced_message(each_message, all_prior_by_identity)
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
|
|
94
193
|
def validator_diagnostics(
|
|
95
194
|
document: Document,
|
|
96
195
|
repository_root: Path,
|
|
97
196
|
load_module: adapter_support.HookModuleLoader,
|
|
98
197
|
) -> tuple[Diagnostic, ...]:
|
|
99
|
-
"""Run the fast validator set on one document.
|
|
198
|
+
"""Run the fast validator set on one document, scoped to the change.
|
|
199
|
+
|
|
200
|
+
A module already over the file-length cap reported that breach on every edit
|
|
201
|
+
it received, so a one-line change failed on debt it did not create. Grading
|
|
202
|
+
the prior text the same way and subtracting leaves the breaches this change
|
|
203
|
+
introduced or made worse.
|
|
100
204
|
|
|
101
205
|
Args:
|
|
102
|
-
document: Current source text and path.
|
|
206
|
+
document: Current source text, prior text, and path.
|
|
103
207
|
repository_root: Request repository root for configuration resolution.
|
|
104
208
|
load_module: Hook module loader.
|
|
105
209
|
|
|
106
210
|
Returns:
|
|
107
|
-
Path-normalized diagnostics for
|
|
211
|
+
Path-normalized diagnostics for the failures this change is answerable for.
|
|
108
212
|
"""
|
|
109
213
|
absolute_path = adapter_support._document_path(repository_root, document)
|
|
110
214
|
validator_module = load_module("validators.run_all_validators")
|
|
111
|
-
|
|
112
|
-
absolute_path.
|
|
113
|
-
document.text,
|
|
114
|
-
config_source_path=absolute_path,
|
|
115
|
-
include_ruff=False,
|
|
116
|
-
excluded_validator_names=constants.ALL_OVERLAPPING_VALIDATOR_NAMES,
|
|
215
|
+
all_messages = _validator_messages_for_text(
|
|
216
|
+
document, absolute_path, document.text, validator_module
|
|
117
217
|
)
|
|
118
|
-
|
|
218
|
+
if document.prior_text is not None:
|
|
219
|
+
all_prior_messages = _validator_messages_for_text(
|
|
220
|
+
document, absolute_path, document.prior_text, validator_module
|
|
221
|
+
)
|
|
222
|
+
all_messages = _messages_introduced_by_the_change(
|
|
223
|
+
all_messages, all_prior_messages
|
|
224
|
+
)
|
|
119
225
|
return adapter_support._diagnostics_for_messages(document, "validators", all_messages)
|
|
120
226
|
|
|
121
227
|
|
|
@@ -30,6 +30,18 @@ APPROVED_TEST_PATHS_BY_PRODUCTION_PATH: dict[
|
|
|
30
30
|
),
|
|
31
31
|
}
|
|
32
32
|
),
|
|
33
|
+
PurePosixPath(
|
|
34
|
+
"packages/claude-dev-env/hooks/blocking/code_rules_enforcer.py"
|
|
35
|
+
): frozenset(
|
|
36
|
+
{
|
|
37
|
+
PurePosixPath(
|
|
38
|
+
"packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_dispatch_wiring.py"
|
|
39
|
+
),
|
|
40
|
+
PurePosixPath(
|
|
41
|
+
"packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_cap_meta.py"
|
|
42
|
+
),
|
|
43
|
+
}
|
|
44
|
+
),
|
|
33
45
|
PurePosixPath(
|
|
34
46
|
"packages/claude-dev-env/hooks/git-hooks/verification_notice_context.py"
|
|
35
47
|
): frozenset(
|
|
@@ -72,6 +72,9 @@ FAST_SAVE_VALIDATORS_STEM = "fast_save_validators"
|
|
|
72
72
|
ALL_OVERLAPPING_VALIDATOR_NAMES = frozenset(
|
|
73
73
|
{"Python Style", "Magic Values", "Type Safety", "Test Safety", "React"}
|
|
74
74
|
)
|
|
75
|
+
VALIDATOR_MESSAGE_PREFIX_SEPARATOR = ": "
|
|
76
|
+
VALIDATOR_MESSAGE_DIGIT_PATTERN = re.compile(r"\d+")
|
|
77
|
+
VALIDATOR_MESSAGE_DIGIT_PLACEHOLDER = "#"
|
|
75
78
|
ALL_PR_LOOP_SCRIPTS_PATH_SEGMENTS = ("_shared", "pr-loop", "scripts")
|
|
76
79
|
TERMINOLOGY_SWEEP_MODULE_NAME = "terminology_sweep"
|
|
77
80
|
TERMINOLOGY_SWEEP_RULE_ID = "terminology-sweep"
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
Set-StrictMode -Version Latest
|
|
2
|
+
|
|
3
|
+
BeforeAll {
|
|
4
|
+
$scriptUnderTest = Join-Path (Split-Path -Parent $PSScriptRoot) 'Sync-RepoMain.ps1'
|
|
5
|
+
|
|
6
|
+
function Invoke-TestGit {
|
|
7
|
+
param(
|
|
8
|
+
[string]$RepoPath,
|
|
9
|
+
[string[]]$Arguments
|
|
10
|
+
)
|
|
11
|
+
$output = & git -C $RepoPath -c user.name=tester -c user.email=tester@example.com -c core.autocrlf=false @Arguments 2>&1
|
|
12
|
+
if ($LASTEXITCODE -ne 0) {
|
|
13
|
+
throw "git $($Arguments -join ' ') failed: $output"
|
|
14
|
+
}
|
|
15
|
+
return ($output | Out-String).Trim()
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function Write-TestFile {
|
|
19
|
+
param(
|
|
20
|
+
[string]$RepoPath,
|
|
21
|
+
[string]$RelativePath,
|
|
22
|
+
[string]$Content
|
|
23
|
+
)
|
|
24
|
+
[System.IO.File]::WriteAllText((Join-Path $RepoPath $RelativePath), $Content)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function New-MirrorBehindRemote {
|
|
28
|
+
<#
|
|
29
|
+
Builds a bare remote plus a mirror clone one commit behind it. The
|
|
30
|
+
remote's newer commit edits tracked.txt and adds added.txt.
|
|
31
|
+
#>
|
|
32
|
+
param([string]$Name)
|
|
33
|
+
$rootPath = Join-Path $TestDrive $Name
|
|
34
|
+
$remotePath = Join-Path $rootPath 'remote.git'
|
|
35
|
+
$authorPath = Join-Path $rootPath 'author'
|
|
36
|
+
$mirrorPath = Join-Path $rootPath 'mirror'
|
|
37
|
+
New-Item -ItemType Directory -Force -Path $remotePath, $authorPath | Out-Null
|
|
38
|
+
|
|
39
|
+
Invoke-TestGit -RepoPath $remotePath -Arguments @('init', '-q', '--bare', '-b', 'main') | Out-Null
|
|
40
|
+
Invoke-TestGit -RepoPath $authorPath -Arguments @('init', '-q', '-b', 'main') | Out-Null
|
|
41
|
+
Write-TestFile -RepoPath $authorPath -RelativePath 'tracked.txt' -Content "old`n"
|
|
42
|
+
Invoke-TestGit -RepoPath $authorPath -Arguments @('add', '-A') | Out-Null
|
|
43
|
+
Invoke-TestGit -RepoPath $authorPath -Arguments @('commit', '-q', '-m', 'first') | Out-Null
|
|
44
|
+
Invoke-TestGit -RepoPath $authorPath -Arguments @('remote', 'add', 'origin', $remotePath) | Out-Null
|
|
45
|
+
Invoke-TestGit -RepoPath $authorPath -Arguments @('push', '-q', 'origin', 'main') | Out-Null
|
|
46
|
+
|
|
47
|
+
& git -c core.autocrlf=false clone -q $remotePath $mirrorPath 2>&1 | Out-Null
|
|
48
|
+
Invoke-TestGit -RepoPath $mirrorPath -Arguments @('config', 'core.autocrlf', 'false') | Out-Null
|
|
49
|
+
$oldTip = Invoke-TestGit -RepoPath $mirrorPath -Arguments @('rev-parse', 'HEAD')
|
|
50
|
+
|
|
51
|
+
Write-TestFile -RepoPath $authorPath -RelativePath 'tracked.txt' -Content "new`n"
|
|
52
|
+
Write-TestFile -RepoPath $authorPath -RelativePath 'added.txt' -Content "added`n"
|
|
53
|
+
Invoke-TestGit -RepoPath $authorPath -Arguments @('add', '-A') | Out-Null
|
|
54
|
+
Invoke-TestGit -RepoPath $authorPath -Arguments @('commit', '-q', '-m', 'second') | Out-Null
|
|
55
|
+
Invoke-TestGit -RepoPath $authorPath -Arguments @('push', '-q', 'origin', 'main') | Out-Null
|
|
56
|
+
$remoteTip = Invoke-TestGit -RepoPath $authorPath -Arguments @('rev-parse', 'HEAD')
|
|
57
|
+
|
|
58
|
+
return [pscustomobject]@{
|
|
59
|
+
MirrorPath = $mirrorPath
|
|
60
|
+
LogPath = Join-Path $rootPath 'sync.log'
|
|
61
|
+
OldTip = $oldTip
|
|
62
|
+
RemoteTip = $remoteTip
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function Invoke-SyncScript {
|
|
67
|
+
param([pscustomobject]$Fixture)
|
|
68
|
+
& pwsh -NoProfile -File $scriptUnderTest -RepoPath $Fixture.MirrorPath -StashDirty -LogPath $Fixture.LogPath | Out-Null
|
|
69
|
+
return $LASTEXITCODE
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
Describe 'Sync-RepoMain' {
|
|
74
|
+
It 'fast-forwards a clean mirror that is behind the remote' {
|
|
75
|
+
$fixture = New-MirrorBehindRemote -Name 'clean-behind'
|
|
76
|
+
|
|
77
|
+
$exitCode = Invoke-SyncScript -Fixture $fixture
|
|
78
|
+
|
|
79
|
+
$exitCode | Should -Be 0
|
|
80
|
+
(Invoke-TestGit -RepoPath $fixture.MirrorPath -Arguments @('rev-parse', 'HEAD')) | Should -Be $fixture.RemoteTip
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
It 'adopts the remote tip when copied files already match the remote' {
|
|
84
|
+
$fixture = New-MirrorBehindRemote -Name 'copied-match'
|
|
85
|
+
Write-TestFile -RepoPath $fixture.MirrorPath -RelativePath 'tracked.txt' -Content "new`n"
|
|
86
|
+
Write-TestFile -RepoPath $fixture.MirrorPath -RelativePath 'added.txt' -Content "added`n"
|
|
87
|
+
|
|
88
|
+
$exitCode = Invoke-SyncScript -Fixture $fixture
|
|
89
|
+
|
|
90
|
+
$exitCode | Should -Be 0
|
|
91
|
+
(Invoke-TestGit -RepoPath $fixture.MirrorPath -Arguments @('rev-parse', 'HEAD')) | Should -Be $fixture.RemoteTip
|
|
92
|
+
(Invoke-TestGit -RepoPath $fixture.MirrorPath -Arguments @('status', '--porcelain')) | Should -BeNullOrEmpty
|
|
93
|
+
(Invoke-TestGit -RepoPath $fixture.MirrorPath -Arguments @('stash', 'list')) | Should -BeNullOrEmpty
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
It 'keeps the old tip and the copied file when a copied file differs from the remote' {
|
|
97
|
+
$fixture = New-MirrorBehindRemote -Name 'copied-differs'
|
|
98
|
+
Write-TestFile -RepoPath $fixture.MirrorPath -RelativePath 'tracked.txt' -Content "new`n"
|
|
99
|
+
Write-TestFile -RepoPath $fixture.MirrorPath -RelativePath 'added.txt' -Content "local only`n"
|
|
100
|
+
|
|
101
|
+
$exitCode = Invoke-SyncScript -Fixture $fixture
|
|
102
|
+
|
|
103
|
+
$exitCode | Should -Be 1
|
|
104
|
+
(Invoke-TestGit -RepoPath $fixture.MirrorPath -Arguments @('rev-parse', 'HEAD')) | Should -Be $fixture.OldTip
|
|
105
|
+
[System.IO.File]::ReadAllText((Join-Path $fixture.MirrorPath 'added.txt')) | Should -Be "local only`n"
|
|
106
|
+
[System.IO.File]::ReadAllText((Join-Path $fixture.MirrorPath 'tracked.txt')) | Should -Be "new`n"
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -826,7 +826,7 @@ def test_failure_blast_radius_projection_uses_the_canonical_excerpt() -> None:
|
|
|
826
826
|
projected_instruction = materializer.render_codex_failure_blast_radius(canonical_rule)
|
|
827
827
|
|
|
828
828
|
assert projected_instruction.startswith("Failure handling for this run")
|
|
829
|
-
assert "Three
|
|
829
|
+
assert "Three attempts, then park." in projected_instruction
|
|
830
830
|
assert "Report as: N of M complete" in projected_instruction
|
|
831
831
|
|
|
832
832
|
|
|
@@ -851,7 +851,7 @@ def test_build_plan_publishes_owned_agents_projection_and_tracks_drift(
|
|
|
851
851
|
assert manifest_record["source"] == "rules/failure-blast-radius.md"
|
|
852
852
|
|
|
853
853
|
rule_path.write_text(
|
|
854
|
-
rule_path.read_text(encoding="utf-8").replace("Three
|
|
854
|
+
rule_path.read_text(encoding="utf-8").replace("Three attempts, then park.", "Three tested attempts, then park."),
|
|
855
855
|
encoding="utf-8",
|
|
856
856
|
)
|
|
857
857
|
drifted_plan, _ = build_plan(config, all_agents=[])
|
|
@@ -1021,7 +1021,7 @@ def _assert_projection_and_manifest(
|
|
|
1021
1021
|
) -> None:
|
|
1022
1022
|
assert not retired_owned_path.exists()
|
|
1023
1023
|
projected_instruction = (target / "AGENTS.md").read_text(encoding="utf-8")
|
|
1024
|
-
assert "Three
|
|
1024
|
+
assert "Three attempts, then park." in projected_instruction
|
|
1025
1025
|
manifest = json.loads((target / "hooks.json").read_text(encoding="utf-8"))
|
|
1026
1026
|
assert manifest["hooks"]["PreToolUse"][0]["matcher"] == "apply_patch"
|
|
1027
1027
|
assert manifest["hooks"]["PreToolUse"][1] == existing_hooks["hooks"]["PreToolUse"][1]
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Validator diagnostics report only the findings a change introduces.
|
|
2
|
+
|
|
3
|
+
The code-rule adapter already compares a document against its prior text, so a
|
|
4
|
+
long function nobody touched stays quiet there. The validator adapter graded the
|
|
5
|
+
post-edit text alone, so a single-line edit to a long module reported every
|
|
6
|
+
pre-existing cap breach in it and failed the change on debt it did not create.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from pathlib import Path, PurePosixPath
|
|
12
|
+
|
|
13
|
+
from policy_lint import adapters
|
|
14
|
+
from policy_lint.model import ContentOrigin, Diagnostic, Document
|
|
15
|
+
from policy_lint.selection import _changed_lines
|
|
16
|
+
|
|
17
|
+
FUNCTION_LENGTH_MESSAGE_FRAGMENT = "lines (max"
|
|
18
|
+
FILE_LENGTH_MESSAGE_FRAGMENT = "File is"
|
|
19
|
+
PRODUCTION_RELATIVE_PATH = "packages/claude-dev-env/scripts/sample_capped_module.py"
|
|
20
|
+
OVER_THRESHOLD_BODY_LINE_COUNT = 45
|
|
21
|
+
UNDER_THRESHOLD_BODY_LINE_COUNT = 10
|
|
22
|
+
OVER_FILE_CAP_FUNCTION_COUNT = 12
|
|
23
|
+
REPOSITORY_ROOT = Path(__file__).resolve().parents[4]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _counting_function_source(function_name: str, body_line_count: int) -> str:
|
|
27
|
+
all_body_lines = [
|
|
28
|
+
f" running_total = running_total + {each_index}"
|
|
29
|
+
for each_index in range(body_line_count)
|
|
30
|
+
]
|
|
31
|
+
return (
|
|
32
|
+
"\n".join(
|
|
33
|
+
[f"def {function_name}(running_total: int) -> int:"]
|
|
34
|
+
+ all_body_lines
|
|
35
|
+
+ [" return running_total"]
|
|
36
|
+
)
|
|
37
|
+
+ "\n"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _document(prior_text: str | None, current_text: str) -> Document:
|
|
42
|
+
return Document(
|
|
43
|
+
PurePosixPath(PRODUCTION_RELATIVE_PATH),
|
|
44
|
+
current_text,
|
|
45
|
+
prior_text,
|
|
46
|
+
_changed_lines(prior_text, current_text),
|
|
47
|
+
ContentOrigin.REVISION_DIFF,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _messages_carrying(document: Document, fragment: str) -> tuple[str, ...]:
|
|
52
|
+
all_diagnostics: tuple[Diagnostic, ...] = adapters.validator_diagnostics(
|
|
53
|
+
document, REPOSITORY_ROOT
|
|
54
|
+
)
|
|
55
|
+
return tuple(
|
|
56
|
+
each_diagnostic.message
|
|
57
|
+
for each_diagnostic in all_diagnostics
|
|
58
|
+
if fragment in each_diagnostic.message
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _function_length_messages(
|
|
63
|
+
prior_text: str | None, current_text: str
|
|
64
|
+
) -> tuple[str, ...]:
|
|
65
|
+
return _messages_carrying(
|
|
66
|
+
_document(prior_text, current_text), FUNCTION_LENGTH_MESSAGE_FRAGMENT
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _over_file_cap_source(extra_line: str = "") -> str:
|
|
71
|
+
all_functions = [
|
|
72
|
+
_counting_function_source(
|
|
73
|
+
f"each_step_{each_index}", OVER_THRESHOLD_BODY_LINE_COUNT
|
|
74
|
+
)
|
|
75
|
+
for each_index in range(OVER_FILE_CAP_FUNCTION_COUNT)
|
|
76
|
+
]
|
|
77
|
+
return extra_line + "\n".join(all_functions)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_should_leave_an_untouched_long_function_alone() -> None:
|
|
81
|
+
prior_text = _counting_function_source("main", OVER_THRESHOLD_BODY_LINE_COUNT)
|
|
82
|
+
current_text = f"import os\nimport sys\n{prior_text}"
|
|
83
|
+
assert _function_length_messages(prior_text, current_text) == ()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_should_report_an_over_long_function_the_change_adds() -> None:
|
|
87
|
+
prior_text = _counting_function_source("existing", UNDER_THRESHOLD_BODY_LINE_COUNT)
|
|
88
|
+
added_text = _counting_function_source("added", OVER_THRESHOLD_BODY_LINE_COUNT)
|
|
89
|
+
assert _function_length_messages(prior_text, f"{prior_text}\n{added_text}") != ()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_should_report_a_function_the_change_grew_past_the_cap() -> None:
|
|
93
|
+
prior_text = _counting_function_source("main", UNDER_THRESHOLD_BODY_LINE_COUNT)
|
|
94
|
+
current_text = _counting_function_source("main", OVER_THRESHOLD_BODY_LINE_COUNT)
|
|
95
|
+
assert _function_length_messages(prior_text, current_text) != ()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_should_report_every_cap_breach_in_a_wholly_new_document() -> None:
|
|
99
|
+
current_text = _counting_function_source("main", OVER_THRESHOLD_BODY_LINE_COUNT)
|
|
100
|
+
assert _function_length_messages(None, current_text) != ()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_should_report_a_cap_breach_when_the_selection_names_no_prior_text() -> None:
|
|
104
|
+
current_text = _counting_function_source("main", OVER_THRESHOLD_BODY_LINE_COUNT)
|
|
105
|
+
document = Document(
|
|
106
|
+
PurePosixPath(PRODUCTION_RELATIVE_PATH),
|
|
107
|
+
current_text,
|
|
108
|
+
None,
|
|
109
|
+
None,
|
|
110
|
+
ContentOrigin.WORKTREE,
|
|
111
|
+
)
|
|
112
|
+
assert _messages_carrying(document, FUNCTION_LENGTH_MESSAGE_FRAGMENT) != ()
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_should_leave_an_over_cap_file_alone_when_the_change_only_shrinks_it() -> None:
|
|
116
|
+
prior_text = _over_file_cap_source("REMOVED_SETTING = 1\n")
|
|
117
|
+
current_text = _over_file_cap_source()
|
|
118
|
+
document = _document(prior_text, current_text)
|
|
119
|
+
assert _messages_carrying(document, FILE_LENGTH_MESSAGE_FRAGMENT) == ()
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_should_report_a_file_the_change_pushed_over_the_cap() -> None:
|
|
123
|
+
prior_text = _counting_function_source("main", UNDER_THRESHOLD_BODY_LINE_COUNT)
|
|
124
|
+
current_text = _over_file_cap_source()
|
|
125
|
+
document = _document(prior_text, current_text)
|
|
126
|
+
assert _messages_carrying(document, FILE_LENGTH_MESSAGE_FRAGMENT) != ()
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
@AGENTS.md
|