claude-dev-env 8.88.0 → 8.88.2
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/bin/codex-trimmed-sol.mjs +37 -2
- package/bin/codex-trimmed-sol.test.mjs +39 -1
- package/bin/folder-lockers.mjs +142 -0
- package/bin/folder-lockers.test.mjs +55 -0
- package/bin/global-install-shadow.mjs +75 -0
- package/bin/global-install-shadow.test.mjs +56 -0
- package/bin/install-deepstack-plugin.mjs +45 -16
- package/bin/install-deepstack-plugin.test.mjs +65 -1
- package/bin/install-pstack-plugin.mjs +20 -4
- package/bin/install-pstack-plugin.test.mjs +53 -0
- package/bin/install.mjs +45 -6
- package/bin/install.test.mjs +54 -0
- package/bin/python-probe.mjs +20 -0
- package/bin/python-probe.test.mjs +37 -0
- package/docs/agents-guidance/asks.md +1 -0
- package/docs/codex-accounts.md +4 -1
- package/docs/decisions/deepstack-gate.md +6 -2
- package/hooks/features/session-context.md +2 -1
- package/hooks/hooks_constants/deepen_default_rewrite_constants.py +39 -0
- package/hooks/hooks_constants/deepen_first_prompt_constants.py +8 -0
- package/hooks/session/deepen_default_rewrite.py +128 -0
- package/hooks/session/deepen_first_prompt.py +15 -46
- package/hooks/session/deepen_turn_marker.py +75 -0
- package/hooks/session/test_deepen_default_rewrite.py +90 -0
- package/hooks/session/test_deepen_first_prompt.py +75 -1
- package/hooks/session/test_deepen_turn_marker.py +59 -0
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
2
|
import { existsSync, readdirSync, rmSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
+
import { findFolderLockers, lockedFolderAdvice, namesALockedFolder } from './folder-lockers.mjs';
|
|
4
5
|
|
|
5
6
|
export const PSTACK_MARKETPLACE_REPOSITORY = 'jl-cmd/pstack-claude';
|
|
6
7
|
export const PSTACK_MARKETPLACE_NAME = 'pstack-claude';
|
|
@@ -248,7 +249,17 @@ function absentHostOutcome(allSpecs, host, executable) {
|
|
|
248
249
|
};
|
|
249
250
|
}
|
|
250
251
|
|
|
251
|
-
function
|
|
252
|
+
function lockedFolderDetail(allSpecs, host, homeDirectory, errorText, findLockers) {
|
|
253
|
+
const hostDefinition = HOST_DEFINITIONS[host];
|
|
254
|
+
if (!namesALockedFolder(errorText) || !hostDefinition.orphanedVersionsDirectory || !homeDirectory) return '';
|
|
255
|
+
return allSpecs.map(eachSpec => {
|
|
256
|
+
const pluginCacheDirectory = hostDefinition.orphanedVersionsDirectory(homeDirectory, eachSpec);
|
|
257
|
+
const allLockers = findLockers(pluginCacheDirectory);
|
|
258
|
+
return allLockers === null ? '' : ` ${lockedFolderAdvice(pluginCacheDirectory, allLockers)}`;
|
|
259
|
+
}).join('');
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function installForHost(allSpecs, host, homeDirectory, environment, runCommand, findLockers) {
|
|
252
263
|
const plan = marketplacePluginsPlan(allSpecs, host);
|
|
253
264
|
const executable = environment[plan.executableVariable] || plan.executable;
|
|
254
265
|
const commandEnvironment = { [plan.homeVariable]: homeDirectory };
|
|
@@ -265,11 +276,12 @@ function installForHost(allSpecs, host, homeDirectory, environment, runCommand)
|
|
|
265
276
|
if (namesAnAbsentCommand(outcome)) return absentHostOutcome(allSpecs, host, executable);
|
|
266
277
|
if (outcome.status !== 0 || outcome.error) {
|
|
267
278
|
const detail = singleLineDetail(outcome.stderr) || outcome.error?.message || `exit ${outcome.status}`;
|
|
279
|
+
const lockAdvice = lockedFolderDetail(allSpecs, host, homeDirectory, detail, findLockers);
|
|
268
280
|
return {
|
|
269
281
|
host,
|
|
270
282
|
executable,
|
|
271
283
|
status: 'failed',
|
|
272
|
-
warning: `${executable} ${commandArguments.join(' ')} failed: ${detail}`,
|
|
284
|
+
warning: `${executable} ${commandArguments.join(' ')} failed: ${detail}${lockAdvice}`,
|
|
273
285
|
};
|
|
274
286
|
}
|
|
275
287
|
}
|
|
@@ -294,7 +306,9 @@ function installForHost(allSpecs, host, homeDirectory, environment, runCommand)
|
|
|
294
306
|
* and skills this run already wrote still reach their durable places. An
|
|
295
307
|
* absent tool arrives as a spawn `ENOENT` on other platforms and as
|
|
296
308
|
* `cmd.exe`'s command-not-found exit code on Windows, and both read as
|
|
297
|
-
* skipped.
|
|
309
|
+
* skipped. When a host cannot replace a plugin folder because something holds
|
|
310
|
+
* it, the warning names each process that holds it, so the user knows what to
|
|
311
|
+
* close before running the install again.
|
|
298
312
|
*
|
|
299
313
|
* @param {ReadonlyArray<typeof PSTACK_PLUGIN_SPEC>} allSpecs The plugins to install, from one marketplace.
|
|
300
314
|
* @param {object} [options] Install targets.
|
|
@@ -302,12 +316,13 @@ function installForHost(allSpecs, host, homeDirectory, environment, runCommand)
|
|
|
302
316
|
* @param {string} [options.codexHome] The Codex home to install into.
|
|
303
317
|
* @param {string[]} [options.hosts] The hosts to install. Defaults to the hosts every spec names.
|
|
304
318
|
* @param {Record<string, string|undefined>} [options.environment] The process environment.
|
|
305
|
-
* @param {object} [dependencies] Seams for the command runner.
|
|
319
|
+
* @param {object} [dependencies] Seams for the command runner and the folder locker search.
|
|
306
320
|
* @returns {{status: string, hosts: object[], warning: string|null}} The outcome per host.
|
|
307
321
|
*/
|
|
308
322
|
export function installMarketplacePlugins(allSpecs, options = {}, dependencies = {}) {
|
|
309
323
|
const environment = options.environment ?? process.env;
|
|
310
324
|
const runCommand = dependencies.runCommand ?? runHostCommand;
|
|
325
|
+
const findLockers = dependencies.findLockers ?? findFolderLockers;
|
|
311
326
|
const homeDirectories = { claude: options.claudeRoot, codex: options.codexHome };
|
|
312
327
|
const hosts = options.hosts ?? allSpecs[0].hosts.filter(
|
|
313
328
|
eachHost => allSpecs.every(eachSpec => eachSpec.hosts.includes(eachHost)),
|
|
@@ -318,6 +333,7 @@ export function installMarketplacePlugins(allSpecs, options = {}, dependencies =
|
|
|
318
333
|
homeDirectories[host],
|
|
319
334
|
environment,
|
|
320
335
|
runCommand,
|
|
336
|
+
findLockers,
|
|
321
337
|
));
|
|
322
338
|
const failedCount = hostOutcomes.filter(outcome => outcome.status === 'failed').length;
|
|
323
339
|
const installedCount = hostOutcomes.filter(outcome => outcome.status === 'installed').length;
|
|
@@ -392,3 +392,56 @@ test('a clean install deletes the orphaned pstack versions and keeps the install
|
|
|
392
392
|
assert.equal(existsSync(join(versionsDirectory, '0.9.64')), true);
|
|
393
393
|
assert.deepEqual(removeOrphanedPluginVersions(versionsDirectory), []);
|
|
394
394
|
});
|
|
395
|
+
|
|
396
|
+
test('a plugin folder held by another program names the process to close', () => {
|
|
397
|
+
const lockedError = 'Failed to install plugin "pstack@pstack-claude": The installed copy of this plugin version'
|
|
398
|
+
+ ' could not be replaced: it is in use by another program (EPERM).';
|
|
399
|
+
const searchedFolders = [];
|
|
400
|
+
const runner = recordingRunner((executable, commandArguments) => (
|
|
401
|
+
executable === 'claude' && commandArguments[1] === 'install'
|
|
402
|
+
? { status: 1, stderr: lockedError }
|
|
403
|
+
: { status: 0, stderr: '' }));
|
|
404
|
+
const outcome = installMarketplacePlugins([PSTACK_PLUGIN_SPEC], ROOTS, {
|
|
405
|
+
runCommand: runner.runCommand,
|
|
406
|
+
findLockers: folderPath => {
|
|
407
|
+
searchedFolders.push(folderPath);
|
|
408
|
+
return [{ pid: 19056, name: 'Cursor.exe', reason: 'command-line' }];
|
|
409
|
+
},
|
|
410
|
+
});
|
|
411
|
+
const claude = outcome.hosts.find(host => host.host === 'claude');
|
|
412
|
+
assert.equal(claude.status, 'failed');
|
|
413
|
+
assert.deepEqual(searchedFolders, [join('/managed/.claude', 'plugins', 'cache', 'pstack-claude', 'pstack')]);
|
|
414
|
+
assert.match(claude.warning, /Close Cursor\.exe \(PID 19056, was started on a path inside it\)/);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
test('a failure that is not a held folder runs no locker search', () => {
|
|
418
|
+
const runner = recordingRunner((executable, commandArguments) => (
|
|
419
|
+
executable === 'claude' && commandArguments[2] === 'add'
|
|
420
|
+
? { status: 1, stderr: 'owner: Invalid input: expected object, received undefined' }
|
|
421
|
+
: { status: 0, stderr: '' }));
|
|
422
|
+
let searched = false;
|
|
423
|
+
installMarketplacePlugins([PSTACK_PLUGIN_SPEC], ROOTS, {
|
|
424
|
+
runCommand: runner.runCommand,
|
|
425
|
+
findLockers: () => {
|
|
426
|
+
searched = true;
|
|
427
|
+
return [];
|
|
428
|
+
},
|
|
429
|
+
});
|
|
430
|
+
assert.equal(searched, false);
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
test('a held folder that was not searched adds no claim about what holds it', () => {
|
|
434
|
+
const lockedError = 'Failed to install plugin "pstack@pstack-claude": EPERM: operation not permitted';
|
|
435
|
+
const runner = recordingRunner((executable, commandArguments) => (
|
|
436
|
+
executable === 'claude' && commandArguments[1] === 'install'
|
|
437
|
+
? { status: 1, stderr: lockedError }
|
|
438
|
+
: { status: 0, stderr: '' }));
|
|
439
|
+
const outcome = installMarketplacePlugins([PSTACK_PLUGIN_SPEC], ROOTS, {
|
|
440
|
+
runCommand: runner.runCommand,
|
|
441
|
+
findLockers: () => null,
|
|
442
|
+
});
|
|
443
|
+
const claude = outcome.hosts.find(host => host.host === 'claude');
|
|
444
|
+
assert.equal(claude.status, 'failed');
|
|
445
|
+
assert.match(claude.warning, /EPERM: operation not permitted$/);
|
|
446
|
+
assert.doesNotMatch(claude.warning, /likely holder|Close /);
|
|
447
|
+
});
|
package/bin/install.mjs
CHANGED
|
@@ -72,7 +72,9 @@ import {
|
|
|
72
72
|
writeCursorAgentsGuidanceHooks,
|
|
73
73
|
} from './cursor-agents-guidance.mjs';
|
|
74
74
|
import {
|
|
75
|
+
codexSolPromptPath,
|
|
75
76
|
codexSolSettingSnapshotPaths,
|
|
77
|
+
installCodexSolPrompt,
|
|
76
78
|
removeCodexSolSetting,
|
|
77
79
|
SOL_PROMPT_DIRECTORY_NAME,
|
|
78
80
|
SOL_PROMPT_FILE_NAME,
|
|
@@ -126,6 +128,8 @@ import {
|
|
|
126
128
|
resolveFaultPhaseFromEnvironment,
|
|
127
129
|
runWithInstallTransaction,
|
|
128
130
|
} from './install-transaction.mjs';
|
|
131
|
+
import { findShadowingGlobalInstall, shadowingGlobalAdvice } from './global-install-shadow.mjs';
|
|
132
|
+
import { pythonExecutableProbeCommand } from './python-probe.mjs';
|
|
129
133
|
|
|
130
134
|
const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
131
135
|
const INSTALL_ROOT_RESOLUTION = resolveInstallRoot({
|
|
@@ -442,16 +446,28 @@ export function installPstackPluginForHosts() {
|
|
|
442
446
|
});
|
|
443
447
|
}
|
|
444
448
|
|
|
449
|
+
/** Side steps that need the user, repeated at the end of the run so none scrolls past unseen. */
|
|
450
|
+
const allActionNeededLines = [];
|
|
451
|
+
|
|
445
452
|
function printPluginHostOutcomes(allSpecs, pluginOutcome) {
|
|
446
453
|
for (const eachSpec of allSpecs) {
|
|
447
454
|
for (const eachHost of pluginOutcome.hosts) {
|
|
448
|
-
|
|
449
|
-
?
|
|
450
|
-
:
|
|
455
|
+
const outcomeLine = eachHost.warning
|
|
456
|
+
? `${eachSpec.label} (${eachHost.host}): ${eachHost.status} — ${eachHost.warning}`
|
|
457
|
+
: `${eachSpec.label} (${eachHost.host}): ${eachHost.status}`;
|
|
458
|
+
console.log(` ${outcomeLine}`);
|
|
459
|
+
if (eachHost.status === 'failed') allActionNeededLines.push(outcomeLine);
|
|
451
460
|
}
|
|
452
461
|
}
|
|
453
462
|
}
|
|
454
463
|
|
|
464
|
+
function printActionNeeded() {
|
|
465
|
+
if (allActionNeededLines.length === 0) return;
|
|
466
|
+
console.warn(`Action needed (${allActionNeededLines.length}):`);
|
|
467
|
+
for (const eachLine of allActionNeededLines) console.warn(` - ${eachLine}`);
|
|
468
|
+
console.warn('');
|
|
469
|
+
}
|
|
470
|
+
|
|
455
471
|
function seedSubagentModelPolicy() {
|
|
456
472
|
const policyDirectory = join(AGENTS_HOME, CURSOR_RULES_DIRECTORY_NAME);
|
|
457
473
|
const policyPath = join(policyDirectory, SUBAGENT_MODEL_POLICY_FILE_NAME);
|
|
@@ -550,7 +566,9 @@ function quoteStandalonePluginRootHookPath(commandString, pluginRootForward) {
|
|
|
550
566
|
* sys.executable and that path is baked in, so a later PATH change or Microsoft
|
|
551
567
|
* Store update that re-points the `py`/`python` launcher cannot silently break
|
|
552
568
|
* the hooks; candidates resolving to the non-spawnable WindowsApps stub are
|
|
553
|
-
* skipped.
|
|
569
|
+
* skipped. The path probe runs in isolated mode so the folder the installer
|
|
570
|
+
* runs from cannot slow or change it. Other platforms keep the bare command
|
|
571
|
+
* (e.g. `python3`).
|
|
554
572
|
*
|
|
555
573
|
* @returns {string|null} The interpreter command, or null when none is usable.
|
|
556
574
|
*/
|
|
@@ -561,7 +579,7 @@ export function detectPython() {
|
|
|
561
579
|
const version = execSync(`${command} ${versionFlag}`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], timeout: PYTHON_PROBE_TIMEOUT_MILLISECONDS }).trim();
|
|
562
580
|
if (!version.includes('Python 3.')) continue;
|
|
563
581
|
if (process.platform !== 'win32') return command;
|
|
564
|
-
const executablePath = execSync(
|
|
582
|
+
const executablePath = execSync(pythonExecutableProbeCommand(command), { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], timeout: PYTHON_PROBE_TIMEOUT_MILLISECONDS }).trim();
|
|
565
583
|
if (!executablePath || isWindowsStorePythonStub(executablePath)) continue;
|
|
566
584
|
return interpreterCommandFromPath(executablePath);
|
|
567
585
|
} catch { /* try next */ }
|
|
@@ -2688,6 +2706,7 @@ function executeInstallPlan(plan) {
|
|
|
2688
2706
|
...(plan.priorManifest.files || []),
|
|
2689
2707
|
join(INSTALL_ROOT_RESOLUTION.codexHomeDirectory, 'AGENTS.md'),
|
|
2690
2708
|
...codexSolSettingSnapshotPaths(INSTALL_ROOT_RESOLUTION.codexHomeDirectory),
|
|
2709
|
+
codexSolPromptPath(INSTALL_ROOT_RESOLUTION.codexHomeDirectory),
|
|
2691
2710
|
],
|
|
2692
2711
|
journalParentDirectory: join(CLAUDE_HOME, TRANSACTION_JOURNAL_DIRECTORY_NAME),
|
|
2693
2712
|
});
|
|
@@ -2734,6 +2753,16 @@ function executeInstallPlanMutations(plan, transactionHelpers) {
|
|
|
2734
2753
|
const groupLabel = selectedGroups ? `groups: ${selectedGroups.join(', ')}` : 'all';
|
|
2735
2754
|
console.log(`\nInstalling ${PACKAGE_NAME} (${groupLabel})...\n`);
|
|
2736
2755
|
console.log(` Python: ${pythonCommand}`);
|
|
2756
|
+
const shadowingGlobal = findShadowingGlobalInstall({
|
|
2757
|
+
packageName: PACKAGE_NAME,
|
|
2758
|
+
packageRoot: PACKAGE_ROOT,
|
|
2759
|
+
packageVersion: PACKAGE_VERSION,
|
|
2760
|
+
});
|
|
2761
|
+
if (shadowingGlobal) {
|
|
2762
|
+
const shadowAdvice = shadowingGlobalAdvice(PACKAGE_NAME, shadowingGlobal, PACKAGE_VERSION);
|
|
2763
|
+
console.warn(` Warning: ${shadowAdvice}`);
|
|
2764
|
+
allActionNeededLines.push(shadowAdvice);
|
|
2765
|
+
}
|
|
2737
2766
|
mkdirSync(CLAUDE_HOME, { recursive: true });
|
|
2738
2767
|
const publishedPointerPaths = publishManagedLookupPointers();
|
|
2739
2768
|
|
|
@@ -2993,6 +3022,9 @@ function executeInstallPlanMutations(plan, transactionHelpers) {
|
|
|
2993
3022
|
console.log(cursorOutcome.warning
|
|
2994
3023
|
? ` DeepStack (cursor): ${cursorOutcome.status} — ${cursorOutcome.warning}`
|
|
2995
3024
|
: ` DeepStack (cursor): ${cursorOutcome.status}`);
|
|
3025
|
+
if (cursorOutcome.status === 'failed') {
|
|
3026
|
+
allActionNeededLines.push(`DeepStack (cursor): failed — ${cursorOutcome.warning}`);
|
|
3027
|
+
}
|
|
2996
3028
|
}
|
|
2997
3029
|
const allRepositoryPluginSpecs = [
|
|
2998
3030
|
USAGE_WRAPUP_PLUGIN_SPEC,
|
|
@@ -3025,9 +3057,14 @@ function executeInstallPlanMutations(plan, transactionHelpers) {
|
|
|
3025
3057
|
INSTALL_ROOT_RESOLUTION.codexHomeDirectory,
|
|
3026
3058
|
);
|
|
3027
3059
|
if (deepenGuidancePath) allCodexGuidancePaths.push(deepenGuidancePath);
|
|
3060
|
+
const solPromptPath = installCodexSolPrompt(
|
|
3061
|
+
INSTALL_ROOT_RESOLUTION.codexHomeDirectory,
|
|
3062
|
+
join(PACKAGE_ROOT, SOL_PROMPT_DIRECTORY_NAME, SOL_PROMPT_FILE_NAME),
|
|
3063
|
+
);
|
|
3064
|
+
if (solPromptPath) allCodexGuidancePaths.push(solPromptPath);
|
|
3028
3065
|
const solSettingPath = writeCodexSolSetting(
|
|
3029
3066
|
INSTALL_ROOT_RESOLUTION.codexHomeDirectory,
|
|
3030
|
-
|
|
3067
|
+
codexSolPromptPath(INSTALL_ROOT_RESOLUTION.codexHomeDirectory),
|
|
3031
3068
|
);
|
|
3032
3069
|
if (solSettingPath) {
|
|
3033
3070
|
allCodexGuidancePaths.push(solSettingPath);
|
|
@@ -3245,6 +3282,7 @@ function executeInstallPlanMutations(plan, transactionHelpers) {
|
|
|
3245
3282
|
console.log(` hooks: ${summary.hookFiles.created + summary.hookFiles.updated} files, ${summary.hookGroups} groups in settings.json`);
|
|
3246
3283
|
}
|
|
3247
3284
|
console.log(` python: ${pythonCommand}\n`);
|
|
3285
|
+
printActionNeeded();
|
|
3248
3286
|
}
|
|
3249
3287
|
|
|
3250
3288
|
function normalizePathForComparison(rawPath) {
|
|
@@ -3491,6 +3529,7 @@ function uninstall() {
|
|
|
3491
3529
|
...plan.removableFiles,
|
|
3492
3530
|
join(INSTALL_ROOT_RESOLUTION.codexHomeDirectory, 'AGENTS.md'),
|
|
3493
3531
|
...codexSolSettingSnapshotPaths(INSTALL_ROOT_RESOLUTION.codexHomeDirectory),
|
|
3532
|
+
codexSolPromptPath(INSTALL_ROOT_RESOLUTION.codexHomeDirectory),
|
|
3494
3533
|
],
|
|
3495
3534
|
journalParentDirectory: join(CLAUDE_HOME, TRANSACTION_JOURNAL_DIRECTORY_NAME),
|
|
3496
3535
|
});
|
package/bin/install.test.mjs
CHANGED
|
@@ -3633,3 +3633,57 @@ test('a full install clears a retired session-continuity registration from every
|
|
|
3633
3633
|
);
|
|
3634
3634
|
}
|
|
3635
3635
|
});
|
|
3636
|
+
|
|
3637
|
+
function installWithGlobalPrefix(context, globalPrefix) {
|
|
3638
|
+
const homeDirectory = mkdtempSync(join(tmpdir(), 'cde-action-needed-'));
|
|
3639
|
+
context.after(() => rmSync(homeDirectory, { recursive: true, force: true }));
|
|
3640
|
+
return spawnSync(process.execPath, [join(dirname(fileURLToPath(import.meta.url)), 'install.mjs')], {
|
|
3641
|
+
cwd: dirname(dirname(fileURLToPath(import.meta.url))),
|
|
3642
|
+
encoding: 'utf8',
|
|
3643
|
+
env: {
|
|
3644
|
+
...process.env,
|
|
3645
|
+
CLAUDE_CONFIG_DIR: undefined,
|
|
3646
|
+
LLM_SETTINGS_PROFILES_ROOT: undefined,
|
|
3647
|
+
CLAUDE_DEV_ENV_INSTALL_FAULT: undefined,
|
|
3648
|
+
CDE_INSTALL_PSTACK: '0',
|
|
3649
|
+
CDE_INSTALL_DEEPSTACK: '0',
|
|
3650
|
+
CDE_INSTALL_USAGE_WRAPUP: '0',
|
|
3651
|
+
CDE_INSTALL_SUBAGENT_MODELS: '0',
|
|
3652
|
+
CDE_INSTALL_DEEPEN_FIRST_PROMPT: '0',
|
|
3653
|
+
HOME: homeDirectory,
|
|
3654
|
+
USERPROFILE: homeDirectory,
|
|
3655
|
+
APPDATA: join(homeDirectory, 'AppData', 'Roaming'),
|
|
3656
|
+
CODEX_HOME: join(homeDirectory, '.codex'),
|
|
3657
|
+
GIT_CONFIG_GLOBAL: join(homeDirectory, '.gitconfig'),
|
|
3658
|
+
TEMP: homeDirectory,
|
|
3659
|
+
TMP: homeDirectory,
|
|
3660
|
+
npm_config_prefix: undefined,
|
|
3661
|
+
npm_config_global_prefix: globalPrefix ?? join(homeDirectory, 'no-global-prefix'),
|
|
3662
|
+
},
|
|
3663
|
+
});
|
|
3664
|
+
}
|
|
3665
|
+
|
|
3666
|
+
test('install ends with an Action needed block naming an older global copy that shadows npx', (context) => {
|
|
3667
|
+
const globalPrefix = mkdtempSync(join(tmpdir(), 'cde-global-prefix-'));
|
|
3668
|
+
context.after(() => rmSync(globalPrefix, { recursive: true, force: true }));
|
|
3669
|
+
const globalPackage = process.platform === 'win32'
|
|
3670
|
+
? join(globalPrefix, 'node_modules', 'claude-dev-env')
|
|
3671
|
+
: join(globalPrefix, 'lib', 'node_modules', 'claude-dev-env');
|
|
3672
|
+
mkdirSync(globalPackage, { recursive: true });
|
|
3673
|
+
writeFileSync(join(globalPackage, 'package.json'), JSON.stringify({ name: 'claude-dev-env', version: '0.0.1' }));
|
|
3674
|
+
|
|
3675
|
+
const result = installWithGlobalPrefix(context, globalPrefix);
|
|
3676
|
+
|
|
3677
|
+
assert.equal(result.status, 0, result.stderr);
|
|
3678
|
+
const actionIndex = result.stderr.lastIndexOf('Action needed (1):');
|
|
3679
|
+
assert.ok(actionIndex >= 0, result.stderr);
|
|
3680
|
+
assert.match(result.stderr.slice(actionIndex), /claude-dev-env 0\.0\.1/);
|
|
3681
|
+
assert.match(result.stderr.slice(actionIndex), /npm uninstall -g claude-dev-env/);
|
|
3682
|
+
});
|
|
3683
|
+
|
|
3684
|
+
test('install prints no Action needed block when every side step finished', (context) => {
|
|
3685
|
+
const result = installWithGlobalPrefix(context, null);
|
|
3686
|
+
|
|
3687
|
+
assert.equal(result.status, 0, result.stderr);
|
|
3688
|
+
assert.equal(result.stderr.includes('Action needed'), false, result.stderr);
|
|
3689
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command line for the probe that asks a Python candidate for its absolute
|
|
3
|
+
* interpreter path.
|
|
4
|
+
*
|
|
5
|
+
* `python -c` puts the current directory first on `sys.path`, and Python
|
|
6
|
+
* lists that whole directory the first time it imports a module from disk.
|
|
7
|
+
* When the installer runs from a large folder, such as a temp folder with
|
|
8
|
+
* hundreds of thousands of entries, that listing can outlast the probe
|
|
9
|
+
* timeout, so every candidate fails and the install stops with "No usable
|
|
10
|
+
* Python 3 found". A module in that folder with a standard library name is
|
|
11
|
+
* imported in place of the real one. `-I` (isolated mode, Python 3.4+) keeps
|
|
12
|
+
* the current directory off `sys.path`, so the probe gives the same answer
|
|
13
|
+
* from any folder.
|
|
14
|
+
*
|
|
15
|
+
* @param {string} candidateCommand A Python launcher command, such as `py -3`.
|
|
16
|
+
* @returns {string} The shell command that prints the interpreter path.
|
|
17
|
+
*/
|
|
18
|
+
export function pythonExecutableProbeCommand(candidateCommand) {
|
|
19
|
+
return `${candidateCommand} -I -c "import sys; print(sys.executable)"`;
|
|
20
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import { strict as assert } from 'node:assert';
|
|
3
|
+
import { execSync } from 'node:child_process';
|
|
4
|
+
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { detectPython } from './install.mjs';
|
|
8
|
+
import { pythonExecutableProbeCommand } from './python-probe.mjs';
|
|
9
|
+
|
|
10
|
+
const SHADOWED_STANDARD_LIBRARY_MODULES = ['linecache.py', 'encodings.py', 'site.py', 'os.py'];
|
|
11
|
+
|
|
12
|
+
test('the interpreter path probe ignores modules in the current directory', () => {
|
|
13
|
+
const candidateCommand = detectPython();
|
|
14
|
+
assert.ok(candidateCommand, 'a Python 3 interpreter is needed for this test');
|
|
15
|
+
const shadowDirectory = mkdtempSync(join(tmpdir(), 'cde-python-probe-'));
|
|
16
|
+
try {
|
|
17
|
+
for (const moduleFileName of SHADOWED_STANDARD_LIBRARY_MODULES) {
|
|
18
|
+
writeFileSync(join(shadowDirectory, moduleFileName), 'raise SystemExit("shadow module imported")\n');
|
|
19
|
+
}
|
|
20
|
+
const printedPath = execSync(pythonExecutableProbeCommand(candidateCommand), {
|
|
21
|
+
cwd: shadowDirectory,
|
|
22
|
+
encoding: 'utf8',
|
|
23
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
24
|
+
}).trim();
|
|
25
|
+
assert.ok(printedPath.length > 0);
|
|
26
|
+
assert.doesNotMatch(printedPath, /shadow module imported/);
|
|
27
|
+
} finally {
|
|
28
|
+
rmSync(shadowDirectory, { recursive: true, force: true });
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('the interpreter path probe runs Python in isolated mode', () => {
|
|
33
|
+
assert.equal(
|
|
34
|
+
pythonExecutableProbeCommand('py -3'),
|
|
35
|
+
'py -3 -I -c "import sys; print(sys.executable)"',
|
|
36
|
+
);
|
|
37
|
+
});
|
|
@@ -40,6 +40,7 @@ What has been seen to work, in an interactive `cursor-agent` session (version 20
|
|
|
40
40
|
- The `sessionStart` entry runs, and the session can quote the Asks line. Without it, the session cannot.
|
|
41
41
|
- The `stop` entry runs at the end of each turn. With an open todo left, the gate sent one follow-up, and the agent closed the turn with a `Blocked:` line. Without the gate, the turn ended at "Done." with the todo still open.
|
|
42
42
|
- A turn cancelled with Esc sends `stop` with status `aborted`, then `error`. The gate stays silent for both.
|
|
43
|
+
- On Windows, each hook command runs in PowerShell: Cursor writes it into a temporary `.ps1` script and starts `pwsh -NonInteractive -File` on it. A command that starts with a quote gets the `&` call operator first, so a quoted Python path with a space runs. A probe from `C:/.../py space/venv/Scripts/python.exe` ran with both a bare and a quoted script path. The Cursor editor (3.24.12) has the same `&` rule in its hook runner.
|
|
43
44
|
|
|
44
45
|
Limits:
|
|
45
46
|
|
package/docs/codex-accounts.md
CHANGED
|
@@ -133,7 +133,10 @@ Once installed, that is `~/.claude/scripts/account_broker.py`.
|
|
|
133
133
|
|
|
134
134
|
The installer writes one top-level line into `~/.codex/config.toml`, under a
|
|
135
135
|
`# claude-dev-env trimmed Sol prompt` marker. The line points
|
|
136
|
-
`model_instructions_file` at `~/.
|
|
136
|
+
`model_instructions_file` at `~/.codex/codex-sol.md`, a copy of the package's
|
|
137
|
+
`system-prompts/codex-sol.md`. The copy sits beside `config.toml`, so pruning
|
|
138
|
+
the managed files under `~/.claude`, which an older package version does on
|
|
139
|
+
`--update`, cannot leave Codex pointing at a missing file. The file
|
|
137
140
|
is Codex's default `gpt-6.1-sol` prompt with the lines that conflict with the
|
|
138
141
|
package rules rewritten. The line names no model, so every Codex run uses the
|
|
139
142
|
file, whichever model it selects. `sync` links `config.toml` into every account
|
|
@@ -14,15 +14,19 @@ The filled four-answer stub printed `decision` `deepen`. The rewritten prompt ca
|
|
|
14
14
|
|
|
15
15
|
| Reason | When |
|
|
16
16
|
|---|---|
|
|
17
|
-
| `restates-four-questions` | The rewrite contains every canonical question stem. The paste path
|
|
17
|
+
| `restates-four-questions` | The rewrite contains every canonical question stem. The old paste path was this case. |
|
|
18
18
|
| `missing-role-lines` | `SCOPE (role):`, `EXAMPLES (illustration only):`, or `ZOOM-IN BLOCKER:` is missing or blank. |
|
|
19
19
|
| `example-noun-in-scope` | The scope line contains an example noun and `ZOOM-IN BLOCKER:` is `none`. |
|
|
20
20
|
| `dropped-hard-constraint` | A bullet under `HARD CONSTRAINTS` in the task is absent as its own line in the rewrite. |
|
|
21
21
|
|
|
22
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
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
|
+
|
|
24
28
|
## Proof shape
|
|
25
29
|
|
|
26
|
-
Run the hook on stdin with `CDE_DEEPEN_OUTPUT=json`.
|
|
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.
|
|
27
31
|
|
|
28
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.
|
|
@@ -18,6 +18,7 @@ This family injects reminders and task guidance when an agent starts, resumes, s
|
|
|
18
18
|
- `session/skill_loaded_reminder.py` runs on `PreToolUse` matcher `Agent|Task` at `10` seconds, `UserPromptSubmit` matcher empty at `10` seconds, `SessionStart` matcher `compact` at `10` seconds, and `SubagentStart` matcher `workflow-subagent` at `10` seconds in `hooks.json`.
|
|
19
19
|
- `session/task_tool_prompt.py`, `session/working_style_prompt.py`, `session/advisor_rules_prompt.py`, and `session/orchestrator_auto_starter.py` run on `SessionStart`, matcher empty, timeout `10` seconds each in `hooks.json`.
|
|
20
20
|
- `session/deepen_first_prompt.py` runs on `UserPromptSubmit`, matcher empty, timeout `10` seconds in `hooks.json`.
|
|
21
|
+
- In Cursor, `session/deepen_first_prompt.py` also runs on `beforeSubmitPrompt`. Cursor passes that event name to the `UserPromptSubmit` copy it imports from the Claude Code settings too. When both copies run, `session/deepen_turn_marker.py` creates the turn marker only when it does not exist yet, so one copy injects. A later prompt skips when either the transcript marker or the session marker exists, or when the transcript already has a user line in Claude Code's or Cursor's shape.
|
|
21
22
|
- `session/artifact_template_pointer.py` runs on `PreToolUse` matcher `Artifact|Skill` at `10` seconds in `hooks.json`. It speaks only on an `Artifact` call with action `quickstart` or a `Skill` call that loads `artifact-design`.
|
|
22
23
|
- `advisory/auto_mode_denial_quick_fix.py` runs on `PermissionDenied`, matcher `*`, timeout `10` seconds in `hooks.json`.
|
|
23
24
|
|
|
@@ -32,7 +33,7 @@ Preconditions:
|
|
|
32
33
|
- **Working style.** Input is a SessionStart event. Run `python -m pytest packages/claude-dev-env/hooks/session/test_working_style_prompt.py -q`. The adjacent test observes the fixed context text.
|
|
33
34
|
- **Advisor guidance.** Input is SessionStart with `advisorModel` set. Run `python -m pytest packages/claude-dev-env/hooks/session/test_advisor_rules_prompt.py -q`. The adjacent test observes guidance only when the advisor setting is enabled.
|
|
34
35
|
- **Orchestrator start.** Input is SessionStart with its opt-in flag set. Run `python -m pytest packages/claude-dev-env/hooks/session/test_orchestrator_auto_starter.py -q`. The adjacent test observes an orchestrator directive.
|
|
35
|
-
- **First-task deepen.** Input is a first UserPromptSubmit task. Run `python -m pytest packages/claude-dev-env/hooks/session/test_deepen_first_prompt.py -q`. The adjacent test observes a skip table, a refusal of the paste path,
|
|
36
|
+
- **First-task deepen.** Input is a first UserPromptSubmit task. Run `python -m pytest packages/claude-dev-env/hooks/session/test_deepen_first_prompt.py -q`. The adjacent test observes a skip table, a refusal of the paste path, a rewrite that carries the SCOPE, EXAMPLES, and ZOOM-IN BLOCKER lines, and one injection when two copies see the same Cursor prompt. `python -m pytest packages/claude-dev-env/hooks/session/test_deepen_turn_marker.py -q` observes that only the first marker claim wins. With nothing configured, `deepen_default_rewrite.py` builds that role-shaped rewrite, and `test_deepen_default_rewrite.py` checks the gate accepts it.
|
|
36
37
|
- **Artifact template pointer.** Input is an `Artifact` quickstart call. Run `python -m pytest packages/claude-dev-env/hooks/session/test_artifact_template_pointer.py -q`. The adjacent test observes the status flowchart path first, the html-plan path ahead of the template path in `additionalContext`, and silence on a publish call or another skill.
|
|
37
38
|
- **Denial quick fix.** Input is a `PermissionDenied` event with a bracketed rule label and a classifier verdict. Run `python -m pytest packages/claude-dev-env/hooks/advisory/test_auto_mode_denial_quick_fix.py -q`. The adjacent test observes the named rule, the allow entry, and the PowerShell block; a denial with no verdict gets a note and no block.
|
|
38
39
|
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Named values for the role-shaped default rewrite of a first task."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"ALL_EXAMPLE_SENTENCE_MARKERS",
|
|
7
|
+
"DEFAULT_BLOCKER_TEMPLATE",
|
|
8
|
+
"DEFAULT_CLOSING_LINE",
|
|
9
|
+
"DEFAULT_SCOPE_MAX_CHARACTERS",
|
|
10
|
+
"DEFAULT_TASK_HEADING",
|
|
11
|
+
"ALL_EXAMPLE_END_MARKERS",
|
|
12
|
+
"EXAMPLE_LEAD_PUNCTUATION",
|
|
13
|
+
"EXAMPLE_LIST_SEPARATOR",
|
|
14
|
+
"EXAMPLE_MARKER_ALTERNATION_SEPARATOR",
|
|
15
|
+
"EXAMPLE_MARKER_PATTERN_TEMPLATE",
|
|
16
|
+
"SCOPE_CLIPPED_SUFFIX",
|
|
17
|
+
"SENTENCE_BREAK_PATTERN",
|
|
18
|
+
"WORD_SEPARATOR",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
ALL_EXAMPLE_SENTENCE_MARKERS: tuple[str, ...] = ("for example", "for instance", "e.g.", "such as")
|
|
22
|
+
EXAMPLE_LEAD_PUNCTUATION: str = " ,:;-"
|
|
23
|
+
EXAMPLE_MARKER_ALTERNATION_SEPARATOR: str = "|"
|
|
24
|
+
EXAMPLE_MARKER_PATTERN_TEMPLATE: str = r"(?<!\w)(?:{alternatives})(?!\w)"
|
|
25
|
+
EXAMPLE_LIST_SEPARATOR: str = "; "
|
|
26
|
+
WORD_SEPARATOR: str = " "
|
|
27
|
+
ALL_EXAMPLE_END_MARKERS: tuple[str, ...] = (", and ", ", but ", ", then ", ", so ", ";", ")")
|
|
28
|
+
SENTENCE_BREAK_PATTERN: str = r"(?<=[.!?])\s+(?=[A-Z0-9\"'`(\[])"
|
|
29
|
+
DEFAULT_SCOPE_MAX_CHARACTERS: int = 240
|
|
30
|
+
SCOPE_CLIPPED_SUFFIX: str = "(the rest is in the task below)"
|
|
31
|
+
DEFAULT_TASK_HEADING: str = "Task as given:"
|
|
32
|
+
DEFAULT_BLOCKER_TEMPLATE: str = (
|
|
33
|
+
"the scope names something the task also gives as an example ({examples}); "
|
|
34
|
+
"confirm the job is only that case before narrowing to it"
|
|
35
|
+
)
|
|
36
|
+
DEFAULT_CLOSING_LINE: str = (
|
|
37
|
+
"Work the whole scope as a leading practitioner in this field would, "
|
|
38
|
+
"keep every hard constraint, and then do exactly that job."
|
|
39
|
+
)
|
|
@@ -45,6 +45,9 @@ __all__ = [
|
|
|
45
45
|
"DEEPEN_SLASH_COMMAND",
|
|
46
46
|
"DEEPEN_TIMEOUT_SECONDS",
|
|
47
47
|
"DEEPEN_TURN_MARKER_SUFFIX",
|
|
48
|
+
"DEEPEN_SESSION_MARKER_PREFIX",
|
|
49
|
+
"ALL_FIRST_PROMPT_EVENT_NAMES",
|
|
50
|
+
"CURSOR_BEFORE_SUBMIT_PROMPT_EVENT_NAME",
|
|
48
51
|
"HOOK_EVENT_NAME_KEY",
|
|
49
52
|
"HOOK_SPECIFIC_OUTPUT_KEY",
|
|
50
53
|
"JSONL_ROLE_KEY",
|
|
@@ -66,10 +69,15 @@ DEEPEN_MARKER: str = "CDE_DEEPEN:v1"
|
|
|
66
69
|
DEEPEN_AGENT_MARKER: str = "CDE_DEEPEN_AGENT"
|
|
67
70
|
DEEPEN_SLASH_COMMAND: str = "/deepen"
|
|
68
71
|
DEEPEN_TURN_MARKER_SUFFIX: str = ".cde-deepen-turn1"
|
|
72
|
+
DEEPEN_SESSION_MARKER_PREFIX: str = "cde-deepen-"
|
|
69
73
|
DEEPEN_TIMEOUT_SECONDS: int = 10
|
|
70
74
|
DEEPEN_COMMAND_TIMEOUT_MARGIN_SECONDS: int = 2
|
|
71
75
|
DEEPEN_COMMAND_TIMEOUT_SECONDS: int = DEEPEN_TIMEOUT_SECONDS - DEEPEN_COMMAND_TIMEOUT_MARGIN_SECONDS
|
|
72
76
|
USER_PROMPT_SUBMIT_EVENT_NAME: str = "UserPromptSubmit"
|
|
77
|
+
CURSOR_BEFORE_SUBMIT_PROMPT_EVENT_NAME: str = "beforeSubmitPrompt"
|
|
78
|
+
ALL_FIRST_PROMPT_EVENT_NAMES: frozenset[str] = frozenset(
|
|
79
|
+
{USER_PROMPT_SUBMIT_EVENT_NAME, CURSOR_BEFORE_SUBMIT_PROMPT_EVENT_NAME}
|
|
80
|
+
)
|
|
73
81
|
PROMPT_FIELD_NAME: str = "prompt"
|
|
74
82
|
TRANSCRIPT_PATH_FIELD_NAME: str = "transcript_path"
|
|
75
83
|
SESSION_ID_FIELD_NAME: str = "session_id"
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Role-shaped default rewrite for a first task when no rewrite command is set.
|
|
2
|
+
|
|
3
|
+
``refusal_reason`` in ``deepen_role_gate.py`` refuses a rewrite that pastes the
|
|
4
|
+
four question stems back at the worker, and one without the SCOPE, EXAMPLES,
|
|
5
|
+
and ZOOM-IN BLOCKER lines. The default rewrite is built to carry those lines,
|
|
6
|
+
so a first task deepens on every host with nothing configured: the scope is
|
|
7
|
+
the task's own sentences with the marked examples taken out, the examples are
|
|
8
|
+
the parts the task marks as examples, and the blocker asks for confirmation
|
|
9
|
+
when the scope names one of those examples.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import re
|
|
15
|
+
|
|
16
|
+
from hooks_constants.deepen_default_rewrite_constants import (
|
|
17
|
+
ALL_EXAMPLE_END_MARKERS,
|
|
18
|
+
ALL_EXAMPLE_SENTENCE_MARKERS,
|
|
19
|
+
DEFAULT_BLOCKER_TEMPLATE,
|
|
20
|
+
DEFAULT_CLOSING_LINE,
|
|
21
|
+
DEFAULT_SCOPE_MAX_CHARACTERS,
|
|
22
|
+
DEFAULT_TASK_HEADING,
|
|
23
|
+
EXAMPLE_LEAD_PUNCTUATION,
|
|
24
|
+
EXAMPLE_LIST_SEPARATOR,
|
|
25
|
+
EXAMPLE_MARKER_ALTERNATION_SEPARATOR,
|
|
26
|
+
EXAMPLE_MARKER_PATTERN_TEMPLATE,
|
|
27
|
+
SCOPE_CLIPPED_SUFFIX,
|
|
28
|
+
SENTENCE_BREAK_PATTERN,
|
|
29
|
+
WORD_SEPARATOR,
|
|
30
|
+
)
|
|
31
|
+
from hooks_constants.deepen_first_prompt_constants import (
|
|
32
|
+
DEEPEN_MARKER,
|
|
33
|
+
HARD_CONSTRAINTS_LABEL,
|
|
34
|
+
REFUSAL_EXAMPLE_NOUN_IN_SCOPE,
|
|
35
|
+
ROLE_BLOCKER_LABEL,
|
|
36
|
+
ROLE_BLOCKER_NONE,
|
|
37
|
+
ROLE_EXAMPLES_LABEL,
|
|
38
|
+
ROLE_SCOPE_LABEL,
|
|
39
|
+
)
|
|
40
|
+
from deepen_role_gate import refusal_reason
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _body_before_constraints(task_text: str) -> str:
|
|
44
|
+
all_body_lines: list[str] = []
|
|
45
|
+
for each_line in task_text.splitlines():
|
|
46
|
+
if each_line.strip().upper().startswith(HARD_CONSTRAINTS_LABEL):
|
|
47
|
+
break
|
|
48
|
+
all_body_lines.append(each_line)
|
|
49
|
+
return WORD_SEPARATOR.join(WORD_SEPARATOR.join(all_body_lines).split())
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _sentences(body_text: str) -> list[str]:
|
|
53
|
+
return [each.strip() for each in re.split(SENTENCE_BREAK_PATTERN, body_text) if each.strip()]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _example_marker_pattern() -> re.Pattern[str]:
|
|
57
|
+
alternatives = EXAMPLE_MARKER_ALTERNATION_SEPARATOR.join(re.escape(each_marker) for each_marker in ALL_EXAMPLE_SENTENCE_MARKERS)
|
|
58
|
+
return re.compile(EXAMPLE_MARKER_PATTERN_TEMPLATE.format(alternatives=alternatives), re.IGNORECASE)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _split_example(sentence: str) -> tuple[str, list[str]]:
|
|
62
|
+
"""Return the sentence without its marked examples, and the example texts.
|
|
63
|
+
|
|
64
|
+
A marker counts only as a whole phrase, so "for examples of" and "such
|
|
65
|
+
assumptions" stay in the scope. The earliest marker splits first, and the
|
|
66
|
+
text after the example is split again for later markers.
|
|
67
|
+
"""
|
|
68
|
+
marker_match = _example_marker_pattern().search(sentence)
|
|
69
|
+
if marker_match is None:
|
|
70
|
+
return sentence, []
|
|
71
|
+
head = sentence[: marker_match.start()].rstrip(EXAMPLE_LEAD_PUNCTUATION + "(")
|
|
72
|
+
tail = sentence[marker_match.end() :].lstrip(EXAMPLE_LEAD_PUNCTUATION)
|
|
73
|
+
end_index = min(
|
|
74
|
+
(tail.find(each_end) for each_end in ALL_EXAMPLE_END_MARKERS if tail.find(each_end) >= 0),
|
|
75
|
+
default=len(tail),
|
|
76
|
+
)
|
|
77
|
+
example = tail[:end_index].rstrip(".!? ")
|
|
78
|
+
remainder_scope, all_remainder_examples = _split_example(
|
|
79
|
+
tail[end_index:].lstrip(EXAMPLE_LEAD_PUNCTUATION + ")")
|
|
80
|
+
)
|
|
81
|
+
all_examples = ([example] if example else []) + all_remainder_examples
|
|
82
|
+
scope_part = f"{head} {remainder_scope}".strip() if head else ""
|
|
83
|
+
return scope_part, all_examples
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _clipped(text: str) -> str:
|
|
87
|
+
if len(text) <= DEFAULT_SCOPE_MAX_CHARACTERS:
|
|
88
|
+
return text
|
|
89
|
+
cut = text[:DEFAULT_SCOPE_MAX_CHARACTERS]
|
|
90
|
+
kept = cut[: cut.rfind(" ")] if " " in cut else cut
|
|
91
|
+
return f"{kept} {SCOPE_CLIPPED_SUFFIX}"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _scope_and_examples(task_text: str) -> tuple[str, str]:
|
|
95
|
+
all_splits = [_split_example(each) for each in _sentences(_body_before_constraints(task_text))]
|
|
96
|
+
all_examples = [each_example for _, all_sentence_examples in all_splits for each_example in all_sentence_examples]
|
|
97
|
+
all_scope_parts = [each_part for each_part, _ in all_splits if each_part]
|
|
98
|
+
scope_text = WORD_SEPARATOR.join(all_scope_parts) or task_text.strip()
|
|
99
|
+
examples_text = EXAMPLE_LIST_SEPARATOR.join(all_examples) if all_examples else ROLE_BLOCKER_NONE
|
|
100
|
+
return _clipped(scope_text), examples_text
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _rewrite_with_blocker(scope: str, examples: str, blocker: str, task_text: str) -> str:
|
|
104
|
+
return (
|
|
105
|
+
f"{DEEPEN_MARKER}\n\n"
|
|
106
|
+
f"{ROLE_SCOPE_LABEL} {scope}\n"
|
|
107
|
+
f"{ROLE_EXAMPLES_LABEL} {examples}\n"
|
|
108
|
+
f"{ROLE_BLOCKER_LABEL} {blocker}\n\n"
|
|
109
|
+
f"{DEFAULT_TASK_HEADING}\n{task_text}\n\n"
|
|
110
|
+
f"{DEFAULT_CLOSING_LINE}"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def role_default_rewrite(task_text: str) -> str:
|
|
115
|
+
"""Return the default rewrite: role lines from the task, the task itself, and one closing order.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
task_text: The first task with any leading /deepen removed.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
A rewrite that opens with the deepen marker and carries the three role lines.
|
|
122
|
+
"""
|
|
123
|
+
scope, examples = _scope_and_examples(task_text)
|
|
124
|
+
rewrite = _rewrite_with_blocker(scope, examples, ROLE_BLOCKER_NONE, task_text)
|
|
125
|
+
if refusal_reason(task_text, rewrite) != REFUSAL_EXAMPLE_NOUN_IN_SCOPE:
|
|
126
|
+
return rewrite
|
|
127
|
+
blocker = DEFAULT_BLOCKER_TEMPLATE.format(examples=examples)
|
|
128
|
+
return _rewrite_with_blocker(scope, examples, blocker, task_text)
|