claude-dev-env 2.7.1 → 2.8.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/CLAUDE.md +1 -0
- package/_shared/advisor/advisor-protocol.md +19 -9
- package/_shared/pr-loop/audit-contract.md +4 -4
- package/_shared/pr-loop/precatch-rubric.md +2 -2
- package/_shared/process-tree/CLAUDE.md +41 -0
- package/_shared/process-tree/scripts/config/process_tree_scripts_constants/__init__.py +1 -0
- package/_shared/process-tree/scripts/config/process_tree_scripts_constants/process_tree_kill_constants.py +27 -0
- package/_shared/process-tree/scripts/process_tree_kill.py +141 -0
- package/_shared/process-tree/scripts/pyproject.toml +16 -0
- package/_shared/process-tree/scripts/test_process_tree_kill.py +278 -0
- package/agents/code-quality-agent.md +6 -5
- package/agents/deep-research.md +7 -24
- package/agents/docs-agent.md +1 -27
- package/agents/issue-tracker.md +1 -7
- package/agents/skill-writer-agent.md +1 -2
- package/agents/test_agent_frontmatter.py +309 -12
- package/hooks/blocking/CLAUDE.md +2 -0
- package/hooks/blocking/fable_spawn_gate.py +187 -0
- package/hooks/blocking/piped_pytest_blocker.py +1223 -0
- package/hooks/blocking/plain_language_blocker.py +287 -15
- package/hooks/blocking/test_fable_spawn_gate.py +374 -0
- package/hooks/blocking/test_piped_pytest_blocker.py +587 -0
- package/hooks/blocking/test_plain_language_blocker.py +277 -2
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +34 -2
- package/hooks/git-hooks/CLAUDE.md +2 -2
- package/hooks/git-hooks/git_hooks_constants/__init__.py +28 -0
- package/hooks/git-hooks/pre_push.py +343 -54
- package/hooks/git-hooks/test_pre_push.py +852 -6
- package/hooks/hooks.json +9 -19
- package/hooks/hooks_constants/CLAUDE.md +2 -0
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +1 -0
- package/hooks/hooks_constants/fable_spawn_gate_constants.py +62 -0
- package/hooks/hooks_constants/piped_pytest_blocker_constants.py +360 -0
- package/hooks/hooks_constants/plain_language_blocker_constants.py +64 -1
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +10 -0
- package/hooks/hooks_constants/shell_command_segments.py +1 -1
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +1 -0
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +27 -0
- package/hooks/hooks_constants/test_prose_metrics_parity.py +124 -0
- package/package.json +1 -1
- package/rules/CLAUDE.md +1 -0
- package/rules/ask-user-question-required.md +26 -0
- package/rules/claims-as-quotes.md +65 -0
- package/scripts/CLAUDE.md +1 -1
- package/scripts/_code_review_test_support.py +6 -0
- package/scripts/check.ps1 +18 -5
- package/scripts/claude_chain_runner.py +203 -31
- package/scripts/codec_forwarding_test_support.py +2 -0
- package/scripts/dev_env_scripts_constants/CLAUDE.md +4 -4
- package/scripts/dev_env_scripts_constants/claude_chain_constants.py +38 -0
- package/scripts/dev_env_scripts_constants/code_review_constants.py +403 -2
- package/scripts/dev_env_scripts_constants/grok_worker_constants.py +28 -24
- package/scripts/grok_headless_runner.py +16 -83
- package/scripts/spawn_grok_batch.py +38 -1
- package/scripts/test_claude_chain_runner.py +358 -0
- package/scripts/test_grok_headless_runner.py +18 -63
- package/scripts/test_invoke_code_review.py +298 -0
- package/scripts/test_resolve_worker_spawn.py +6 -0
- package/scripts/test_spawn_grok_batch.py +101 -0
- package/scripts/tests/CLAUDE.md +1 -0
- package/scripts/tests/test_grok_worker_constants.py +59 -0
- package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +46 -0
- package/skills/autoconverge/workflow/converge.contract.test.mjs +105 -2
- package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +34 -1
- package/skills/autoconverge/workflow/converge.mjs +66 -33
- package/skills/codex-review/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +0 -4
- package/skills/codex-review/scripts/codex_usage_probe.py +20 -33
- package/skills/codex-review/scripts/run_codex_review.py +16 -64
- package/skills/codex-review/scripts/test_codex_usage_probe.py +46 -41
- package/skills/codex-review/scripts/test_run_codex_review.py +1 -33
- package/skills/e-code-review/SKILL.md +9 -8
- package/skills/e-code-review/reference/fix.md +29 -7
- package/skills/e-code-review/reference/loop.md +230 -14
- package/skills/e-code-review/reference/low.md +33 -15
- package/skills/e-code-review/reference/medium.md +55 -21
- package/skills/e-code-review/reference/xhigh.md +30 -12
- package/skills/fresh-branch/CLAUDE.md +5 -5
- package/skills/fresh-branch/SKILL.md +14 -6
- package/skills/fresh-branch/scripts/create_fresh_branch.py +122 -39
- package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +10 -3
- package/skills/fresh-branch/scripts/test_create_fresh_branch.py +251 -0
- package/skills/grok-spawn/SKILL.md +5 -0
- package/skills/orchestrator/SKILL.md +4 -1
- package/skills/orchestrator-refresh/SKILL.md +5 -1
- package/skills/team-advisor/SKILL.md +4 -1
|
@@ -411,8 +411,13 @@ test('the shared verdict-fence builder names the binding-hash command and the ve
|
|
|
411
411
|
/verification_verdict_store\.py/,
|
|
412
412
|
'expected the verdict-store script that computes the binding hash to be named',
|
|
413
413
|
);
|
|
414
|
-
assert.match(fenceBuilder,
|
|
415
|
-
|
|
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
|
+
);
|
|
416
421
|
assert.match(
|
|
417
422
|
fenceBuilder,
|
|
418
423
|
/gh pr view/,
|
|
@@ -425,6 +430,100 @@ test('the shared verdict-fence builder names the binding-hash command and the ve
|
|
|
425
430
|
);
|
|
426
431
|
});
|
|
427
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
|
+
|
|
428
527
|
test('the verdict-fence binding does not self-resolve a cwd via git rev-parse for the manifest hash', () => {
|
|
429
528
|
const fenceBuilder = lensPromptBody('buildVerdictFenceSteps');
|
|
430
529
|
assert.doesNotMatch(
|
|
@@ -1006,6 +1105,7 @@ test('the whole priming spawn-agent family is removed — every dispatcher spawn
|
|
|
1006
1105
|
|
|
1007
1106
|
test('parseLastVerdictFence returns non-null for a verdict fence with valid JSON', () => {
|
|
1008
1107
|
const parseModule = new Function(
|
|
1108
|
+
`${functionSource('findLastVerdictFence')}\n` +
|
|
1009
1109
|
`${functionSource('parseLastVerdictFence')}\n` +
|
|
1010
1110
|
'return { parseLastVerdictFence };',
|
|
1011
1111
|
)();
|
|
@@ -1016,6 +1116,7 @@ test('parseLastVerdictFence returns non-null for a verdict fence with valid JSON
|
|
|
1016
1116
|
|
|
1017
1117
|
test('parseLastVerdictFence returns null for non-string input', () => {
|
|
1018
1118
|
const parseModule = new Function(
|
|
1119
|
+
`${functionSource('findLastVerdictFence')}\n` +
|
|
1019
1120
|
`${functionSource('parseLastVerdictFence')}\n` +
|
|
1020
1121
|
'return { parseLastVerdictFence };',
|
|
1021
1122
|
)();
|
|
@@ -1025,6 +1126,7 @@ test('parseLastVerdictFence returns null for non-string input', () => {
|
|
|
1025
1126
|
|
|
1026
1127
|
test('parseLastVerdictFence returns null when no verdict fence is present', () => {
|
|
1027
1128
|
const parseModule = new Function(
|
|
1129
|
+
`${functionSource('findLastVerdictFence')}\n` +
|
|
1028
1130
|
`${functionSource('parseLastVerdictFence')}\n` +
|
|
1029
1131
|
'return { parseLastVerdictFence };',
|
|
1030
1132
|
)();
|
|
@@ -1033,6 +1135,7 @@ test('parseLastVerdictFence returns null when no verdict fence is present', () =
|
|
|
1033
1135
|
|
|
1034
1136
|
test('parseLastVerdictFence returns null for malformed JSON in the fence', () => {
|
|
1035
1137
|
const parseModule = new Function(
|
|
1138
|
+
`${functionSource('findLastVerdictFence')}\n` +
|
|
1036
1139
|
`${functionSource('parseLastVerdictFence')}\n` +
|
|
1037
1140
|
'return { parseLastVerdictFence };',
|
|
1038
1141
|
)();
|
|
@@ -255,7 +255,8 @@ test('the round-loop fix-stalled blockers survive the recovery wiring', () => {
|
|
|
255
255
|
});
|
|
256
256
|
|
|
257
257
|
const verifyObjectionModule = new Function(
|
|
258
|
-
`${functionSource('
|
|
258
|
+
`${functionSource('findLastVerdictFence')}\n` +
|
|
259
|
+
`${functionSource('parseLastVerdictFence')}\n` +
|
|
259
260
|
`${constantLine('VERIFY_OBJECTION_FALLBACK')}\n` +
|
|
260
261
|
`${functionSource('renderVerifyObjectionLine')}\n` +
|
|
261
262
|
`${functionSource('extractPreFenceProse')}\n` +
|
|
@@ -350,6 +351,38 @@ test('the prose reader anchors on the last CLOSED fence, not a stray unterminate
|
|
|
350
351
|
assert.doesNotMatch(objection, /all_pass/, 'expected the verdict body never to be read back as prose');
|
|
351
352
|
});
|
|
352
353
|
|
|
354
|
+
test('both fence readers locate the fence through the one shared locator', () => {
|
|
355
|
+
assert.match(
|
|
356
|
+
functionSource('parseLastVerdictFence'),
|
|
357
|
+
/findLastVerdictFence\(/,
|
|
358
|
+
'expected the verdict parser to locate its fence through findLastVerdictFence',
|
|
359
|
+
);
|
|
360
|
+
assert.match(
|
|
361
|
+
functionSource('extractPreFenceProse'),
|
|
362
|
+
/findLastVerdictFence\(/,
|
|
363
|
+
'expected the prose reader to locate its fence through findLastVerdictFence, not a second search',
|
|
364
|
+
);
|
|
365
|
+
assert.doesNotMatch(
|
|
366
|
+
functionSource('extractPreFenceProse'),
|
|
367
|
+
/lastIndexOf\(/,
|
|
368
|
+
'expected no second fence-search procedure in the prose reader to drift from the parser',
|
|
369
|
+
);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
test('the prose reader and the verdict parser land on the same fence when a later marker carries no newline', () => {
|
|
373
|
+
const transcript =
|
|
374
|
+
'check X never showed red\n\n' +
|
|
375
|
+
'```verdict\n{"all_pass": false, "findings": []}\n```\n\n' +
|
|
376
|
+
'```verdict {"all_pass": true, "findings": []}```';
|
|
377
|
+
const objection = extractVerifyObjection(transcript);
|
|
378
|
+
assert.doesNotMatch(
|
|
379
|
+
objection,
|
|
380
|
+
/all_pass/,
|
|
381
|
+
'expected the prose reader to anchor on the fence the parser read, never to slice a verdict body in as prose',
|
|
382
|
+
);
|
|
383
|
+
assert.equal(objection, 'check X never showed red');
|
|
384
|
+
});
|
|
385
|
+
|
|
353
386
|
test('the prose reader skips a scaffolding-only paragraph above the fence', () => {
|
|
354
387
|
const transcript =
|
|
355
388
|
'check X never showed red\n\n' + '## Verdict\n\n' + '```verdict\n{"all_pass": false, "findings": []}\n```';
|
|
@@ -466,11 +466,7 @@ function runVerifierTask(task, context) {
|
|
|
466
466
|
` Run exactly:\n` +
|
|
467
467
|
` "C:\\Python313\\python.exe" "<REPO>/packages/claude-dev-env/hooks/blocking/verification_verdict_store.py" --manifest-hash-for-branch "${context.hardeningBranch}"\n` +
|
|
468
468
|
` (substitute the REPO path you resolved for the script path). That prints a single 64-char hex hash on stdout — capture it.\n` +
|
|
469
|
-
|
|
470
|
-
" ```verdict\n" +
|
|
471
|
-
` {"all_pass": true, "findings": [], "manifest_sha256": "<that hash>"}\n` +
|
|
472
|
-
" ```\n" +
|
|
473
|
-
` Set all_pass to false when verification fails, and list every code defect you found in findings. When the verdict is incomplete because a check it rests on never showed red, set all_pass to false and name that check in prose directly above the fence rather than in findings. Always include the manifest_sha256. The verdict fence must be the last thing in your message.`,
|
|
469
|
+
buildVerdictFenceTail(' '),
|
|
474
470
|
{ label, phase: 'Converge', agentType: 'code-verifier', ...TIERS.sonnetMedium },
|
|
475
471
|
)
|
|
476
472
|
}
|
|
@@ -849,6 +845,29 @@ const HARDENING_COMMIT_SCHEMA = {
|
|
|
849
845
|
required: ['hardeningPrUrl', 'summary'],
|
|
850
846
|
}
|
|
851
847
|
|
|
848
|
+
const VERDICT_FENCE_CONTRACT_SENTENCE =
|
|
849
|
+
'Set all_pass to false when verification fails, and list every code defect you found in findings. When the verdict is incomplete because a check it rests on never showed red, set all_pass to false and name that check in prose directly above the fence rather than in findings. Always include the manifest_sha256. The verdict fence must be the last thing in your message.'
|
|
850
|
+
|
|
851
|
+
/**
|
|
852
|
+
* Build the closing fence recipe every verify prompt ends with: the lead-in
|
|
853
|
+
* sentence, the ```verdict block shape carrying the binding hash, and the
|
|
854
|
+
* contract sentence. Each verify path resolves the binding hash its own way
|
|
855
|
+
* and sits at its own nesting depth, so the caller passes the indent its
|
|
856
|
+
* surrounding prose uses; the fence body itself sits one level deeper.
|
|
857
|
+
* @param {string} indent leading whitespace the caller's prose sits at
|
|
858
|
+
* @returns {string} the fence recipe tail for a verify prompt
|
|
859
|
+
*/
|
|
860
|
+
function buildVerdictFenceTail(indent) {
|
|
861
|
+
const fenceIndent = `${indent} `
|
|
862
|
+
return (
|
|
863
|
+
`${indent}Then END your message with a fenced verdict block exactly in this shape, on its own, carrying that hash:\n` +
|
|
864
|
+
`${fenceIndent}` + "```verdict\n" +
|
|
865
|
+
`${fenceIndent}{"all_pass": true, "findings": [], "manifest_sha256": "<that hash>"}\n` +
|
|
866
|
+
`${fenceIndent}` + "```\n" +
|
|
867
|
+
`${fenceIndent}${VERDICT_FENCE_CONTRACT_SENTENCE}`
|
|
868
|
+
)
|
|
869
|
+
}
|
|
870
|
+
|
|
852
871
|
/**
|
|
853
872
|
* Build the verdict-fence step instructions for a verify agent, binding the
|
|
854
873
|
* surface hash by branch name rather than by a self-resolved cwd. Resolving
|
|
@@ -869,11 +888,7 @@ function buildVerdictFenceSteps(prOwner, prRepo, prNumber) {
|
|
|
869
888
|
` b. Run exactly:\n` +
|
|
870
889
|
` "C:\\Python313\\python.exe" "<REPO>/packages/claude-dev-env/hooks/blocking/verification_verdict_store.py" --manifest-hash-for-branch "<that branch>"\n` +
|
|
871
890
|
` (substitute the REPO path you resolved for the script path, and the branch name for <that branch>). That prints a single 64-char hex hash on stdout — capture it.\n` +
|
|
872
|
-
|
|
873
|
-
" ```verdict\n" +
|
|
874
|
-
` {"all_pass": true, "findings": [], "manifest_sha256": "<that hash>"}\n` +
|
|
875
|
-
" ```\n" +
|
|
876
|
-
` Set all_pass to false when verification fails, and list every code defect you found in findings. When the verdict is incomplete because a check it rests on never showed red, set all_pass to false and name that check in prose directly above the fence rather than in findings. Always include the manifest_sha256. The verdict fence must be the last thing in your message.`
|
|
891
|
+
buildVerdictFenceTail('')
|
|
877
892
|
)
|
|
878
893
|
}
|
|
879
894
|
|
|
@@ -1282,23 +1297,45 @@ function normalizeShaForComparison(sha) {
|
|
|
1282
1297
|
}
|
|
1283
1298
|
|
|
1284
1299
|
/**
|
|
1285
|
-
*
|
|
1286
|
-
*
|
|
1287
|
-
*
|
|
1300
|
+
* Locate the LAST ```verdict ...``` fence in a transcript: the marker, optional
|
|
1301
|
+
* whitespace, a required newline, then a body closed by ```. This is the ONE
|
|
1302
|
+
* definition of "the verdict fence" — the verdict parser and the pre-fence prose
|
|
1303
|
+
* reader both anchor here, so they cannot land on different fences.
|
|
1304
|
+
*
|
|
1305
|
+
* ::
|
|
1306
|
+
*
|
|
1307
|
+
* ok: 'note\n\n```verdict\n{}\n```' -> body '{}\n' starting at index 6
|
|
1308
|
+
* flag: 'note\n\n```verdict {}```' -> null (no newline after the marker)
|
|
1309
|
+
*
|
|
1310
|
+
* A marker with no newline after it is not a fence, so a later one cannot pull
|
|
1311
|
+
* either reader off the fence the other reads.
|
|
1312
|
+
*
|
|
1288
1313
|
* @param {string|null|undefined} transcript the agent transcript text
|
|
1289
|
-
* @returns {
|
|
1314
|
+
* @returns {{bodyText: string, startIndex: number}|null} the last fence, or null when there is none
|
|
1290
1315
|
*/
|
|
1291
|
-
function
|
|
1316
|
+
function findLastVerdictFence(transcript) {
|
|
1292
1317
|
if (typeof transcript !== 'string') return null
|
|
1293
1318
|
const fencePattern = /```verdict\s*\n([\s\S]*?)```/g
|
|
1294
|
-
let
|
|
1319
|
+
let lastFence = null
|
|
1295
1320
|
let eachMatch
|
|
1296
1321
|
while ((eachMatch = fencePattern.exec(transcript)) !== null) {
|
|
1297
|
-
|
|
1322
|
+
lastFence = { bodyText: eachMatch[1], startIndex: eachMatch.index }
|
|
1298
1323
|
}
|
|
1299
|
-
|
|
1324
|
+
return lastFence
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
/**
|
|
1328
|
+
* Parse the LAST ```verdict ...``` fenced JSON block from a transcript.
|
|
1329
|
+
* Guards against non-string input, takes the fence findLastVerdictFence locates,
|
|
1330
|
+
* parses the JSON, and returns the object or null on any failure.
|
|
1331
|
+
* @param {string|null|undefined} transcript the agent transcript text
|
|
1332
|
+
* @returns {object|null} the parsed verdict object, or null when absent or malformed
|
|
1333
|
+
*/
|
|
1334
|
+
function parseLastVerdictFence(transcript) {
|
|
1335
|
+
const lastFence = findLastVerdictFence(transcript)
|
|
1336
|
+
if (lastFence === null) return null
|
|
1300
1337
|
try {
|
|
1301
|
-
return JSON.parse(
|
|
1338
|
+
return JSON.parse(lastFence.bodyText)
|
|
1302
1339
|
} catch {
|
|
1303
1340
|
return null
|
|
1304
1341
|
}
|
|
@@ -1351,12 +1388,13 @@ function renderVerifyObjectionLine(eachFinding) {
|
|
|
1351
1388
|
* is not a code defect, so the fence contract puts that reason here rather than
|
|
1352
1389
|
* in findings; this reader is how the reason survives into the re-fix step.
|
|
1353
1390
|
*
|
|
1354
|
-
* Anchors
|
|
1355
|
-
*
|
|
1356
|
-
*
|
|
1357
|
-
*
|
|
1358
|
-
*
|
|
1359
|
-
*
|
|
1391
|
+
* Anchors through findLastVerdictFence, the single locator parseLastVerdictFence
|
|
1392
|
+
* reads its JSON from, so both readers see the same fence by construction rather
|
|
1393
|
+
* than by two searches that happen to agree: a stray marker after the real fence
|
|
1394
|
+
* — unterminated, or closed but carrying no newline after the marker — moves
|
|
1395
|
+
* neither reader. A candidate paragraph carrying only markdown scaffolding (a
|
|
1396
|
+
* heading, a fence delimiter) is skipped, since handing the fixer a heading is
|
|
1397
|
+
* worse than the generic fallback the caller keeps.
|
|
1360
1398
|
*
|
|
1361
1399
|
* ::
|
|
1362
1400
|
*
|
|
@@ -1368,15 +1406,10 @@ function renderVerifyObjectionLine(eachFinding) {
|
|
|
1368
1406
|
* @returns {string|null} the last prose paragraph above the final fence, or null when there is none
|
|
1369
1407
|
*/
|
|
1370
1408
|
function extractPreFenceProse(verifyTranscript) {
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
let lastFenceStart = verifyTranscript.lastIndexOf(fenceMarker)
|
|
1374
|
-
while (lastFenceStart !== -1 && !verifyTranscript.slice(lastFenceStart + fenceMarker.length).includes('```')) {
|
|
1375
|
-
lastFenceStart = verifyTranscript.lastIndexOf(fenceMarker, lastFenceStart - 1)
|
|
1376
|
-
}
|
|
1377
|
-
if (lastFenceStart === -1) return null
|
|
1409
|
+
const lastFence = findLastVerdictFence(verifyTranscript)
|
|
1410
|
+
if (lastFence === null) return null
|
|
1378
1411
|
const proseParagraphs = verifyTranscript
|
|
1379
|
-
.slice(0,
|
|
1412
|
+
.slice(0, lastFence.startIndex)
|
|
1380
1413
|
.split(/\n\s*\n/)
|
|
1381
1414
|
.map((eachParagraph) => eachParagraph.trim())
|
|
1382
1415
|
.filter((eachParagraph) =>
|
package/skills/codex-review/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py
CHANGED
|
@@ -22,10 +22,6 @@ WINDOWS_OS_NAME = "nt"
|
|
|
22
22
|
WINDOWS_COMMAND_SHELL = "cmd"
|
|
23
23
|
WINDOWS_COMMAND_SHELL_RUN_FLAG = "/c"
|
|
24
24
|
ALL_WINDOWS_SCRIPT_SUFFIXES = (".cmd", ".bat")
|
|
25
|
-
WINDOWS_TASKKILL_COMMAND = "taskkill"
|
|
26
|
-
WINDOWS_TASKKILL_FORCE_FLAG = "/F"
|
|
27
|
-
WINDOWS_TASKKILL_TREE_FLAG = "/T"
|
|
28
|
-
WINDOWS_TASKKILL_PID_FLAG = "/PID"
|
|
29
25
|
|
|
30
26
|
JSONRPC_VERSION = "2.0"
|
|
31
27
|
JSONRPC_KEY_ID = "id"
|
|
@@ -40,9 +40,21 @@ import sys
|
|
|
40
40
|
import threading
|
|
41
41
|
from collections.abc import Mapping, Sequence
|
|
42
42
|
from datetime import datetime, timezone
|
|
43
|
+
from pathlib import Path
|
|
43
44
|
from typing import IO, Protocol
|
|
44
45
|
|
|
45
|
-
|
|
46
|
+
_shared_process_tree_scripts_directory = (
|
|
47
|
+
Path(__file__).resolve().parents[3] / "_shared" / "process-tree" / "scripts"
|
|
48
|
+
)
|
|
49
|
+
if str(_shared_process_tree_scripts_directory) not in sys.path:
|
|
50
|
+
sys.path.insert(0, str(_shared_process_tree_scripts_directory))
|
|
51
|
+
|
|
52
|
+
from process_tree_kill import ( # noqa: E402
|
|
53
|
+
should_start_new_session,
|
|
54
|
+
terminate_process_tree,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
from codex_review_scripts_constants.codex_usage_probe_constants import ( # noqa: E402
|
|
46
58
|
ALL_APP_SERVER_COMMAND_PARTS,
|
|
47
59
|
ALL_WINDOWS_SCRIPT_SUFFIXES,
|
|
48
60
|
APP_SERVER_TIMEOUT_SECONDS,
|
|
@@ -94,10 +106,6 @@ from codex_review_scripts_constants.codex_usage_probe_constants import (
|
|
|
94
106
|
WINDOWS_COMMAND_SHELL,
|
|
95
107
|
WINDOWS_COMMAND_SHELL_RUN_FLAG,
|
|
96
108
|
WINDOWS_OS_NAME,
|
|
97
|
-
WINDOWS_TASKKILL_COMMAND,
|
|
98
|
-
WINDOWS_TASKKILL_FORCE_FLAG,
|
|
99
|
-
WINDOWS_TASKKILL_PID_FLAG,
|
|
100
|
-
WINDOWS_TASKKILL_TREE_FLAG,
|
|
101
109
|
)
|
|
102
110
|
|
|
103
111
|
|
|
@@ -387,41 +395,19 @@ def _collect_server_lines(
|
|
|
387
395
|
|
|
388
396
|
|
|
389
397
|
def _terminate_process_tree(server_process: subprocess.Popen[str]) -> None:
|
|
390
|
-
"""Stop the app-server process and any children it launched.
|
|
398
|
+
"""Stop the app-server process and any children it launched, then reap it.
|
|
391
399
|
|
|
392
400
|
On Windows the probe may wrap a ``.cmd``/``.bat`` shim as ``cmd /c``, so
|
|
393
401
|
``Popen.pid`` is ``cmd.exe``. Killing only that process leaves the real
|
|
394
|
-
Codex app-server grandchild alive and holding stdout.
|
|
395
|
-
|
|
402
|
+
Codex app-server grandchild alive and holding stdout. The shared tree kill
|
|
403
|
+
ends the whole tree on either platform; the timed wait that follows reaps
|
|
404
|
+
the direct child so the enclosing ``with`` block exits without a long wait.
|
|
396
405
|
"""
|
|
397
|
-
|
|
398
|
-
return
|
|
399
|
-
process_id = server_process.pid
|
|
400
|
-
if process_id is None:
|
|
401
|
-
return
|
|
402
|
-
if os.name == WINDOWS_OS_NAME:
|
|
403
|
-
subprocess.run(
|
|
404
|
-
[
|
|
405
|
-
WINDOWS_TASKKILL_COMMAND,
|
|
406
|
-
WINDOWS_TASKKILL_FORCE_FLAG,
|
|
407
|
-
WINDOWS_TASKKILL_TREE_FLAG,
|
|
408
|
-
WINDOWS_TASKKILL_PID_FLAG,
|
|
409
|
-
str(process_id),
|
|
410
|
-
],
|
|
411
|
-
check=False,
|
|
412
|
-
stdout=subprocess.DEVNULL,
|
|
413
|
-
stderr=subprocess.DEVNULL,
|
|
414
|
-
)
|
|
415
|
-
try:
|
|
416
|
-
server_process.wait(timeout=PROCESS_TREE_WAIT_TIMEOUT_SECONDS)
|
|
417
|
-
except subprocess.TimeoutExpired:
|
|
418
|
-
server_process.kill()
|
|
419
|
-
return
|
|
420
|
-
server_process.kill()
|
|
406
|
+
terminate_process_tree(server_process)
|
|
421
407
|
try:
|
|
422
408
|
server_process.wait(timeout=PROCESS_TREE_WAIT_TIMEOUT_SECONDS)
|
|
423
409
|
except subprocess.TimeoutExpired:
|
|
424
|
-
|
|
410
|
+
return
|
|
425
411
|
|
|
426
412
|
|
|
427
413
|
def _teardown_app_server_exchange(
|
|
@@ -467,6 +453,7 @@ def _exchange_app_server_messages_via_subprocess(
|
|
|
467
453
|
text=True,
|
|
468
454
|
encoding=UTF8_ENCODING,
|
|
469
455
|
shell=False,
|
|
456
|
+
start_new_session=should_start_new_session(),
|
|
470
457
|
) as server_process:
|
|
471
458
|
server_stdin = server_process.stdin
|
|
472
459
|
server_stdout = server_process.stdout
|
|
@@ -18,22 +18,29 @@ import json
|
|
|
18
18
|
import os
|
|
19
19
|
import re
|
|
20
20
|
import shutil
|
|
21
|
-
import signal
|
|
22
21
|
import subprocess
|
|
22
|
+
import sys
|
|
23
23
|
from dataclasses import dataclass
|
|
24
24
|
from pathlib import Path
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
_shared_process_tree_scripts_directory = (
|
|
27
|
+
Path(__file__).resolve().parents[3] / "_shared" / "process-tree" / "scripts"
|
|
28
|
+
)
|
|
29
|
+
if str(_shared_process_tree_scripts_directory) not in sys.path:
|
|
30
|
+
sys.path.insert(0, str(_shared_process_tree_scripts_directory))
|
|
31
|
+
|
|
32
|
+
from process_tree_kill import ( # noqa: E402
|
|
33
|
+
should_start_new_session,
|
|
34
|
+
terminate_process_tree,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
from codex_review_scripts_constants.codex_usage_probe_constants import ( # noqa: E402
|
|
27
38
|
ALL_WINDOWS_SCRIPT_SUFFIXES,
|
|
28
39
|
WINDOWS_COMMAND_SHELL,
|
|
29
40
|
WINDOWS_COMMAND_SHELL_RUN_FLAG,
|
|
30
41
|
WINDOWS_OS_NAME,
|
|
31
|
-
WINDOWS_TASKKILL_COMMAND,
|
|
32
|
-
WINDOWS_TASKKILL_FORCE_FLAG,
|
|
33
|
-
WINDOWS_TASKKILL_PID_FLAG,
|
|
34
|
-
WINDOWS_TASKKILL_TREE_FLAG,
|
|
35
42
|
)
|
|
36
|
-
from codex_review_scripts_constants.run_constants import (
|
|
43
|
+
from codex_review_scripts_constants.run_constants import ( # noqa: E402
|
|
37
44
|
ALL_SHAPE_PROBE_REQUIRED_FLAGS,
|
|
38
45
|
BASE_TARGET_FLAG,
|
|
39
46
|
CAPTURE_STREAMS_KEYWORD,
|
|
@@ -74,61 +81,6 @@ from codex_review_scripts_constants.run_constants import (
|
|
|
74
81
|
)
|
|
75
82
|
|
|
76
83
|
|
|
77
|
-
def _kill_windows_process_tree(process_identifier: int) -> None:
|
|
78
|
-
"""Kill a Windows process and every descendant it started, by PID.
|
|
79
|
-
|
|
80
|
-
Swallows taskkill failures so the caller can fall back to ``Popen.kill()``
|
|
81
|
-
and a timed drain. A raised ``TimeoutExpired`` here would replace the
|
|
82
|
-
original review-timeout exception and skip that drain path.
|
|
83
|
-
"""
|
|
84
|
-
try:
|
|
85
|
-
subprocess.run(
|
|
86
|
-
[
|
|
87
|
-
WINDOWS_TASKKILL_COMMAND,
|
|
88
|
-
WINDOWS_TASKKILL_TREE_FLAG,
|
|
89
|
-
WINDOWS_TASKKILL_FORCE_FLAG,
|
|
90
|
-
WINDOWS_TASKKILL_PID_FLAG,
|
|
91
|
-
str(process_identifier),
|
|
92
|
-
],
|
|
93
|
-
stdout=subprocess.DEVNULL,
|
|
94
|
-
stderr=subprocess.DEVNULL,
|
|
95
|
-
check=False,
|
|
96
|
-
timeout=PROCESS_TREE_KILL_TIMEOUT_SECONDS,
|
|
97
|
-
)
|
|
98
|
-
except (subprocess.TimeoutExpired, OSError):
|
|
99
|
-
return
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
def _kill_posix_process_group(process_identifier: int) -> None:
|
|
103
|
-
"""Kill a POSIX process group so no grandchild keeps the capture pipe open."""
|
|
104
|
-
try:
|
|
105
|
-
process_group_identifier = os.getpgid(process_identifier)
|
|
106
|
-
os.killpg(process_group_identifier, signal.SIGKILL)
|
|
107
|
-
except (ProcessLookupError, PermissionError):
|
|
108
|
-
return
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
def _terminate_process_tree(review_process: subprocess.Popen[str]) -> None:
|
|
112
|
-
"""Kill the review process and every descendant it spawned.
|
|
113
|
-
|
|
114
|
-
Tree kill first (taskkill /T or killpg). When the direct child is still
|
|
115
|
-
alive after that, fall back to ``Popen.kill()`` so ``Popen.__exit__`` never
|
|
116
|
-
hits an unbounded wait on a surviving process.
|
|
117
|
-
"""
|
|
118
|
-
if review_process.poll() is not None:
|
|
119
|
-
return
|
|
120
|
-
if os.name == WINDOWS_OS_NAME:
|
|
121
|
-
_kill_windows_process_tree(review_process.pid)
|
|
122
|
-
else:
|
|
123
|
-
_kill_posix_process_group(review_process.pid)
|
|
124
|
-
if review_process.poll() is not None:
|
|
125
|
-
return
|
|
126
|
-
try:
|
|
127
|
-
review_process.kill()
|
|
128
|
-
except ProcessLookupError:
|
|
129
|
-
return
|
|
130
|
-
|
|
131
|
-
|
|
132
84
|
def _open_codex_popen(
|
|
133
85
|
all_arguments: list[str],
|
|
134
86
|
all_keyword_arguments: dict[str, object],
|
|
@@ -147,7 +99,7 @@ def _open_codex_popen(
|
|
|
147
99
|
text=bool(all_keyword_arguments.get(TEXT_MODE_KEYWORD, False)),
|
|
148
100
|
encoding=stream_encoding if isinstance(stream_encoding, str) else None,
|
|
149
101
|
env=process_environment if isinstance(process_environment, dict) else None,
|
|
150
|
-
start_new_session=
|
|
102
|
+
start_new_session=should_start_new_session(),
|
|
151
103
|
)
|
|
152
104
|
|
|
153
105
|
|
|
@@ -198,7 +150,7 @@ def _communicate_with_tree_kill_on_timeout(
|
|
|
198
150
|
try:
|
|
199
151
|
return review_process.communicate(timeout=timeout_seconds)
|
|
200
152
|
except subprocess.TimeoutExpired:
|
|
201
|
-
|
|
153
|
+
terminate_process_tree(review_process)
|
|
202
154
|
_drain_process_after_tree_kill(review_process)
|
|
203
155
|
raise
|
|
204
156
|
|