claude-dev-env 2.12.0 → 2.13.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/code_rules_gate.py +2 -3
- package/_shared/pr-loop/scripts/test_code_rules_gate.py +0 -26
- package/agents/CLAUDE.md +1 -2
- package/agents/pr-description-writer.md +11 -81
- package/bin/install.mjs +48 -9
- package/bin/install.prune.test.mjs +26 -0
- package/bin/install.settings-defaults.test.mjs +60 -33
- package/bin/install.test.mjs +2 -4
- package/commands/sr-loop.md +9 -5
- package/docs/CLAUDE.md +0 -1
- package/docs/references/CLAUDE.md +0 -1
- package/hooks/CLAUDE.md +1 -1
- package/hooks/blocking/CLAUDE.md +2 -27
- package/hooks/blocking/config/CLAUDE.md +1 -13
- package/hooks/blocking/conventional_pr_title_gate.py +1 -2
- package/hooks/blocking/send_user_file_open_locally_blocker.py +1 -1
- package/hooks/blocking/stop_dispatcher.py +4 -4
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +0 -3
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +6 -7
- package/hooks/blocking/test_send_user_file_open_locally_blocker.py +4 -3
- package/hooks/blocking/test_shared_stdin_adoption.py +0 -2
- package/hooks/diagnostic/CLAUDE.md +3 -3
- package/hooks/diagnostic/hook_log_extractor.py +2 -36
- package/hooks/diagnostic/hook_log_stop_wrapper.py +6 -155
- package/hooks/diagnostic/test_hook_log_extractor.py +8 -21
- package/hooks/diagnostic/test_hook_log_stop_wrapper.py +3 -331
- package/hooks/git-hooks/git_hooks_constants/__init__.py +0 -6
- package/hooks/git-hooks/pre_push.py +3 -85
- package/hooks/git-hooks/test_pre_push.py +0 -130
- package/hooks/hooks.json +0 -35
- package/hooks/hooks_constants/CLAUDE.md +0 -9
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +0 -11
- package/hooks/hooks_constants/convergence_branch_constants.py +0 -1
- package/hooks/hooks_constants/fable_spawn_gate_constants.py +3 -4
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +0 -8
- package/hooks/hooks_constants/send_user_file_open_locally_blocker_constants.py +1 -6
- package/hooks/hooks_constants/stop_dispatcher_constants.py +0 -1
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +1 -26
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +0 -5
- package/hooks/hooks_constants/test_stop_dispatcher_constants.py +0 -1
- package/hooks/lifecycle/CLAUDE.md +1 -3
- package/package.json +1 -1
- package/rules/CLAUDE.md +0 -2
- package/rules/gh-cli-conventions.md +0 -1
- package/rules/git-workflow.md +1 -3
- package/scripts/CLAUDE.md +0 -1
- package/scripts/_code_review_test_support.py +0 -95
- package/scripts/dev_env_scripts_constants/code_review_constants.py +9 -98
- package/scripts/dev_env_scripts_constants/test_code_review_constants.py +1 -44
- package/scripts/invoke_code_review.py +8 -451
- package/scripts/test_invoke_code_review.py +5 -61
- package/scripts/test_invoke_code_review_cli.py +1 -45
- package/scripts/test_invoke_code_review_contract.py +1 -118
- package/settings.json +1 -10
- package/skills/CLAUDE.md +8 -0
- package/skills/autoconverge/reference/CLAUDE.md +1 -1
- package/skills/autoconverge/reference/convergence.md +7 -7
- package/skills/autoconverge/reference/gotchas.md +3 -10
- package/skills/autoconverge/workflow/converge.contract.test.mjs +53 -1388
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +4 -3
- package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +33 -493
- package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +15 -97
- package/skills/autoconverge/workflow/converge.mjs +201 -238
- package/skills/autoconverge/workflow/converge.precatch.test.mjs +1 -1
- package/skills/bugteam/CONSTRAINTS.md +1 -1
- package/skills/bugteam/SKILL.md +9 -0
- package/skills/bugteam/reference/audit-and-teammates.md +10 -6
- package/skills/bugteam/test_skill_additions.py +9 -0
- package/skills/comments/SKILL.md +65 -0
- package/skills/descriptions/SKILL.md +51 -0
- package/skills/emergencies/SKILL.md +42 -0
- package/skills/imagegen/SKILL.md +21 -0
- package/skills/imagegen/scripts/config/__init__.py +1 -0
- package/skills/imagegen/scripts/config/constants.py +28 -0
- package/skills/imagegen/scripts/imagegen.py +45 -0
- package/skills/imagegen/scripts/imagegen_core.py +528 -0
- package/skills/imagegen/scripts/test_imagegen.py +204 -0
- package/skills/orchestrator/SKILL.md +2 -4
- package/skills/plan-to-pr/SKILL.md +6 -7
- package/skills/plan-to-pr/reference/final-validation-tasks.md +1 -1
- package/skills/plan-to-pr/reference/process-inventory.md +2 -2
- package/skills/plan-to-pr/reference/review-loop.md +2 -2
- package/skills/plan-to-pr/reference/run-record.schema.json +1 -1
- package/skills/plan-to-pr/reference/task-seeds.md +1 -1
- package/skills/plan-to-pr/reference/task-ticket.md +3 -4
- package/skills/plan-to-pr/scripts/config/constants.py +1 -2
- package/skills/plan-to-pr/scripts/test_validate_protocol.py +1 -1
- package/skills/plan-to-pr/test_skill_contract.py +2 -2
- package/skills/pr-converge/SKILL.md +13 -16
- package/skills/pr-converge/reference/progress-checklist.md +6 -5
- package/skills/pr-converge/reference/state-schema.md +1 -27
- package/skills/pr-loop-cloud-transport/SKILL.md +1 -1
- package/skills/pr-loop-cloud-transport/reference/identity-and-hooks.md +3 -3
- package/skills/pr-loop-lifecycle/SKILL.md +1 -1
- package/skills/pr-loop-lifecycle/reference/teardown-publish-permissions.md +1 -1
- package/skills/prototype/SKILL.md +4 -4
- package/skills/prototype/reference/honest-limitations.md +4 -4
- package/skills/prototype/reference/promotion-tasks.md +2 -2
- package/skills/prototype/workflows/promotion.md +4 -6
- package/skills/reviews/SKILL.md +85 -0
- package/skills/small-cl/SKILL.md +52 -0
- package/agents/code-verifier.md +0 -74
- package/docs/PR_DESCRIPTION_GUIDE.md +0 -157
- package/docs/references/code-review-enforcement.md +0 -132
- package/hooks/blocking/code_review_enforcement_config_bootstrap.py +0 -53
- package/hooks/blocking/code_review_gate_deny.py +0 -74
- package/hooks/blocking/code_review_pr_create_gate.py +0 -198
- package/hooks/blocking/code_review_push_gate.py +0 -145
- package/hooks/blocking/code_review_stamp_directory_write_blocker.py +0 -348
- package/hooks/blocking/code_review_stamp_store.py +0 -233
- package/hooks/blocking/code_review_stamp_write_blocker_parts/__init__.py +0 -7
- package/hooks/blocking/code_review_stamp_write_blocker_parts/conftest.py +0 -15
- package/hooks/blocking/code_review_stamp_write_blocker_parts/obfuscated_stamp_path_reference.py +0 -212
- package/hooks/blocking/code_review_stamp_write_blocker_parts/split_directory_change_into_stamp.py +0 -138
- package/hooks/blocking/code_review_stamp_write_blocker_parts/test_obfuscated_stamp_path_reference.py +0 -49
- package/hooks/blocking/code_review_stamp_write_blocker_parts/test_split_directory_change_into_stamp.py +0 -38
- package/hooks/blocking/code_verifier_spawn_preflight_gate.py +0 -483
- package/hooks/blocking/config/code_review_enforcement_constants.py +0 -143
- package/hooks/blocking/config/test_code_review_enforcement_constants.py +0 -166
- package/hooks/blocking/config/verified_commit_constants.py +0 -160
- package/hooks/blocking/config/verified_commit_context_constants.py +0 -21
- package/hooks/blocking/config/verified_commit_gate_output_constants.py +0 -14
- package/hooks/blocking/conftest.py +0 -123
- package/hooks/blocking/convergence_gate_blocker.py +0 -310
- package/hooks/blocking/pr_converge_bugteam_enforcer.py +0 -170
- package/hooks/blocking/pr_description_body_audit.py +0 -148
- package/hooks/blocking/pr_description_command_parser.py +0 -237
- package/hooks/blocking/pr_description_enforcer.py +0 -227
- package/hooks/blocking/pr_description_pr_number.py +0 -155
- package/hooks/blocking/pr_description_proof_of_work.py +0 -385
- package/hooks/blocking/pr_description_readability.py +0 -364
- package/hooks/blocking/reviewer_spawn_gate.py +0 -182
- package/hooks/blocking/test_code_review_enforcement_config_bootstrap.py +0 -62
- package/hooks/blocking/test_code_review_gate_deny.py +0 -54
- package/hooks/blocking/test_code_review_pr_create_gate.py +0 -199
- package/hooks/blocking/test_code_review_push_gate.py +0 -205
- package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +0 -199
- package/hooks/blocking/test_code_review_stamp_store.py +0 -205
- package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +0 -710
- package/hooks/blocking/test_code_verifier_tools_contract.py +0 -28
- package/hooks/blocking/test_convergence_gate_blocker.py +0 -428
- package/hooks/blocking/test_pr_converge_bugteam_enforcer.py +0 -311
- package/hooks/blocking/test_pr_converge_bugteam_enforcer_state_tolerance.py +0 -184
- package/hooks/blocking/test_pr_description_enforcer.py +0 -221
- package/hooks/blocking/test_pr_description_enforcer_body_audit.py +0 -247
- package/hooks/blocking/test_pr_description_enforcer_body_rules.py +0 -493
- package/hooks/blocking/test_pr_description_enforcer_command_parser.py +0 -366
- package/hooks/blocking/test_pr_description_enforcer_pr_number.py +0 -159
- package/hooks/blocking/test_pr_description_enforcer_proof_gate.py +0 -175
- package/hooks/blocking/test_pr_description_enforcer_readability.py +0 -443
- package/hooks/blocking/test_pr_description_proof_of_work.py +0 -313
- package/hooks/blocking/test_reviewer_spawn_gate.py +0 -230
- package/hooks/blocking/test_verdict_directory_write_blocker.py +0 -804
- package/hooks/blocking/test_verification_verdict_store.py +0 -974
- package/hooks/blocking/test_verified_commit_config_bootstrap.py +0 -67
- package/hooks/blocking/test_verified_commit_docs_delta.py +0 -176
- package/hooks/blocking/test_verified_commit_gate.py +0 -581
- package/hooks/blocking/test_verified_commit_gate_additional_context.py +0 -134
- package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +0 -131
- package/hooks/blocking/test_verifier_verdict_minter.py +0 -299
- package/hooks/blocking/tests/test_verified_commit_gate.py +0 -41
- package/hooks/blocking/verdict_directory_write_blocker.py +0 -687
- package/hooks/blocking/verification_verdict_store.py +0 -1039
- package/hooks/blocking/verified_commit_config_bootstrap.py +0 -63
- package/hooks/blocking/verified_commit_gate.py +0 -113
- package/hooks/blocking/verified_commit_gate_parts/CLAUDE.md +0 -28
- package/hooks/blocking/verified_commit_gate_parts/__init__.py +0 -1
- package/hooks/blocking/verified_commit_gate_parts/command_tokenization.py +0 -174
- package/hooks/blocking/verified_commit_gate_parts/deny_payload.py +0 -53
- package/hooks/blocking/verified_commit_gate_parts/deny_reason.py +0 -80
- package/hooks/blocking/verified_commit_gate_parts/directory_resolution.py +0 -170
- package/hooks/blocking/verified_commit_gate_parts/gated_invocations.py +0 -217
- package/hooks/blocking/verified_commit_gate_parts/tests/conftest.py +0 -10
- package/hooks/blocking/verified_commit_gate_parts/tests/test_command_tokenization.py +0 -94
- package/hooks/blocking/verified_commit_gate_parts/tests/test_deny_payload.py +0 -17
- package/hooks/blocking/verified_commit_gate_parts/tests/test_deny_reason.py +0 -38
- package/hooks/blocking/verified_commit_gate_parts/tests/test_directory_resolution.py +0 -71
- package/hooks/blocking/verified_commit_gate_parts/tests/test_gated_invocations.py +0 -96
- package/hooks/blocking/verified_commit_message_accuracy_blocker.py +0 -167
- package/hooks/blocking/verifier_verdict_minter.py +0 -280
- package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +0 -64
- package/hooks/hooks_constants/convergence_gate_blocker_constants.py +0 -53
- package/hooks/hooks_constants/pr_converge_bugteam_enforcer_constants.py +0 -55
- package/hooks/hooks_constants/pr_converge_bugteam_enforcer_state.py +0 -67
- package/hooks/hooks_constants/pr_description_enforcer_constants.py +0 -167
- package/hooks/hooks_constants/pr_description_proof_of_work_constants.py +0 -107
- package/hooks/hooks_constants/reviewer_spawn_gate_constants.py +0 -41
- package/hooks/hooks_constants/test_pr_description_enforcer_constants.py +0 -292
- package/hooks/lifecycle/pr_converge_bugteam_skill_tracker.py +0 -198
- package/hooks/lifecycle/test_pr_converge_bugteam_skill_tracker.py +0 -283
- package/rules/proof-of-work-pr-comments.md +0 -22
- package/rules/verified-commit-gate-skip.md +0 -4
- package/scripts/Show-Asset.ps1 +0 -106
|
@@ -1,1388 +1,53 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
)
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
assert.
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
test('bug-audit lens prompt no longer instructs a per-lens git fetch', () => {
|
|
56
|
-
assert.doesNotMatch(lensPromptBody('runAuditLens'), /git fetch origin main/);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
test('the merged preflight-git task fetches origin/main once before the parallel lenses', () => {
|
|
60
|
-
assert.ok(convergeSource.includes("runGitTask('preflight-git')"));
|
|
61
|
-
const preflightCallIndex = convergeSource.indexOf("runGitTask('preflight-git')");
|
|
62
|
-
const parallelLensIndex = convergeSource.indexOf('const lenses = await parallel(');
|
|
63
|
-
assert.notEqual(preflightCallIndex, -1, 'expected the merged preflight to be invoked');
|
|
64
|
-
assert.notEqual(parallelLensIndex, -1, 'expected the parallel lens block to exist');
|
|
65
|
-
assert.ok(
|
|
66
|
-
preflightCallIndex < parallelLensIndex,
|
|
67
|
-
'expected the merged preflight to run before the parallel lenses spawn',
|
|
68
|
-
);
|
|
69
|
-
const gitTaskBody = functionSource('runGitTask');
|
|
70
|
-
assert.match(gitTaskBody, /git fetch origin main/, 'expected the merged task to carry the base-ref fetch');
|
|
71
|
-
assert.match(gitTaskBody, /--jq \.head\.sha/, 'expected the merged task to resolve the PR HEAD SHA');
|
|
72
|
-
assert.match(gitTaskBody, /PREFLIGHT_GIT_SCHEMA/, 'expected the merged task to return the {sha, conflicting, fetched, copilot, bugbot} schema');
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
test('the merged preflight-git agent spreads the haikuLow tier', () => {
|
|
76
|
-
const gitTaskBody = functionSource('runGitTask');
|
|
77
|
-
assert.match(
|
|
78
|
-
gitTaskBody,
|
|
79
|
-
/\.\.\.TIERS\.haikuLow/,
|
|
80
|
-
'expected the git-utility agent to spread the single-sourced haikuLow tier rather than inline model/effort literals',
|
|
81
|
-
);
|
|
82
|
-
assert.doesNotMatch(
|
|
83
|
-
gitTaskBody,
|
|
84
|
-
/model: 'haiku', effort: 'low'/,
|
|
85
|
-
'expected no inline haikuLow literals duplicating the TIERS definition',
|
|
86
|
-
);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
test('the preflight-git prompt states the correct number of read-only steps it enumerates', () => {
|
|
90
|
-
const body = functionSource('runGitTask');
|
|
91
|
-
assert.match(
|
|
92
|
-
body,
|
|
93
|
-
/Run five read-only preflight steps/,
|
|
94
|
-
'expected the stated step count to match the five enumerated STEP blocks',
|
|
95
|
-
);
|
|
96
|
-
assert.match(body, /STEP 5 —/, 'expected the fifth step to be enumerated');
|
|
97
|
-
assert.doesNotMatch(body, /Run four read-only preflight steps/);
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
test('the runGitTask docstring names the diff enumeration and its changedFiles/diffstat return fields', () => {
|
|
101
|
-
const doc = docstringBefore('runGitTask');
|
|
102
|
-
assert.match(doc, /diff|changed-file|enumerat/i, 'expected the docstring to name the diff enumeration operation');
|
|
103
|
-
assert.match(doc, /changedFiles/, 'expected the docstring return shape to name changedFiles');
|
|
104
|
-
assert.match(doc, /diffstat/, 'expected the docstring return shape to name diffstat');
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
test('the reviewer-availability probe rides the merged preflight-git spawn, not a separate agent', () => {
|
|
108
|
-
assert.equal(
|
|
109
|
-
convergeSource.indexOf('runReviewerAvailabilityCheck'),
|
|
110
|
-
-1,
|
|
111
|
-
'expected no separate reviewer-availability agent — the probe rides the preflight-git git-utility spawn',
|
|
112
|
-
);
|
|
113
|
-
const gitTaskBody = functionSource('runGitTask');
|
|
114
|
-
assert.match(gitTaskBody, /reviewer_availability\.py/, 'expected the merged preflight to run the reviewer-availability probe');
|
|
115
|
-
assert.match(gitTaskBody, /--reviewer copilot/);
|
|
116
|
-
assert.match(gitTaskBody, /--reviewer bugbot/);
|
|
117
|
-
const preflightAssignIndex = convergeSource.indexOf('reviewerAvailability = preflight');
|
|
118
|
-
const parallelLensIndex = convergeSource.indexOf('const lenses = await parallel(');
|
|
119
|
-
assert.notEqual(preflightAssignIndex, -1, 'expected reviewerAvailability to be read from the preflight-git result');
|
|
120
|
-
assert.ok(
|
|
121
|
-
preflightAssignIndex < parallelLensIndex,
|
|
122
|
-
'expected reviewer availability to be carried from preflight before the parallel lenses spawn',
|
|
123
|
-
);
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
test('the round parallel array holds exactly the three internal lenses in order and no Bugbot slot', () => {
|
|
127
|
-
const parallelLensIndex = convergeSource.indexOf('const lenses = await parallel(');
|
|
128
|
-
assert.notEqual(parallelLensIndex, -1, 'expected the parallel lens block to exist');
|
|
129
|
-
const lensArrayEnd = convergeSource.indexOf('])', parallelLensIndex);
|
|
130
|
-
const lensArray = convergeSource.slice(parallelLensIndex, lensArrayEnd);
|
|
131
|
-
const codeReviewIndex = lensArray.indexOf('runCodeReviewLens(head, reviewerAvailability)');
|
|
132
|
-
const auditIndex = lensArray.indexOf('runAuditLens(head, reviewerAvailability)');
|
|
133
|
-
const selfReviewIndex = lensArray.indexOf('runSelfReviewLens(head, reviewerAvailability)');
|
|
134
|
-
assert.ok(codeReviewIndex !== -1 && auditIndex !== -1 && selfReviewIndex !== -1, 'expected the three internal lenses in the parallel array');
|
|
135
|
-
assert.ok(
|
|
136
|
-
codeReviewIndex < auditIndex && auditIndex < selfReviewIndex,
|
|
137
|
-
'expected the code-review, bug-audit, self-review order to match LENS_NAMES',
|
|
138
|
-
);
|
|
139
|
-
assert.doesNotMatch(lensArray, /runBugbotLens/, 'expected Bugbot to leave the per-round parallel and become a terminal gate');
|
|
140
|
-
assert.doesNotMatch(convergeSource, /isBugbotDownPreSpawn/, 'expected the pre-spawn Bugbot-down decision to be gone from the round');
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
test('both reviewer trigger commands carry the CLAUDE_REVIEWER_GATE=autoconverge sentinel', () => {
|
|
144
|
-
assert.match(
|
|
145
|
-
convergeSource,
|
|
146
|
-
/const REVIEWER_GATE_SENTINEL = 'CLAUDE_REVIEWER_GATE=autoconverge '/,
|
|
147
|
-
'expected the shared sentinel constant to exist',
|
|
148
|
-
);
|
|
149
|
-
const copilotPrompt = lensPromptBody('runCopilotGate');
|
|
150
|
-
assert.match(
|
|
151
|
-
copilotPrompt,
|
|
152
|
-
/\$\{REVIEWER_GATE_SENTINEL\}gh api --method POST[^\n]*requested_reviewers/,
|
|
153
|
-
'expected the Copilot requested_reviewers trigger to carry the sentinel prefix',
|
|
154
|
-
);
|
|
155
|
-
const bugbotPrompt = lensPromptBody('runBugbotLens');
|
|
156
|
-
assert.match(
|
|
157
|
-
bugbotPrompt,
|
|
158
|
-
/\$\{REVIEWER_GATE_SENTINEL\}python "\$\{CONFIG\.sharedScripts\}\/post_fix_reply\.py"[^\n]*--body "bugbot run"/,
|
|
159
|
-
'expected the Bugbot rerun-comment trigger to carry the sentinel prefix',
|
|
160
|
-
);
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
test('bugbot lens preamble does not blanket-instruct passing --owner/--repo to every script', () => {
|
|
164
|
-
const bugbotPrompt = lensPromptBody('runBugbotLens');
|
|
165
|
-
assert.doesNotMatch(
|
|
166
|
-
bugbotPrompt,
|
|
167
|
-
/use the existing scripts; pass --owner/,
|
|
168
|
-
'the blanket clause breaks reviews_disabled.py, which accepts only --reviewer',
|
|
169
|
-
);
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
test('bugbot lens invokes reviews_disabled.py with only --reviewer', () => {
|
|
173
|
-
const bugbotPrompt = lensPromptBody('runBugbotLens');
|
|
174
|
-
const reviewsDisabledIndex = bugbotPrompt.indexOf('reviews_disabled.py');
|
|
175
|
-
assert.notEqual(reviewsDisabledIndex, -1, 'expected reviews_disabled.py invocation');
|
|
176
|
-
const invocationLineEnd = bugbotPrompt.indexOf('\\n', reviewsDisabledIndex);
|
|
177
|
-
const invocationLine = bugbotPrompt.slice(reviewsDisabledIndex, invocationLineEnd);
|
|
178
|
-
assert.match(invocationLine, /--reviewer bugbot/);
|
|
179
|
-
assert.doesNotMatch(
|
|
180
|
-
invocationLine,
|
|
181
|
-
/--owner|--repo/,
|
|
182
|
-
'reviews_disabled.py argparse rejects --owner/--repo with SystemExit(2)',
|
|
183
|
-
);
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
test('gotchas doc states parallel lenses must avoid concurrent git operations', () => {
|
|
187
|
-
assert.doesNotMatch(gotchasSource, /cannot race on git state/);
|
|
188
|
-
assert.match(gotchasSource, /fetch.*once.*before/i);
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
test('repair-convergence edit step filters unresolved threads to bot authors and skips human threads', () => {
|
|
192
|
-
const repairPrompt = functionSource('runCodeEditorTask');
|
|
193
|
-
assert.match(
|
|
194
|
-
repairPrompt,
|
|
195
|
-
/cursor.*claude.*copilot|copilot.*cursor.*claude|claude.*cursor.*copilot/is,
|
|
196
|
-
'expected the bot-author allowlist (Cursor/Claude/Copilot) to be named',
|
|
197
|
-
);
|
|
198
|
-
assert.match(
|
|
199
|
-
repairPrompt,
|
|
200
|
-
/skip.*human|human.*skip/is,
|
|
201
|
-
'expected an explicit instruction to skip human reviewer threads',
|
|
202
|
-
);
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
test('repair-convergence edit step no longer instructs resolving every unresolved thread without an author filter', () => {
|
|
206
|
-
const repairPrompt = functionSource('runCodeEditorTask');
|
|
207
|
-
assert.doesNotMatch(
|
|
208
|
-
repairPrompt,
|
|
209
|
-
/fetch every thread where isResolved is false/,
|
|
210
|
-
'the unfiltered instruction could resolve human reviewer threads',
|
|
211
|
-
);
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
test('the bugbot lens waits through the Monitor tool, not a foreground sleep', () => {
|
|
215
|
-
const bugbotPrompt = lensPromptBody('runBugbotLens');
|
|
216
|
-
assert.match(bugbotPrompt, /Monitor tool/, 'expected the bugbot poll to wait via the Monitor tool');
|
|
217
|
-
assert.doesNotMatch(
|
|
218
|
-
bugbotPrompt,
|
|
219
|
-
/sleep 60|sleep 8|Start-Sleep/,
|
|
220
|
-
'expected no foreground sleep in the bugbot poll delays',
|
|
221
|
-
);
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
test('the copilot gate waits through the Monitor tool, not a foreground sleep', () => {
|
|
225
|
-
const copilotPrompt = lensPromptBody('runCopilotGate');
|
|
226
|
-
assert.match(copilotPrompt, /Monitor tool/, 'expected the copilot poll to wait via the Monitor tool');
|
|
227
|
-
assert.doesNotMatch(copilotPrompt, /sleep 360|Start-Sleep/, 'expected no foreground sleep in the copilot poll delay');
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
test('gotchas doc describes the reviewer wait as a Monitor poll, not a foreground sleep', () => {
|
|
231
|
-
assert.match(gotchasSource, /Monitor tool/, 'expected the gotcha to name the Monitor-based reviewer wait');
|
|
232
|
-
assert.doesNotMatch(
|
|
233
|
-
gotchasSource,
|
|
234
|
-
/shell-agnostic/i,
|
|
235
|
-
'the reviewer wait is a Monitor poll, not a shell-agnostic sleep loop',
|
|
236
|
-
);
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
function finalizeRepairBranch() {
|
|
240
|
-
const repairCallIndex = convergeSource.indexOf('await repairConvergence(');
|
|
241
|
-
assert.notEqual(repairCallIndex, -1, 'expected the FINALIZE repair call to exist');
|
|
242
|
-
const transitionIndex = convergeSource.indexOf("phase = 'CONVERGE'", repairCallIndex);
|
|
243
|
-
assert.notEqual(transitionIndex, -1, 'expected a CONVERGE transition after the repair call');
|
|
244
|
-
const continueIndex = convergeSource.indexOf('continue', transitionIndex);
|
|
245
|
-
assert.notEqual(continueIndex, -1, 'expected a continue statement to close the FINALIZE repair branch');
|
|
246
|
-
const branchEnd = continueIndex + 'continue'.length;
|
|
247
|
-
return convergeSource.slice(repairCallIndex, branchEnd);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
test('the FINALIZE repair branch does not re-assign head from the repair before re-converging', () => {
|
|
251
|
-
assert.doesNotMatch(
|
|
252
|
-
finalizeRepairBranch(),
|
|
253
|
-
/head\s*=\s*repair/,
|
|
254
|
-
'the next CONVERGE pass re-resolves HEAD from GitHub, so assigning the repair SHA here is dead',
|
|
255
|
-
);
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
function fixBranchAfter(branchLabel) {
|
|
259
|
-
const labelIndex = convergeSource.indexOf(branchLabel);
|
|
260
|
-
assert.notEqual(labelIndex, -1, `expected the ${branchLabel} marker to exist`);
|
|
261
|
-
const applyFixesIndex = convergeSource.indexOf('await applyFixes(', labelIndex);
|
|
262
|
-
assert.notEqual(applyFixesIndex, -1, `expected an applyFixes call after ${branchLabel}`);
|
|
263
|
-
const continueIndex = convergeSource.indexOf('continue', applyFixesIndex);
|
|
264
|
-
assert.notEqual(continueIndex, -1, `expected a continue statement to close the ${branchLabel} branch`);
|
|
265
|
-
const branchEnd = continueIndex + 'continue'.length;
|
|
266
|
-
return convergeSource.slice(applyFixesIndex, branchEnd);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
test('the CONVERGE fix branch does not re-assign head from the fix before re-converging', () => {
|
|
270
|
-
assert.doesNotMatch(
|
|
271
|
-
fixBranchAfter('${findings.length} finding(s) — applying fixes'),
|
|
272
|
-
/head\s*=\s*fixProgress/,
|
|
273
|
-
'the next CONVERGE pass re-resolves HEAD from GitHub, so assigning the fix SHA here is dead',
|
|
274
|
-
);
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
test('the COPILOT fix branch does not re-assign head from the fix before re-converging', () => {
|
|
278
|
-
assert.doesNotMatch(
|
|
279
|
-
fixBranchAfter('${roundFindings.length} finding(s) — fixing and re-converging'),
|
|
280
|
-
/head\s*=\s*fixProgress/,
|
|
281
|
-
'the CONVERGE pass it transitions to re-resolves HEAD from GitHub, so assigning the fix SHA here is dead',
|
|
282
|
-
);
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
test('the CONVERGE branch refreshes HEAD via the merged preflight-git task only when the threaded head is invalidated', () => {
|
|
286
|
-
const convergeBranchStart = convergeSource.indexOf("if (phase === 'CONVERGE')");
|
|
287
|
-
assert.notEqual(convergeBranchStart, -1, 'expected the CONVERGE branch to exist');
|
|
288
|
-
const invalidGuardIndex = convergeSource.indexOf('if (!isResolvedHeadUsable(head)', convergeBranchStart);
|
|
289
|
-
const headRefreshCallIndex = convergeSource.indexOf("runGitTask('preflight-git')", convergeBranchStart);
|
|
290
|
-
assert.notEqual(invalidGuardIndex, -1, 'expected CONVERGE to gate the refresh on an invalidated head');
|
|
291
|
-
assert.notEqual(headRefreshCallIndex, -1, 'expected CONVERGE to refresh HEAD via the merged preflight-git task');
|
|
292
|
-
assert.ok(
|
|
293
|
-
invalidGuardIndex < headRefreshCallIndex,
|
|
294
|
-
'expected the invalidated-head guard to precede the merged refresh so a valid threaded head spawns no git agent',
|
|
295
|
-
);
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
test('the CONVERGE branch refreshes preflight when the availability enumeration was computed against a different SHA than the head under review', () => {
|
|
299
|
-
const convergeBranchStart = convergeSource.indexOf("if (phase === 'CONVERGE')");
|
|
300
|
-
assert.notEqual(convergeBranchStart, -1, 'expected the CONVERGE branch to exist');
|
|
301
|
-
const guardIndex = convergeSource.indexOf('if (!isResolvedHeadUsable(head)', convergeBranchStart);
|
|
302
|
-
assert.notEqual(guardIndex, -1, 'expected the refresh guard to exist');
|
|
303
|
-
const guardLine = convergeSource.slice(guardIndex, convergeSource.indexOf('\n', guardIndex));
|
|
304
|
-
assert.match(
|
|
305
|
-
guardLine,
|
|
306
|
-
/reviewerAvailability\?\.sha !== head/,
|
|
307
|
-
'expected the refresh guard to fire when the reviewerAvailability changed-file enumeration was computed against a SHA other than the current head, so a rebase or reuse push that advances HEAD before round 1 hands the lenses the diff for the head they actually review',
|
|
308
|
-
);
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
test('each fix push, each lens-retry, and the convergence repair invalidate the threaded head so the next CONVERGE entry refreshes it', () => {
|
|
312
|
-
const invalidationMatches = convergeSource.match(/^ +head = null$/gm) || [];
|
|
313
|
-
assert.equal(
|
|
314
|
-
invalidationMatches.length,
|
|
315
|
-
9,
|
|
316
|
-
'expected head invalidation after the static-sweep fix push, the CONVERGE fix push, the terminal-Bugbot fix push, the COPILOT fix push, the CODEX fix push, the convergence repair, the all-lenses-dead retry, the not-clean-no-findings retry, and the all-clean no-lens-ran clean-audit refusal',
|
|
317
|
-
);
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
function convergeRetryBranch(guardMarker) {
|
|
321
|
-
const guardIndex = convergeSource.indexOf(guardMarker);
|
|
322
|
-
assert.notEqual(guardIndex, -1, `expected the ${guardMarker} retry branch to exist`);
|
|
323
|
-
const continueIndex = convergeSource.indexOf('continue', guardIndex);
|
|
324
|
-
assert.notEqual(continueIndex, -1, `expected a continue statement to close the ${guardMarker} branch`);
|
|
325
|
-
return convergeSource.slice(guardIndex, continueIndex + 'continue'.length);
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
test('the all-lenses-dead retry invalidates the threaded head so the next round re-fetches origin/main before spawning lenses', () => {
|
|
329
|
-
const branch = convergeRetryBranch('if (roundOutcome.allLensesDead) {');
|
|
330
|
-
const headNullIndex = branch.indexOf('head = null');
|
|
331
|
-
assert.notEqual(
|
|
332
|
-
headNullIndex,
|
|
333
|
-
-1,
|
|
334
|
-
'expected the all-lenses-dead retry to null head so the next round re-enters preflight-git — otherwise the lenses are told origin/main was fetched this round when no fetch ran',
|
|
335
|
-
);
|
|
336
|
-
assert.ok(
|
|
337
|
-
headNullIndex < branch.indexOf('continue'),
|
|
338
|
-
'expected head to be invalidated before the retry continues to the next round',
|
|
339
|
-
);
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
test('the not-clean-no-findings retry invalidates the threaded head so the next round re-fetches origin/main before spawning lenses', () => {
|
|
343
|
-
const branch = convergeRetryBranch('if (!roundOutcome.roundClean) {');
|
|
344
|
-
const headNullIndex = branch.indexOf('head = null');
|
|
345
|
-
assert.notEqual(
|
|
346
|
-
headNullIndex,
|
|
347
|
-
-1,
|
|
348
|
-
'expected the not-clean-no-findings retry to null head so the next round re-enters preflight-git — otherwise the lenses are told origin/main was fetched this round when no fetch ran',
|
|
349
|
-
);
|
|
350
|
-
assert.ok(
|
|
351
|
-
headNullIndex < branch.indexOf('continue'),
|
|
352
|
-
'expected head to be invalidated before the retry continues to the next round',
|
|
353
|
-
);
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
test('the preflight consumes the head returned by resolveMergeConflicts without an immediate re-resolve', () => {
|
|
357
|
-
const consumedHeadIndex = convergeSource.indexOf('head = await resolveMergeConflicts(');
|
|
358
|
-
assert.notEqual(consumedHeadIndex, -1, 'expected the post-rebase head from resolveMergeConflicts to be captured');
|
|
359
|
-
const whileLoopIndex = convergeSource.indexOf('while (iterations < CONFIG.maxIterations)');
|
|
360
|
-
const betweenPreflightAndLoop = convergeSource.slice(consumedHeadIndex, whileLoopIndex);
|
|
361
|
-
assert.doesNotMatch(
|
|
362
|
-
betweenPreflightAndLoop,
|
|
363
|
-
/runGitTask\(/,
|
|
364
|
-
'expected the reuse pass to reuse the threaded head with no git-utility agent spawn',
|
|
365
|
-
);
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
test('fix edit prompt resolves threads by PRRT thread node id looked up from the comment databaseId', () => {
|
|
369
|
-
const editPrompt = functionSource('runCodeEditorTask');
|
|
370
|
-
assert.match(editPrompt, /PRRT/, 'expected the thread node id form (PRRT_...) to be named');
|
|
371
|
-
assert.match(
|
|
372
|
-
editPrompt,
|
|
373
|
-
/databaseId/,
|
|
374
|
-
'expected the GraphQL lookup matching comment databaseId to be named',
|
|
375
|
-
);
|
|
376
|
-
assert.match(
|
|
377
|
-
editPrompt,
|
|
378
|
-
/not the numeric comment id/,
|
|
379
|
-
'expected an explicit guard against passing the numeric comment id to resolve_thread',
|
|
380
|
-
);
|
|
381
|
-
});
|
|
382
|
-
|
|
383
|
-
test('fix edit prompt does not pass the numeric comment id straight to resolve_thread', () => {
|
|
384
|
-
assert.doesNotMatch(
|
|
385
|
-
functionSource('runCodeEditorTask'),
|
|
386
|
-
/then resolve that thread \(use the github MCP pull_request_review_write/,
|
|
387
|
-
'resolve_thread and resolveReviewThread require a PRRT_... thread node id, not the comment id',
|
|
388
|
-
);
|
|
389
|
-
});
|
|
390
|
-
|
|
391
|
-
test('the fix flow runs the edit task then fixerWithRecovery after the edit step', () => {
|
|
392
|
-
const applyFixesBody = lensPromptBody('applyFixes');
|
|
393
|
-
assert.match(applyFixesBody, /runCodeEditorTask\('fix-edit'/, "expected applyFixes to call runCodeEditorTask('fix-edit')");
|
|
394
|
-
assert.match(applyFixesBody, /fixerWithRecovery\(/, 'expected applyFixes to call fixerWithRecovery');
|
|
395
|
-
});
|
|
396
|
-
|
|
397
|
-
test('the shared verdict-fence builder names the binding-hash command and the verdict fence', () => {
|
|
398
|
-
const fenceBuilder = lensPromptBody('buildVerdictFenceSteps');
|
|
399
|
-
assert.match(
|
|
400
|
-
fenceBuilder,
|
|
401
|
-
/--manifest-hash-for-branch/,
|
|
402
|
-
'expected the binding-hash command to use --manifest-hash-for-branch (cwd-immune)',
|
|
403
|
-
);
|
|
404
|
-
assert.doesNotMatch(
|
|
405
|
-
fenceBuilder,
|
|
406
|
-
/--manifest-hash(?!-for-branch)/,
|
|
407
|
-
'expected the old --manifest-hash <REPO> form to be removed in favour of --manifest-hash-for-branch',
|
|
408
|
-
);
|
|
409
|
-
assert.match(
|
|
410
|
-
fenceBuilder,
|
|
411
|
-
/verification_verdict_store\.py/,
|
|
412
|
-
'expected the verdict-store script that computes the binding hash to be named',
|
|
413
|
-
);
|
|
414
|
-
assert.match(fenceBuilder, /buildVerdictFenceTail\(/, 'expected the fence builder to close with the shared fence tail');
|
|
415
|
-
const renderedFenceTail = loadVerdictFenceTailBuilder()('');
|
|
416
|
-
assert.match(renderedFenceTail, /```verdict/, 'expected the verdict fence to be specified');
|
|
417
|
-
assert.ok(
|
|
418
|
-
renderedFenceTail.includes(VERDICT_FENCE_JSON_LINE),
|
|
419
|
-
'expected the verdict fence to carry manifest_sha256',
|
|
420
|
-
);
|
|
421
|
-
assert.match(
|
|
422
|
-
fenceBuilder,
|
|
423
|
-
/gh pr view/,
|
|
424
|
-
'expected buildVerdictFenceSteps to resolve the head branch via gh pr view (cwd-immune)',
|
|
425
|
-
);
|
|
426
|
-
assert.match(
|
|
427
|
-
fenceBuilder,
|
|
428
|
-
/headRefName/,
|
|
429
|
-
'expected buildVerdictFenceSteps to extract the headRefName from gh pr view output',
|
|
430
|
-
);
|
|
431
|
-
});
|
|
432
|
-
|
|
433
|
-
test('the incomplete-verdict contract sentence is written once and used by both verify paths', () => {
|
|
434
|
-
const sentenceOccurrences = convergeSource.split(
|
|
435
|
-
'name that check in prose directly above the fence rather than in findings',
|
|
436
|
-
).length - 1;
|
|
437
|
-
assert.equal(
|
|
438
|
-
sentenceOccurrences,
|
|
439
|
-
1,
|
|
440
|
-
'expected the incomplete-verdict contract sentence to be written exactly once so the two verify paths cannot drift',
|
|
441
|
-
);
|
|
442
|
-
assert.match(
|
|
443
|
-
lensPromptBody('buildVerdictFenceTail'),
|
|
444
|
-
/VERDICT_FENCE_CONTRACT_SENTENCE/,
|
|
445
|
-
'expected the shared fence tail to use the one contract sentence',
|
|
446
|
-
);
|
|
447
|
-
assert.match(
|
|
448
|
-
lensPromptBody('buildVerdictFenceSteps'),
|
|
449
|
-
/buildVerdictFenceTail\(/,
|
|
450
|
-
'expected the gh-lookup fence builder to reach the contract sentence through the shared fence tail',
|
|
451
|
-
);
|
|
452
|
-
assert.match(
|
|
453
|
-
lensPromptBody('runVerifierTask'),
|
|
454
|
-
/buildVerdictFenceTail\(/,
|
|
455
|
-
'expected the hardening-verify prompt to reach the contract sentence through the shared fence tail',
|
|
456
|
-
);
|
|
457
|
-
});
|
|
458
|
-
|
|
459
|
-
const VERDICT_FENCE_JSON_LINE = '{"all_pass": true, "findings": [], "manifest_sha256": "<that hash>"}';
|
|
460
|
-
const VERDICT_FENCE_LEAD_IN = 'END your message with a fenced verdict block exactly in this shape';
|
|
461
|
-
|
|
462
|
-
function countOccurrences(haystack, needle) {
|
|
463
|
-
return haystack.split(needle).length - 1;
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
function loadVerdictFenceTailBuilder() {
|
|
467
|
-
const sentenceMatch = /const VERDICT_FENCE_CONTRACT_SENTENCE =\s*\n?\s*'[^']*'/.exec(convergeSource);
|
|
468
|
-
assert.notEqual(sentenceMatch, null, 'expected VERDICT_FENCE_CONTRACT_SENTENCE to be declared as one string literal');
|
|
469
|
-
const builderSource = lensPromptBody('buildVerdictFenceTail');
|
|
470
|
-
return new Function(
|
|
471
|
-
`${sentenceMatch[0]}\n${builderSource}\nreturn buildVerdictFenceTail;`,
|
|
472
|
-
)();
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
test('the verdict fence recipe is written once — no verify path re-inlines its own copy', () => {
|
|
476
|
-
assert.equal(
|
|
477
|
-
countOccurrences(convergeSource, VERDICT_FENCE_JSON_LINE),
|
|
478
|
-
1,
|
|
479
|
-
'expected the verdict fence JSON skeleton to be written exactly once so no verify path can drift from the shared recipe',
|
|
480
|
-
);
|
|
481
|
-
assert.equal(
|
|
482
|
-
countOccurrences(convergeSource, VERDICT_FENCE_LEAD_IN),
|
|
483
|
-
1,
|
|
484
|
-
'expected the fence lead-in sentence to be written exactly once so no verify path can drift from the shared recipe',
|
|
485
|
-
);
|
|
486
|
-
assert.match(
|
|
487
|
-
lensPromptBody('buildVerdictFenceSteps'),
|
|
488
|
-
/buildVerdictFenceTail\(/,
|
|
489
|
-
'expected the gh-lookup binding path to render its fence through the shared tail builder',
|
|
490
|
-
);
|
|
491
|
-
assert.match(
|
|
492
|
-
lensPromptBody('runVerifierTask'),
|
|
493
|
-
/buildVerdictFenceTail\(/,
|
|
494
|
-
'expected the hardening-verify path to render its fence through the shared tail builder',
|
|
495
|
-
);
|
|
496
|
-
});
|
|
497
|
-
|
|
498
|
-
test('the shared fence tail renders the same fence JSON and contract sentence at every indent', () => {
|
|
499
|
-
const buildVerdictFenceTail = loadVerdictFenceTailBuilder();
|
|
500
|
-
const topLevelTail = buildVerdictFenceTail('');
|
|
501
|
-
const nestedTail = buildVerdictFenceTail(' ');
|
|
502
|
-
for (const [siteName, renderedTail] of [['top-level', topLevelTail], ['nested', nestedTail]]) {
|
|
503
|
-
assert.ok(
|
|
504
|
-
renderedTail.includes(VERDICT_FENCE_JSON_LINE),
|
|
505
|
-
`expected the ${siteName} fence tail to carry the verdict JSON skeleton`,
|
|
506
|
-
);
|
|
507
|
-
assert.ok(
|
|
508
|
-
renderedTail.includes(VERDICT_FENCE_LEAD_IN),
|
|
509
|
-
`expected the ${siteName} fence tail to carry the fence lead-in sentence`,
|
|
510
|
-
);
|
|
511
|
-
assert.ok(
|
|
512
|
-
renderedTail.includes('```verdict'),
|
|
513
|
-
`expected the ${siteName} fence tail to open a verdict fence`,
|
|
514
|
-
);
|
|
515
|
-
assert.ok(
|
|
516
|
-
renderedTail.includes('name that check in prose directly above the fence rather than in findings'),
|
|
517
|
-
`expected the ${siteName} fence tail to carry the one incomplete-verdict contract sentence`,
|
|
518
|
-
);
|
|
519
|
-
}
|
|
520
|
-
assert.equal(
|
|
521
|
-
nestedTail,
|
|
522
|
-
topLevelTail.split('\n').map((eachLine) => ` ${eachLine}`).join('\n'),
|
|
523
|
-
'expected indent to be the only difference between the two rendered fence tails',
|
|
524
|
-
);
|
|
525
|
-
});
|
|
526
|
-
|
|
527
|
-
test('the verdict-fence binding does not self-resolve a cwd via git rev-parse for the manifest hash', () => {
|
|
528
|
-
const fenceBuilder = lensPromptBody('buildVerdictFenceSteps');
|
|
529
|
-
assert.doesNotMatch(
|
|
530
|
-
fenceBuilder,
|
|
531
|
-
/git rev-parse --show-toplevel/,
|
|
532
|
-
'expected the binding hash to be cwd-immune (no git rev-parse in the binding step)',
|
|
533
|
-
);
|
|
534
|
-
});
|
|
535
|
-
|
|
536
|
-
test('every verify step calls buildVerdictFenceSteps, uses code-verifier, and forbids editing the tree under verification', () => {
|
|
537
|
-
for (const verifyFunctionName of ['runVerifierTask']) {
|
|
538
|
-
const verifyBody = lensPromptBody(verifyFunctionName);
|
|
539
|
-
assert.match(
|
|
540
|
-
verifyBody,
|
|
541
|
-
/buildVerdictFenceSteps\(/,
|
|
542
|
-
`expected ${verifyFunctionName} to call buildVerdictFenceSteps (cwd-immune branch binding)`,
|
|
543
|
-
);
|
|
544
|
-
assert.match(
|
|
545
|
-
verifyBody,
|
|
546
|
-
/agentType:\s*'code-verifier'/,
|
|
547
|
-
`expected ${verifyFunctionName} to spawn the code-verifier agent type`,
|
|
548
|
-
);
|
|
549
|
-
assert.doesNotMatch(
|
|
550
|
-
verifyBody,
|
|
551
|
-
/schema:/,
|
|
552
|
-
`expected ${verifyFunctionName} to pass no schema so its verdict fence stays as assistant text`,
|
|
553
|
-
);
|
|
554
|
-
assert.match(
|
|
555
|
-
verifyBody,
|
|
556
|
-
/(?:no|not|never)[^.\n]{0,30}edit[^.\n]{0,40}tree[^.\n]{0,30}(?:under|being)\s+verif/i,
|
|
557
|
-
`expected ${verifyFunctionName} to forbid editing the tree under verification (a deliberate break off that tree stays allowed)`,
|
|
558
|
-
);
|
|
559
|
-
}
|
|
560
|
-
});
|
|
561
|
-
|
|
562
|
-
test('runFixerTask never verifies — verification belongs to the separate verifier', () => {
|
|
563
|
-
const fixerBody = lensPromptBody('runFixerTask');
|
|
564
|
-
assert.doesNotMatch(fixerBody, /buildVerdictFenceSteps\(/, 'expected the fixer to not emit a verdict fence — the separate verifier does');
|
|
565
|
-
assert.doesNotMatch(fixerBody, /agentType:\s*'code-verifier'/, 'expected the fixer to be clean-coder only');
|
|
566
|
-
assert.match(fixerBody, /agentType:\s*'clean-coder'/, 'expected the fixer to use clean-coder for its commit and recovery edits');
|
|
567
|
-
});
|
|
568
|
-
|
|
569
|
-
test('runVerifierTask uses --manifest-hash-for-branch with the hardening branch and forbids editing the tree under verification', () => {
|
|
570
|
-
const verifyBody = lensPromptBody('runVerifierTask');
|
|
571
|
-
assert.match(
|
|
572
|
-
verifyBody,
|
|
573
|
-
/--manifest-hash-for-branch/,
|
|
574
|
-
'expected the verifier to bind by hardening branch (cwd-immune)',
|
|
575
|
-
);
|
|
576
|
-
assert.match(
|
|
577
|
-
verifyBody,
|
|
578
|
-
/(?:no|not|never)[^.\n]{0,30}edit[^.\n]{0,40}tree[^.\n]{0,30}(?:under|being)\s+verif/i,
|
|
579
|
-
'expected the verifier to forbid editing the tree under verification (a deliberate break off that tree stays allowed)',
|
|
580
|
-
);
|
|
581
|
-
});
|
|
582
|
-
|
|
583
|
-
test('runVerifierTask passes PR coordinates to buildVerdictFenceSteps for the fix-verify and repair-verify tasks', () => {
|
|
584
|
-
const verifyBody = lensPromptBody('runVerifierTask');
|
|
585
|
-
assert.match(verifyBody, /task === 'fix-verify'/, 'expected runVerifierTask to carry the fix-path verify task');
|
|
586
|
-
assert.match(
|
|
587
|
-
verifyBody,
|
|
588
|
-
/buildVerdictFenceSteps\(input\.owner, input\.repo, input\.prNumber\)/,
|
|
589
|
-
'expected runVerifierTask to pass PR coordinates to buildVerdictFenceSteps',
|
|
590
|
-
);
|
|
591
|
-
});
|
|
592
|
-
|
|
593
|
-
test('the commit path in runFixerTask forbids further edits and uses clean-coder', () => {
|
|
594
|
-
const fixerBody = lensPromptBody('runFixerTask');
|
|
595
|
-
assert.match(
|
|
596
|
-
fixerBody,
|
|
597
|
-
/no (?:further |additional )?(?:file )?edits|do not edit|make no edits/i,
|
|
598
|
-
'expected the commit path to forbid further edits',
|
|
599
|
-
);
|
|
600
|
-
assert.match(
|
|
601
|
-
fixerBody,
|
|
602
|
-
/agentType:\s*'clean-coder'/,
|
|
603
|
-
'expected the commit path to use clean-coder',
|
|
604
|
-
);
|
|
605
|
-
});
|
|
606
|
-
|
|
607
|
-
test('the repair flow uses the direct-spawn task helpers for edit, verify, and commit', () => {
|
|
608
|
-
const repairBody = lensPromptBody('repairConvergence');
|
|
609
|
-
assert.match(repairBody, /runCodeEditorTask\(/, 'expected repairConvergence to call runCodeEditorTask');
|
|
610
|
-
assert.match(repairBody, /runVerifierTask\(/, 'expected repairConvergence to call runVerifierTask');
|
|
611
|
-
assert.match(repairBody, /verdictPassed\(/, 'expected the verify verdict to gate the repair commit step');
|
|
612
|
-
});
|
|
613
|
-
|
|
614
|
-
test('the standards-deferral flow uses the direct-spawn task helpers for edit, verify, and commit', () => {
|
|
615
|
-
const standardsBody = lensPromptBody('spawnStandardsFollowUp');
|
|
616
|
-
assert.match(standardsBody, /runCodeEditorTask\(/, 'expected spawnStandardsFollowUp to call runCodeEditorTask');
|
|
617
|
-
assert.match(standardsBody, /runVerifierTask\(/, 'expected spawnStandardsFollowUp to call runVerifierTask');
|
|
618
|
-
assert.match(standardsBody, /verdictPassed\(/, 'expected the verify verdict to gate the hardening commit step');
|
|
619
|
-
});
|
|
620
|
-
|
|
621
|
-
test('repair-commit and hardening-commit paths use clean-coder and forbid edits', () => {
|
|
622
|
-
const codeEditorBody = lensPromptBody('runCodeEditorTask');
|
|
623
|
-
assert.match(
|
|
624
|
-
codeEditorBody,
|
|
625
|
-
/no (?:further |additional )?(?:file )?edits|do not edit|make no edits/i,
|
|
626
|
-
'expected the commit paths to forbid further edits',
|
|
627
|
-
);
|
|
628
|
-
assert.match(
|
|
629
|
-
codeEditorBody,
|
|
630
|
-
/agentType:\s*'clean-coder'/,
|
|
631
|
-
'expected the commit paths to use clean-coder',
|
|
632
|
-
);
|
|
633
|
-
});
|
|
634
|
-
|
|
635
|
-
test('the code-editor standards-edit path stages hardening without committing and uses clean-coder', () => {
|
|
636
|
-
const editBody = lensPromptBody('runCodeEditorTask');
|
|
637
|
-
assert.match(
|
|
638
|
-
editBody,
|
|
639
|
-
/do not commit and do not push|NO commit and NO push|Do NOT commit/i,
|
|
640
|
-
'expected the standards edit path to leave the hardening change uncommitted',
|
|
641
|
-
);
|
|
642
|
-
assert.match(
|
|
643
|
-
editBody,
|
|
644
|
-
/agentType:\s*'clean-coder'/,
|
|
645
|
-
'expected the edit paths to use clean-coder',
|
|
646
|
-
);
|
|
647
|
-
});
|
|
648
|
-
|
|
649
|
-
test('spawnStandardsFollowUp reports whether a hardening PR opened on every path', () => {
|
|
650
|
-
const body = lensPromptBody('spawnStandardsFollowUp');
|
|
651
|
-
const falseReturns = body.match(/hardeningPrOpened:\s*false/g) || [];
|
|
652
|
-
assert.ok(
|
|
653
|
-
falseReturns.length >= 3,
|
|
654
|
-
'expected every skip path (hardening PR already opened, no hardening staged, verify failed) to return hardeningPrOpened:false',
|
|
655
|
-
);
|
|
656
|
-
assert.match(
|
|
657
|
-
body,
|
|
658
|
-
/const hardeningPrOpened =\s*typeof commitResult\?\.hardeningPrUrl === 'string' && commitResult\.hardeningPrUrl\.length > 0/,
|
|
659
|
-
'expected the commit path to derive hardeningPrOpened from a non-empty hardeningPrUrl, so a PR that opened with an unparseable URL still latches the guard while an empty URL (no PR opened) leaves it clear to retry',
|
|
660
|
-
);
|
|
661
|
-
});
|
|
662
|
-
|
|
663
|
-
test('spawnStandardsFollowUp reports the deferred PR identity on every path', () => {
|
|
664
|
-
const body = lensPromptBody('spawnStandardsFollowUp');
|
|
665
|
-
const nullDeferred = body.match(/deferredPr:\s*null/g) || [];
|
|
666
|
-
assert.ok(
|
|
667
|
-
nullDeferred.length >= 2,
|
|
668
|
-
'expected both skip paths (no hardening staged, verify failed) to return deferredPr:null',
|
|
669
|
-
);
|
|
670
|
-
assert.match(
|
|
671
|
-
body,
|
|
672
|
-
/parseDeferredPr\(commitResult\?\.hardeningPrUrl\)/,
|
|
673
|
-
'expected the commit path to parse the deferred PR identity from the commit step hardeningPrUrl',
|
|
674
|
-
);
|
|
675
|
-
assert.doesNotMatch(
|
|
676
|
-
body,
|
|
677
|
-
/hardeningPrOpened:\s*deferredPr !== null/,
|
|
678
|
-
'expected the commit path to keep hardeningPrOpened separate from the parsed deferredPr so an unparseable URL still latches the guard',
|
|
679
|
-
);
|
|
680
|
-
});
|
|
681
|
-
|
|
682
|
-
test('the workflow return objects carry the accumulated deferredPrs list', () => {
|
|
683
|
-
const converged = convergeSource.match(/deferredPrs/g) || [];
|
|
684
|
-
assert.ok(
|
|
685
|
-
converged.length >= 4,
|
|
686
|
-
'expected deferredPrs to be declared, pushed at both deferral call sites, and returned',
|
|
687
|
-
);
|
|
688
|
-
assert.match(
|
|
689
|
-
convergeSource,
|
|
690
|
-
/if \(standardsOutcome\?\.deferredPr\) deferredPrs\.push\(standardsOutcome\.deferredPr\)/,
|
|
691
|
-
'expected each deferral call site to accumulate the deferred PR into deferredPrs',
|
|
692
|
-
);
|
|
693
|
-
});
|
|
694
|
-
|
|
695
|
-
test('the standards-deferral surfaces disclose the hardening-PR state unconditionally, present or absent', () => {
|
|
696
|
-
const clauseBody = functionSource('standardsHardeningClause');
|
|
697
|
-
assert.match(
|
|
698
|
-
clauseBody,
|
|
699
|
-
/environment-hardening PR/,
|
|
700
|
-
'expected the opened-PR branch to name the hardening PR',
|
|
701
|
-
);
|
|
702
|
-
assert.match(
|
|
703
|
-
clauseBody,
|
|
704
|
-
/no environment-hardening PR/i,
|
|
705
|
-
'expected the absent-PR branch to disclose that no hardening PR was opened',
|
|
706
|
-
);
|
|
707
|
-
const coreBody = functionSource('standardsDeferralCore');
|
|
708
|
-
assert.match(
|
|
709
|
-
coreBody,
|
|
710
|
-
/remain untracked/,
|
|
711
|
-
'expected an untracked core that makes no hardening-PR claim',
|
|
712
|
-
);
|
|
713
|
-
});
|
|
714
|
-
|
|
715
|
-
test('every standards-deferral call site builds standardsNote from the spawnStandardsFollowUp outcome', () => {
|
|
716
|
-
const callSiteUses = convergeSource.match(/standardsNote = standardsDeferralNote\(/g) || [];
|
|
717
|
-
assert.equal(
|
|
718
|
-
callSiteUses.length,
|
|
719
|
-
4,
|
|
720
|
-
'expected the converge-round, terminal-Bugbot, Copilot, and Codex standards call sites to each build standardsNote via standardsDeferralNote(...)',
|
|
721
|
-
);
|
|
722
|
-
assert.doesNotMatch(
|
|
723
|
-
convergeSource,
|
|
724
|
-
/standardsNote = `\$\{[^}]+\} code-standard finding\(s\) deferred to a follow-up fix issue plus an environment-hardening PR/,
|
|
725
|
-
'expected no unconditional hardening-PR claim in standardsNote',
|
|
726
|
-
);
|
|
727
|
-
});
|
|
728
|
-
|
|
729
|
-
test('a reuse-audit lens builder exists', () => {
|
|
730
|
-
assert.match(convergeSource, /function runReuseAuditPass\(/);
|
|
731
|
-
});
|
|
732
|
-
|
|
733
|
-
test('the reuse pass runs once before the convergence loop', () => {
|
|
734
|
-
const reuseCallIndex = convergeSource.indexOf('await runReuseAuditPass(');
|
|
735
|
-
const loopIndex = convergeSource.indexOf('while (iterations < CONFIG.maxIterations)');
|
|
736
|
-
assert.notEqual(reuseCallIndex, -1, 'expected the reuse pass to be invoked');
|
|
737
|
-
assert.notEqual(loopIndex, -1, 'expected the convergence loop to exist');
|
|
738
|
-
assert.ok(
|
|
739
|
-
reuseCallIndex < loopIndex,
|
|
740
|
-
'expected the reuse pass to run before the convergence loop starts',
|
|
741
|
-
);
|
|
742
|
-
});
|
|
743
|
-
|
|
744
|
-
test('the reuse lens prompt enumerates all three qualifying criteria and an omit rule', () => {
|
|
745
|
-
const reusePrompt = lensPromptBody('runReuseAuditPass');
|
|
746
|
-
assert.match(reusePrompt, /CERTAIN/);
|
|
747
|
-
assert.match(reusePrompt, /BEHAVIORALLY IDENTICAL/);
|
|
748
|
-
assert.match(reusePrompt, /AUTONOMOUSLY IMPLEMENTABLE/);
|
|
749
|
-
assert.match(
|
|
750
|
-
reusePrompt,
|
|
751
|
-
/when any one is in doubt, omit the finding/i,
|
|
752
|
-
'expected the reuse lens to drop any finding that fails a criterion',
|
|
753
|
-
);
|
|
754
|
-
});
|
|
755
|
-
|
|
756
|
-
test('the reuse lens reviews the full diff and does not edit', () => {
|
|
757
|
-
const reusePrompt = lensPromptBody('runReuseAuditPass');
|
|
758
|
-
assert.match(reusePrompt, /origin\/main\.\.\.HEAD/);
|
|
759
|
-
assert.match(
|
|
760
|
-
reusePrompt,
|
|
761
|
-
/Do NOT edit, commit, or push/,
|
|
762
|
-
'expected the reuse lens to report findings without editing',
|
|
763
|
-
);
|
|
764
|
-
});
|
|
765
|
-
|
|
766
|
-
test('the reuse pass applies its findings through applyFixes, not the standards-deferral path', () => {
|
|
767
|
-
const reuseCallIndex = convergeSource.indexOf('await runReuseAuditPass(');
|
|
768
|
-
const loopIndex = convergeSource.indexOf('while (iterations < CONFIG.maxIterations)');
|
|
769
|
-
const reuseBlock = convergeSource.slice(reuseCallIndex, loopIndex);
|
|
770
|
-
assert.match(
|
|
771
|
-
reuseBlock,
|
|
772
|
-
/applyFixes\(head, reuseFindings, 'reuse-pass'\)/,
|
|
773
|
-
'expected the reuse pass to apply its findings via applyFixes',
|
|
774
|
-
);
|
|
775
|
-
assert.doesNotMatch(
|
|
776
|
-
reuseBlock,
|
|
777
|
-
/spawnStandardsFollowUp/,
|
|
778
|
-
'expected the reuse pass to apply improvements, not defer them',
|
|
779
|
-
);
|
|
780
|
-
});
|
|
781
|
-
|
|
782
|
-
test('the reuse lens runs under the Reuse phase', () => {
|
|
783
|
-
const reusePrompt = lensPromptBody('runReuseAuditPass');
|
|
784
|
-
assert.match(reusePrompt, /phase: 'Reuse'/);
|
|
785
|
-
});
|
|
786
|
-
|
|
787
|
-
test('the pre-commit gate step is a shared constant that dry-runs the CODE_RULES commit gate', () => {
|
|
788
|
-
assert.match(convergeSource, /const PRE_COMMIT_GATE_STEP =/);
|
|
789
|
-
const stepStart = convergeSource.indexOf('const PRE_COMMIT_GATE_STEP =');
|
|
790
|
-
const stepEnd = convergeSource.indexOf('\n\n', stepStart);
|
|
791
|
-
const stepBody = convergeSource.slice(stepStart, stepEnd);
|
|
792
|
-
assert.match(stepBody, /code_rules_gate\.py/);
|
|
793
|
-
assert.match(stepBody, /--staged/);
|
|
794
|
-
assert.match(
|
|
795
|
-
stepBody,
|
|
796
|
-
/do NOT commit/i,
|
|
797
|
-
'expected the gate step to forbid committing — it is a dry committability check',
|
|
798
|
-
);
|
|
799
|
-
});
|
|
800
|
-
|
|
801
|
-
const editStepTaskDispatchers = ['runCodeEditorTask', 'runFixerTask'];
|
|
802
|
-
|
|
803
|
-
for (const helperName of editStepTaskDispatchers) {
|
|
804
|
-
test(`${helperName} appends the pre-commit gate step to its edit prompts`, () => {
|
|
805
|
-
assert.match(
|
|
806
|
-
functionSource(helperName),
|
|
807
|
-
/\+\s*PRE_COMMIT_GATE_STEP/,
|
|
808
|
-
`expected ${helperName} to append PRE_COMMIT_GATE_STEP to its edit-task prompts`,
|
|
809
|
-
);
|
|
810
|
-
});
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
const editStepTasks = [
|
|
814
|
-
['runCodeEditorTask', 'fix-edit'],
|
|
815
|
-
['runCodeEditorTask', 'repair-edit'],
|
|
816
|
-
['runCodeEditorTask', 'standards-edit'],
|
|
817
|
-
['runCodeEditorTask', 'commit-recover'],
|
|
818
|
-
];
|
|
819
|
-
|
|
820
|
-
for (const [helperName, taskName] of editStepTasks) {
|
|
821
|
-
test(`${helperName} routes the ${taskName} task to a pre-commit-gated edit prompt`, () => {
|
|
822
|
-
assert.match(
|
|
823
|
-
functionSource(helperName),
|
|
824
|
-
new RegExp(`task === '${taskName}'`),
|
|
825
|
-
`expected ${helperName} to handle the ${taskName} task`,
|
|
826
|
-
);
|
|
827
|
-
});
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
function preambleText() {
|
|
831
|
-
const preambleStart = convergeSource.indexOf('const HEADLESS_EDIT_PREAMBLE =');
|
|
832
|
-
assert.notEqual(preambleStart, -1, 'expected HEADLESS_EDIT_PREAMBLE to exist');
|
|
833
|
-
const preambleEnd = convergeSource.indexOf('\n\nlet ', preambleStart);
|
|
834
|
-
return convergeSource.slice(preambleStart, preambleEnd === -1 ? undefined : preambleEnd);
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
test('preamble prescribes authoring a Python helper for variable-built or multi-step sandboxes', () => {
|
|
838
|
-
assert.match(
|
|
839
|
-
preambleText(),
|
|
840
|
-
/python\s+<file>\.py|python\s+<.*>\.py|author.*python.*helper|python.*helper.*sandbox|sandbox.*python.*helper/i,
|
|
841
|
-
'expected the preamble to prescribe running a Python helper file for multi-step sandbox teardown',
|
|
842
|
-
);
|
|
843
|
-
});
|
|
844
|
-
|
|
845
|
-
test('preamble does not claim the standalone or both rm auto-allow paths fail closed on any $', () => {
|
|
846
|
-
const text = preambleText().replace(/\$\(\.\.\.\)/g, 'SUBSHELL').replace(/\s+/g, ' ');
|
|
847
|
-
const overstatesStandalone =
|
|
848
|
-
/\b(?:both|standalone|neither)\b[^.]*fail closed[^.]*any \$/i.test(text) ||
|
|
849
|
-
/\b(?:both|standalone|neither)\b[^.]*any \$[^.]*fail closed/i.test(text);
|
|
850
|
-
assert.equal(
|
|
851
|
-
overstatesStandalone,
|
|
852
|
-
false,
|
|
853
|
-
'only the compound path fails closed on any $ in the target; the standalone path accepts a $-bearing target whose literal path already sits under an ephemeral root',
|
|
854
|
-
);
|
|
855
|
-
});
|
|
856
|
-
|
|
857
|
-
test('preamble does not claim $CLAUDE_JOB_DIR/tmp is blocked', () => {
|
|
858
|
-
assert.doesNotMatch(
|
|
859
|
-
preambleText(),
|
|
860
|
-
/CLAUDE_JOB_DIR\/tmp is NOT auto-allowed/i,
|
|
861
|
-
'under an ephemeral cwd the hook auto-allows rm targeting $CLAUDE_JOB_DIR/tmp',
|
|
862
|
-
);
|
|
863
|
-
});
|
|
864
|
-
|
|
865
|
-
test('preamble scopes its rm-shape claim to the narrowest auto-allow path, not the full set', () => {
|
|
866
|
-
assert.doesNotMatch(
|
|
867
|
-
preambleText(),
|
|
868
|
-
/auto-allows rm only when ALL of these hold/i,
|
|
869
|
-
'the hook has three rm auto-allow paths, so the preamble must not assert one narrow shape is the complete set',
|
|
870
|
-
);
|
|
871
|
-
});
|
|
872
|
-
|
|
873
|
-
test('SKILL.md does not claim the standalone or both rm auto-allow paths fail closed on any $', () => {
|
|
874
|
-
const text = skillSource.replace(/`/g, '').replace(/\$\(\.\.\.\)/g, 'SUBSHELL').replace(/\s+/g, ' ');
|
|
875
|
-
const overstatesStandalone =
|
|
876
|
-
/\b(?:both|standalone|neither)\b[^.]*fail closed[^.]*any \$/i.test(text) ||
|
|
877
|
-
/\b(?:both|standalone|neither)\b[^.]*any \$[^.]*fail closed/i.test(text);
|
|
878
|
-
assert.equal(
|
|
879
|
-
overstatesStandalone,
|
|
880
|
-
false,
|
|
881
|
-
'only the compound path fails closed on any $ in the target; the standalone path accepts a $-bearing target whose literal path already sits under an ephemeral root',
|
|
882
|
-
);
|
|
883
|
-
});
|
|
884
|
-
|
|
885
|
-
test('SKILL.md does not claim it enforces the exact rm shape the hook auto-allows', () => {
|
|
886
|
-
assert.doesNotMatch(
|
|
887
|
-
skillSource,
|
|
888
|
-
/exact rm shape the hook auto-allows/i,
|
|
889
|
-
'the hook has multiple rm auto-allow paths, so SKILL.md must not assert one narrow shape is the exact set',
|
|
890
|
-
);
|
|
891
|
-
});
|
|
892
|
-
|
|
893
|
-
test('preamble does not attribute the known-temp-var resolution to the standalone or compound paths', () => {
|
|
894
|
-
assert.doesNotMatch(
|
|
895
|
-
preambleText().replace(/\s+/g, ' '),
|
|
896
|
-
/Across these paths[\s\S]*?CLAUDE_JOB_DIR/i,
|
|
897
|
-
'the temp-var resolution lives only in the broad cwd-scoped path; the standalone and compound paths do not resolve known temp variables',
|
|
898
|
-
);
|
|
899
|
-
});
|
|
900
|
-
|
|
901
|
-
test('preamble attributes the known-temp-var resolution to a third cwd-scoped auto-allow path', () => {
|
|
902
|
-
const text = preambleText().replace(/\s+/g, ' ');
|
|
903
|
-
const tempVarSentenceMatch =
|
|
904
|
-
/[^.]*\bTMPDIR\b[^.]*CLAUDE_JOB_DIR[^.]*\./i.exec(text);
|
|
905
|
-
assert.notEqual(
|
|
906
|
-
tempVarSentenceMatch,
|
|
907
|
-
null,
|
|
908
|
-
'expected a sentence describing the TEMP/TMP/TMPDIR/CLAUDE_JOB_DIR resolution',
|
|
909
|
-
);
|
|
910
|
-
assert.match(
|
|
911
|
-
tempVarSentenceMatch[0],
|
|
912
|
-
/declares? an ephemeral cwd|declared ephemeral cwd|ephemeral-cwd path|third (?:auto-allow )?path|cwd-scoped path/i,
|
|
913
|
-
'expected the temp-var resolution to be tied to the cwd-scoped path that declares an ephemeral working directory, not the standalone or compound paths',
|
|
914
|
-
);
|
|
915
|
-
});
|
|
916
|
-
|
|
917
|
-
test('preamble forbids bash rm on a repo-path build cache and points cleanup at Remove-Item', () => {
|
|
918
|
-
const text = preambleText().replace(/\s+/g, ' ');
|
|
919
|
-
assert.match(
|
|
920
|
-
text,
|
|
921
|
-
/\.mypy_cache|\.pytest_cache|__pycache__/,
|
|
922
|
-
'expected the preamble to name a gitignored build cache agents must not rm',
|
|
923
|
-
);
|
|
924
|
-
assert.match(
|
|
925
|
-
text,
|
|
926
|
-
/never delete it|leave it in place/i,
|
|
927
|
-
'expected the preamble to tell agents to leave a gitignored cache rather than rm it',
|
|
928
|
-
);
|
|
929
|
-
assert.match(
|
|
930
|
-
text,
|
|
931
|
-
/Remove-Item -Recurse -Force -Confirm:\$false/,
|
|
932
|
-
'expected the preamble to point a genuine delete at the PowerShell Remove-Item form',
|
|
933
|
-
);
|
|
934
|
-
});
|
|
935
|
-
|
|
936
|
-
test('SKILL.md does not attribute the known-temp-var resolution to the standalone or compound paths', () => {
|
|
937
|
-
assert.doesNotMatch(
|
|
938
|
-
skillSource.replace(/\s+/g, ' '),
|
|
939
|
-
/Across those paths[\s\S]*?CLAUDE_JOB_DIR/i,
|
|
940
|
-
'the temp-var resolution lives only in the broad cwd-scoped path; the standalone and compound paths do not resolve known temp variables',
|
|
941
|
-
);
|
|
942
|
-
});
|
|
943
|
-
|
|
944
|
-
test('headless-safety.md attributes the known-temp-var resolution to the cwd-scoped auto-allow path', () => {
|
|
945
|
-
const standaloneSectionMatch =
|
|
946
|
-
/1\.\s*\*\*Standalone path\*\*[\s\S]*?(?=2\.\s*\*\*Compound path\*\*)/i.exec(
|
|
947
|
-
headlessSafetySource,
|
|
948
|
-
);
|
|
949
|
-
assert.notEqual(standaloneSectionMatch, null, 'expected numbered standalone path section');
|
|
950
|
-
assert.doesNotMatch(
|
|
951
|
-
standaloneSectionMatch[0],
|
|
952
|
-
/resolves the known temporary variables/i,
|
|
953
|
-
'standalone path must not claim the known-temp-var resolution',
|
|
954
|
-
);
|
|
955
|
-
|
|
956
|
-
const compoundSectionMatch =
|
|
957
|
-
/2\.\s*\*\*Compound path\*\*[\s\S]*?(?=3\.\s*\*\*Cwd-scoped path\*\*)/i.exec(
|
|
958
|
-
headlessSafetySource,
|
|
959
|
-
);
|
|
960
|
-
assert.notEqual(compoundSectionMatch, null, 'expected numbered compound path section');
|
|
961
|
-
assert.doesNotMatch(
|
|
962
|
-
compoundSectionMatch[0],
|
|
963
|
-
/resolves the known temporary variables/i,
|
|
964
|
-
'compound path must not claim the known-temp-var resolution',
|
|
965
|
-
);
|
|
966
|
-
|
|
967
|
-
const cwdScopedSectionMatch =
|
|
968
|
-
/3\.\s*\*\*Cwd-scoped path\*\*[\s\S]*?(?=\n\nEven so|\n## |$)/i.exec(
|
|
969
|
-
headlessSafetySource,
|
|
970
|
-
);
|
|
971
|
-
assert.notEqual(cwdScopedSectionMatch, null, 'expected numbered cwd-scoped path section');
|
|
972
|
-
const cwdScopedSection = cwdScopedSectionMatch[0].replace(/\s+/g, ' ');
|
|
973
|
-
assert.match(
|
|
974
|
-
cwdScopedSection,
|
|
975
|
-
/resolves the known temporary variables[\s\S]{0,80}\bTEMP\b[\s\S]{0,40}\bTMP\b[\s\S]{0,40}\bTMPDIR\b[\s\S]{0,40}\bCLAUDE_JOB_DIR\b/i,
|
|
976
|
-
'expected a sentence describing the TEMP/TMP/TMPDIR/CLAUDE_JOB_DIR resolution',
|
|
977
|
-
);
|
|
978
|
-
assert.match(
|
|
979
|
-
cwdScopedSection,
|
|
980
|
-
/declares an ephemeral working directory|declared ephemeral cwd|cwd-scoped path/i,
|
|
981
|
-
'expected the temp-var resolution to be tied to the cwd-scoped path that declares an ephemeral working directory, not the standalone or compound paths',
|
|
982
|
-
);
|
|
983
|
-
});
|
|
984
|
-
|
|
985
|
-
test('convergeAgent prepends HEADLESS_EDIT_PREAMBLE and the worktree directive to every prompt', () => {
|
|
986
|
-
const convergeAgentBody = lensPromptBody('convergeAgent');
|
|
987
|
-
assert.match(
|
|
988
|
-
convergeAgentBody,
|
|
989
|
-
/HEADLESS_EDIT_PREAMBLE.*worktreeDirective/,
|
|
990
|
-
'expected convergeAgent to prepend both preamble and worktree directive',
|
|
991
|
-
);
|
|
992
|
-
});
|
|
993
|
-
|
|
994
|
-
test('convergeReadOnlyAgent prepends HEADLESS_READONLY_PREAMBLE and the worktree directive to every read-only prompt', () => {
|
|
995
|
-
const readOnlyAgentBody = lensPromptBody('convergeReadOnlyAgent');
|
|
996
|
-
assert.match(
|
|
997
|
-
readOnlyAgentBody,
|
|
998
|
-
/HEADLESS_READONLY_PREAMBLE.*worktreeDirective/,
|
|
999
|
-
'expected convergeReadOnlyAgent to prepend the trimmed read-only preamble and the worktree directive',
|
|
1000
|
-
);
|
|
1001
|
-
});
|
|
1002
|
-
|
|
1003
|
-
test('the read-only destructive pointer scopes its no-edit clause to the tree it reads', () => {
|
|
1004
|
-
const destructivePointer = convergeSource
|
|
1005
|
-
.split('\n')
|
|
1006
|
-
.find((eachLine) => eachLine.includes('Never run a destructive command'));
|
|
1007
|
-
assert.ok(destructivePointer, 'expected the read-only destructive pointer to be declared');
|
|
1008
|
-
assert.match(
|
|
1009
|
-
destructivePointer,
|
|
1010
|
-
/Never run a destructive command/,
|
|
1011
|
-
'expected the destructive-command prohibition to stay absolute',
|
|
1012
|
-
);
|
|
1013
|
-
assert.match(
|
|
1014
|
-
destructivePointer,
|
|
1015
|
-
/no edit to the tree it reads/,
|
|
1016
|
-
'expected the no-edit clause to be scoped to the tree under verification',
|
|
1017
|
-
);
|
|
1018
|
-
assert.doesNotMatch(
|
|
1019
|
-
destructivePointer,
|
|
1020
|
-
/edits nothing/,
|
|
1021
|
-
'expected no blanket edits-nothing wording, which forbids the deliberate break off that tree',
|
|
1022
|
-
);
|
|
1023
|
-
});
|
|
1024
|
-
|
|
1025
|
-
const taskDispatchers = [
|
|
1026
|
-
{ name: 'runGitTask', isAsync: false },
|
|
1027
|
-
{ name: 'runFixerTask', isAsync: false },
|
|
1028
|
-
{ name: 'fixerWithRecovery', isAsync: true },
|
|
1029
|
-
{ name: 'runCodeEditorTask', isAsync: false },
|
|
1030
|
-
{ name: 'runVerifierTask', isAsync: false },
|
|
1031
|
-
{ name: 'runGeneralUtilityTask', isAsync: false },
|
|
1032
|
-
{ name: 'runConvergenceCheck', isAsync: false },
|
|
1033
|
-
];
|
|
1034
|
-
|
|
1035
|
-
for (const { name, isAsync } of taskDispatchers) {
|
|
1036
|
-
const prefix = isAsync ? 'async ' : '';
|
|
1037
|
-
test(`function ${prefix}${name} exists in converge.mjs`, () => {
|
|
1038
|
-
const needle = isAsync ? `async function ${name}(` : `function ${name}(`;
|
|
1039
|
-
assert.ok(convergeSource.includes(needle), `expected ${name} to exist`);
|
|
1040
|
-
});
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
test('runGeneralUtilityTask only handles the post-clean-audit task it is called with', () => {
|
|
1044
|
-
const generalBody = functionSource('runGeneralUtilityTask');
|
|
1045
|
-
assert.match(
|
|
1046
|
-
generalBody,
|
|
1047
|
-
/task === 'post-clean-audit'/,
|
|
1048
|
-
'expected runGeneralUtilityTask to handle the post-clean-audit task',
|
|
1049
|
-
);
|
|
1050
|
-
assert.doesNotMatch(
|
|
1051
|
-
generalBody,
|
|
1052
|
-
/task === 'mark-ready'/,
|
|
1053
|
-
'the mark-ready step is merged into the FINALIZE convergence check (runConvergenceCheck), so its branch must be removed here',
|
|
1054
|
-
);
|
|
1055
|
-
assert.doesNotMatch(
|
|
1056
|
-
generalBody,
|
|
1057
|
-
/task === 'bugbot-lens'/,
|
|
1058
|
-
'the live Bugbot lens is runBugbotLens; the dead bugbot-lens branch must be removed',
|
|
1059
|
-
);
|
|
1060
|
-
assert.doesNotMatch(
|
|
1061
|
-
generalBody,
|
|
1062
|
-
/Copilot can run out of usage/,
|
|
1063
|
-
'the live Copilot gate is runCopilotGate; the dead copilot-gate branch must be removed',
|
|
1064
|
-
);
|
|
1065
|
-
assert.doesNotMatch(
|
|
1066
|
-
generalBody,
|
|
1067
|
-
/convergence summary/,
|
|
1068
|
-
'the convergence-summary producer was removed; the dead branch must not return',
|
|
1069
|
-
);
|
|
1070
|
-
});
|
|
1071
|
-
|
|
1072
|
-
const orphanedHelperNames = [
|
|
1073
|
-
'applyFixesEdit',
|
|
1074
|
-
'recoverCommitBlockEdit',
|
|
1075
|
-
'recoverVerifyFailEdit',
|
|
1076
|
-
'checkConvergence',
|
|
1077
|
-
'markReady',
|
|
1078
|
-
'repairConvergenceEdit',
|
|
1079
|
-
'verifyRepairChanges',
|
|
1080
|
-
'commitRepairFixes',
|
|
1081
|
-
'resolveConflictsEdit',
|
|
1082
|
-
'standardsFollowUpEdit',
|
|
1083
|
-
'verifyHardeningChanges',
|
|
1084
|
-
'commitHardeningPr',
|
|
1085
|
-
'postCleanAudit',
|
|
1086
|
-
'spawnConvergenceSummary',
|
|
1087
|
-
];
|
|
1088
|
-
|
|
1089
|
-
for (const orphanName of orphanedHelperNames) {
|
|
1090
|
-
test(`${orphanName} is removed — its behavior lives in a direct-spawn task dispatcher`, () => {
|
|
1091
|
-
assert.ok(
|
|
1092
|
-
!convergeSource.includes(`function ${orphanName}(`),
|
|
1093
|
-
`expected the orphaned ${orphanName} definition to be deleted (CODE_RULES 9.8)`,
|
|
1094
|
-
);
|
|
1095
|
-
});
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
|
-
test('the whole priming spawn-agent family is removed — every dispatcher spawns fresh', () => {
|
|
1099
|
-
assert.doesNotMatch(
|
|
1100
|
-
convergeSource,
|
|
1101
|
-
/function\s+spawn\w+Agent\s*\(/,
|
|
1102
|
-
'expected no spawn<Role>Agent priming function to survive — each task dispatcher spawns a fresh agent',
|
|
1103
|
-
);
|
|
1104
|
-
});
|
|
1105
|
-
|
|
1106
|
-
test('parseLastVerdictFence returns non-null for a verdict fence with valid JSON', () => {
|
|
1107
|
-
const parseModule = new Function(
|
|
1108
|
-
`${functionSource('findLastVerdictFence')}\n` +
|
|
1109
|
-
`${functionSource('parseLastVerdictFence')}\n` +
|
|
1110
|
-
'return { parseLastVerdictFence };',
|
|
1111
|
-
)();
|
|
1112
|
-
const result = parseModule.parseLastVerdictFence('```verdict\n{"all_pass":true,"findings":[],"manifest_sha256":"abc"}\n```');
|
|
1113
|
-
assert.notEqual(result, null);
|
|
1114
|
-
assert.equal(result.all_pass, true);
|
|
1115
|
-
});
|
|
1116
|
-
|
|
1117
|
-
test('parseLastVerdictFence returns null for non-string input', () => {
|
|
1118
|
-
const parseModule = new Function(
|
|
1119
|
-
`${functionSource('findLastVerdictFence')}\n` +
|
|
1120
|
-
`${functionSource('parseLastVerdictFence')}\n` +
|
|
1121
|
-
'return { parseLastVerdictFence };',
|
|
1122
|
-
)();
|
|
1123
|
-
assert.equal(parseModule.parseLastVerdictFence(null), null);
|
|
1124
|
-
assert.equal(parseModule.parseLastVerdictFence(undefined), null);
|
|
1125
|
-
});
|
|
1126
|
-
|
|
1127
|
-
test('parseLastVerdictFence returns null when no verdict fence is present', () => {
|
|
1128
|
-
const parseModule = new Function(
|
|
1129
|
-
`${functionSource('findLastVerdictFence')}\n` +
|
|
1130
|
-
`${functionSource('parseLastVerdictFence')}\n` +
|
|
1131
|
-
'return { parseLastVerdictFence };',
|
|
1132
|
-
)();
|
|
1133
|
-
assert.equal(parseModule.parseLastVerdictFence('plain text with no fence'), null);
|
|
1134
|
-
});
|
|
1135
|
-
|
|
1136
|
-
test('parseLastVerdictFence returns null for malformed JSON in the fence', () => {
|
|
1137
|
-
const parseModule = new Function(
|
|
1138
|
-
`${functionSource('findLastVerdictFence')}\n` +
|
|
1139
|
-
`${functionSource('parseLastVerdictFence')}\n` +
|
|
1140
|
-
'return { parseLastVerdictFence };',
|
|
1141
|
-
)();
|
|
1142
|
-
assert.equal(parseModule.parseLastVerdictFence('```verdict\nnot json\n```'), null);
|
|
1143
|
-
});
|
|
1144
|
-
|
|
1145
|
-
test('verdictPassed calls parseLastVerdictFence', () => {
|
|
1146
|
-
const verdictBody = lensPromptBody('verdictPassed');
|
|
1147
|
-
assert.match(verdictBody, /parseLastVerdictFence\(/, 'expected verdictPassed to call the shared parser');
|
|
1148
|
-
});
|
|
1149
|
-
|
|
1150
|
-
test('extractVerifyObjection calls parseLastVerdictFence', () => {
|
|
1151
|
-
const objectionBody = lensPromptBody('extractVerifyObjection');
|
|
1152
|
-
assert.match(objectionBody, /parseLastVerdictFence\(/, 'expected extractVerifyObjection to call the shared parser');
|
|
1153
|
-
});
|
|
1154
|
-
|
|
1155
|
-
test('the headless preamble routes waits through the Monitor tool and forbids ending a turn to await work', () => {
|
|
1156
|
-
const preambleStart = convergeSource.indexOf('const HEADLESS_EDIT_PREAMBLE =');
|
|
1157
|
-
assert.notEqual(preambleStart, -1, 'expected a HEADLESS_EDIT_PREAMBLE definition');
|
|
1158
|
-
const preambleEnd = convergeSource.indexOf('\n\nlet activeRepoPath', preambleStart);
|
|
1159
|
-
assert.notEqual(preambleEnd, -1, 'expected the preamble to end before activeRepoPath');
|
|
1160
|
-
const preamble = convergeSource.slice(preambleStart, preambleEnd);
|
|
1161
|
-
assert.match(preamble, /foreground sleep is blocked/i, 'expected the preamble to state foreground sleep is blocked');
|
|
1162
|
-
assert.match(preamble, /Monitor tool/, 'expected the preamble to route waits through the Monitor tool');
|
|
1163
|
-
assert.match(preamble, /StructuredOutput/, 'expected the preamble to require a schema agent to always call StructuredOutput');
|
|
1164
|
-
assert.match(preamble, /never end your turn to wait/i, 'expected the preamble to forbid ending a turn to await background work');
|
|
1165
|
-
});
|
|
1166
|
-
|
|
1167
|
-
test('the preamble describes the Monitor wait by its real contract: a bounded until-loop consuming notifications as they arrive, rather than an in-turn synchronous return', () => {
|
|
1168
|
-
const blockStart = convergeSource.indexOf('const HEADLESS_EDIT_PREAMBLE =');
|
|
1169
|
-
assert.ok(blockStart !== -1, 'expected a wait-safety preamble to exist');
|
|
1170
|
-
const blockEnd = convergeSource.indexOf('\n\nlet activeRepoPath', blockStart);
|
|
1171
|
-
assert.ok(blockEnd !== -1, 'expected the preamble to end before activeRepoPath');
|
|
1172
|
-
const preamble = convergeSource.slice(blockStart, blockEnd);
|
|
1173
|
-
assert.doesNotMatch(
|
|
1174
|
-
preamble,
|
|
1175
|
-
/never move a wait to a background process/i,
|
|
1176
|
-
'the Monitor tool is itself a background monitor, so the preamble must avoid forbidding a wait that moves to a background process',
|
|
1177
|
-
);
|
|
1178
|
-
assert.doesNotMatch(
|
|
1179
|
-
preamble,
|
|
1180
|
-
/return to you when the condition holds/i,
|
|
1181
|
-
'the Monitor tool streams notifications across turns rather than returning in-turn, so the preamble must avoid describing an in-turn synchronous return',
|
|
1182
|
-
);
|
|
1183
|
-
assert.match(
|
|
1184
|
-
preamble,
|
|
1185
|
-
/until-loop/i,
|
|
1186
|
-
'expected the preamble to describe the wait as a bounded until-loop paired with the Monitor tool',
|
|
1187
|
-
);
|
|
1188
|
-
assert.match(
|
|
1189
|
-
preamble,
|
|
1190
|
-
/notifications? as they arrive/i,
|
|
1191
|
-
'expected the preamble to say Monitor notifications are consumed as they arrive',
|
|
1192
|
-
);
|
|
1193
|
-
});
|
|
1194
|
-
|
|
1195
|
-
test('the background-wait clause gives "await" an explicit object, not a dangling "await never resumes"', () => {
|
|
1196
|
-
const text = preambleText();
|
|
1197
|
-
assert.doesNotMatch(
|
|
1198
|
-
text,
|
|
1199
|
-
/end your turn to await never resumes/i,
|
|
1200
|
-
'expected the background-wait clause to name what is awaited, not read as "await never resumes" with no object',
|
|
1201
|
-
);
|
|
1202
|
-
assert.match(
|
|
1203
|
-
text,
|
|
1204
|
-
/end your turn to await it/i,
|
|
1205
|
-
'expected the background-wait clause to name the wait as the object of "await"',
|
|
1206
|
-
);
|
|
1207
|
-
});
|
|
1208
|
-
|
|
1209
|
-
test('the wait guidance names the full down-result schema, not a bare down flag', () => {
|
|
1210
|
-
const text = preambleText();
|
|
1211
|
-
assert.doesNotMatch(
|
|
1212
|
-
text,
|
|
1213
|
-
/Copilot gate, down: true\)/,
|
|
1214
|
-
'expected the down signal to name the full schema, not a bare down flag that reads as a partial object',
|
|
1215
|
-
);
|
|
1216
|
-
assert.match(
|
|
1217
|
-
text,
|
|
1218
|
-
/down:true, findings:\[\]/,
|
|
1219
|
-
'expected the wait guidance to name the full down-result fields',
|
|
1220
|
-
);
|
|
1221
|
-
});
|
|
1222
|
-
|
|
1223
|
-
test('no agent prompt instructs a foreground sleep as the poll delay', () => {
|
|
1224
|
-
assert.doesNotMatch(
|
|
1225
|
-
convergeSource,
|
|
1226
|
-
/delay each (?:attempt|iteration|retry) with "sleep/,
|
|
1227
|
-
'expected no poll directive to instruct a foreground sleep as the between-attempt delay',
|
|
1228
|
-
);
|
|
1229
|
-
assert.doesNotMatch(
|
|
1230
|
-
convergeSource,
|
|
1231
|
-
/Start-Sleep -Seconds/,
|
|
1232
|
-
'expected no agent prompt to instruct a foreground PowerShell Start-Sleep',
|
|
1233
|
-
);
|
|
1234
|
-
});
|
|
1235
|
-
|
|
1236
|
-
test('the wait guidance sizes the Monitor timeout_ms above the 300s default', () => {
|
|
1237
|
-
const text = preambleText();
|
|
1238
|
-
assert.match(text, /timeout_ms/, 'names the timeout_ms parameter the wait rule tells the agent to set');
|
|
1239
|
-
assert.match(text, /persistent/, 'offers persistent as an alternative to a sized timeout_ms');
|
|
1240
|
-
assert.match(text, /300000|300 second/, 'names the 300000 default that truncates a long wait');
|
|
1241
|
-
assert.match(
|
|
1242
|
-
text,
|
|
1243
|
-
/interval[\s\S]{0,60}attempt|interval-times-attempts/i,
|
|
1244
|
-
'sizes timeout_ms to the interval-times-attempts span the step names',
|
|
1245
|
-
);
|
|
1246
|
-
});
|
|
1247
|
-
|
|
1248
|
-
test('the Monitor ceiling the guidance names covers the longest interval-times-attempts wait the steps prescribe', () => {
|
|
1249
|
-
const cap = /(\d{7})\s*(?:ms|millisecond)/i.exec(preambleText());
|
|
1250
|
-
assert.ok(cap !== null, 'the guidance names a seven-digit Monitor ceiling in milliseconds');
|
|
1251
|
-
const ceiling = Number(cap[1]);
|
|
1252
|
-
|
|
1253
|
-
const copilot = lensPromptBody('runCopilotGate');
|
|
1254
|
-
const bugbot = lensPromptBody('runBugbotLens');
|
|
1255
|
-
const apart = Number(/(\d+)\s*seconds apart/.exec(copilot)[1]);
|
|
1256
|
-
const polls = Number(/copilotMaxPolls:\s*(\d+)/.exec(convergeSource)[1]);
|
|
1257
|
-
const seconds = Number(/every\s*(\d+)\s*seconds/.exec(bugbot)[1]);
|
|
1258
|
-
const iterations = Number(/up to\s*(\d+)\s*iterations/.exec(bugbot)[1]);
|
|
1259
|
-
const spans = [apart * polls * 1000, seconds * iterations * 1000];
|
|
1260
|
-
|
|
1261
|
-
for (const span of spans) {
|
|
1262
|
-
assert.ok(
|
|
1263
|
-
span <= ceiling,
|
|
1264
|
-
`a ${span}ms wait span exceeds the ${ceiling}ms Monitor ceiling the guidance names`,
|
|
1265
|
-
);
|
|
1266
|
-
}
|
|
1267
|
-
});
|
|
1268
|
-
|
|
1269
|
-
function optionsLineForLabel(label) {
|
|
1270
|
-
const labelIndex = convergeSource.indexOf(`label: '${label}'`);
|
|
1271
|
-
assert.notEqual(labelIndex, -1, `expected an agent options object labeled ${label}`);
|
|
1272
|
-
const lineStart = convergeSource.lastIndexOf('\n', labelIndex);
|
|
1273
|
-
const lineEnd = convergeSource.indexOf('\n', labelIndex);
|
|
1274
|
-
return convergeSource.slice(lineStart, lineEnd);
|
|
1275
|
-
}
|
|
1276
|
-
|
|
1277
|
-
test('the TIERS map defines the opus/sonnet/haiku model-effort tiers', () => {
|
|
1278
|
-
assert.match(convergeSource, /opusMedium:\s*\{ model: 'opus', effort: 'medium' \}/);
|
|
1279
|
-
assert.match(convergeSource, /sonnetMedium:\s*\{ model: 'sonnet', effort: 'medium' \}/);
|
|
1280
|
-
assert.match(convergeSource, /haikuLow:\s*\{ model: 'haiku', effort: 'low' \}/);
|
|
1281
|
-
});
|
|
1282
|
-
|
|
1283
|
-
test('every convergeAgent spawn options object carries a model and effort tier', () => {
|
|
1284
|
-
const optionObjects = convergeSource.match(/\{ label[^\n]*\}/g) || [];
|
|
1285
|
-
assert.ok(
|
|
1286
|
-
optionObjects.length >= 20,
|
|
1287
|
-
`expected to find the per-spawn options objects, found ${optionObjects.length}`,
|
|
1288
|
-
);
|
|
1289
|
-
for (const optionObject of optionObjects) {
|
|
1290
|
-
const hasTier = optionObject.includes('...TIERS.');
|
|
1291
|
-
const hasInlineModelEffort = /model:\s*'/.test(optionObject) && /effort:\s*'/.test(optionObject);
|
|
1292
|
-
assert.ok(
|
|
1293
|
-
hasTier || hasInlineModelEffort,
|
|
1294
|
-
`expected the spawn options to carry a model and effort tier: ${optionObject}`,
|
|
1295
|
-
);
|
|
1296
|
-
}
|
|
1297
|
-
});
|
|
1298
|
-
|
|
1299
|
-
test('the semantic review lenses and the terminal Bugbot gate run on the opusMedium tier', () => {
|
|
1300
|
-
for (const label of ['lens:bugbot', 'lens:code-review', 'lens:bug-audit', 'lens:self-review', 'lens:reuse']) {
|
|
1301
|
-
assert.match(
|
|
1302
|
-
optionsLineForLabel(label),
|
|
1303
|
-
/\.\.\.TIERS\.opusMedium/,
|
|
1304
|
-
`expected ${label} to run on the opusMedium tier`,
|
|
1305
|
-
);
|
|
1306
|
-
}
|
|
1307
|
-
});
|
|
1308
|
-
|
|
1309
|
-
test('the deterministic static sweep runs on the cheaper sonnetMedium tier', () => {
|
|
1310
|
-
assert.match(
|
|
1311
|
-
optionsLineForLabel('lens:static-sweep'),
|
|
1312
|
-
/\.\.\.TIERS\.sonnetMedium/,
|
|
1313
|
-
'expected the deterministic static sweep to run on the sonnet tier, not opus',
|
|
1314
|
-
);
|
|
1315
|
-
});
|
|
1316
|
-
|
|
1317
|
-
test('the copilot gate runs on the haikuLow tier', () => {
|
|
1318
|
-
assert.match(optionsLineForLabel('copilot-gate'), /\.\.\.TIERS\.haikuLow/);
|
|
1319
|
-
});
|
|
1320
|
-
|
|
1321
|
-
test('the merged finalize convergence check runs on the haikuLow tier', () => {
|
|
1322
|
-
assert.match(functionSource('runConvergenceCheck'), /\.\.\.TIERS\.haikuLow/);
|
|
1323
|
-
});
|
|
1324
|
-
|
|
1325
|
-
test('the preflight-git task returns the changed-file list and diffstat for the lenses', () => {
|
|
1326
|
-
const body = functionSource('runGitTask');
|
|
1327
|
-
assert.match(body, /git diff --name-status origin\/main\.\.\.HEAD/);
|
|
1328
|
-
assert.match(body, /git diff --stat origin\/main\.\.\.HEAD/);
|
|
1329
|
-
assert.match(body, /changedFiles/);
|
|
1330
|
-
assert.match(body, /diffstat/);
|
|
1331
|
-
const schemaStart = convergeSource.indexOf('const PREFLIGHT_GIT_SCHEMA =');
|
|
1332
|
-
const schemaEnd = convergeSource.indexOf('\n}', schemaStart);
|
|
1333
|
-
const schema = convergeSource.slice(schemaStart, schemaEnd);
|
|
1334
|
-
assert.match(schema, /changedFiles:/);
|
|
1335
|
-
assert.match(schema, /diffstat:/);
|
|
1336
|
-
assert.match(schema, /required:[^\n]*'changedFiles'[^\n]*'diffstat'/);
|
|
1337
|
-
});
|
|
1338
|
-
|
|
1339
|
-
test('each per-round lens, the static sweep, the reuse lens, and the terminal Bugbot gate inject the preflight changed-file context', () => {
|
|
1340
|
-
for (const builder of ['runStaticSweep', 'runCodeReviewLens', 'runAuditLens', 'runSelfReviewLens', 'runReuseAuditPass', 'runBugbotLens']) {
|
|
1341
|
-
assert.match(
|
|
1342
|
-
lensPromptBody(builder),
|
|
1343
|
-
/renderLensDiffContext\(preflightResult\)/,
|
|
1344
|
-
`expected ${builder} to inject the changed-file context from the preflight result`,
|
|
1345
|
-
);
|
|
1346
|
-
}
|
|
1347
|
-
});
|
|
1348
|
-
|
|
1349
|
-
test('renderLensDiffContext reuses the changed-file list when present and falls back to self-enumeration otherwise', () => {
|
|
1350
|
-
const renderLensDiffContext = new Function(
|
|
1351
|
-
`${functionSource('renderLensDiffContext')}\nreturn renderLensDiffContext;`,
|
|
1352
|
-
)();
|
|
1353
|
-
const withList = renderLensDiffContext({ changedFiles: 'M\ta.py', diffstat: ' a.py | 2 +-' });
|
|
1354
|
-
assert.match(withList, /Changed files/);
|
|
1355
|
-
assert.match(withList, /a\.py/);
|
|
1356
|
-
const fallback = renderLensDiffContext(null);
|
|
1357
|
-
assert.match(fallback, /git diff --name-only/, 'expected a missing file list to fall back to self-enumeration');
|
|
1358
|
-
});
|
|
1359
|
-
|
|
1360
|
-
test('the merged FINALIZE check runs check_convergence then marks the PR ready on pass in one agent', () => {
|
|
1361
|
-
const body = functionSource('runConvergenceCheck');
|
|
1362
|
-
assert.match(body, /check_convergence\.py/, 'expected the merged check to run check_convergence.py');
|
|
1363
|
-
assert.match(body, /gh pr ready/, 'expected the merged check to mark the PR ready on the passing path');
|
|
1364
|
-
assert.match(body, /schema: FINALIZE_SCHEMA/, 'expected the merged check to return the {pass, failures, ready} schema');
|
|
1365
|
-
assert.match(body, /context\.copilotDown/, 'expected the merged check to carry the copilotDown opt-out context');
|
|
1366
|
-
});
|
|
1367
|
-
|
|
1368
|
-
test('FINALIZE_SCHEMA carries pass, failures, and ready', () => {
|
|
1369
|
-
const schemaStart = convergeSource.indexOf('const FINALIZE_SCHEMA =');
|
|
1370
|
-
assert.notEqual(schemaStart, -1, 'expected FINALIZE_SCHEMA to exist');
|
|
1371
|
-
const schema = convergeSource.slice(schemaStart, convergeSource.indexOf('\n}', schemaStart));
|
|
1372
|
-
for (const field of ['pass', 'failures', 'ready']) {
|
|
1373
|
-
assert.match(schema, new RegExp(`${field}:`), `expected FINALIZE_SCHEMA to carry ${field}`);
|
|
1374
|
-
}
|
|
1375
|
-
});
|
|
1376
|
-
|
|
1377
|
-
test('the FINALIZE phase drives the merged check and reads ready from its result without a separate mark-ready spawn', () => {
|
|
1378
|
-
const finalizeStart = convergeSource.indexOf("if (phase === 'FINALIZE') {");
|
|
1379
|
-
assert.notEqual(finalizeStart, -1, 'expected a FINALIZE phase block');
|
|
1380
|
-
const finalizeBody = convergeSource.slice(finalizeStart, finalizeStart + 1000);
|
|
1381
|
-
assert.match(finalizeBody, /runConvergenceCheck\(\{ head, bugbotDown, copilotDown, codexDown, codexCleanAt, bugteamPostBlocked: cleanAuditNote !== null \}\)/);
|
|
1382
|
-
assert.match(finalizeBody, /classifyReadyOutcome\(finalizeResult\)/);
|
|
1383
|
-
assert.doesNotMatch(
|
|
1384
|
-
finalizeBody,
|
|
1385
|
-
/runGeneralUtilityTask\('mark-ready'/,
|
|
1386
|
-
'the separate mark-ready spawn is merged into the FINALIZE convergence check',
|
|
1387
|
-
);
|
|
1388
|
-
});
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { readFileSync } from 'node:fs'
|
|
3
|
+
import test from 'node:test'
|
|
4
|
+
import vm from 'node:vm'
|
|
5
|
+
|
|
6
|
+
const sourcePath = new URL('./converge.mjs', import.meta.url)
|
|
7
|
+
const workflowSource = readFileSync(sourcePath, 'utf8')
|
|
8
|
+
|
|
9
|
+
function loadReviewResultParser() {
|
|
10
|
+
const passMarker = workflowSource.match(/const REVIEW_RESULT_PASS_MARKER = .+/)[0]
|
|
11
|
+
const findingsMarker = workflowSource.match(/const REVIEW_RESULT_FINDINGS_MARKER = .+/)[0]
|
|
12
|
+
const parserStart = workflowSource.indexOf('function parseReviewResult')
|
|
13
|
+
const parserEnd = workflowSource.indexOf('\n}\n', parserStart) + 2
|
|
14
|
+
const parserSource = `${passMarker}\n${findingsMarker}\n${workflowSource.slice(parserStart, parserEnd)}`
|
|
15
|
+
const parserContext = {}
|
|
16
|
+
vm.runInNewContext(`${parserSource}\nglobalThis.parseReviewResult = parseReviewResult`, parserContext)
|
|
17
|
+
return parserContext.parseReviewResult
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
test('review tasks apply the canonical review guide and return a review result', () => {
|
|
21
|
+
assert.match(workflowSource, /function runReviewTask\(task, context\)/)
|
|
22
|
+
assert.match(workflowSource, /skills\/reviews\/SKILL\.md#review-workflow/)
|
|
23
|
+
assert.match(workflowSource, /REVIEW_RESULT: PASS/)
|
|
24
|
+
assert.match(workflowSource, /REVIEW_RESULT: FINDINGS/)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test('review tasks use code-quality-agent and keep the working tree read-only', () => {
|
|
28
|
+
const reviewTaskStart = workflowSource.indexOf('function runReviewTask')
|
|
29
|
+
const reviewTaskEnd = workflowSource.indexOf('function serializeOneLineJson', reviewTaskStart)
|
|
30
|
+
const reviewTaskSource = workflowSource.slice(reviewTaskStart, reviewTaskEnd)
|
|
31
|
+
assert.match(reviewTaskSource, /agentType: 'code-quality-agent'/)
|
|
32
|
+
assert.match(reviewTaskSource, /Make no edit to the tree under verification/)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('review result parser accepts one standalone terminal disposition only', () => {
|
|
36
|
+
const parseReviewResult = loadReviewResultParser()
|
|
37
|
+
|
|
38
|
+
assert.equal(parseReviewResult('review complete\nREVIEW_RESULT: PASS'), 'PASS')
|
|
39
|
+
assert.equal(parseReviewResult('finding: missing test\nREVIEW_RESULT: FINDINGS'), 'FINDINGS')
|
|
40
|
+
assert.equal(parseReviewResult('REVIEW_RESULT: PASSING'), null)
|
|
41
|
+
assert.equal(parseReviewResult('REVIEW_RESULT: PASS\nREVIEW_RESULT: FINDINGS'), null)
|
|
42
|
+
assert.equal(parseReviewResult('REVIEW_RESULT: PASS\n> REVIEW_RESULT: FINDINGS'), null)
|
|
43
|
+
assert.equal(parseReviewResult('review complete'), null)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('the fix flow keeps edit, review, and commit as distinct operational steps', () => {
|
|
47
|
+
const fixerStart = workflowSource.indexOf('async function fixerWithRecovery')
|
|
48
|
+
const fixerEnd = workflowSource.indexOf('function runCodeEditorTask', fixerStart)
|
|
49
|
+
const fixerSource = workflowSource.slice(fixerStart, fixerEnd)
|
|
50
|
+
assert.match(fixerSource, /runReviewTask\('fix-verify'/)
|
|
51
|
+
assert.match(fixerSource, /runFixerTask\('commit'/)
|
|
52
|
+
assert.match(fixerSource, /reviewPassed\(verifyTranscript\)/)
|
|
53
|
+
})
|