claude-dev-env 8.89.0 → 8.90.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/pr-visual/README.md +29 -0
- package/.agents/skills/pr-visual/SKILL.md +62 -0
- package/.agents/skills/pr-visual/assets/icons.symbols +27 -0
- package/.agents/skills/pr-visual/assets/viewer.html +147 -0
- package/.agents/skills/pr-visual/fixtures/bar.scene.json +193 -0
- package/.agents/skills/pr-visual/fixtures/flow.scene.json +201 -0
- package/.agents/skills/pr-visual/fixtures/head/metrics.json +16 -0
- package/.agents/skills/pr-visual/fixtures/integrated.scene.json +239 -0
- package/.agents/skills/pr-visual/fixtures/line.scene.json +193 -0
- package/.agents/skills/pr-visual/fixtures/scatter.scene.json +193 -0
- package/.agents/skills/pr-visual/fixtures/snapshot.json +21 -0
- package/.agents/skills/pr-visual/references/evidence.md +9 -0
- package/.agents/skills/pr-visual/references/formats.md +30 -0
- package/.agents/skills/pr-visual/references/scene-contract.md +48 -0
- package/.agents/skills/pr-visual/references/visual-contract.md +23 -0
- package/.agents/skills/pr-visual/scripts/collect.py +111 -0
- package/.agents/skills/pr-visual/scripts/metrics.py +99 -0
- package/.agents/skills/pr-visual/scripts/render.py +133 -0
- package/.agents/skills/pr-visual/scripts/test_collect.py +58 -0
- package/.agents/skills/pr-visual/scripts/test_metrics.py +47 -0
- package/.agents/skills/pr-visual/scripts/test_render.py +77 -0
- package/.agents/skills/pr-visual/scripts/verify.py +218 -0
- package/.agents/skills/pr-visual/scripts/visual_config/config/constants.py +104 -0
- package/bin/codex-skill-load-block.mjs +0 -25
- package/bin/codex-skill-load-block.test.mjs +5 -17
- package/bin/codex-trimmed-sol.test.mjs +0 -1
- package/bin/cursor-native-hooks.test.mjs +3 -3
- package/bin/ever-shipped-skills.mjs +1 -0
- package/bin/install-deepstack-plugin.mjs +42 -25
- package/bin/install-deepstack-plugin.test.mjs +37 -11
- package/bin/install-pstack-plugin.mjs +58 -11
- package/bin/install-pstack-plugin.test.mjs +54 -21
- package/bin/install.agents-home.test.mjs +0 -1
- package/bin/install.codex-rules.test.mjs +0 -2
- package/bin/install.cursor-rules.test.mjs +90 -26
- package/bin/install.mjs +30 -36
- package/bin/install.pr-visual.test.mjs +40 -0
- package/bin/install.prompt-context.test.mjs +3 -4
- package/bin/install.prune.test.mjs +0 -1
- package/bin/install.question-presentation.test.mjs +0 -4
- package/bin/install.settings-defaults.test.mjs +0 -1
- package/bin/install.shared-settings.test.mjs +0 -1
- package/bin/install.test.mjs +14 -44
- package/bin/install.transaction.test.mjs +0 -8
- package/bin/install.uninstall-transaction.test.mjs +0 -2
- package/bin/install.update-keeps-hooks.test.mjs +0 -1
- package/docs/BDD_SCENARIO_QUALITY.md +1 -1
- package/docs/BDD_TEST_LAYOUT.md +1 -1
- package/docs/CODE_RULES.md +1 -1
- package/docs/rule-guides/deepstack-wording.md +3 -5
- package/docs/rule-guides/session-title.md +5 -4
- package/hooks/blocking/session_title_turn_gate.py +287 -0
- package/hooks/blocking/test_session_title_format_gate.py +5 -0
- package/hooks/blocking/test_session_title_turn_gate.py +264 -0
- package/hooks/features/conduct-gates.md +3 -0
- package/hooks/features/session-context.md +0 -4
- package/hooks/hooks.json +5 -5
- package/hooks/hooks_constants/mod_handoff_constants.py +0 -2
- package/hooks/hooks_constants/session_title_constants.py +41 -3
- package/hooks/lifecycle/nested_project_hooks.py +2 -2
- package/hooks/lifecycle/test_nested_project_hooks.py +2 -2
- package/hooks/session/compaction_user_messages.py +2 -2
- package/package.json +2 -2
- package/rules/deepstack-wording.md +3 -3
- package/scripts/ci/install-playtest.mjs +58 -314
- package/scripts/ci/install-playtest.test.mjs +24 -6
- package/scripts/policy_lint/adapter_configuration.py +1 -0
- package/scripts/policy_lint/config/constants.py +1 -0
- package/scripts/test_deepstack_review_criterion.py +2 -3
- package/scripts/test_deepstack_wording_rule.py +6 -4
- package/scripts/tests/test_adapter_configuration.py +8 -0
- package/.agents/skills/deepen/SKILL.md +0 -26
- package/commands/deepen.md +0 -14
- package/docs/decisions/deepstack-gate.md +0 -32
- package/hooks/hooks_constants/deepen_first_prompt_constants.py +0 -187
- package/hooks/session/deepen_default_rewrite.py +0 -128
- package/hooks/session/deepen_first_prompt.py +0 -383
- package/hooks/session/deepen_role_gate.py +0 -171
- package/hooks/session/deepen_turn_marker.py +0 -75
- package/hooks/session/fixtures/deepstack_replay.json +0 -15
- package/hooks/session/grade_deepstack_replay.py +0 -78
- package/hooks/session/test_deepen_default_rewrite.py +0 -90
- package/hooks/session/test_deepen_first_prompt.py +0 -452
- package/hooks/session/test_deepen_role_gate.py +0 -83
- package/hooks/session/test_deepen_turn_marker.py +0 -59
- package/hooks/session/test_grade_deepstack_replay.py +0 -47
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* The driver installs this package into a scratch home, reads the hook roster
|
|
6
6
|
* the install wrote into `settings.json`, starts one SessionStart hook and one
|
|
7
7
|
* PreToolUse hook from that installed tree against fixture payloads, and reads
|
|
8
|
-
* the output envelope each one returns.
|
|
8
|
+
* the output envelope each one returns. A last stage checks that the install
|
|
9
|
+
* left no deepen piece behind.
|
|
9
10
|
*
|
|
10
11
|
* Every stage writes the envelope it read to the evidence directory and prints
|
|
11
12
|
* one line carrying the stage name, the artifact path and the artifact's
|
|
@@ -31,6 +32,7 @@ import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from
|
|
|
31
32
|
import { tmpdir } from 'node:os';
|
|
32
33
|
import { join } from 'node:path';
|
|
33
34
|
import { pathToFileURL } from 'node:url';
|
|
35
|
+
import { RETIRED_HOOK_STAND_IN_MARKER } from '../../bin/install.mjs';
|
|
34
36
|
import {
|
|
35
37
|
createScratchHome,
|
|
36
38
|
removeScratchHome,
|
|
@@ -54,59 +56,16 @@ export const PRE_TOOL_USE_EVENT_NAME = 'PreToolUse';
|
|
|
54
56
|
export const SESSION_START_HOOK_SCRIPT_NAME = 'working_style_prompt.py';
|
|
55
57
|
export const BASH_HOOK_SCRIPT_NAME = 'bash_pre_tool_use_dispatcher.py';
|
|
56
58
|
export const BASH_MATCHER = 'Bash';
|
|
57
|
-
export const
|
|
58
|
-
export const
|
|
59
|
-
export const
|
|
60
|
-
export const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
export const DEEPEN_STUB_REWRITE = [
|
|
68
|
-
'SCOPE (role): Ship a contrast change the screen can show',
|
|
69
|
-
'EXAMPLES (illustration only): none',
|
|
70
|
-
'ZOOM-IN BLOCKER: none',
|
|
71
|
-
'Omitted: ship contrast tokens with the theme.',
|
|
72
|
-
'Source: src/settings/theme.ts',
|
|
73
|
-
'Methods: existing token file and a visual check.',
|
|
74
|
-
'Proof: screenshot of the settings page in dark mode.',
|
|
75
|
-
].join('\n');
|
|
76
|
-
export const CLAUDE_SUBAGENT_PROMPT_PREFIX = (
|
|
77
|
-
'Before any other work, invoke the poteto-mode skill with the Skill tool. '
|
|
78
|
-
+ 'Every prompt you write for a subagent opens with the same instruction.'
|
|
79
|
-
);
|
|
80
|
-
export const CLOUD_GAP_EVIDENCE = {
|
|
81
|
-
cursorCloud: {
|
|
82
|
-
doc: 'https://cursor.com/docs/hooks',
|
|
83
|
-
quotes: [
|
|
84
|
-
'User-level hooks (`~/.cursor/hooks.json`) are not available in cloud agents. Cloud agent VMs don\'t have access to your local home directory configuration.',
|
|
85
|
-
'sessionStart: Deferred while cloud agents can still start in a read-only environment. Hooks don\'t load there, so a cloud sessionStart would fire too late (after the first write) rather than at true session start.',
|
|
86
|
-
'beforeSubmitPrompt output is continue and user_message only.',
|
|
87
|
-
],
|
|
88
|
-
path: 'the deepen skill under ~/.cursor/skills',
|
|
89
|
-
},
|
|
90
|
-
codexCloud: {
|
|
91
|
-
doc: 'https://learn.chatgpt.com/docs/hooks',
|
|
92
|
-
quotes: [
|
|
93
|
-
'Command/shell, prompt, and agent handlers; hooks from local configuration, plugins, or local directories; environment-scoped hooks; and SessionEnd MCP hooks are not supported with cloud orchestration, even when tools execute locally.',
|
|
94
|
-
],
|
|
95
|
-
},
|
|
96
|
-
claudeUserPromptSubmit: {
|
|
97
|
-
doc: 'https://code.claude.com/docs/en/hooks',
|
|
98
|
-
quotes: [
|
|
99
|
-
'UserPromptSubmit: can\'t replace the prompt; it only injects additionalContext alongside it',
|
|
100
|
-
],
|
|
101
|
-
},
|
|
102
|
-
claudePromptSubmitMod: {
|
|
103
|
-
doc: 'https://code.claude.com/docs/en/plugins/mods/events',
|
|
104
|
-
quotes: [
|
|
105
|
-
'Rewrite the prompt. The transcript and your prompt history show the new text. next({ ...e, text: newText })',
|
|
106
|
-
'A cloud session: Yes, for a plugin that reaches the cloud session. This package still keeps the Python hook as the cloud fallback because mods that replace settings hooks stay off when CLAUDE_CODE_REMOTE is true.',
|
|
107
|
-
],
|
|
108
|
-
},
|
|
109
|
-
};
|
|
59
|
+
export const RETIRED_DEEPEN_STAGE_NAME = 'retired_deepen_absent';
|
|
60
|
+
export const RETIRED_DEEPEN_HOOK_SCRIPT_NAME = 'deepen_first_prompt.py';
|
|
61
|
+
export const RETIRED_DEEPEN_CODEX_BLOCK_START = '<!-- claude-dev-env deepen: start -->';
|
|
62
|
+
export const ALL_RETIRED_DEEPEN_RELATIVE_PATHS = Object.freeze([
|
|
63
|
+
join('.claude', 'skills', 'deepen'),
|
|
64
|
+
join('.agents', 'skills', 'deepen'),
|
|
65
|
+
join('.claude', 'commands', 'deepen.md'),
|
|
66
|
+
join('.claude', 'hooks', 'session', RETIRED_DEEPEN_HOOK_SCRIPT_NAME),
|
|
67
|
+
join('.cursor', 'skills', 'deepen'),
|
|
68
|
+
]);
|
|
110
69
|
|
|
111
70
|
export const SESSION_START_PAYLOAD = {
|
|
112
71
|
hook_event_name: SESSION_START_EVENT_NAME,
|
|
@@ -280,273 +239,68 @@ function runSessionStartStage(settingsPath, scratchHome, evidenceDirectory) {
|
|
|
280
239
|
if (typeof envelope.additionalContext !== 'string' || envelope.additionalContext.length === 0) {
|
|
281
240
|
throw new Error('The SessionStart envelope carries no additionalContext');
|
|
282
241
|
}
|
|
283
|
-
if (envelope.additionalContext.includes(DEEPEN_MARKER)) {
|
|
284
|
-
throw new Error('The SessionStart envelope repeated first-task deepen text');
|
|
285
|
-
}
|
|
286
242
|
const evidencePath = writeEvidence(evidenceDirectory, SESSION_START_STAGE_NAME, JSON.stringify(envelope));
|
|
287
243
|
reportStage(SESSION_START_STAGE_NAME, evidencePath);
|
|
288
244
|
}
|
|
289
245
|
|
|
290
|
-
function
|
|
291
|
-
|
|
292
|
-
hook_event_name: USER_PROMPT_SUBMIT_EVENT_NAME,
|
|
293
|
-
prompt,
|
|
294
|
-
session_id: 'playtest-session',
|
|
295
|
-
};
|
|
296
|
-
if (transcriptPath) payload.transcript_path = transcriptPath;
|
|
297
|
-
return payload;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
function deepenHookEnvironment(extraEnvironment = {}) {
|
|
301
|
-
return {
|
|
302
|
-
CDE_DEEPEN_REWRITE: DEEPEN_STUB_REWRITE,
|
|
303
|
-
...extraEnvironment,
|
|
304
|
-
};
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
function parseJsonDecision(stdout) {
|
|
308
|
-
const trimmed = stdout.trim();
|
|
309
|
-
if (!trimmed) return { decision: 'silent' };
|
|
310
|
-
return JSON.parse(trimmed);
|
|
246
|
+
function readTextWhenPresent(filePath) {
|
|
247
|
+
return existsSync(filePath) ? readFileSync(filePath, 'utf8') : '';
|
|
311
248
|
}
|
|
312
249
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
command,
|
|
322
|
-
scratchHome,
|
|
323
|
-
userPromptPayload('Add dark mode to the settings page', transcriptPath),
|
|
324
|
-
extraEnvironment,
|
|
325
|
-
);
|
|
326
|
-
const envelope = readEnvelope(envelopeResult.stdout, USER_PROMPT_SUBMIT_EVENT_NAME);
|
|
327
|
-
if (!String(envelope.additionalContext || '').includes(DEEPEN_MARKER)) {
|
|
328
|
-
throw new Error('The UserPromptSubmit envelope carries no deepen marker');
|
|
329
|
-
}
|
|
330
|
-
if (!String(envelope.additionalContext).includes('src/settings/theme.ts')) {
|
|
331
|
-
throw new Error('The UserPromptSubmit envelope omitted the stub source');
|
|
332
|
-
}
|
|
333
|
-
const evidencePath = writeEvidence(
|
|
334
|
-
evidenceDirectory,
|
|
335
|
-
DEEPEN_STAGE_NAME,
|
|
336
|
-
JSON.stringify(envelope),
|
|
337
|
-
);
|
|
338
|
-
reportStage(DEEPEN_STAGE_NAME, evidencePath);
|
|
339
|
-
return { command, transcriptPath };
|
|
250
|
+
/**
|
|
251
|
+
* Whether a path holds the inert stand-in the installer leaves for a stale registration.
|
|
252
|
+
*
|
|
253
|
+
* @param {string} absolutePath Path to read.
|
|
254
|
+
* @returns {boolean} True when the file carries the stand-in marker.
|
|
255
|
+
*/
|
|
256
|
+
function isRetiredHookStandIn(absolutePath) {
|
|
257
|
+
return absolutePath.endsWith('.py') && readTextWhenPresent(absolutePath).includes(RETIRED_HOOK_STAND_IN_MARKER);
|
|
340
258
|
}
|
|
341
259
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
260
|
+
/**
|
|
261
|
+
* Check that the install left no deepen piece behind.
|
|
262
|
+
*
|
|
263
|
+
* The deepen skill, the /deepen command, the first-task hook and its
|
|
264
|
+
* registrations in Claude, Codex and Cursor, and the Codex AGENTS.md block
|
|
265
|
+
* were all retired. Any one of them in the scratch home fails the stage.
|
|
266
|
+
*
|
|
267
|
+
* @param {string} scratchHome Scratch home the install wrote into.
|
|
268
|
+
* @param {string} evidenceDirectory Directory the stage writes its evidence to.
|
|
269
|
+
* @returns {void}
|
|
270
|
+
* @throws {Error} The install left a deepen piece.
|
|
271
|
+
*/
|
|
272
|
+
function runRetiredDeepenStage(scratchHome, evidenceDirectory) {
|
|
273
|
+
const allLeftoverPaths = ALL_RETIRED_DEEPEN_RELATIVE_PATHS
|
|
274
|
+
.map((relativePath) => join(scratchHome, relativePath))
|
|
275
|
+
.filter((absolutePath) => existsSync(absolutePath))
|
|
276
|
+
.filter((absolutePath) => !isRetiredHookStandIn(absolutePath));
|
|
277
|
+
if (allLeftoverPaths.length > 0) {
|
|
278
|
+
throw new Error(`The install left retired deepen paths: ${allLeftoverPaths.join(', ')}`);
|
|
356
279
|
}
|
|
357
|
-
const
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
);
|
|
362
|
-
reportStage(DEEPEN_REWRITE_STAGE_NAME, evidencePath);
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
function runDeepenSkipStage(command, scratchHome, evidenceDirectory, firstTranscriptPath) {
|
|
366
|
-
const jsonEnvironment = deepenHookEnvironment({ CDE_DEEPEN_OUTPUT: 'json' });
|
|
367
|
-
const cases = [
|
|
368
|
-
{
|
|
369
|
-
name: 'thanks',
|
|
370
|
-
payload: userPromptPayload('thanks', join(evidenceDirectory, 'thanks.jsonl')),
|
|
371
|
-
extraEnvironment: jsonEnvironment,
|
|
372
|
-
expected: { decision: 'skip', reason: 'trivial' },
|
|
373
|
-
},
|
|
374
|
-
{
|
|
375
|
-
name: 'slash',
|
|
376
|
-
payload: userPromptPayload('/sr-loop', join(evidenceDirectory, 'slash.jsonl')),
|
|
377
|
-
extraEnvironment: jsonEnvironment,
|
|
378
|
-
expected: { decision: 'skip', reason: 'slash-command' },
|
|
379
|
-
},
|
|
380
|
-
{
|
|
381
|
-
name: 'spawn',
|
|
382
|
-
payload: userPromptPayload(
|
|
383
|
-
`${CLAUDE_SUBAGENT_PROMPT_PREFIX}\n\nReply leaf.`,
|
|
384
|
-
join(evidenceDirectory, 'spawn.jsonl'),
|
|
385
|
-
),
|
|
386
|
-
extraEnvironment: jsonEnvironment,
|
|
387
|
-
expected: { decision: 'skip', reason: 'spawn-rewrite' },
|
|
388
|
-
},
|
|
389
|
-
{
|
|
390
|
-
name: 'skip-flag',
|
|
391
|
-
payload: userPromptPayload('Add dark mode to the settings page', join(evidenceDirectory, 'skip-flag.jsonl')),
|
|
392
|
-
extraEnvironment: deepenHookEnvironment({ CDE_DEEPEN_OUTPUT: 'json', CDE_SKIP_DEEPEN: '1' }),
|
|
393
|
-
expected: { decision: 'skip', reason: 'skip-flag' },
|
|
394
|
-
},
|
|
395
|
-
{
|
|
396
|
-
name: 'later-turn',
|
|
397
|
-
payload: userPromptPayload('and also the billing page', firstTranscriptPath),
|
|
398
|
-
extraEnvironment: jsonEnvironment,
|
|
399
|
-
expected: { decision: 'skip', reason: 'later-turn' },
|
|
400
|
-
},
|
|
401
|
-
{
|
|
402
|
-
name: 'marker',
|
|
403
|
-
payload: userPromptPayload(`${DEEPEN_MARKER}\n\nDo the job.`, join(evidenceDirectory, 'marker.jsonl')),
|
|
404
|
-
extraEnvironment: jsonEnvironment,
|
|
405
|
-
expected: { decision: 'skip', reason: 'marker-present' },
|
|
406
|
-
},
|
|
407
|
-
{
|
|
408
|
-
name: 'loop',
|
|
409
|
-
payload: userPromptPayload('/loop until tests pass', join(evidenceDirectory, 'loop.jsonl')),
|
|
410
|
-
extraEnvironment: jsonEnvironment,
|
|
411
|
-
expected: { decision: 'skip', reason: 'loop-or-helper' },
|
|
412
|
-
},
|
|
413
|
-
{
|
|
414
|
-
name: 'deepen-agent',
|
|
415
|
-
payload: userPromptPayload(
|
|
416
|
-
`${DEEPEN_AGENT_MARKER}\nAnswer the four deepening questions.`,
|
|
417
|
-
join(evidenceDirectory, 'agent.jsonl'),
|
|
418
|
-
),
|
|
419
|
-
extraEnvironment: jsonEnvironment,
|
|
420
|
-
expected: { decision: 'skip', reason: 'deepen-agent' },
|
|
421
|
-
},
|
|
280
|
+
const allHookConfigurationPaths = [
|
|
281
|
+
join(scratchHome, SETTINGS_RELATIVE_PATH),
|
|
282
|
+
join(scratchHome, '.codex', 'hooks.json'),
|
|
283
|
+
join(scratchHome, '.cursor', 'hooks.json'),
|
|
422
284
|
];
|
|
423
|
-
const
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
scratchHome,
|
|
428
|
-
eachCase.payload,
|
|
429
|
-
eachCase.extraEnvironment,
|
|
430
|
-
);
|
|
431
|
-
const decision = parseJsonDecision(hookResult.stdout);
|
|
432
|
-
if (decision.decision !== eachCase.expected.decision || decision.reason !== eachCase.expected.reason) {
|
|
433
|
-
throw new Error(
|
|
434
|
-
`Skip ${eachCase.name} returned ${JSON.stringify(decision)} expected ${JSON.stringify(eachCase.expected)}`,
|
|
435
|
-
);
|
|
436
|
-
}
|
|
437
|
-
results.push({ name: eachCase.name, decision });
|
|
438
|
-
}
|
|
439
|
-
const silentThanks = runInstalledHook(
|
|
440
|
-
command,
|
|
441
|
-
scratchHome,
|
|
442
|
-
userPromptPayload('thanks', join(evidenceDirectory, 'thanks-silent.jsonl')),
|
|
443
|
-
deepenHookEnvironment(),
|
|
444
|
-
);
|
|
445
|
-
if (silentThanks.stdout.trim() !== '') {
|
|
446
|
-
throw new Error('A skipped prompt printed command-hook output');
|
|
447
|
-
}
|
|
448
|
-
const evidencePath = writeEvidence(
|
|
449
|
-
evidenceDirectory,
|
|
450
|
-
DEEPEN_SKIP_STAGE_NAME,
|
|
451
|
-
JSON.stringify({ results, silentThanks: silentThanks.stdout }),
|
|
452
|
-
);
|
|
453
|
-
reportStage(DEEPEN_SKIP_STAGE_NAME, evidencePath);
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
function runDeepenNestedStage(settingsPath, scratchHome, evidenceDirectory) {
|
|
457
|
-
const nestedCommand = selectHookCommand(
|
|
458
|
-
hookCommandsForEvent(settingsPath, USER_PROMPT_SUBMIT_EVENT_NAME),
|
|
459
|
-
NESTED_HOOK_SCRIPT_NAME,
|
|
460
|
-
);
|
|
461
|
-
const deepenCommand = selectHookCommand(
|
|
462
|
-
hookCommandsForEvent(settingsPath, USER_PROMPT_SUBMIT_EVENT_NAME),
|
|
463
|
-
DEEPEN_HOOK_SCRIPT_NAME,
|
|
464
|
-
);
|
|
465
|
-
const sessionDirectory = join(evidenceDirectory, 'nested-session');
|
|
466
|
-
const checkoutDirectory = join(sessionDirectory, 'alpha');
|
|
467
|
-
mkdirSync(join(checkoutDirectory, '.git'), { recursive: true });
|
|
468
|
-
mkdirSync(join(checkoutDirectory, '.claude'), { recursive: true });
|
|
469
|
-
writeFileSync(
|
|
470
|
-
join(checkoutDirectory, '.claude', 'settings.json'),
|
|
471
|
-
`${JSON.stringify({
|
|
472
|
-
hooks: {
|
|
473
|
-
UserPromptSubmit: [
|
|
474
|
-
{
|
|
475
|
-
matcher: '',
|
|
476
|
-
hooks: [{ type: 'command', command: deepenCommand, timeout: 10 }],
|
|
477
|
-
},
|
|
478
|
-
],
|
|
479
|
-
},
|
|
480
|
-
}, null, 2)}\n`,
|
|
481
|
-
);
|
|
482
|
-
const hookResult = spawnSync(nestedCommand, {
|
|
483
|
-
shell: true,
|
|
484
|
-
input: JSON.stringify(userPromptPayload(
|
|
485
|
-
'Add dark mode to the settings page',
|
|
486
|
-
join(evidenceDirectory, 'nested.jsonl'),
|
|
487
|
-
)),
|
|
488
|
-
encoding: 'utf8',
|
|
489
|
-
cwd: sessionDirectory,
|
|
490
|
-
timeout: HOOK_TIMEOUT_MILLISECONDS,
|
|
491
|
-
env: {
|
|
492
|
-
...scratchHomeEnvironment(scratchHome),
|
|
493
|
-
...deepenHookEnvironment(),
|
|
494
|
-
CLAUDE_PROJECT_DIR: sessionDirectory,
|
|
495
|
-
},
|
|
496
|
-
});
|
|
497
|
-
const envelope = readEnvelope(hookResult.stdout, USER_PROMPT_SUBMIT_EVENT_NAME);
|
|
498
|
-
if (!String(envelope.additionalContext || '').includes(DEEPEN_MARKER)) {
|
|
499
|
-
throw new Error('The nested home-folder session carried no deepen marker');
|
|
500
|
-
}
|
|
501
|
-
const evidencePath = writeEvidence(
|
|
502
|
-
evidenceDirectory,
|
|
503
|
-
DEEPEN_NESTED_STAGE_NAME,
|
|
504
|
-
JSON.stringify(envelope),
|
|
505
|
-
);
|
|
506
|
-
reportStage(DEEPEN_NESTED_STAGE_NAME, evidencePath);
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
function runDeepenInstallSurfaceStage(scratchHome, evidenceDirectory) {
|
|
510
|
-
const settingsPath = join(scratchHome, SETTINGS_RELATIVE_PATH);
|
|
511
|
-
const userPromptCommands = hookCommandsForEvent(settingsPath, USER_PROMPT_SUBMIT_EVENT_NAME);
|
|
512
|
-
if (!userPromptCommands.some((each) => each.command.includes(DEEPEN_HOOK_SCRIPT_NAME))) {
|
|
513
|
-
throw new Error('The installed Claude settings declare no deepen_first_prompt.py');
|
|
514
|
-
}
|
|
515
|
-
const codexHooksPath = join(scratchHome, '.codex', 'hooks.json');
|
|
516
|
-
const codexHooks = JSON.parse(readFileSync(codexHooksPath, 'utf8'));
|
|
517
|
-
const codexUserPrompt = JSON.stringify(codexHooks.hooks?.UserPromptSubmit ?? []);
|
|
518
|
-
if (!codexUserPrompt.includes(DEEPEN_HOOK_SCRIPT_NAME)) {
|
|
519
|
-
throw new Error('The installed Codex hooks declare no UserPromptSubmit deepen_first_prompt.py');
|
|
520
|
-
}
|
|
521
|
-
const cursorSkillPath = join(scratchHome, '.cursor', 'skills', 'deepen', 'SKILL.md');
|
|
522
|
-
if (!existsSync(cursorSkillPath)) {
|
|
523
|
-
throw new Error(`The install wrote no ${cursorSkillPath}`);
|
|
285
|
+
const allRegisteringPaths = allHookConfigurationPaths
|
|
286
|
+
.filter((configurationPath) => readTextWhenPresent(configurationPath).includes(RETIRED_DEEPEN_HOOK_SCRIPT_NAME));
|
|
287
|
+
if (allRegisteringPaths.length > 0) {
|
|
288
|
+
throw new Error(`The install registered ${RETIRED_DEEPEN_HOOK_SCRIPT_NAME} in ${allRegisteringPaths.join(', ')}`);
|
|
524
289
|
}
|
|
525
290
|
const codexAgentsPath = join(scratchHome, '.codex', 'AGENTS.md');
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
throw new Error(`The install wrote no deepen block into ${codexAgentsPath}`);
|
|
291
|
+
if (readTextWhenPresent(codexAgentsPath).includes(RETIRED_DEEPEN_CODEX_BLOCK_START)) {
|
|
292
|
+
throw new Error(`The install wrote the deepen block into ${codexAgentsPath}`);
|
|
529
293
|
}
|
|
530
294
|
const evidencePath = writeEvidence(
|
|
531
295
|
evidenceDirectory,
|
|
532
|
-
|
|
296
|
+
RETIRED_DEEPEN_STAGE_NAME,
|
|
533
297
|
JSON.stringify({
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
cursorSkillPath,
|
|
298
|
+
checkedPaths: ALL_RETIRED_DEEPEN_RELATIVE_PATHS,
|
|
299
|
+
checkedHookConfigurations: allHookConfigurationPaths,
|
|
537
300
|
codexAgentsPath,
|
|
538
301
|
}),
|
|
539
302
|
);
|
|
540
|
-
reportStage(
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
function runDeepenCloudGapStage(evidenceDirectory) {
|
|
544
|
-
const evidencePath = writeEvidence(
|
|
545
|
-
evidenceDirectory,
|
|
546
|
-
DEEPEN_CLOUD_GAP_STAGE_NAME,
|
|
547
|
-
JSON.stringify(CLOUD_GAP_EVIDENCE),
|
|
548
|
-
);
|
|
549
|
-
reportStage(DEEPEN_CLOUD_GAP_STAGE_NAME, evidencePath);
|
|
303
|
+
reportStage(RETIRED_DEEPEN_STAGE_NAME, evidencePath);
|
|
550
304
|
}
|
|
551
305
|
|
|
552
306
|
function runBlockingStage(settingsPath, scratchHome, evidenceDirectory) {
|
|
@@ -592,18 +346,8 @@ export function runPlaytest(argv) {
|
|
|
592
346
|
runSessionStartStage(settingsPath, scratchHome, evidenceDirectory);
|
|
593
347
|
stageName = BLOCKING_STAGE_NAME;
|
|
594
348
|
runBlockingStage(settingsPath, scratchHome, evidenceDirectory);
|
|
595
|
-
stageName =
|
|
596
|
-
|
|
597
|
-
stageName = DEEPEN_STAGE_NAME;
|
|
598
|
-
const deepen = runDeepenFirstTaskStage(settingsPath, scratchHome, evidenceDirectory);
|
|
599
|
-
stageName = DEEPEN_REWRITE_STAGE_NAME;
|
|
600
|
-
runDeepenRewriteStage(deepen.command, scratchHome, evidenceDirectory);
|
|
601
|
-
stageName = DEEPEN_SKIP_STAGE_NAME;
|
|
602
|
-
runDeepenSkipStage(deepen.command, scratchHome, evidenceDirectory, deepen.transcriptPath);
|
|
603
|
-
stageName = DEEPEN_NESTED_STAGE_NAME;
|
|
604
|
-
runDeepenNestedStage(settingsPath, scratchHome, evidenceDirectory);
|
|
605
|
-
stageName = DEEPEN_CLOUD_GAP_STAGE_NAME;
|
|
606
|
-
runDeepenCloudGapStage(evidenceDirectory);
|
|
349
|
+
stageName = RETIRED_DEEPEN_STAGE_NAME;
|
|
350
|
+
runRetiredDeepenStage(scratchHome, evidenceDirectory);
|
|
607
351
|
} catch (error) {
|
|
608
352
|
reportFailure(stageName, error instanceof Error ? error.message : String(error));
|
|
609
353
|
return 1;
|
|
@@ -7,6 +7,7 @@ import test from 'node:test';
|
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
8
|
|
|
9
9
|
import { parseArguments, readEnvelope, selectHookCommand } from './install-playtest.mjs';
|
|
10
|
+
import { RETIRED_HOOK_STAND_IN_MARKER } from '../../bin/install.mjs';
|
|
10
11
|
import { SANDBOX_GIT_CONFIG_BODY, SANDBOX_GIT_CONFIG_NAME } from './scratch-home-install.mjs';
|
|
11
12
|
|
|
12
13
|
const CI_DIRECTORY = dirname(fileURLToPath(import.meta.url));
|
|
@@ -30,12 +31,7 @@ test('the driver reads an envelope from every stage of a fresh install, and repo
|
|
|
30
31
|
assert.match(greenRun.stdout, /\[PASS\] playtest install .* sha256=[0-9a-f]{64}/);
|
|
31
32
|
assert.match(greenRun.stdout, /\[PASS\] playtest session_start_hook .* sha256=[0-9a-f]{64}/);
|
|
32
33
|
assert.match(greenRun.stdout, /\[PASS\] playtest blocking_hook .* sha256=[0-9a-f]{64}/);
|
|
33
|
-
assert.match(greenRun.stdout, /\[PASS\] playtest
|
|
34
|
-
assert.match(greenRun.stdout, /\[PASS\] playtest deepen_first_prompt .* sha256=[0-9a-f]{64}/);
|
|
35
|
-
assert.match(greenRun.stdout, /\[PASS\] playtest deepen_mod_text .* sha256=[0-9a-f]{64}/);
|
|
36
|
-
assert.match(greenRun.stdout, /\[PASS\] playtest deepen_skips .* sha256=[0-9a-f]{64}/);
|
|
37
|
-
assert.match(greenRun.stdout, /\[PASS\] playtest deepen_nested_home .* sha256=[0-9a-f]{64}/);
|
|
38
|
-
assert.match(greenRun.stdout, /\[PASS\] playtest deepen_cloud_gaps .* sha256=[0-9a-f]{64}/);
|
|
34
|
+
assert.match(greenRun.stdout, /\[PASS\] playtest retired_deepen_absent .* sha256=[0-9a-f]{64}/);
|
|
39
35
|
const blockingEnvelope = JSON.parse(readFileSync(join(evidenceDirectory, 'blocking_hook.json'), 'utf8'));
|
|
40
36
|
assert.equal(Object.hasOwn(blockingEnvelope, 'permissionDecision'), false);
|
|
41
37
|
assert.ok(
|
|
@@ -90,3 +86,25 @@ test('readEnvelope refuses output that names another event or no envelope at all
|
|
|
90
86
|
/names PreToolUse rather than SessionStart/,
|
|
91
87
|
);
|
|
92
88
|
});
|
|
89
|
+
|
|
90
|
+
test('the retired deepen stage accepts the installer stand-in and fails on a restored deepen hook', () => {
|
|
91
|
+
const scratchHome = mkdtempSync(join(tmpdir(), 'cde-playtest-home-'));
|
|
92
|
+
const evidenceDirectory = mkdtempSync(join(tmpdir(), 'cde-playtest-evidence-'));
|
|
93
|
+
const retiredHookPath = join(scratchHome, '.claude', 'hooks', 'session', 'deepen_first_prompt.py');
|
|
94
|
+
try {
|
|
95
|
+
const installRun = runDriver(['--home', scratchHome, '--evidence', evidenceDirectory]);
|
|
96
|
+
assert.equal(installRun.status, 0, `${installRun.stdout}\n${installRun.stderr}`);
|
|
97
|
+
|
|
98
|
+
writeFileSync(retiredHookPath, `"""${RETIRED_HOOK_STAND_IN_MARKER}."""\n`);
|
|
99
|
+
const standInRun = runDriver(['--home', scratchHome, '--evidence', evidenceDirectory, '--skip-install']);
|
|
100
|
+
assert.match(standInRun.stdout, /\[PASS\] playtest retired_deepen_absent/);
|
|
101
|
+
|
|
102
|
+
writeFileSync(retiredHookPath, SILENT_HOOK_SOURCE);
|
|
103
|
+
const restoredRun = runDriver(['--home', scratchHome, '--evidence', evidenceDirectory, '--skip-install']);
|
|
104
|
+
assert.equal(restoredRun.status, 1, `${restoredRun.stdout}\n${restoredRun.stderr}`);
|
|
105
|
+
assert.match(restoredRun.stdout, /\[FAIL\] playtest retired_deepen_absent The install left retired deepen paths/);
|
|
106
|
+
} finally {
|
|
107
|
+
rmSync(scratchHome, { recursive: true, force: true });
|
|
108
|
+
rmSync(evidenceDirectory, { recursive: true, force: true });
|
|
109
|
+
}
|
|
110
|
+
});
|
|
@@ -140,6 +140,7 @@ def _names_exempt_registration_path(registered_string: str) -> bool:
|
|
|
140
140
|
hooks/blocking/merge_over_review_offer_gate.py -> exempt
|
|
141
141
|
hooks/blocking/final_reply_gate.py -> exempt
|
|
142
142
|
hooks/blocking/open_tasks_stop_gate.py -> exempt
|
|
143
|
+
hooks/blocking/session_title_turn_gate.py -> exempt
|
|
143
144
|
hooks/blocking/some_new_blocker.py -> flagged
|
|
144
145
|
|
|
145
146
|
The Bash PreToolUse dispatcher sits under ``blocking/`` for layout reasons
|
|
@@ -79,6 +79,7 @@ ALL_ACTION_BOUNDARY_EXEMPT_REGISTRATION_PATHS = (
|
|
|
79
79
|
"blocking/merge_over_review_offer_gate.py",
|
|
80
80
|
"blocking/final_reply_gate.py",
|
|
81
81
|
"blocking/open_tasks_stop_gate.py",
|
|
82
|
+
"blocking/session_title_turn_gate.py",
|
|
82
83
|
)
|
|
83
84
|
ALL_TEST_DIRECTORY_NAMES = frozenset({"tests"})
|
|
84
85
|
ALL_TEST_FILE_PREFIXES = ("test_",)
|
|
@@ -7,9 +7,8 @@ from pathlib import Path
|
|
|
7
7
|
_RULES = Path(__file__).resolve().parent.parent / "docs" / "CODE_RULES.md"
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
def
|
|
10
|
+
def test_new_instruction_criterion_names_the_manifest() -> None:
|
|
11
11
|
text = _RULES.read_text(encoding="utf-8")
|
|
12
12
|
assert "## 11.7 A NEW INSTRUCTION NAMES THE TRIGGER THAT COVERS IT" in text
|
|
13
|
-
assert "hooks/session/deepen_first_prompt.py" in text
|
|
14
13
|
assert "rules-archived/ARCHIVE-MANIFEST.md" in text
|
|
15
|
-
assert "
|
|
14
|
+
assert "deepen_first_prompt.py" not in text
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""The wording rule
|
|
1
|
+
"""The wording rule keeps the three DeepStack labels and cites no retired gate."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -7,13 +7,15 @@ from pathlib import Path
|
|
|
7
7
|
_PACKAGE_ROOT = Path(__file__).resolve().parent.parent
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
def
|
|
10
|
+
def test_wording_rule_keeps_the_labels_and_drops_the_retired_gate() -> None:
|
|
11
11
|
rule_text = (_PACKAGE_ROOT / "rules" / "deepstack-wording.md").read_text(encoding="utf-8")
|
|
12
12
|
guide_text = (_PACKAGE_ROOT / "docs" / "rule-guides" / "deepstack-wording.md").read_text(
|
|
13
13
|
encoding="utf-8"
|
|
14
14
|
)
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
for each_text in (rule_text, guide_text):
|
|
16
|
+
assert "deepen_first_prompt.py" not in each_text
|
|
17
|
+
assert "CDE_DEEPEN" not in each_text
|
|
18
|
+
assert "**Enforcement:** none." in rule_text
|
|
17
19
|
assert "SCOPE (role):" in guide_text
|
|
18
20
|
assert "EXAMPLES (illustration only):" in guide_text
|
|
19
21
|
assert "ZOOM-IN BLOCKER:" in guide_text
|
|
@@ -138,6 +138,14 @@ def test_new_session_title_gate_registration_is_clean(tmp_path: Path) -> None:
|
|
|
138
138
|
assert adapters.hook_configuration_diagnostics(current_document, tmp_path) == ()
|
|
139
139
|
|
|
140
140
|
|
|
141
|
+
def test_new_session_title_turn_gate_registration_is_clean(tmp_path: Path) -> None:
|
|
142
|
+
current_document = _hook_document(
|
|
143
|
+
["hooks/blocking/session_title_turn_gate.py"],
|
|
144
|
+
'{"hooks": {}}',
|
|
145
|
+
)
|
|
146
|
+
assert adapters.hook_configuration_diagnostics(current_document, tmp_path) == ()
|
|
147
|
+
|
|
148
|
+
|
|
141
149
|
def test_new_artifact_page_gate_registration_is_clean(tmp_path: Path) -> None:
|
|
142
150
|
current_document = _hook_document(
|
|
143
151
|
["hooks/blocking/artifact_dark_mode_gate.py"],
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: deepen
|
|
3
|
-
description: >-
|
|
4
|
-
Deepen a first user task before work starts. Triggers: /deepen, deepen this
|
|
5
|
-
task, first-task deepening, Cursor Cloud launch prompt. DeepStack intake
|
|
6
|
-
holds the contract.
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# deepen
|
|
10
|
-
|
|
11
|
-
On a first task, `/deepen` plus a task, or a routine wake that has work, read DeepStack intake and follow it: [intake](https://github.com/jl-cmd-projects/DeepStack/blob/main/plugins/deepstack/skills/deepstack-mode/references/intake.md).
|
|
12
|
-
|
|
13
|
-
Skip when the prompt already carries `CDE_DEEPEN:v1`, when the text is a thanks or yes/no or status line, when it is a slash command other than `/deepen`, when it is a `/loop` or helper report, or when it is a spawn rewrite that already starts with the poteto-mode prefix.
|
|
14
|
-
|
|
15
|
-
A prompt that is only `/deepen` with no task is skipped. `/deepen` plus a task deepens the remainder.
|
|
16
|
-
|
|
17
|
-
The deepening pass writes the worker prompt and stops. The worker runs that prompt.
|
|
18
|
-
|
|
19
|
-
When you spawn a helper to fill the four answers, that helper's prompt starts with `CDE_DEEPEN_AGENT` so the first-task hook leaves it alone.
|
|
20
|
-
|
|
21
|
-
## Hosts
|
|
22
|
-
|
|
23
|
-
- Claude desktop and CLI: a `prompt.submit` mod rewrites the visible text. The Python `UserPromptSubmit` hook is silent when that mod is on outside a cloud session.
|
|
24
|
-
- Claude cloud and Codex local: the Python hook injects `additionalContext`. Command hooks cannot replace the prompt string.
|
|
25
|
-
- Cursor Cloud: run DeepStack intake on the launch prompt. The host adapter for the three lines and the refusal lives with that host. This package keeps the gate.
|
|
26
|
-
- Codex cloud orchestration skips command hooks. Local Codex only.
|
package/commands/deepen.md
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: Deepen a task into the standing job before work starts
|
|
3
|
-
argument-hint: [task to deepen]
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
Deepen this task before any work starts: `$ARGUMENTS`
|
|
7
|
-
|
|
8
|
-
When `$ARGUMENTS` is empty, ask the user to paste the task and stop.
|
|
9
|
-
|
|
10
|
-
When `$ARGUMENTS` holds a task, follow DeepStack intake at https://github.com/jl-cmd-projects/DeepStack/blob/main/plugins/deepstack/skills/deepstack-mode/references/intake.md.
|
|
11
|
-
|
|
12
|
-
Write `SCOPE (role):`, `EXAMPLES (illustration only):`, and `ZOOM-IN BLOCKER:` before any plan. Put `CDE_DEEPEN:v1` on line 1 of the worker prompt and carry the scope line there. The deepening pass writes that prompt and stops.
|
|
13
|
-
|
|
14
|
-
Skip this command when the prompt already carries `CDE_DEEPEN:v1`.
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
# DeepStack gate
|
|
2
|
-
|
|
3
|
-
The first-task hook is the control. A rewrite ships only when `refusal_reason` in `hooks/session/deepen_role_gate.py` returns no reason. DeepStack, the wording rule, and the review rule cite this function. They do not restate it.
|
|
4
|
-
|
|
5
|
-
## What the unchanged hook did
|
|
6
|
-
|
|
7
|
-
Recorded before this gate edited `deepen_first_prompt.py`, with `CDE_DEEPEN_OUTPUT=json` on stdin.
|
|
8
|
-
|
|
9
|
-
The paste path, a first task and no rewrite stub, printed `decision` `deepen`. The rewritten prompt restated the four question stems and ended by telling the same agent to do the job.
|
|
10
|
-
|
|
11
|
-
The filled four-answer stub printed `decision` `deepen`. The rewritten prompt carried the four answer lines and no role lines.
|
|
12
|
-
|
|
13
|
-
## What the gate refuses
|
|
14
|
-
|
|
15
|
-
| Reason | When |
|
|
16
|
-
|---|---|
|
|
17
|
-
| `restates-four-questions` | The rewrite contains every canonical question stem. The old paste path was this case. |
|
|
18
|
-
| `missing-role-lines` | `SCOPE (role):`, `EXAMPLES (illustration only):`, or `ZOOM-IN BLOCKER:` is missing or blank. |
|
|
19
|
-
| `example-noun-in-scope` | The scope line contains an example noun and `ZOOM-IN BLOCKER:` is `none`. |
|
|
20
|
-
| `dropped-hard-constraint` | A bullet under `HARD CONSTRAINTS` in the task is absent as its own line in the rewrite. |
|
|
21
|
-
|
|
22
|
-
Json mode prints `{"decision": "refuse", "reason": "..."}`. Rewrite mode and the command-hook path print nothing, so the prompt-submit mod leaves the submitted text in place.
|
|
23
|
-
|
|
24
|
-
## The default rewrite
|
|
25
|
-
|
|
26
|
-
With no rewrite stub and no `CDE_DEEPEN_COMMAND`, `hooks/session/deepen_default_rewrite.py` builds the rewrite. It takes the scope from the task's own sentences, moves clauses the task marks as examples (`for example`, `for instance`, `e.g.`, `such as`) to the examples line, clips a scope over 240 characters with a pointer to the full task, copies the task with its hard constraints, and ends with one order to do the job. When the scope still names an example, the blocker line asks the worker to confirm before narrowing to it. The gate checks this rewrite like any other, so a first task deepens on every host with nothing configured.
|
|
27
|
-
|
|
28
|
-
## Proof shape
|
|
29
|
-
|
|
30
|
-
Run the hook on stdin with `CDE_DEEPEN_OUTPUT=json`. A stub that pastes the four question stems refuses, and the default rewrite deepens. A role rewrite that leaves the example noun out deepens. Removing `refusal_reason` from `decide_first_prompt` lets the paste path deepen again, and `test_should_refuse_the_paste_path_as_the_known_bad` fails. The wrong twin puts the example noun in the scope line with `ZOOM-IN BLOCKER: none` and is refused.
|
|
31
|
-
|
|
32
|
-
`hooks/session/grade_deepstack_replay.py` scores a fixture whose task names one failure as an example. It calls `refusal_reason` and does not rewrite.
|