claude-dev-env 7.7.0 → 8.0.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/agents/clean-coder.md +5 -6
- package/.agents/agents/issue-tracker.md +1 -1
- package/.agents/agents/pr-description-writer.md +1 -1
- package/.agents/agents/skill-writer-agent.md +1 -12
- package/.agents/agents/test_agent_frontmatter.py +2 -19
- package/.agents/skills/e-code-review/reference/fix.md +4 -2
- package/.agents/skills/e-code-review/reference/loop.md +4 -2
- package/.agents/skills/issue-tracker/SKILL.md +1 -1
- package/.agents/skills/orchestrator/SKILL.md +4 -5
- package/.agents/skills/pull-request/SKILL.md +8 -7
- package/.agents/skills/pull-request/reference/publication-tasks.md +1 -1
- package/.agents/skills/team-advisor/SKILL.md +2 -3
- package/_shared/advisor/advisor-protocol.md +0 -1
- package/_shared/advisor/reference/lifecycle.md +0 -1
- package/_shared/advisor/reference/warm-up.md +1 -2
- package/audit-rubrics/prompts/category-e-dead-code.md +3 -3
- package/bin/install-constants.mjs +0 -5
- package/bin/install-pstack-plugin.mjs +192 -0
- package/bin/install-pstack-plugin.test.mjs +181 -0
- package/bin/install.agents-home.test.mjs +0 -4
- package/bin/install.codex-rules.test.mjs +6 -0
- package/bin/install.cursor-rules.test.mjs +3 -253
- package/bin/install.mjs +55 -160
- package/bin/install.test.mjs +110 -149
- package/bin/prune-retired-pstack-store.mjs +83 -0
- package/bin/prune-retired-pstack-store.test.mjs +105 -0
- package/bin/prune-session-continuity.mjs +57 -0
- package/bin/prune-session-continuity.test.mjs +70 -0
- package/bin/resolve-install-root.mjs +2 -0
- package/bin/resolve-install-root.test.mjs +69 -0
- package/bin/subagent-model-policy.test.mjs +56 -19
- package/docs/CODE_RULES.md +1 -1
- package/docs/agent-spawn-protocol.md +1 -1
- package/docs/codex-compatibility.md +4 -16
- package/docs/nas-ssh-invocation.md +1 -1
- package/docs/references/prose-style-enforcement.md +2 -2
- package/docs/subagent-model-routing.md +21 -2
- package/hooks/HOOK_SPECS_PROMPT_WORKFLOW.md +1 -3
- package/hooks/blocking/code_rules_constants_config.py +1 -108
- package/hooks/blocking/code_rules_enforcer.py +9 -5
- package/hooks/blocking/code_rules_test_layout.py +6 -7
- package/hooks/blocking/test_code_rules_constants_config.py +13 -0
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +0 -1
- package/hooks/blocking/test_code_rules_enforcer_dispatch_wiring.py +15 -0
- package/hooks/blocking/test_code_rules_enforcer_split_constants_config.py +0 -60
- package/hooks/blocking/test_context_control_policy_files.py +0 -11
- package/hooks/hooks_constants/blocking_check_limits.py +1 -1
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +0 -1
- package/hooks/hooks_constants/setup_project_paths_constants.py +1 -2
- package/hooks/routing/subagent_model_routing.test.mjs +65 -28
- package/hooks/session/test_untracked_repo_detector.py +1 -1
- package/hooks/validators/code_quality_checks.py +15 -2
- package/hooks/validators/test_code_quality_checks.py +22 -0
- package/package.json +1 -2
- package/rules/ci-owns-the-gate.md +58 -0
- package/rules/code-standards.md +1 -2
- package/rules/docstring-prose-matches-implementation.md +2 -2
- package/rules/failure-blast-radius.md +1 -1
- package/rules/git-workflow.md +23 -11
- package/rules/orphan-css-class.md +1 -1
- package/rules/paired-test-coverage.md +1 -1
- package/rules/retired-hook-prose.md +1 -1
- package/rules/subagent-model-policy.json +1 -1
- package/rules/verify-before-asking.md +1 -1
- package/rules/windows-filesystem-safe.md +1 -1
- package/rules/workflow-substitution-slots.md +1 -1
- package/scripts/Sync-RepoMain.ps1 +71 -6
- package/scripts/policy_lint/adapter_detectors.py +116 -10
- package/scripts/policy_lint/config/approved_test_pairs.py +12 -0
- package/scripts/policy_lint/config/constants.py +3 -0
- package/scripts/tests/Sync-RepoMain.Tests.ps1 +108 -0
- package/scripts/tests/test_policy_lint_rules_validator_baseline.py +126 -0
- package/.agents/skills/session-continuity/AGENTS.md +0 -1
- package/.agents/skills/session-continuity/CLAUDE.md +0 -1
- package/.agents/skills/session-continuity/README.md +0 -223
- package/.agents/skills/session-continuity/SKILL.md +0 -125
- package/.agents/skills/session-continuity/continuity.mjs +0 -445
- package/.agents/skills/session-continuity/continuity.test.mjs +0 -381
- package/bin/install-session-continuity.mjs +0 -131
- package/bin/install-session-continuity.test.mjs +0 -106
- package/bin/install.pstack-plugin.test.mjs +0 -115
- package/bin/pstack-codex-discovery.test.mjs +0 -79
- package/bin/pstack-model-preferences.claude.json +0 -23
- package/bin/pstack-model-preferences.codex.json +0 -24
- package/bin/pstack.mjs +0 -392
- package/bin/pstack.test.mjs +0 -347
- package/bin/refresh-pstack-plugin-skills.test.mjs +0 -211
- package/bin/select-pstack-models.test.mjs +0 -727
- package/hooks/blocking/test_code_rules_enforcer_file_global_constants.py +0 -231
- package/rules/agent-spawn-protocol.md +0 -9
- package/rules/ask-user-question-required.md +0 -31
- package/rules/claims-as-quotes.md +0 -55
- package/rules/confirm-implementation-forks.md +0 -11
- package/rules/file-global-constants.md +0 -81
- package/rules/hedging-claims.md +0 -7
- package/rules/measurement-denominators.md +0 -39
- package/rules/prompt-workflow-context-controls.md +0 -19
- package/rules/pstack-host-mapping.md +0 -48
- package/rules/pstack-models.md +0 -142
- package/rules/vault-context.md +0 -5
- package/scripts/pstack-adapters/common.md +0 -17
- package/scripts/pstack-adapters/create-skill.md +0 -14
- package/scripts/pstack-adapters/host-claude.md +0 -7
- package/scripts/pstack-adapters/host-codex.md +0 -11
- package/scripts/pstack-adapters/host-cursor.md +0 -7
- package/scripts/pstack.lock.json +0 -11
- package/scripts/refresh_pstack_plugin_skills.mjs +0 -200
- package/scripts/select_pstack_models.mjs +0 -469
- /package/.agents/{skills → skills-archived}/source-command-logifix/SKILL.md +0 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
lstatSync,
|
|
4
|
+
readdirSync,
|
|
5
|
+
readlinkSync,
|
|
6
|
+
rmSync,
|
|
7
|
+
unlinkSync,
|
|
8
|
+
} from 'node:fs';
|
|
9
|
+
import { isAbsolute, join, resolve, sep } from 'node:path';
|
|
10
|
+
|
|
11
|
+
export const RETIRED_PSTACK_STORE_DIRECTORY_NAME = 'pstack';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Name the store directory the retired pstack installer kept its releases in.
|
|
15
|
+
*
|
|
16
|
+
* @param {string} managedRoot The managed Claude root.
|
|
17
|
+
* @returns {string} The retired store root.
|
|
18
|
+
*/
|
|
19
|
+
export function retiredPstackStoreRoot(managedRoot) {
|
|
20
|
+
return join(managedRoot, RETIRED_PSTACK_STORE_DIRECTORY_NAME);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function pointsIntoStore(entryPath, storeRoot) {
|
|
24
|
+
let entryStats;
|
|
25
|
+
try {
|
|
26
|
+
entryStats = lstatSync(entryPath);
|
|
27
|
+
} catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
if (!entryStats.isSymbolicLink()) return false;
|
|
31
|
+
let linkTarget;
|
|
32
|
+
try {
|
|
33
|
+
linkTarget = readlinkSync(entryPath);
|
|
34
|
+
} catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
const resolvedTarget = isAbsolute(linkTarget)
|
|
38
|
+
? resolve(linkTarget)
|
|
39
|
+
: resolve(join(entryPath, '..', linkTarget));
|
|
40
|
+
return resolvedTarget.startsWith(resolve(storeRoot) + sep);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function removePointersUnder(skillsRoot, storeRoot) {
|
|
44
|
+
if (!skillsRoot || !existsSync(skillsRoot)) return [];
|
|
45
|
+
const removedPointerPaths = [];
|
|
46
|
+
for (const entryName of readdirSync(skillsRoot)) {
|
|
47
|
+
const entryPath = join(skillsRoot, entryName);
|
|
48
|
+
if (!pointsIntoStore(entryPath, storeRoot)) continue;
|
|
49
|
+
unlinkSync(entryPath);
|
|
50
|
+
removedPointerPaths.push(entryPath);
|
|
51
|
+
}
|
|
52
|
+
return removedPointerPaths;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Remove the retired pstack installation this package used to publish.
|
|
57
|
+
*
|
|
58
|
+
* The retired installer kept a release store under the managed root and
|
|
59
|
+
* published one symlink per skill into it. Claude Code loads the `pstack`
|
|
60
|
+
* pointer as a skills-directory plugin, so a pointer left behind collides with
|
|
61
|
+
* the `pstack@pstack-claude` plugin the marketplace now installs.
|
|
62
|
+
*
|
|
63
|
+
* Only a symlink whose target resolves inside the store is removed, so a
|
|
64
|
+
* directory a user keeps under a similar name stays in place. A pointer left
|
|
65
|
+
* dangling by a store someone already deleted is removed too.
|
|
66
|
+
*
|
|
67
|
+
* @param {object} roots An `InstallRootResolution`.
|
|
68
|
+
* @returns {{removedPointerPaths: string[], removedStorePath: string|null}} What this call removed.
|
|
69
|
+
*/
|
|
70
|
+
export function removeRetiredPstackInstallation(roots) {
|
|
71
|
+
const storeRoot = retiredPstackStoreRoot(roots.managedRoot);
|
|
72
|
+
const allSkillsRoots = [...new Set([
|
|
73
|
+
roots.skillsLookupDirectory,
|
|
74
|
+
roots.skillsInstallDirectory,
|
|
75
|
+
])];
|
|
76
|
+
const removedPointerPaths = allSkillsRoots
|
|
77
|
+
.flatMap(skillsRoot => removePointersUnder(skillsRoot, storeRoot));
|
|
78
|
+
if (!existsSync(storeRoot)) {
|
|
79
|
+
return { removedPointerPaths, removedStorePath: null };
|
|
80
|
+
}
|
|
81
|
+
rmSync(storeRoot, { recursive: true, force: true });
|
|
82
|
+
return { removedPointerPaths, removedStorePath: storeRoot };
|
|
83
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import { strict as assert } from 'node:assert';
|
|
3
|
+
import {
|
|
4
|
+
existsSync,
|
|
5
|
+
lstatSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
mkdtempSync,
|
|
8
|
+
rmSync,
|
|
9
|
+
symlinkSync,
|
|
10
|
+
writeFileSync,
|
|
11
|
+
} from 'node:fs';
|
|
12
|
+
import { tmpdir } from 'node:os';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
import {
|
|
15
|
+
removeRetiredPstackInstallation,
|
|
16
|
+
retiredPstackStoreRoot,
|
|
17
|
+
} from './prune-retired-pstack-store.mjs';
|
|
18
|
+
|
|
19
|
+
function retiredInstallationFixture(t) {
|
|
20
|
+
const homeDirectory = mkdtempSync(join(tmpdir(), 'cdev-retired-pstack-'));
|
|
21
|
+
t.after(() => rmSync(homeDirectory, { recursive: true, force: true }));
|
|
22
|
+
const managedRoot = join(homeDirectory, '.claude');
|
|
23
|
+
const agentsHome = join(homeDirectory, '.agents');
|
|
24
|
+
const releaseRoot = join(managedRoot, 'pstack', 'releases', 'abc123', 'runtime');
|
|
25
|
+
mkdirSync(join(releaseRoot, 'pstack', 'skills', 'poteto-mode'), { recursive: true });
|
|
26
|
+
mkdirSync(join(releaseRoot, 'cursor-team-kit', 'skills', 'deslop'), { recursive: true });
|
|
27
|
+
writeFileSync(join(managedRoot, 'pstack', 'installed.json'), '{}\n');
|
|
28
|
+
const managedSkillsRoot = join(managedRoot, 'skills');
|
|
29
|
+
const agentsSkillsRoot = join(agentsHome, 'skills');
|
|
30
|
+
mkdirSync(managedSkillsRoot, { recursive: true });
|
|
31
|
+
mkdirSync(agentsSkillsRoot, { recursive: true });
|
|
32
|
+
const pointers = [
|
|
33
|
+
[join(managedSkillsRoot, 'pstack'), join(releaseRoot, 'pstack', 'skills')],
|
|
34
|
+
[join(agentsSkillsRoot, 'pstack'), join(releaseRoot, 'pstack', 'skills')],
|
|
35
|
+
[
|
|
36
|
+
join(agentsSkillsRoot, 'cursor-team-kit-deslop'),
|
|
37
|
+
join(releaseRoot, 'cursor-team-kit', 'skills', 'deslop'),
|
|
38
|
+
],
|
|
39
|
+
];
|
|
40
|
+
for (const [pointerPath, targetPath] of pointers) {
|
|
41
|
+
symlinkSync(targetPath, pointerPath, 'dir');
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
roots: { managedRoot, agentsHome, skillsLookupDirectory: managedSkillsRoot, skillsInstallDirectory: agentsSkillsRoot },
|
|
45
|
+
managedSkillsRoot,
|
|
46
|
+
agentsSkillsRoot,
|
|
47
|
+
pointerPaths: pointers.map(([pointerPath]) => pointerPath),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
test('the store root sits under the managed root', () => {
|
|
52
|
+
assert.equal(
|
|
53
|
+
retiredPstackStoreRoot(join('/managed', '.claude')),
|
|
54
|
+
join('/managed', '.claude', 'pstack'),
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('every pointer into the retired store is removed along with the store', t => {
|
|
59
|
+
const fixture = retiredInstallationFixture(t);
|
|
60
|
+
|
|
61
|
+
const outcome = removeRetiredPstackInstallation(fixture.roots);
|
|
62
|
+
|
|
63
|
+
assert.deepEqual(outcome.removedPointerPaths.sort(), fixture.pointerPaths.sort());
|
|
64
|
+
assert.equal(outcome.removedStorePath, retiredPstackStoreRoot(fixture.roots.managedRoot));
|
|
65
|
+
for (const pointerPath of fixture.pointerPaths) {
|
|
66
|
+
assert.equal(existsSync(pointerPath), false);
|
|
67
|
+
}
|
|
68
|
+
assert.equal(existsSync(retiredPstackStoreRoot(fixture.roots.managedRoot)), false);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('a skill that is not a pointer into the store stays in place', t => {
|
|
72
|
+
const fixture = retiredInstallationFixture(t);
|
|
73
|
+
const ownSkillDirectory = join(fixture.agentsSkillsRoot, 'pull-request');
|
|
74
|
+
mkdirSync(ownSkillDirectory, { recursive: true });
|
|
75
|
+
writeFileSync(join(ownSkillDirectory, 'SKILL.md'), '---\nname: pull-request\n---\n');
|
|
76
|
+
const unrelatedTarget = join(fixture.roots.managedRoot, 'elsewhere', 'pstack');
|
|
77
|
+
mkdirSync(unrelatedTarget, { recursive: true });
|
|
78
|
+
const unrelatedPointer = join(fixture.agentsSkillsRoot, 'pstack-of-my-own');
|
|
79
|
+
symlinkSync(unrelatedTarget, unrelatedPointer, 'dir');
|
|
80
|
+
|
|
81
|
+
removeRetiredPstackInstallation(fixture.roots);
|
|
82
|
+
|
|
83
|
+
assert.equal(existsSync(join(ownSkillDirectory, 'SKILL.md')), true);
|
|
84
|
+
assert.equal(lstatSync(unrelatedPointer).isSymbolicLink(), true);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('a home with no retired installation reports nothing removed', t => {
|
|
88
|
+
const fixture = retiredInstallationFixture(t);
|
|
89
|
+
removeRetiredPstackInstallation(fixture.roots);
|
|
90
|
+
|
|
91
|
+
const secondOutcome = removeRetiredPstackInstallation(fixture.roots);
|
|
92
|
+
|
|
93
|
+
assert.deepEqual(secondOutcome.removedPointerPaths, []);
|
|
94
|
+
assert.equal(secondOutcome.removedStorePath, null);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('a dangling pointer into the store is removed after the store is gone', t => {
|
|
98
|
+
const fixture = retiredInstallationFixture(t);
|
|
99
|
+
rmSync(retiredPstackStoreRoot(fixture.roots.managedRoot), { recursive: true, force: true });
|
|
100
|
+
|
|
101
|
+
const outcome = removeRetiredPstackInstallation(fixture.roots);
|
|
102
|
+
|
|
103
|
+
assert.deepEqual(outcome.removedPointerPaths.sort(), fixture.pointerPaths.sort());
|
|
104
|
+
assert.equal(outcome.removedStorePath, null);
|
|
105
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { dirname, join } from 'node:path';
|
|
2
|
+
|
|
3
|
+
export const CONTINUITY_SCRIPT_MARKER = '/session-continuity/continuity.mjs';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* True when a hook command calls the retired session-continuity companion.
|
|
7
|
+
*
|
|
8
|
+
* @param {unknown} command The `command` field of one host hook entry.
|
|
9
|
+
* @returns {boolean} True when this entry calls the retired companion.
|
|
10
|
+
*/
|
|
11
|
+
export function isContinuityHookCommand(command) {
|
|
12
|
+
return typeof command === 'string' && command.includes(CONTINUITY_SCRIPT_MARKER);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Locate the hook configuration file each host keeps its registrations in.
|
|
17
|
+
*
|
|
18
|
+
* @param {object} roots An `InstallRootResolution`.
|
|
19
|
+
* @returns {Record<string, string>} One configuration path per host.
|
|
20
|
+
*/
|
|
21
|
+
export function continuityHostConfigurationPaths(roots) {
|
|
22
|
+
return {
|
|
23
|
+
claude: join(roots.managedRoot, 'settings.json'),
|
|
24
|
+
codex: join(dirname(roots.codexRulesInstallDirectory), 'hooks.json'),
|
|
25
|
+
cursor: join(roots.cursorInstallDirectory, 'hooks.json'),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Strip every retired session-continuity registration from one host
|
|
31
|
+
* configuration, in place, and drop an event left with no entries.
|
|
32
|
+
*
|
|
33
|
+
* An earlier claude-dev-env registered this companion in each host. The pstack
|
|
34
|
+
* plugin now carries the SessionStart context the companion supplied, so a
|
|
35
|
+
* registration left behind points every session at a deleted script.
|
|
36
|
+
*
|
|
37
|
+
* @param {object} configuration A parsed host hook configuration.
|
|
38
|
+
* @returns {number} How many registrations this call removed.
|
|
39
|
+
*/
|
|
40
|
+
export function removeContinuityHooks(configuration) {
|
|
41
|
+
let removedCount = 0;
|
|
42
|
+
for (const [event, entries] of Object.entries(configuration.hooks || {})) {
|
|
43
|
+
const kept = entries.flatMap(entry => {
|
|
44
|
+
if (!entry.hooks) {
|
|
45
|
+
if (!isContinuityHookCommand(entry.command)) return [entry];
|
|
46
|
+
removedCount += 1;
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
const hooks = entry.hooks.filter(hook => !isContinuityHookCommand(hook.command));
|
|
50
|
+
removedCount += entry.hooks.length - hooks.length;
|
|
51
|
+
return hooks.length ? [{ ...entry, hooks }] : [];
|
|
52
|
+
});
|
|
53
|
+
if (kept.length) configuration.hooks[event] = kept;
|
|
54
|
+
else delete configuration.hooks[event];
|
|
55
|
+
}
|
|
56
|
+
return removedCount;
|
|
57
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import { strict as assert } from 'node:assert';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import {
|
|
5
|
+
continuityHostConfigurationPaths,
|
|
6
|
+
isContinuityHookCommand,
|
|
7
|
+
removeContinuityHooks,
|
|
8
|
+
} from './prune-session-continuity.mjs';
|
|
9
|
+
|
|
10
|
+
const ROOTS = {
|
|
11
|
+
managedRoot: join('/managed', '.claude'),
|
|
12
|
+
codexRulesInstallDirectory: join('/managed', '.codex', 'rules'),
|
|
13
|
+
cursorInstallDirectory: join('/managed', '.cursor'),
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const COMPANION_COMMAND = 'node "/managed/.agents/skills/session-continuity/continuity.mjs" hook claude';
|
|
17
|
+
|
|
18
|
+
test('a companion command is recognized and another hook command is not', () => {
|
|
19
|
+
assert.equal(isContinuityHookCommand(COMPANION_COMMAND), true);
|
|
20
|
+
assert.equal(isContinuityHookCommand('node "/managed/.agents/hooks/routing/other.mjs"'), false);
|
|
21
|
+
assert.equal(isContinuityHookCommand(undefined), false);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('each host names the configuration file that holds its registrations', () => {
|
|
25
|
+
assert.deepEqual(continuityHostConfigurationPaths(ROOTS), {
|
|
26
|
+
claude: join('/managed', '.claude', 'settings.json'),
|
|
27
|
+
codex: join('/managed', '.codex', 'hooks.json'),
|
|
28
|
+
cursor: join('/managed', '.cursor', 'hooks.json'),
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('a grouped registration is removed and the sibling hook stays', () => {
|
|
33
|
+
const configuration = {
|
|
34
|
+
hooks: {
|
|
35
|
+
SessionStart: [{
|
|
36
|
+
matcher: 'startup',
|
|
37
|
+
hooks: [
|
|
38
|
+
{ type: 'command', command: COMPANION_COMMAND },
|
|
39
|
+
{ type: 'command', command: 'node "/managed/.agents/hooks/keep.mjs"' },
|
|
40
|
+
],
|
|
41
|
+
}],
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
assert.equal(removeContinuityHooks(configuration), 1);
|
|
45
|
+
assert.deepEqual(configuration.hooks.SessionStart[0].hooks, [
|
|
46
|
+
{ type: 'command', command: 'node "/managed/.agents/hooks/keep.mjs"' },
|
|
47
|
+
]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('an event left with no entries is dropped rather than left empty', () => {
|
|
51
|
+
const configuration = {
|
|
52
|
+
hooks: {
|
|
53
|
+
UserPromptExpansion: [{ hooks: [{ type: 'command', command: COMPANION_COMMAND }] }],
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
assert.equal(removeContinuityHooks(configuration), 1);
|
|
57
|
+
assert.deepEqual(configuration.hooks, {});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("Cursor's flat entry shape is removed the same way", () => {
|
|
61
|
+
const configuration = { version: 1, hooks: { sessionStart: [{ command: COMPANION_COMMAND }] } };
|
|
62
|
+
assert.equal(removeContinuityHooks(configuration), 1);
|
|
63
|
+
assert.deepEqual(configuration.hooks, {});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('a configuration holding no registration is left untouched', () => {
|
|
67
|
+
const configuration = { hooks: { SessionStart: [{ hooks: [{ command: 'node "/keep.mjs"' }] }] } };
|
|
68
|
+
assert.equal(removeContinuityHooks(configuration), 0);
|
|
69
|
+
assert.deepEqual(configuration.hooks.SessionStart, [{ hooks: [{ command: 'node "/keep.mjs"' }] }]);
|
|
70
|
+
});
|
|
@@ -60,6 +60,7 @@ export const MANIFEST_FILE_NAME = '.claude-dev-env-manifest.json';
|
|
|
60
60
|
* hooksLookupDirectory: string,
|
|
61
61
|
* scriptsLookupDirectory: string,
|
|
62
62
|
* codexHooksInstallDirectory: string,
|
|
63
|
+
* codexHomeDirectory: string,
|
|
63
64
|
* }} InstallRootResolution
|
|
64
65
|
*/
|
|
65
66
|
|
|
@@ -179,6 +180,7 @@ export function resolveInstallRoot(options = {}) {
|
|
|
179
180
|
hooksLookupDirectory,
|
|
180
181
|
scriptsLookupDirectory,
|
|
181
182
|
codexHooksInstallDirectory,
|
|
183
|
+
codexHomeDirectory,
|
|
182
184
|
};
|
|
183
185
|
}
|
|
184
186
|
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import { strict as assert } from 'node:assert';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import {
|
|
6
|
+
resolveInstallRoot,
|
|
7
|
+
CLAUDE_CONFIG_DIR_ENVIRONMENT_VARIABLE,
|
|
8
|
+
DEFAULT_CLAUDE_DIRECTORY_NAME,
|
|
9
|
+
} from './resolve-install-root.mjs';
|
|
10
|
+
import {
|
|
11
|
+
CODEX_HOME_ENVIRONMENT_VARIABLE,
|
|
12
|
+
CODEX_HOOKS_DIRECTORY_NAME,
|
|
13
|
+
CODEX_RULES_DIRECTORY_NAME,
|
|
14
|
+
DEFAULT_CODEX_DIRECTORY_NAME,
|
|
15
|
+
MANAGED_SKILLS_DIRECTORY_NAME,
|
|
16
|
+
PACKAGE_AGENTS_HOME_DIRECTORY_NAME,
|
|
17
|
+
} from './install-constants.mjs';
|
|
18
|
+
|
|
19
|
+
const HOME_DIRECTORY = join(tmpdir(), 'cdev-resolve-roots-home');
|
|
20
|
+
|
|
21
|
+
function resolveFrom(environment, explicitTarget = null) {
|
|
22
|
+
return resolveInstallRoot({ homeDirectory: HOME_DIRECTORY, environment, explicitTarget });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
test('the managed root falls back to the default Claude directory under the home', () => {
|
|
26
|
+
const resolution = resolveFrom({});
|
|
27
|
+
assert.equal(resolution.managedRoot, join(HOME_DIRECTORY, DEFAULT_CLAUDE_DIRECTORY_NAME));
|
|
28
|
+
assert.equal(resolution.source, 'default-home');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('CLAUDE_CONFIG_DIR names the managed root and an explicit target outranks it', () => {
|
|
32
|
+
const configuredRoot = join(tmpdir(), 'cdev-configured');
|
|
33
|
+
const explicitRoot = join(tmpdir(), 'cdev-explicit');
|
|
34
|
+
const configured = resolveFrom({ [CLAUDE_CONFIG_DIR_ENVIRONMENT_VARIABLE]: configuredRoot });
|
|
35
|
+
assert.equal(configured.managedRoot, configuredRoot);
|
|
36
|
+
assert.equal(configured.source, 'claude-config-dir');
|
|
37
|
+
|
|
38
|
+
const explicit = resolveFrom(
|
|
39
|
+
{ [CLAUDE_CONFIG_DIR_ENVIRONMENT_VARIABLE]: configuredRoot },
|
|
40
|
+
explicitRoot,
|
|
41
|
+
);
|
|
42
|
+
assert.equal(explicit.managedRoot, explicitRoot);
|
|
43
|
+
assert.equal(explicit.source, 'explicit-target');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('the Codex home is the directory holding the rules and hooks destinations', () => {
|
|
47
|
+
const resolution = resolveFrom({});
|
|
48
|
+
const codexHome = join(HOME_DIRECTORY, DEFAULT_CODEX_DIRECTORY_NAME);
|
|
49
|
+
assert.equal(resolution.codexHomeDirectory, codexHome);
|
|
50
|
+
assert.equal(resolution.codexRulesInstallDirectory, join(codexHome, CODEX_RULES_DIRECTORY_NAME));
|
|
51
|
+
assert.equal(resolution.codexHooksInstallDirectory, join(codexHome, CODEX_HOOKS_DIRECTORY_NAME));
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('CODEX_HOME relocates the Codex home the pstack plugin step installs into', () => {
|
|
55
|
+
const relocatedHome = join(tmpdir(), 'cdev-relocated-codex');
|
|
56
|
+
const resolution = resolveFrom({ [CODEX_HOME_ENVIRONMENT_VARIABLE]: relocatedHome });
|
|
57
|
+
assert.equal(resolution.codexHomeDirectory, relocatedHome);
|
|
58
|
+
assert.equal(resolution.codexRulesInstallDirectory, join(relocatedHome, CODEX_RULES_DIRECTORY_NAME));
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('a managed root named .claude pairs with the sibling agents home', () => {
|
|
62
|
+
const resolution = resolveFrom({});
|
|
63
|
+
assert.equal(resolution.agentsHome, join(HOME_DIRECTORY, PACKAGE_AGENTS_HOME_DIRECTORY_NAME));
|
|
64
|
+
assert.equal(resolution.skillsInstallDirectory, join(HOME_DIRECTORY, PACKAGE_AGENTS_HOME_DIRECTORY_NAME, MANAGED_SKILLS_DIRECTORY_NAME));
|
|
65
|
+
assert.equal(
|
|
66
|
+
resolution.skillsLookupDirectory,
|
|
67
|
+
join(HOME_DIRECTORY, DEFAULT_CLAUDE_DIRECTORY_NAME, MANAGED_SKILLS_DIRECTORY_NAME),
|
|
68
|
+
);
|
|
69
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { test } from 'node:test';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
|
-
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
4
4
|
import { cpSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
5
5
|
import { tmpdir } from 'node:os';
|
|
6
6
|
import { dirname, join } from 'node:path';
|
|
@@ -35,6 +35,7 @@ const replacements = [
|
|
|
35
35
|
[['Luna', 'low'], ['gpt-5.6-luna', 'high']],
|
|
36
36
|
[['Luna', 'medium'], ['gpt-5.6-luna', 'high']],
|
|
37
37
|
[['Sol', 'low'], ['gpt-5.6-luna', 'xhigh']],
|
|
38
|
+
[['Sol', 'medium'], ['gpt-5.6-luna', 'max']],
|
|
38
39
|
[['Terra', 'low'], ['gpt-5.6-luna', 'xhigh']],
|
|
39
40
|
[['Terra', 'medium'], ['gpt-5.6-luna', 'xhigh']],
|
|
40
41
|
[['Terra', 'high'], ['gpt-5.6-luna', 'xhigh']],
|
|
@@ -59,7 +60,6 @@ test('every approved pair returns its literal native pair', () => {
|
|
|
59
60
|
[['Luna', 'high'], ['gpt-5.6-luna', 'high']],
|
|
60
61
|
[['Luna', 'xhigh'], ['gpt-5.6-luna', 'xhigh']],
|
|
61
62
|
[['Luna', 'max'], ['gpt-5.6-luna', 'max']],
|
|
62
|
-
[['Sol', 'medium'], ['gpt-5.6-sol', 'medium']],
|
|
63
63
|
[['Astra', 'low'], ['gpt-6-astra', 'low']],
|
|
64
64
|
[['Astra', 'medium'], ['gpt-6-astra', 'medium']],
|
|
65
65
|
];
|
|
@@ -67,6 +67,9 @@ test('every approved pair returns its literal native pair', () => {
|
|
|
67
67
|
const result = route({ model, reasoning_effort: effort });
|
|
68
68
|
assert.ok(['pass', 'remapped'].includes(result.status));
|
|
69
69
|
assert.deepEqual(result.selected, { model: expectedModel, effort: expectedEffort });
|
|
70
|
+
const nativeRoute = route({ model: expectedModel, reasoning_effort: expectedEffort });
|
|
71
|
+
assert.equal(nativeRoute.status, 'pass');
|
|
72
|
+
assert.deepEqual(nativeRoute.selected, { model: expectedModel, effort: expectedEffort });
|
|
70
73
|
}
|
|
71
74
|
const advisor = advisorRoute({
|
|
72
75
|
model: 'Astra',
|
|
@@ -77,12 +80,12 @@ test('every approved pair returns its literal native pair', () => {
|
|
|
77
80
|
assert.deepEqual(advisor.selected, { model: 'gpt-6-astra', effort: 'high' });
|
|
78
81
|
});
|
|
79
82
|
|
|
80
|
-
test('selector exclusions
|
|
83
|
+
test('selector exclusions keep Sol Medium out while direct routing remaps', () => {
|
|
81
84
|
const policy = loadSubagentModelPolicy();
|
|
82
85
|
assert.equal(policy.selectorExclusions.has('sol/medium'), true);
|
|
83
86
|
const result = route({ model: 'Sol', reasoning_effort: 'medium' });
|
|
84
|
-
assert.
|
|
85
|
-
assert.deepEqual(result.selected, { model: 'gpt-5.6-
|
|
87
|
+
assert.equal(result.status, 'remapped');
|
|
88
|
+
assert.deepEqual(result.selected, { model: 'gpt-5.6-luna', effort: 'max' });
|
|
86
89
|
|
|
87
90
|
const rawPolicy = JSON.parse(readFileSync(POLICY_SOURCE, 'utf8'));
|
|
88
91
|
rawPolicy.selectorExclusions = [{ model: 'missing', effort: 'medium' }];
|
|
@@ -174,6 +177,25 @@ test('the advisor bridge blocks malformed input', () => {
|
|
|
174
177
|
);
|
|
175
178
|
});
|
|
176
179
|
|
|
180
|
+
test('the advisor bridge routes Sol Medium to Luna Max', () => {
|
|
181
|
+
const bridgePath = join(PACKAGE_ROOT, 'scripts', 'resolve_advisor_model_route.mjs');
|
|
182
|
+
const bridgeExecution = spawnSync(
|
|
183
|
+
process.execPath,
|
|
184
|
+
[bridgePath],
|
|
185
|
+
{
|
|
186
|
+
encoding: 'utf8',
|
|
187
|
+
input: JSON.stringify({ model: 'Sol', reasoning_effort: 'medium' }),
|
|
188
|
+
},
|
|
189
|
+
);
|
|
190
|
+
assert.equal(bridgeExecution.status, 0);
|
|
191
|
+
assert.equal(bridgeExecution.stderr, '');
|
|
192
|
+
const advisorDecision = JSON.parse(bridgeExecution.stdout);
|
|
193
|
+
assert.equal(advisorDecision.status, 'remapped');
|
|
194
|
+
assert.equal(advisorDecision.role, 'advisor');
|
|
195
|
+
assert.equal(advisorDecision.diagnostic, null);
|
|
196
|
+
assert.deepEqual(advisorDecision.selected, { model: 'gpt-5.6-luna', effort: 'max' });
|
|
197
|
+
});
|
|
198
|
+
|
|
177
199
|
test('missing worker settings preserve parent inheritance', () => {
|
|
178
200
|
const input = { message: 'keep', agent_type: 'worker' };
|
|
179
201
|
const result = routeSubagentToolInput(input);
|
|
@@ -181,6 +203,15 @@ test('missing worker settings preserve parent inheritance', () => {
|
|
|
181
203
|
assert.strictEqual(result.updatedInput, input);
|
|
182
204
|
});
|
|
183
205
|
|
|
206
|
+
test('Sol without an effort stays blocked after its only approved pair is removed', () => {
|
|
207
|
+
const result = route(
|
|
208
|
+
{ model: 'Sol' },
|
|
209
|
+
{ allowMissingEffort: true },
|
|
210
|
+
);
|
|
211
|
+
assert.equal(result.status, 'blocked');
|
|
212
|
+
assert.equal(result.diagnostic, 'effort is required for this model');
|
|
213
|
+
});
|
|
214
|
+
|
|
184
215
|
test('unknown input holds with a short diagnostic', () => {
|
|
185
216
|
assert.match(route({ model: 'Unknown', reasoning_effort: 'medium' }).diagnostic, /model is unknown/);
|
|
186
217
|
assert.match(route({ model: 'Terra', reasoning_effort: 'future' }).diagnostic, /effort is unknown/);
|
|
@@ -201,16 +232,16 @@ test('native values with case or whitespace differences are canonicalized', () =
|
|
|
201
232
|
});
|
|
202
233
|
assert.equal(result.status, 'remapped');
|
|
203
234
|
assert.deepEqual(result.updatedInput, {
|
|
204
|
-
model: 'gpt-5.6-
|
|
205
|
-
reasoning_effort: '
|
|
235
|
+
model: 'gpt-5.6-luna',
|
|
236
|
+
reasoning_effort: 'max',
|
|
206
237
|
message: 'keep',
|
|
207
238
|
});
|
|
208
239
|
});
|
|
209
240
|
|
|
210
241
|
test('unavailable replacements hold the spawn', () => {
|
|
211
242
|
const result = route(
|
|
212
|
-
{ model: '
|
|
213
|
-
{ availableModelIds: ['gpt-5.6-
|
|
243
|
+
{ model: 'Sol', reasoning_effort: 'medium' },
|
|
244
|
+
{ availableModelIds: ['gpt-5.6-sol'] },
|
|
214
245
|
);
|
|
215
246
|
assert.equal(result.status, 'blocked');
|
|
216
247
|
assert.match(result.diagnostic, /replacement model is unavailable/);
|
|
@@ -246,7 +277,7 @@ test('the tool route copies every non-routing field', () => {
|
|
|
246
277
|
|
|
247
278
|
test('a second route pass is idempotent', () => {
|
|
248
279
|
const first = routeSubagentToolInput(
|
|
249
|
-
{ model: '
|
|
280
|
+
{ model: 'Sol', reasoning_effort: 'medium', message: 'keep' },
|
|
250
281
|
{},
|
|
251
282
|
);
|
|
252
283
|
const second = routeSubagentToolInput(first.updatedInput);
|
|
@@ -261,17 +292,22 @@ test('a temporary policy copy changes the next route without source edits', () =
|
|
|
261
292
|
const temporaryPolicy = join(temporaryRoot, 'policy.json');
|
|
262
293
|
const sourceText = readFileSync(POLICY_SOURCE, 'utf8');
|
|
263
294
|
const policy = JSON.parse(sourceText);
|
|
295
|
+
writeFileSync(temporaryPolicy, sourceText);
|
|
296
|
+
assert.deepEqual(route(
|
|
297
|
+
{ model: 'Sol', reasoning_effort: 'medium' },
|
|
298
|
+
{ policyPath: temporaryPolicy },
|
|
299
|
+
).selected, { model: 'gpt-5.6-luna', effort: 'max' });
|
|
264
300
|
policy.replacements = policy.replacements.map(replacement => (
|
|
265
|
-
replacement.requested.model === '
|
|
266
|
-
? { ...replacement, selected: { model: 'luna', effort: '
|
|
301
|
+
replacement.requested.model === 'sol' && replacement.requested.effort === 'medium'
|
|
302
|
+
? { ...replacement, selected: { model: 'luna', effort: 'xhigh' } }
|
|
267
303
|
: replacement
|
|
268
304
|
));
|
|
269
305
|
writeFileSync(temporaryPolicy, JSON.stringify(policy));
|
|
270
|
-
const
|
|
271
|
-
{ model: '
|
|
306
|
+
const updatedRoute = route(
|
|
307
|
+
{ model: 'Sol', reasoning_effort: 'medium' },
|
|
272
308
|
{ policyPath: temporaryPolicy },
|
|
273
309
|
);
|
|
274
|
-
assert.deepEqual(
|
|
310
|
+
assert.deepEqual(updatedRoute.selected, { model: 'gpt-5.6-luna', effort: 'xhigh' });
|
|
275
311
|
assert.equal(readFileSync(POLICY_SOURCE, 'utf8'), sourceText);
|
|
276
312
|
} finally {
|
|
277
313
|
rmSync(temporaryRoot, { recursive: true, force: true });
|
|
@@ -347,10 +383,11 @@ test('model names cannot collide after normalization', () => {
|
|
|
347
383
|
|
|
348
384
|
test('combined role routes must end at an approved terminal pair', () => {
|
|
349
385
|
const policy = JSON.parse(readFileSync(POLICY_SOURCE, 'utf8'));
|
|
350
|
-
policy.replacements.
|
|
351
|
-
requested
|
|
352
|
-
|
|
353
|
-
|
|
386
|
+
const solMediumReplacement = policy.replacements.find(replacement => (
|
|
387
|
+
replacement.requested.model === 'sol'
|
|
388
|
+
&& replacement.requested.effort === 'medium'
|
|
389
|
+
));
|
|
390
|
+
solMediumReplacement.selected = { model: 'astra', effort: 'low' };
|
|
354
391
|
policy.roleReplacements.push({
|
|
355
392
|
role: 'worker',
|
|
356
393
|
requested: { model: 'astra', effort: 'low' },
|
package/docs/CODE_RULES.md
CHANGED
|
@@ -31,7 +31,7 @@ Production and tests follow one rule. Changed directive, TODO, FIXME, HACK, XXX,
|
|
|
31
31
|
|
|
32
32
|
no new comments · imports at top · logging format args (`log_*("...", arg)`) · no `%s`/`%d` printf tokens in a `str.format`-logger message (`log_*` imported from `automation_logging`; `str.format` drops the args — use `{}`) · no magic values in production bodies (0, 1, -1 exempt) · UPPER_SNAKE constants only in `config/` (exempt: `config/*`; `/migrations/`; Workflow registries: path contains any of these substrings — `/workflow/`, `_tab.py`, `/states.py`, or `/modules.py`, each matching independently as a substring, so `pkg/states.py` qualifies while a top-level `states.py` follows the standard `config/` rule; test files — path or filename matches `test_`, `_test.`, `.spec.`, `conftest`, or `/tests/`) · no hardcoded user home paths · guarded `sys.path.insert` · banned identifiers (`ctx`, `cfg`, `msg`, `btn`, `idx`, `cnt`, `tmp`, `elem`, `val`) · banned function prefixes (`handle_`, `process_`, `manage_`, `do_`) · no type escape hatches (`Any` import, `cast()`, inline `Any`, a parameter typed bare `object` whose body reads `param.attribute`) outside boundary files · no bare/broad `except` · no `Any` in signatures or class attributes · no stub bodies (`pass`/`...`/`raise NotImplementedError`) outside abstract/Protocol · TypedDict `_encode_*`/`_decode_*` companions in the same module · no test-mode branching in production (use dependency injection) · no thin wrapper modules · Google-style docstrings on public functions with `Args:` matching the signature · boolean names prefixed `is_`/`has_`/`should_`/`can_`/`was_`/`did_` (assignments AND bool-typed parameters) · must-check returns (`find_and_click`, `write_outcome`) assigned and checked · known pytest fixture parameters in test files annotated with their single documented type (`tmp_path: Path`, `monkeypatch: pytest.MonkeyPatch`, `capsys`, `caplog`, `request`, …) · known pytest fixture parameters a test function declares but never references (drop the unused parameter — pytest still pays its setup cost) · JavaScript/TypeScript boolean declarations (`const`/`let`/`var` bound to a boolean literal or negation) and `@param {boolean}` JSDoc names prefixed `is`/`has`/`should`/`can`/`was`/`did` (camelCase forms) · banned identifiers as `.mjs`/`.js` declaration names (`result`, `data`, `ctx`, `msg`, …), scoped to changed lines · in test files, banned identifiers fire on changed lines, and pytest-collectable `test_*` functions need a return annotation · unused module-level imports and unsorted import blocks are ruff's job (F401, isort I001), not this hook's · a `hooks/blocking/` command classifier anchors its multi-word command regex to the command start (`^`/`\A`) or tokenizes the first word (`shlex.split`), never matching a command as a bare substring
|
|
33
33
|
|
|
34
|
-
Test files follow the comment policy above; other test-specific exemptions are listed here. The one annotation the test-file exemption does NOT cover is a known pytest builtin fixture parameter: `tmp_path`, `monkeypatch`, `capsys`, `capfd`, `caplog`, `request`, and `tmp_path_factory` each have a single documented injected type, so the gate requires that annotation (`tmp_path: Path`) even inside a test file. The same set of fixtures is also subject to a use check: a pytest-collected test function that declares one of these parameters and never references it in its body fails the gate, because pytest materializes the fixture's setup (the temp directory, the monkeypatch context, the output capture) on every run whether or not the body reads the value — drop the unused parameter. A parameter counts as referenced when its name is read, augmented-assigned, or deleted anywhere in the body, including inside a nested function or comprehension. Only pytest-collectable functions are inspected — those at module top level or defined directly in a class body; a function nested inside another function's body is a local helper pytest never collects, so its fixture-named parameter is exempt. A `@pytest.fixture`-decorated function is exempt from the use check, since injecting one fixture into another purely to order its setup is intentional. Ordinary test parameters stay exempt from both checks.
|
|
34
|
+
Test files follow the comment policy above; other test-specific exemptions are listed here. The one annotation the test-file exemption does NOT cover is a known pytest builtin fixture parameter: `tmp_path`, `monkeypatch`, `capsys`, `capfd`, `caplog`, `request`, and `tmp_path_factory` each have a single documented injected type, so the gate requires that annotation (`tmp_path: Path`) even inside a test file. The same set of fixtures is also subject to a use check: a pytest-collected test function that declares one of these parameters and never references it in its body fails the gate, because pytest materializes the fixture's setup (the temp directory, the monkeypatch context, the output capture) on every run whether or not the body reads the value — drop the unused parameter. A parameter counts as referenced when its name is read, augmented-assigned, or deleted anywhere in the body, including inside a nested function or comprehension. Only pytest-collectable functions are inspected — those at module top level or defined directly in a class body; a function nested inside another function's body is a local helper pytest never collects, so its fixture-named parameter is exempt. A `@pytest.fixture`-decorated function is exempt from the use check, since injecting one fixture into another purely to order its setup is intentional. Ordinary test parameters stay exempt from both checks.
|
|
35
35
|
|
|
36
36
|
---
|
|
37
37
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Agent Spawn Protocol
|
|
2
2
|
|
|
3
|
-
Full protocol
|
|
3
|
+
Full protocol for briefing a subagent. It applies before any Agent or Task tool invocation — Explore, implementation, research, or team subagents.
|
|
4
4
|
|
|
5
5
|
## Step 1: Context sufficiency check
|
|
6
6
|
|
|
@@ -65,22 +65,10 @@ hand when the host should receive that context.
|
|
|
65
65
|
|
|
66
66
|
## Luna fast-mode guard
|
|
67
67
|
|
|
68
|
-
`
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
"matcher": "Agent|Task|multi_agent_v1__spawn_agent",
|
|
73
|
-
"hooks": [
|
|
74
|
-
{
|
|
75
|
-
"type": "command",
|
|
76
|
-
"command": "python <CODEX_HOOKS_ROOT>/blocking/luna_fast_mode_gate.py",
|
|
77
|
-
"timeout": 10
|
|
78
|
-
}
|
|
79
|
-
]
|
|
80
|
-
}
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
The guard requires exact `fast` for Luna spawns through `Agent` and `Task`. Native Codex `multi_agent_v1__spawn_agent` accepts exact `fast` or `priority`. Other models and tools pass through.
|
|
68
|
+
Commit `0f21faf8` retired `luna_fast_mode_gate.py` with the other blocking
|
|
69
|
+
policy hooks. The package ships no replacement, so a Codex `hooks.json` needs
|
|
70
|
+
no entry for it, and the spawn tier a Luna spawn asks for is the caller's to
|
|
71
|
+
hold.
|
|
84
72
|
|
|
85
73
|
## Roots and safety
|
|
86
74
|
|
|
@@ -23,7 +23,7 @@ from there when something needs them, and keep them out of anything committed or
|
|
|
23
23
|
|
|
24
24
|
Write the script file with the Write tool. Keeping the commands in a file has a second benefit: a
|
|
25
25
|
destructive word such as `rm -rf` inside the file never appears in a Bash tool command string, so the
|
|
26
|
-
|
|
26
|
+
harness permission prompt never fires on it and an unattended run keeps going.
|
|
27
27
|
|
|
28
28
|
The script runs through `bash -s` in a single shell. Variables, `cd`, and `source` all carry from one
|
|
29
29
|
line to the next. Standard output, standard error and the exit code all come back.
|
|
@@ -10,8 +10,8 @@ The flag is `PROSE_STYLE_ENFORCEMENT_ENABLED` in `hooks/blocking/config/prose_st
|
|
|
10
10
|
|
|
11
11
|
## Always on
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
Historical/comparative language detection in `state_description_blocker.py` runs unconditionally and does not read this flag. Apply `rules/asd-ste100-language.md` for user-facing word choice and sentence style. Code-rules and security blockers are out of scope for this switch.
|
|
14
14
|
|
|
15
15
|
## Hedging claims
|
|
16
16
|
|
|
17
|
-
Hedge-word discipline is prose guidance, not a hook
|
|
17
|
+
Hedge-word discipline is prose guidance, not a hook. State a claim with its evidence, or label it unverified in the same sentence as the hedge word.
|