claude-dev-env 1.81.0 → 1.83.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/CLAUDE.md +16 -13
- package/_shared/pr-loop/scripts/CLAUDE.md +1 -0
- package/_shared/pr-loop/scripts/README.md +1 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviewer_availability_constants.py +12 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +0 -2
- package/_shared/pr-loop/scripts/reviewer_availability.py +182 -0
- package/_shared/pr-loop/scripts/terminology_sweep.py +9 -33
- package/_shared/pr-loop/scripts/tests/CLAUDE.md +2 -0
- package/_shared/pr-loop/scripts/tests/test_reviewer_availability.py +159 -0
- package/_shared/pr-loop/scripts/tests/test_reviewer_availability_constants.py +36 -0
- package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +14 -4
- package/hooks/blocking/CLAUDE.md +3 -0
- package/hooks/blocking/code_rules_constants_config.py +159 -1
- package/hooks/blocking/code_rules_docstrings.py +312 -9
- package/hooks/blocking/code_rules_enforcer.py +29 -0
- package/hooks/blocking/code_rules_imports_logging.py +867 -1
- package/hooks/blocking/code_rules_naming_collection.py +141 -0
- package/hooks/blocking/code_rules_string_magic.py +68 -0
- package/hooks/blocking/conventional_pr_title_gate.py +444 -0
- package/hooks/blocking/pre_tool_use_dispatcher.py +3 -3
- package/hooks/blocking/reviewer_spawn_gate.py +182 -0
- package/hooks/blocking/stale_comment_reference_blocker.py +267 -0
- package/hooks/blocking/state_description_blocker.py +96 -5
- package/hooks/blocking/test_code_rules_config_duplicate_path_anchor.py +132 -0
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +2 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_delegation_summary.py +385 -0
- package/hooks/blocking/test_code_rules_enforcer_join_separator_magic.py +67 -0
- package/hooks/blocking/test_code_rules_enforcer_module_docstring_roster.py +40 -0
- package/hooks/blocking/test_code_rules_enforcer_naive_datetime.py +213 -0
- package/hooks/blocking/test_code_rules_enforcer_referenced_underscore_loop.py +169 -0
- package/hooks/blocking/test_code_rules_js_bare_flag_return_directive.py +266 -0
- package/hooks/blocking/test_code_rules_js_sibling_return_object_key_drift.py +490 -0
- package/hooks/blocking/test_code_rules_logging_adjacent_literals.py +171 -0
- package/hooks/blocking/test_conventional_pr_title_gate.py +640 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +9 -3
- package/hooks/blocking/test_reviewer_spawn_gate.py +230 -0
- package/hooks/blocking/test_stale_comment_reference_blocker.py +236 -0
- package/hooks/blocking/test_state_description_blocker.py +135 -0
- package/hooks/hooks.json +10 -0
- package/hooks/hooks_constants/CLAUDE.md +4 -1
- package/hooks/hooks_constants/blocking_check_limits.py +43 -0
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +41 -0
- package/hooks/hooks_constants/conventional_pr_title_gate_constants.py +58 -0
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
- package/hooks/hooks_constants/reviewer_spawn_gate_constants.py +41 -0
- package/hooks/hooks_constants/stale_comment_reference_blocker_constants.py +76 -0
- package/hooks/hooks_constants/state_description_blocker_constants.py +8 -0
- package/package.json +1 -1
- package/rules/CLAUDE.md +4 -1
- package/rules/claude-md-orphan-file.md +5 -0
- package/rules/docstring-prose-matches-implementation.md +10 -1
- package/rules/env-var-table-code-drift.md +5 -0
- package/rules/es-exe-file-search.md +17 -0
- package/rules/no-historical-clutter.md +12 -1
- package/rules/orphan-css-class.md +5 -0
- package/rules/package-inventory-stale-entry.md +10 -0
- package/rules/paired-test-coverage.md +5 -0
- package/rules/plain-illustrative-docstrings.md +5 -0
- package/rules/verify-before-asking.md +7 -0
- package/rules/verify-runtime-state.md +40 -0
- package/rules/windows-filesystem-safe.md +8 -0
- package/rules/workers-done-before-complete.md +33 -0
- package/rules/workflow-substitution-slots.md +5 -0
- package/skills/CLAUDE.md +1 -1
- package/skills/autoconverge/SKILL.md +96 -5
- package/skills/autoconverge/reference/gotchas.md +5 -5
- package/skills/autoconverge/workflow/converge.contract.test.mjs +228 -27
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +154 -21
- package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +2 -2
- package/skills/autoconverge/workflow/converge.mjs +153 -49
- package/skills/autoconverge/workflow/converge_multi.mjs +6 -2
- package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +26 -0
- package/skills/everything-search/SKILL.md +5 -0
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "**/*.workflow.js"
|
|
4
|
+
---
|
|
5
|
+
|
|
1
6
|
# Workflow Substitution Slot Rule
|
|
2
7
|
|
|
3
8
|
In a `.workflow.js` agent-prompt template, every per-call or per-iteration value an agent must fill in is marked with the angle-bracket convention — `<plate.svg>`, `<object.svg>`, `<glow_hex>`, `cand_<i>`. A bare token such as `cand_i` reads as a fixed literal, so an agent can create one literal directory named `cand_i` and overwrite it across every iteration of a loop, collapsing an N-iteration gate into a single run.
|
package/skills/CLAUDE.md
CHANGED
|
@@ -42,7 +42,7 @@ Skills install to `~/.claude/skills/<skill-name>/` via `packages/claude-dev-env/
|
|
|
42
42
|
- `research-mode` — activates anti-hallucination discipline for a session
|
|
43
43
|
- `recall` — retrieves facts from memory files
|
|
44
44
|
- `remember` — saves a decision, gotcha, or architectural choice to the Obsidian vault
|
|
45
|
-
- `everything-search` — file-system search via the Everything
|
|
45
|
+
- `everything-search` — file-system search via the Everything `es.exe` CLI
|
|
46
46
|
- `caveman` — trims noise from a draft artifact
|
|
47
47
|
|
|
48
48
|
**Session and workflow management**
|
|
@@ -131,7 +131,12 @@ no agent spawned. The workflow runs in the background and notifies this session
|
|
|
131
131
|
on completion. Watch live progress with `/workflows`.
|
|
132
132
|
|
|
133
133
|
The workflow returns
|
|
134
|
-
`{ converged, rounds, finalSha, blocker, standardsNote, copilotNote, reuseNote }`.
|
|
134
|
+
`{ converged, rounds, finalSha, blocker, standardsNote, copilotNote, reuseNote, deferredPrs }`.
|
|
135
|
+
`deferredPrs` is the list of draft environment-hardening PRs the standards-deferral
|
|
136
|
+
path opened this run, each as `{ owner, repo, prNumber, copilotDisabled, bugbotDisabled }`.
|
|
137
|
+
The two flags carry this run's Copilot and Bugbot availability, so the next generation
|
|
138
|
+
skips a reviewer that is down or out of quota without re-probing. This list is the seed the
|
|
139
|
+
[self-closing loop](#self-closing-loop-converge-the-deferred-prs) converges next.
|
|
135
140
|
|
|
136
141
|
## Budget-aware round boundaries
|
|
137
142
|
|
|
@@ -387,10 +392,13 @@ checked out in. The workflow runs in the background and notifies this session on
|
|
|
387
392
|
completion; watch live progress with `/workflows`, where each PR's child run
|
|
388
393
|
appears under its own group.
|
|
389
394
|
|
|
390
|
-
The workflow returns
|
|
391
|
-
|
|
392
|
-
`
|
|
393
|
-
`
|
|
395
|
+
The workflow returns
|
|
396
|
+
`{ converged, prCount, convergedCount, results, allDeferredPrs, blocker }`, where
|
|
397
|
+
`results` is one record per PR carrying
|
|
398
|
+
`{ owner, repo, prNumber, converged, rounds, finalSha, blocker, deferredPrs }`.
|
|
399
|
+
Each record's `deferredPrs` is that PR's own list of draft hardening PRs, and
|
|
400
|
+
`allDeferredPrs` is every record's `deferredPrs` flattened into one list. The
|
|
401
|
+
top-level `converged` is true only when every PR converged.
|
|
394
402
|
|
|
395
403
|
### Multi-PR teardown (on workflow completion)
|
|
396
404
|
|
|
@@ -403,6 +411,89 @@ permissions once per repository after every PR's teardown. Then print one summar
|
|
|
403
411
|
report — a line per PR as
|
|
404
412
|
`#<prNumber>: <converged | blocked> — rounds <N>, final <finalSha>[, blocker <blocker>]`.
|
|
405
413
|
|
|
414
|
+
## Self-closing loop: converge the deferred PRs
|
|
415
|
+
|
|
416
|
+
Every run leaves work behind. When a round holds only code-standard findings, the
|
|
417
|
+
standards-deferral path opens a draft environment-hardening PR and reports it in
|
|
418
|
+
`deferredPrs`. That PR is itself a draft that needs to reach ready, and its own
|
|
419
|
+
convergence can defer more standard findings, opening more hardening PRs. The
|
|
420
|
+
self-closing loop drives that chain to the ground: after teardown, the
|
|
421
|
+
orchestrator (this session, which launched the workflow) converges the deferred
|
|
422
|
+
PRs, then the PRs their runs defer, and so on until a generation opens none.
|
|
423
|
+
|
|
424
|
+
This loop runs by default at the end of every autoconverge run — single-PR and
|
|
425
|
+
multi-PR alike. It stops only when a generation's deferred-PR list comes back
|
|
426
|
+
empty.
|
|
427
|
+
|
|
428
|
+
### Seed the loop
|
|
429
|
+
|
|
430
|
+
Collect the first generation of deferred PRs from the run that just finished:
|
|
431
|
+
|
|
432
|
+
- A single-PR run seeds from its `deferredPrs`.
|
|
433
|
+
- A multi-PR run seeds from its `allDeferredPrs`.
|
|
434
|
+
|
|
435
|
+
When the seed list is empty, the loop is already done — the run deferred nothing,
|
|
436
|
+
so there is nothing to converge. Report that and stop.
|
|
437
|
+
|
|
438
|
+
### Each generation
|
|
439
|
+
|
|
440
|
+
Given a non-empty list of deferred PRs `{ owner, repo, prNumber, copilotDisabled, bugbotDisabled }`
|
|
441
|
+
(a generation may span more than one repository — a hardening PR lands in whichever repo owns
|
|
442
|
+
the surface that blocks the deferred class, so `JonEcho/llm-settings` for hooks
|
|
443
|
+
and `jl-cmd/claude-code-config` for rules and skills both appear):
|
|
444
|
+
|
|
445
|
+
1. **Check out each deferred PR.** Run the
|
|
446
|
+
[Multi-PR pre-flight](#multi-pr-pre-flight-main-session) once per deferred PR.
|
|
447
|
+
Because a deferred PR can live in a repo the first run never touched, first
|
|
448
|
+
find a local checkout of that PR's repo; when none exists, clone it under the
|
|
449
|
+
session temp dir, then add the per-PR worktree on the PR's head ref. Drop any
|
|
450
|
+
PR whose strict pre-flight fails rather than stopping the whole generation.
|
|
451
|
+
Grant project permissions once per repository the generation spans.
|
|
452
|
+
2. **Converge the generation.** Launch `workflow/converge_multi.mjs` with one
|
|
453
|
+
entry per checked-out deferred PR, exactly as the
|
|
454
|
+
[multi-PR launch](#launch-the-multi-pr-workflow) describes. Each child run
|
|
455
|
+
re-checks Copilot and Bugbot availability every round through the workflow's own
|
|
456
|
+
pre-spawn probe, so a reviewer that is down or out of quota is never spawned in any
|
|
457
|
+
generation; the `copilotDisabled`/`bugbotDisabled` flags each deferred PR carries
|
|
458
|
+
seed that check for the first round.
|
|
459
|
+
3. **Tear down.** Run the [multi-PR teardown](#multi-pr-teardown-on-workflow-completion)
|
|
460
|
+
over the generation's `results`, and revoke project permissions once per
|
|
461
|
+
repository.
|
|
462
|
+
4. **Take the next seed.** The generation's `allDeferredPrs` is the next
|
|
463
|
+
generation's seed list. When it is empty, the loop is done.
|
|
464
|
+
|
|
465
|
+
Repeat from step 1 with each new seed. The depth is unbounded: the loop keeps
|
|
466
|
+
opening generations until one converges every deferred PR without deferring
|
|
467
|
+
anything new.
|
|
468
|
+
|
|
469
|
+
### Report each generation
|
|
470
|
+
|
|
471
|
+
Log one line as each generation finishes, so a watcher sees the chain close:
|
|
472
|
+
|
|
473
|
+
```
|
|
474
|
+
Self-closing generation <k>: <converged>/<total> deferred PR(s) converged, <new> new deferred PR(s) opened
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
When the loop ends, print a final line naming the generation count and the total
|
|
478
|
+
deferred PRs converged across every generation.
|
|
479
|
+
|
|
480
|
+
### Conventional titles on deferred PRs
|
|
481
|
+
|
|
482
|
+
Each hardening PR the loop opens targets a repo whose CI validates the PR title
|
|
483
|
+
as a Conventional Commit. The commit step's prompt directs the agent to title
|
|
484
|
+
the hardening PR as a Conventional Commit — a type prefix, an optional scope,
|
|
485
|
+
then a colon and a short summary — so a deferred PR carries a conforming title
|
|
486
|
+
(`feat(hooks): …`, `chore(rules): …`) before it exists. That prompt is where the
|
|
487
|
+
conforming title is enforced.
|
|
488
|
+
|
|
489
|
+
The `conventional_pr_title_gate` hook is a best-effort backstop on that title,
|
|
490
|
+
not the guarantee. It blocks a `gh pr create` with a non-conforming `--title`
|
|
491
|
+
only for a repo whose semantic-pull-request workflow leaves the action's
|
|
492
|
+
`types:` input at the default Conventional Commits list. For a repo that pins
|
|
493
|
+
its own explicit `types:` list — which the main target repo does in
|
|
494
|
+
`.github/workflows/pr-check.yml` — the hook fails open and lets the title
|
|
495
|
+
through, and the CI title check on GitHub has the final say.
|
|
496
|
+
|
|
406
497
|
## Folder map
|
|
407
498
|
|
|
408
499
|
- `SKILL.md` — this hub.
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
Hard-won lessons for the autoconverge workflow. Append a bullet each time a run
|
|
4
4
|
fails in a new way.
|
|
5
5
|
|
|
6
|
-
- **The workflow script cannot sleep.** `Date.now`, `Math.random`, and timers
|
|
7
|
-
are unavailable in the script body, and a foreground sleep is blocked. Every
|
|
8
|
-
reviewer wait lives inside an `agent`'s own poll loop
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
- **The workflow script cannot sleep, and neither can an agent's foreground shell.** `Date.now`, `Math.random`, and timers
|
|
7
|
+
are unavailable in the script body, and a foreground `sleep` / `Start-Sleep` is blocked in the headless harness. Every
|
|
8
|
+
reviewer wait lives inside an `agent`'s own poll loop, and that loop waits with the Monitor tool inside the same turn —
|
|
9
|
+
never a foreground sleep, and never backgrounding a wait and ending the turn to await it, which
|
|
10
|
+
leaves a schema-bearing agent with no `StructuredOutput` call. Never try to wait in the script itself.
|
|
11
11
|
|
|
12
12
|
- **Workflow agents start blank.** Spawned agents do not inherit CLAUDE.md,
|
|
13
13
|
rules, or this skill's context. Each agent prompt is self-contained: it names
|
|
@@ -56,6 +56,75 @@ test('a single round-level prefetch step fetches origin/main before the parallel
|
|
|
56
56
|
);
|
|
57
57
|
});
|
|
58
58
|
|
|
59
|
+
test('the CONVERGE round spawns a single shared reviewer-availability probe before the parallel lenses', () => {
|
|
60
|
+
const prefetchCallIndex = convergeSource.indexOf("runGitTask('prefetch-main')");
|
|
61
|
+
const probeCallIndex = convergeSource.indexOf('reviewerAvailability = await runReviewerAvailabilityCheck()');
|
|
62
|
+
const parallelLensIndex = convergeSource.indexOf('const lenses = await parallel(');
|
|
63
|
+
assert.notEqual(probeCallIndex, -1, 'expected the CONVERGE round to spawn the shared reviewer-availability probe');
|
|
64
|
+
assert.ok(
|
|
65
|
+
prefetchCallIndex < probeCallIndex && probeCallIndex < parallelLensIndex,
|
|
66
|
+
'expected the probe to run after the round prefetch and before the parallel lenses spawn',
|
|
67
|
+
);
|
|
68
|
+
const probeBody = functionSource('runReviewerAvailabilityCheck');
|
|
69
|
+
assert.match(probeBody, /reviewer_availability\.py/);
|
|
70
|
+
assert.match(probeBody, /--reviewer copilot/);
|
|
71
|
+
assert.match(probeBody, /--reviewer bugbot/);
|
|
72
|
+
assert.match(probeBody, /schema:\s*REVIEWER_AVAILABILITY_SCHEMA/);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('the Bugbot lens is not spawned pre-spawn when the shared gate reports Bugbot down', () => {
|
|
76
|
+
const parallelLensIndex = convergeSource.indexOf('const lenses = await parallel(');
|
|
77
|
+
assert.notEqual(parallelLensIndex, -1, 'expected the parallel lens block to exist');
|
|
78
|
+
const isBugbotDownPreSpawnIndex = convergeSource.indexOf('const isBugbotDownPreSpawn = resolveReviewerDown(');
|
|
79
|
+
assert.notEqual(isBugbotDownPreSpawnIndex, -1, 'expected a pre-spawn Bugbot-down decision computed before the lens array');
|
|
80
|
+
assert.ok(
|
|
81
|
+
isBugbotDownPreSpawnIndex < parallelLensIndex,
|
|
82
|
+
'expected the pre-spawn Bugbot-down decision to be computed before the parallel lens array',
|
|
83
|
+
);
|
|
84
|
+
const lensArrayEnd = convergeSource.indexOf('])', parallelLensIndex);
|
|
85
|
+
const lensArray = convergeSource.slice(parallelLensIndex, lensArrayEnd);
|
|
86
|
+
assert.match(
|
|
87
|
+
lensArray,
|
|
88
|
+
/isBugbotDownPreSpawn \? Promise\.resolve\(\{ sha: head, clean: true, down: true, findings: \[\] \}\) : runBugbotLens\(head\)/,
|
|
89
|
+
'expected the Bugbot lens slot to substitute a resolved down placeholder instead of spawning runBugbotLens when isBugbotDownPreSpawn is true',
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('the post-round bugbotDown verdict treats a dead Bugbot lens as down, not fail-open', () => {
|
|
94
|
+
const parallelLensIndex = convergeSource.indexOf('const lenses = await parallel(');
|
|
95
|
+
assert.notEqual(parallelLensIndex, -1, 'expected the parallel lens block to exist');
|
|
96
|
+
const lensArrayEnd = convergeSource.indexOf('])', parallelLensIndex);
|
|
97
|
+
const bugbotDownAssignIndex = convergeSource.indexOf('bugbotDown = ', lensArrayEnd);
|
|
98
|
+
assert.notEqual(bugbotDownAssignIndex, -1, 'expected the post-round bugbotDown assignment after the lens array');
|
|
99
|
+
const bugbotDownAssignLineEnd = convergeSource.indexOf('\n', bugbotDownAssignIndex);
|
|
100
|
+
const bugbotDownAssignLine = convergeSource.slice(bugbotDownAssignIndex, bugbotDownAssignLineEnd);
|
|
101
|
+
assert.match(
|
|
102
|
+
bugbotDownAssignLine,
|
|
103
|
+
/bugbotDown = lenses\[0\] == null \? true : resolveReviewerDown\(lenses\[0\], input\.bugbotDisabled \|\| false\)/,
|
|
104
|
+
'expected a dead (null) Bugbot lens result to report bugbotDown:true — resolveReviewerDown alone is fail-open on null, which would wrongly report bugbotDown:false for a lens agent that actually spawned and died, leaving check_convergence.py to look for a bugbot review that was never posted',
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('both reviewer trigger commands carry the CLAUDE_REVIEWER_GATE=autoconverge sentinel', () => {
|
|
109
|
+
assert.match(
|
|
110
|
+
convergeSource,
|
|
111
|
+
/const REVIEWER_GATE_SENTINEL = 'CLAUDE_REVIEWER_GATE=autoconverge '/,
|
|
112
|
+
'expected the shared sentinel constant to exist',
|
|
113
|
+
);
|
|
114
|
+
const copilotPrompt = lensPromptBody('runCopilotGate');
|
|
115
|
+
assert.match(
|
|
116
|
+
copilotPrompt,
|
|
117
|
+
/\$\{REVIEWER_GATE_SENTINEL\}gh api --method POST[^\n]*requested_reviewers/,
|
|
118
|
+
'expected the Copilot requested_reviewers trigger to carry the sentinel prefix',
|
|
119
|
+
);
|
|
120
|
+
const bugbotPrompt = lensPromptBody('runBugbotLens');
|
|
121
|
+
assert.match(
|
|
122
|
+
bugbotPrompt,
|
|
123
|
+
/\$\{REVIEWER_GATE_SENTINEL\}python "\$\{CONFIG\.sharedScripts\}\/post_fix_reply\.py"[^\n]*--body "bugbot run"/,
|
|
124
|
+
'expected the Bugbot rerun-comment trigger to carry the sentinel prefix',
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
|
|
59
128
|
test('bugbot lens preamble does not blanket-instruct passing --owner/--repo to every script', () => {
|
|
60
129
|
const bugbotPrompt = lensPromptBody('runBugbotLens');
|
|
61
130
|
assert.doesNotMatch(
|
|
@@ -107,42 +176,28 @@ test('repair-convergence edit step no longer instructs resolving every unresolve
|
|
|
107
176
|
);
|
|
108
177
|
});
|
|
109
178
|
|
|
110
|
-
test('bugbot lens
|
|
179
|
+
test('the bugbot lens waits through the Monitor tool, not a foreground sleep', () => {
|
|
111
180
|
const bugbotPrompt = lensPromptBody('runBugbotLens');
|
|
112
|
-
assert.match(bugbotPrompt, /
|
|
113
|
-
assert.match(bugbotPrompt, /sleep 8/, 'expected a concrete 8-second delay command');
|
|
114
|
-
assert.match(
|
|
115
|
-
bugbotPrompt,
|
|
116
|
-
/Start-Sleep[\s\S]*alternative|alternative[\s\S]*Start-Sleep/i,
|
|
117
|
-
'expected PowerShell to be named only as an allowed alternative',
|
|
118
|
-
);
|
|
181
|
+
assert.match(bugbotPrompt, /Monitor tool/, 'expected the bugbot poll to wait via the Monitor tool');
|
|
119
182
|
assert.doesNotMatch(
|
|
120
183
|
bugbotPrompt,
|
|
121
|
-
/
|
|
122
|
-
'
|
|
184
|
+
/sleep 60|sleep 8|Start-Sleep/,
|
|
185
|
+
'expected no foreground sleep in the bugbot poll delays',
|
|
123
186
|
);
|
|
124
187
|
});
|
|
125
188
|
|
|
126
|
-
test('copilot gate
|
|
189
|
+
test('the copilot gate waits through the Monitor tool, not a foreground sleep', () => {
|
|
127
190
|
const copilotPrompt = lensPromptBody('runCopilotGate');
|
|
128
|
-
assert.match(copilotPrompt, /
|
|
129
|
-
assert.
|
|
130
|
-
copilotPrompt,
|
|
131
|
-
/Start-Sleep[\s\S]*alternative|alternative[\s\S]*Start-Sleep/i,
|
|
132
|
-
'expected PowerShell to be named only as an allowed alternative',
|
|
133
|
-
);
|
|
191
|
+
assert.match(copilotPrompt, /Monitor tool/, 'expected the copilot poll to wait via the Monitor tool');
|
|
192
|
+
assert.doesNotMatch(copilotPrompt, /sleep 360|Start-Sleep/, 'expected no foreground sleep in the copilot poll delay');
|
|
134
193
|
});
|
|
135
194
|
|
|
136
|
-
test('gotchas doc describes the reviewer wait as
|
|
137
|
-
assert.match(
|
|
138
|
-
gotchasSource,
|
|
139
|
-
/\bsleep\b/i,
|
|
140
|
-
'expected the wait guidance to name a shell-agnostic sleep',
|
|
141
|
-
);
|
|
195
|
+
test('gotchas doc describes the reviewer wait as a Monitor poll, not a foreground sleep', () => {
|
|
196
|
+
assert.match(gotchasSource, /Monitor tool/, 'expected the gotcha to name the Monitor-based reviewer wait');
|
|
142
197
|
assert.doesNotMatch(
|
|
143
198
|
gotchasSource,
|
|
144
|
-
/
|
|
145
|
-
'
|
|
199
|
+
/shell-agnostic/i,
|
|
200
|
+
'the reviewer wait is a Monitor poll, not a shell-agnostic sleep loop',
|
|
146
201
|
);
|
|
147
202
|
});
|
|
148
203
|
|
|
@@ -390,8 +445,40 @@ test('spawnStandardsFollowUp reports whether a hardening PR opened on every path
|
|
|
390
445
|
);
|
|
391
446
|
assert.match(
|
|
392
447
|
body,
|
|
393
|
-
/hardeningPrOpened
|
|
394
|
-
'expected the commit path to
|
|
448
|
+
/const hardeningPrOpened =\s*typeof commitResult\?\.hardeningPrUrl === 'string' && commitResult\.hardeningPrUrl\.length > 0/,
|
|
449
|
+
'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',
|
|
450
|
+
);
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
test('spawnStandardsFollowUp reports the deferred PR identity on every path', () => {
|
|
454
|
+
const body = lensPromptBody('spawnStandardsFollowUp');
|
|
455
|
+
const nullDeferred = body.match(/deferredPr:\s*null/g) || [];
|
|
456
|
+
assert.ok(
|
|
457
|
+
nullDeferred.length >= 2,
|
|
458
|
+
'expected both skip paths (no hardening staged, verify failed) to return deferredPr:null',
|
|
459
|
+
);
|
|
460
|
+
assert.match(
|
|
461
|
+
body,
|
|
462
|
+
/parseDeferredPr\(commitResult\?\.hardeningPrUrl\)/,
|
|
463
|
+
'expected the commit path to parse the deferred PR identity from the commit step hardeningPrUrl',
|
|
464
|
+
);
|
|
465
|
+
assert.doesNotMatch(
|
|
466
|
+
body,
|
|
467
|
+
/hardeningPrOpened:\s*deferredPr !== null/,
|
|
468
|
+
'expected the commit path to keep hardeningPrOpened separate from the parsed deferredPr so an unparseable URL still latches the guard',
|
|
469
|
+
);
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
test('the workflow return objects carry the accumulated deferredPrs list', () => {
|
|
473
|
+
const converged = convergeSource.match(/deferredPrs/g) || [];
|
|
474
|
+
assert.ok(
|
|
475
|
+
converged.length >= 4,
|
|
476
|
+
'expected deferredPrs to be declared, pushed at both deferral call sites, and returned',
|
|
477
|
+
);
|
|
478
|
+
assert.match(
|
|
479
|
+
convergeSource,
|
|
480
|
+
/if \(standardsOutcome\?\.deferredPr\) deferredPrs\.push\(standardsOutcome\.deferredPr\)/,
|
|
481
|
+
'expected each deferral call site to accumulate the deferred PR into deferredPrs',
|
|
395
482
|
);
|
|
396
483
|
});
|
|
397
484
|
|
|
@@ -758,3 +845,117 @@ test('extractVerifyObjection calls parseLastVerdictFence', () => {
|
|
|
758
845
|
const objectionBody = lensPromptBody('extractVerifyObjection');
|
|
759
846
|
assert.match(objectionBody, /parseLastVerdictFence\(/, 'expected extractVerifyObjection to call the shared parser');
|
|
760
847
|
});
|
|
848
|
+
|
|
849
|
+
test('the headless preamble routes waits through the Monitor tool and forbids ending a turn to await work', () => {
|
|
850
|
+
const preambleStart = convergeSource.indexOf('const HEADLESS_SAFETY_PREAMBLE =');
|
|
851
|
+
assert.notEqual(preambleStart, -1, 'expected a HEADLESS_SAFETY_PREAMBLE definition');
|
|
852
|
+
const preambleEnd = convergeSource.indexOf('\n\nlet activeRepoPath', preambleStart);
|
|
853
|
+
assert.notEqual(preambleEnd, -1, 'expected the preamble to end before activeRepoPath');
|
|
854
|
+
const preamble = convergeSource.slice(preambleStart, preambleEnd);
|
|
855
|
+
assert.match(preamble, /foreground sleep is blocked/i, 'expected the preamble to state foreground sleep is blocked');
|
|
856
|
+
assert.match(preamble, /Monitor tool/, 'expected the preamble to route waits through the Monitor tool');
|
|
857
|
+
assert.match(preamble, /StructuredOutput/, 'expected the preamble to require a schema agent to always call StructuredOutput');
|
|
858
|
+
assert.match(preamble, /never end your turn to wait/i, 'expected the preamble to forbid ending a turn to await background work');
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
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', () => {
|
|
862
|
+
const blockStart = convergeSource.indexOf('const HEADLESS_SAFETY_PREAMBLE =');
|
|
863
|
+
assert.ok(blockStart !== -1, 'expected a wait-safety preamble to exist');
|
|
864
|
+
const blockEnd = convergeSource.indexOf('\n\nlet activeRepoPath', blockStart);
|
|
865
|
+
assert.ok(blockEnd !== -1, 'expected the preamble to end before activeRepoPath');
|
|
866
|
+
const preamble = convergeSource.slice(blockStart, blockEnd);
|
|
867
|
+
assert.doesNotMatch(
|
|
868
|
+
preamble,
|
|
869
|
+
/never move a wait to a background process/i,
|
|
870
|
+
'the Monitor tool is itself a background monitor, so the preamble must avoid forbidding a wait that moves to a background process',
|
|
871
|
+
);
|
|
872
|
+
assert.doesNotMatch(
|
|
873
|
+
preamble,
|
|
874
|
+
/return to you when the condition holds/i,
|
|
875
|
+
'the Monitor tool streams notifications across turns rather than returning in-turn, so the preamble must avoid describing an in-turn synchronous return',
|
|
876
|
+
);
|
|
877
|
+
assert.match(
|
|
878
|
+
preamble,
|
|
879
|
+
/until-loop/i,
|
|
880
|
+
'expected the preamble to describe the wait as a bounded until-loop paired with the Monitor tool',
|
|
881
|
+
);
|
|
882
|
+
assert.match(
|
|
883
|
+
preamble,
|
|
884
|
+
/notifications? as they arrive/i,
|
|
885
|
+
'expected the preamble to say Monitor notifications are consumed as they arrive',
|
|
886
|
+
);
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
test('the background-wait clause gives "await" an explicit object, not a dangling "await never resumes"', () => {
|
|
890
|
+
const text = preambleText();
|
|
891
|
+
assert.doesNotMatch(
|
|
892
|
+
text,
|
|
893
|
+
/end your turn to await never resumes/i,
|
|
894
|
+
'expected the background-wait clause to name what is awaited, not read as "await never resumes" with no object',
|
|
895
|
+
);
|
|
896
|
+
assert.match(
|
|
897
|
+
text,
|
|
898
|
+
/end your turn to await it/i,
|
|
899
|
+
'expected the background-wait clause to name the wait as the object of "await"',
|
|
900
|
+
);
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
test('the wait guidance names the full down-result schema, not a bare down flag', () => {
|
|
904
|
+
const text = preambleText();
|
|
905
|
+
assert.doesNotMatch(
|
|
906
|
+
text,
|
|
907
|
+
/Copilot gate, down: true\)/,
|
|
908
|
+
'expected the down signal to name the full schema, not a bare down flag that reads as a partial object',
|
|
909
|
+
);
|
|
910
|
+
assert.match(
|
|
911
|
+
text,
|
|
912
|
+
/down:true, findings:\[\]/,
|
|
913
|
+
'expected the wait guidance to name the full down-result fields',
|
|
914
|
+
);
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
test('no agent prompt instructs a foreground sleep as the poll delay', () => {
|
|
918
|
+
assert.doesNotMatch(
|
|
919
|
+
convergeSource,
|
|
920
|
+
/delay each (?:attempt|iteration|retry) with "sleep/,
|
|
921
|
+
'expected no poll directive to instruct a foreground sleep as the between-attempt delay',
|
|
922
|
+
);
|
|
923
|
+
assert.doesNotMatch(
|
|
924
|
+
convergeSource,
|
|
925
|
+
/Start-Sleep -Seconds/,
|
|
926
|
+
'expected no agent prompt to instruct a foreground PowerShell Start-Sleep',
|
|
927
|
+
);
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
test('the wait guidance sizes the Monitor timeout_ms above the 300s default', () => {
|
|
931
|
+
const text = preambleText();
|
|
932
|
+
assert.match(text, /timeout_ms/, 'names the timeout_ms parameter the wait rule tells the agent to set');
|
|
933
|
+
assert.match(text, /persistent/, 'offers persistent as an alternative to a sized timeout_ms');
|
|
934
|
+
assert.match(text, /300000|300 second/, 'names the 300000 default that truncates a long wait');
|
|
935
|
+
assert.match(
|
|
936
|
+
text,
|
|
937
|
+
/interval[\s\S]{0,60}attempt|interval-times-attempts/i,
|
|
938
|
+
'sizes timeout_ms to the interval-times-attempts span the step names',
|
|
939
|
+
);
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
test('the Monitor ceiling the guidance names covers the longest interval-times-attempts wait the steps prescribe', () => {
|
|
943
|
+
const cap = /(\d{7})\s*(?:ms|millisecond)/i.exec(preambleText());
|
|
944
|
+
assert.ok(cap !== null, 'the guidance names a seven-digit Monitor ceiling in milliseconds');
|
|
945
|
+
const ceiling = Number(cap[1]);
|
|
946
|
+
|
|
947
|
+
const copilot = lensPromptBody('runCopilotGate');
|
|
948
|
+
const bugbot = lensPromptBody('runBugbotLens');
|
|
949
|
+
const apart = Number(/(\d+)\s*seconds apart/.exec(copilot)[1]);
|
|
950
|
+
const polls = Number(/copilotMaxPolls:\s*(\d+)/.exec(convergeSource)[1]);
|
|
951
|
+
const seconds = Number(/every\s*(\d+)\s*seconds/.exec(bugbot)[1]);
|
|
952
|
+
const iterations = Number(/up to\s*(\d+)\s*iterations/.exec(bugbot)[1]);
|
|
953
|
+
const spans = [apart * polls * 1000, seconds * iterations * 1000];
|
|
954
|
+
|
|
955
|
+
for (const span of spans) {
|
|
956
|
+
assert.ok(
|
|
957
|
+
span <= ceiling,
|
|
958
|
+
`a ${span}ms wait span exceeds the ${ceiling}ms Monitor ceiling the guidance names`,
|
|
959
|
+
);
|
|
960
|
+
}
|
|
961
|
+
});
|