claude-dev-env 2.9.0 → 2.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +1 -1
- package/_shared/pr-loop/CLAUDE.md +1 -1
- package/_shared/pr-loop/audit-contract.md +17 -6
- package/_shared/pr-loop/audit-reply-template.md +4 -4
- package/_shared/pr-loop/code-rules-gate.md +3 -5
- package/_shared/pr-loop/fix-protocol.md +2 -3
- package/_shared/pr-loop/gh-payloads.md +1 -1
- package/_shared/pr-loop/scripts/CLAUDE.md +1 -1
- package/_shared/pr-loop/scripts/README.md +1 -1
- package/_shared/pr-loop/scripts/code_rules_gate.py +2 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_running.py +16 -1
- package/_shared/pr-loop/scripts/code_rules_gate_parts/git_blob_readers.py +11 -5
- package/_shared/pr-loop/scripts/preflight.py +9 -4
- package/_shared/pr-loop/scripts/reviews_disabled.py +50 -22
- package/_shared/pr-loop/scripts/tests/conftest.py +20 -0
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +6 -6
- package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +50 -6
- package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +1 -1
- package/_shared/pr-loop/state-schema.md +5 -14
- package/agents/CLAUDE.md +2 -2
- package/agents/clean-coder.md +58 -548
- package/agents/code-quality-agent.md +10 -2
- package/agents/code-verifier.md +1 -1
- package/agents/test_agent_frontmatter.py +32 -40
- package/audit-rubrics/CLAUDE.md +2 -1
- package/audit-rubrics/audit-categories.json +704 -0
- package/audit-rubrics/prompts/category-i-concurrency.md +1 -1
- package/bin/CLAUDE.md +16 -5
- package/bin/ever-shipped-skills.mjs +2 -0
- package/bin/install-plan.mjs +402 -0
- package/bin/install-transaction.mjs +455 -0
- package/bin/install.mjs +593 -147
- package/bin/install.plan.test.mjs +194 -0
- package/bin/install.profile-root.test.mjs +154 -0
- package/bin/install.profiles.test.mjs +253 -0
- package/bin/install.settings-defaults.test.mjs +200 -0
- package/bin/install.transaction.test.mjs +400 -0
- package/bin/install.uninstall-transaction.test.mjs +418 -0
- package/bin/merge_managed_permissions.mjs +130 -0
- package/bin/resolve-install-root.mjs +181 -0
- package/bin/select-install-targets.mjs +401 -0
- package/commands/CLAUDE.md +0 -2
- package/docs/references/CLAUDE.md +1 -0
- package/docs/references/advisor-tool.md +1 -1
- package/docs/references/prose-style-enforcement.md +25 -0
- package/hooks/blocking/CLAUDE.md +6 -6
- package/hooks/blocking/_path_setup.py +9 -5
- package/hooks/blocking/code_rules_docstrings.py +124 -30
- package/hooks/blocking/code_rules_enforcer.py +161 -16
- package/hooks/blocking/code_rules_shared.py +40 -23
- package/hooks/blocking/config/CLAUDE.md +3 -5
- package/hooks/blocking/config/prose_style_enforcement_constants.py +38 -0
- package/hooks/blocking/config/test_prose_style_enforcement_constants.py +45 -0
- package/hooks/blocking/eli11_reply_enforcer.py +70 -113
- package/hooks/blocking/hedging_language_blocker.py +103 -20
- package/hooks/blocking/hook_prose_detector_consistency.py +6 -0
- package/hooks/blocking/intent_only_ending_blocker.py +6 -0
- package/hooks/blocking/plain_language_blocker.py +139 -20
- package/hooks/blocking/pre_tool_use_dispatcher.py +102 -20
- package/hooks/blocking/state_description_blocker.py +7 -1
- package/hooks/blocking/tdd_enforcer.py +8 -0
- package/hooks/blocking/test__path_setup.py +28 -0
- package/hooks/blocking/test_code_rules_enforcer_agent_home_tooling.py +99 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_args_span_scope.py +232 -10
- package/hooks/blocking/test_code_rules_enforcer_ephemeral.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_join_separator_magic.py +41 -0
- package/hooks/blocking/test_code_rules_enforcer_string_magic.py +98 -0
- package/hooks/blocking/test_eli11_reply_enforcer.py +98 -165
- package/hooks/blocking/test_hedging_language_blocker.py +120 -1
- package/hooks/blocking/test_hook_prose_detector_consistency.py +28 -8
- package/hooks/blocking/test_intent_only_ending_blocker.py +27 -2
- package/hooks/blocking/test_package_inventory_stale_blocker.py +11 -4
- package/hooks/blocking/test_plain_language_blocker.py +129 -19
- package/hooks/blocking/test_plain_language_blocker_allowlist.py +70 -26
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +99 -26
- package/hooks/blocking/test_pre_tool_use_dispatcher_native.py +87 -50
- package/hooks/blocking/test_state_description_blocker.py +45 -2
- package/hooks/blocking/test_stop_dispatcher.py +11 -7
- package/hooks/blocking/test_volatile_path_in_post_blocker.py +12 -12
- package/hooks/blocking/volatile_path_in_post_blocker.py +2 -2
- package/hooks/hooks.json +15 -0
- package/hooks/hooks_constants/CLAUDE.md +14 -3
- package/hooks/hooks_constants/ask_user_question_shape.py +281 -0
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +2 -1
- package/hooks/hooks_constants/eli11_reply_enforcer_constants.py +5 -12
- package/hooks/hooks_constants/hedging_uncertainty_constants.py +42 -0
- package/hooks/hooks_constants/issue_tracker_session_starter_constants.py +23 -0
- package/hooks/hooks_constants/orchestrator_auto_starter_constants.py +23 -0
- package/hooks/hooks_constants/piped_pytest_blocker_constants.py +4 -1
- package/hooks/hooks_constants/plain_language_blocker_constants.py +4 -1
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +6 -0
- package/hooks/hooks_constants/project_paths_reader.py +31 -4
- package/hooks/hooks_constants/prose_matcher_precision_constants.py +40 -0
- package/hooks/hooks_constants/pytest_invocation.py +354 -0
- package/hooks/hooks_constants/session_start_injector.py +163 -0
- package/hooks/hooks_constants/session_start_injector_constants.py +46 -0
- package/hooks/hooks_constants/shell_command_pipeline.py +397 -0
- package/hooks/hooks_constants/shell_command_segments.py +5 -0
- package/hooks/hooks_constants/test_ask_user_question_shape.py +167 -0
- package/hooks/hooks_constants/test_project_paths_reader.py +29 -0
- package/hooks/hooks_constants/test_prose_metrics_parity.py +8 -0
- package/hooks/hooks_constants/test_pytest_invocation.py +130 -0
- package/hooks/hooks_constants/test_session_start_injector.py +168 -0
- package/hooks/hooks_constants/test_shell_command_pipeline.py +135 -0
- package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +1 -1
- package/hooks/hooks_constants/working_style_prompt_constants.py +30 -0
- package/hooks/observability/CLAUDE.md +2 -0
- package/hooks/observability/prose_matcher_advisory.py +237 -0
- package/hooks/observability/test_prose_matcher_advisory.py +143 -0
- package/hooks/session/CLAUDE.md +9 -1
- package/hooks/session/_path_setup.py +13 -0
- package/hooks/session/issue_tracker_session_starter.py +135 -0
- package/hooks/session/orchestrator_auto_starter.py +100 -0
- package/hooks/session/test__path_setup.py +28 -0
- package/hooks/session/test_issue_tracker_session_starter.py +104 -0
- package/hooks/session/test_orchestrator_auto_starter.py +99 -0
- package/hooks/session/test_working_style_prompt.py +47 -0
- package/hooks/session/untracked_repo_detector.py +1 -24
- package/hooks/session/working_style_prompt.py +36 -0
- package/hooks/validators/_path_setup.py +19 -0
- package/hooks/validators/run_all_validators.py +8 -13
- package/installable-surfaces.manifest.json +21 -0
- package/output-styles/CLAUDE.md +1 -3
- package/package.json +4 -2
- package/rules/CLAUDE.md +1 -0
- package/rules/durable-post-artifacts.md +2 -2
- package/rules/eli11-replies.md +6 -1
- package/rules/hedging-claims.md +4 -2
- package/rules/long-horizon-autonomy.md +3 -1
- package/rules/opus5-communication-contract.md +45 -0
- package/rules/plain-language.md +2 -2
- package/rules/research-mode.md +1 -1
- package/scripts/CLAUDE.md +11 -0
- package/scripts/Sync-RepoMain.ps1 +215 -0
- package/scripts/active_capability_references.py +218 -0
- package/scripts/ci/windows-installer-lifecycle.ps1 +78 -0
- package/scripts/claude_chain_runner.py +394 -6
- package/scripts/claude_chain_usage.py +1 -1
- package/scripts/codex_compat_materializer.py +105 -85
- package/scripts/dev_env_scripts_constants/CLAUDE.md +2 -0
- package/scripts/dev_env_scripts_constants/active_capability_constants.py +46 -0
- package/scripts/dev_env_scripts_constants/claude_chain_constants.py +74 -0
- package/scripts/dev_env_scripts_constants/verify_installable_package_constants.py +116 -0
- package/scripts/profile-isolation-launchers/config/mcp-bundles.json +25 -0
- package/scripts/profile-isolation-launchers/config/profile-isolation-constants.mjs +60 -0
- package/scripts/profile-isolation-launchers/config/profiles.manifest.json +54 -0
- package/scripts/profile-isolation-launchers/config/shared-allowlist.json +64 -0
- package/scripts/profile-isolation-launchers/launcher-runtime.mjs +180 -0
- package/scripts/profile-isolation-launchers/lib/profile-manifest.mjs +288 -0
- package/scripts/profile-isolation-launchers/mcp-bundles.mjs +275 -0
- package/scripts/profile-isolation-launchers/profile-isolation-contract.test.mjs +221 -0
- package/scripts/profile-isolation-launchers/tests/launcher-runtime.test.mjs +108 -0
- package/scripts/profile-isolation-launchers/tests/mcp-bundles.test.mjs +147 -0
- package/scripts/profile-isolation-launchers/tests/shortcut-contract.test.ps1 +102 -0
- package/scripts/profile-isolation-launchers/tests/version-compatibility.test.mjs +210 -0
- package/scripts/profile-isolation-launchers/version-compatibility.mjs +299 -0
- package/scripts/profile-isolation-launchers/windows/shortcut-inventory.ps1 +127 -0
- package/scripts/profile-isolation-launchers/windows/shortcut-manifest.json +51 -0
- package/scripts/profile-isolation-launchers/windows/shortcut-reconcile.ps1 +77 -0
- package/scripts/spawn_grok_batch.py +3 -0
- package/scripts/test_active_capability_references.py +108 -0
- package/scripts/test_claude_chain_runner.py +414 -82
- package/scripts/test_claude_chain_usage.py +12 -12
- package/scripts/test_resolve_worker_spawn.py +2 -2
- package/scripts/test_verify_installable_package.py +208 -0
- package/scripts/tests/test_codex_compat_materializer.py +33 -0
- package/scripts/verify_installable_package.py +612 -0
- package/settings.json +10 -0
- package/skills/CLAUDE.md +2 -0
- package/skills/_shared/pr-loop/scripts/CLAUDE.md +1 -0
- package/skills/_shared/pr-loop/scripts/audit_category_schema.py +355 -0
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/CLAUDE.md +1 -0
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/audit_category_schema_constants.py +32 -0
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +7 -19
- package/skills/_shared/pr-loop/scripts/test_audit_category_schema.py +94 -0
- package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +21 -0
- package/skills/autoconverge/reference/convergence.md +2 -1
- package/skills/autoconverge/reference/stop-conditions.md +5 -3
- package/skills/beat-sheet/SKILL.md +54 -0
- package/skills/beat-sheet/reference/visual-beats.md +29 -0
- package/skills/bugteam/CONSTRAINTS.md +4 -4
- package/skills/bugteam/EXAMPLES.md +1 -1
- package/skills/bugteam/reference/README.md +1 -1
- package/skills/e-code-review/SKILL.md +26 -5
- package/skills/e-code-review/reference/effort-evaluation.md +35 -0
- package/skills/e-code-review/reference/medium.md +15 -4
- package/skills/e-code-review/scripts/config/e_code_review_effort_constants/__init__.py +41 -0
- package/skills/e-code-review/scripts/config/e_code_review_effort_constants/effort_constants.py +40 -0
- package/skills/e-code-review/scripts/e_code_review_scripts_constants/finding_pipeline_constants.py +49 -0
- package/skills/e-code-review/scripts/effort_defaults_evidence.json +186 -0
- package/skills/e-code-review/scripts/effort_evaluation.py +362 -0
- package/skills/e-code-review/scripts/finding_pipeline.py +140 -0
- package/skills/e-code-review/scripts/fixtures/demanding.json +26 -0
- package/skills/e-code-review/scripts/fixtures/easy.json +14 -0
- package/skills/e-code-review/scripts/fixtures/medium.json +20 -0
- package/skills/e-code-review/scripts/grok_code_review.py +16 -7
- package/skills/e-code-review/scripts/test_effort_evaluation.py +180 -0
- package/skills/e-code-review/scripts/test_finding_pipeline.py +197 -0
- package/skills/e-code-review/scripts/test_grok_code_review.py +77 -0
- package/skills/plan-to-pr/scripts/create_packet.py +4 -4
- package/skills/plan-to-pr/scripts/load_skill_constants.py +41 -0
- package/skills/plan-to-pr/scripts/validate_packet.py +4 -4
- package/skills/plan-to-pr/scripts/validate_protocol.py +4 -1
- package/skills/plan-to-pr/scripts/validate_run.py +4 -1
- package/skills/pr-converge/scripts/check_convergence.py +21 -19
- package/skills/pr-converge/scripts/check_convergence_availability.py +50 -7
- package/skills/pr-converge/scripts/conftest.py +35 -0
- package/skills/pr-converge/scripts/test_check_convergence_availability.py +65 -0
- package/skills/pr-converge/scripts/test_check_convergence_codex.py +11 -1
- package/skills/pr-converge/scripts/test_check_convergence_contract.py +9 -2
- package/skills/pr-loop-cloud-transport/SKILL.md +1 -1
- package/skills/rebase/SKILL.md +15 -3
- package/skills/reviewer-gates/SKILL.md +2 -2
- package/skills/show/SKILL.md +51 -0
- package/skills/show/references/accessibility.md +7 -0
- package/skills/show/references/art.md +3 -0
- package/skills/show/references/charts.md +3 -0
- package/skills/show/references/core-design.md +14 -0
- package/skills/show/references/erds.md +3 -0
- package/skills/show/references/flowcharts.md +3 -0
- package/skills/show/references/host-and-html.md +3 -0
- package/skills/show/references/illustrative-diagrams.md +10 -0
- package/skills/show/references/interaction.md +3 -0
- package/skills/show/references/mockups.md +3 -0
- package/skills/show/references/quality-gates.md +7 -0
- package/skills/show/references/structural-diagrams.md +3 -0
- package/skills/show/references/subject-inventory.md +21 -0
- package/skills/show/references/svg-contract.md +22 -0
- package/skills/show/routing.yaml +30 -0
- package/skills/show/samples/pr1262-v2.svg +222 -0
- package/skills/show/scripts/README.md +6 -0
- package/skills/show/scripts/validate-artifact.py +91 -0
- package/skills/show/scripts/validate-package.py +18 -0
- package/skills/show/templates/html-widget.html +4 -0
- package/skills/show/templates/svg-base.svg +19 -0
- package/skills/show/tests/fixtures/css-var.svg +6 -0
- package/skills/show/tests/fixtures/dead-ref.svg +7 -0
- package/skills/show/tests/fixtures/filled-glyph.svg +8 -0
- package/skills/show/tests/fixtures/inherited-fill.svg +18 -0
- package/skills/show/tests/fixtures/invalid.svg +1 -0
- package/skills/show/tests/fixtures/large-canvas.svg +21 -0
- package/skills/show/tests/fixtures/unfilled-connector.svg +15 -0
- package/skills/show/tests/fixtures/valid.html +1 -0
- package/skills/show/tests/test_validate-artifact.py +74 -0
- package/skills/show/tests/test_validators.py +59 -0
- package/skills/show/workflows/create-visual.md +13 -0
- package/skills/show/workflows/review-visual.md +20 -0
- package/skills/split-pr/SKILL.md +85 -0
- package/skills/split-pr/reference/path-layers.md +16 -0
- package/skills/split-pr/reference/proposal-format.md +15 -0
- package/skills/split-pr/reference/split-further-loop.md +10 -0
- package/skills/split-pr/reference/splitting-principles.md +26 -0
- package/skills/split-pr/scripts/analyze_pr.py +279 -0
- package/skills/split-pr/scripts/categorize_files.py +106 -0
- package/skills/split-pr/scripts/config/__init__.py +1 -0
- package/skills/split-pr/scripts/config/dependency_constants.py +14 -0
- package/skills/split-pr/scripts/config/git_operations_constants.py +36 -0
- package/skills/split-pr/scripts/config/packing_constants.py +61 -0
- package/skills/split-pr/scripts/config/plan_constants.py +49 -0
- package/skills/split-pr/scripts/config/split_pr_constants.py +110 -0
- package/skills/split-pr/scripts/execute_split_slices.py +82 -0
- package/skills/split-pr/scripts/pack_files_into_slices.py +212 -0
- package/skills/split-pr/scripts/split_pr_dependency_graph.py +70 -0
- package/skills/split-pr/scripts/split_pr_git_operations.py +184 -0
- package/skills/split-pr/scripts/split_pr_layer_order.py +58 -0
- package/skills/split-pr/scripts/split_pr_paginate.py +119 -0
- package/skills/split-pr/scripts/split_pr_process_runner.py +52 -0
- package/skills/split-pr/scripts/split_pr_script_types.py +126 -0
- package/skills/split-pr/scripts/split_pr_title.py +41 -0
- package/skills/split-pr/scripts/test_analyze_pr.py +228 -0
- package/skills/split-pr/scripts/test_categorize_files.py +55 -0
- package/skills/split-pr/scripts/test_categorize_files_packing.py +59 -0
- package/skills/split-pr/scripts/test_execute_split_slices.py +99 -0
- package/skills/split-pr/scripts/test_split_pr_dependency_graph.py +47 -0
- package/skills/split-pr/scripts/test_split_pr_git_operations.py +125 -0
- package/skills/split-pr/scripts/test_split_pr_layer_order.py +36 -0
- package/skills/split-pr/scripts/test_split_pr_paginate.py +65 -0
- package/skills/split-pr/scripts/test_split_pr_script_types.py +73 -0
- package/skills/split-pr/scripts/test_split_pr_title.py +28 -0
- package/skills/split-pr/scripts/test_verify_dependency_graph.py +46 -0
- package/skills/split-pr/scripts/test_verify_plan.py +56 -0
- package/skills/split-pr/scripts/test_verify_plan_contract.py +50 -0
- package/skills/split-pr/scripts/test_verify_plan_path_normalization.py +45 -0
- package/skills/split-pr/scripts/verify_dependency_graph.py +111 -0
- package/skills/split-pr/scripts/verify_plan.py +139 -0
- package/system-prompts/software-engineer.xml +11 -2
- package/commands/initialize.md +0 -90
- package/commands/stubcheck.md +0 -88
- package/output-styles/caveman-agent.md +0 -37
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure install-root resolver for the claude-dev-env installer.
|
|
3
|
+
*
|
|
4
|
+
* Precedence (highest first):
|
|
5
|
+
* 1. explicitTarget — CLI --target or a caller-supplied absolute path
|
|
6
|
+
* 2. CLAUDE_CONFIG_DIR from the environment (profile isolation root)
|
|
7
|
+
* 3. join(homeDirectory, '.claude') — main-profile default
|
|
8
|
+
*
|
|
9
|
+
* CLAUDE_HOME is never honored as a profile root.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { homedir } from 'node:os';
|
|
13
|
+
import { join, normalize, resolve, sep } from 'node:path';
|
|
14
|
+
import { MYPY_INI_FILE_NAME } from './install-constants.mjs';
|
|
15
|
+
|
|
16
|
+
export const CLAUDE_CONFIG_DIR_ENVIRONMENT_VARIABLE = 'CLAUDE_CONFIG_DIR';
|
|
17
|
+
export const DEFAULT_CLAUDE_DIRECTORY_NAME = '.claude';
|
|
18
|
+
export const MANIFEST_FILE_NAME = '.claude-dev-env-manifest.json';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @typedef {{
|
|
22
|
+
* explicitTarget?: string | null,
|
|
23
|
+
* environment?: NodeJS.ProcessEnv | Record<string, string | undefined>,
|
|
24
|
+
* homeDirectory?: string,
|
|
25
|
+
* }} ResolveInstallRootOptions
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @typedef {{
|
|
30
|
+
* managedRoot: string,
|
|
31
|
+
* source: 'explicit-target' | 'claude-config-dir' | 'default-home',
|
|
32
|
+
* homeDirectory: string,
|
|
33
|
+
* manifestFilePath: string,
|
|
34
|
+
* mypyIniInstallPath: string,
|
|
35
|
+
* allDeclaredExternalPaths: string[],
|
|
36
|
+
* }} InstallRootResolution
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Resolve the managed install root and declared external destinations.
|
|
41
|
+
*
|
|
42
|
+
* @param {ResolveInstallRootOptions} [options]
|
|
43
|
+
* @returns {InstallRootResolution}
|
|
44
|
+
*/
|
|
45
|
+
export function resolveInstallRoot(options = {}) {
|
|
46
|
+
const homeDirectory = options.homeDirectory ?? homedir();
|
|
47
|
+
const environment = options.environment ?? process.env;
|
|
48
|
+
const explicitTarget = normalizeOptionalPath(options.explicitTarget);
|
|
49
|
+
|
|
50
|
+
/** @type {InstallRootResolution['source']} */
|
|
51
|
+
let source;
|
|
52
|
+
/** @type {string} */
|
|
53
|
+
let managedRoot;
|
|
54
|
+
|
|
55
|
+
if (explicitTarget) {
|
|
56
|
+
managedRoot = explicitTarget;
|
|
57
|
+
source = 'explicit-target';
|
|
58
|
+
} else {
|
|
59
|
+
const configDir = normalizeOptionalPath(
|
|
60
|
+
environment[CLAUDE_CONFIG_DIR_ENVIRONMENT_VARIABLE],
|
|
61
|
+
);
|
|
62
|
+
if (configDir) {
|
|
63
|
+
managedRoot = configDir;
|
|
64
|
+
source = 'claude-config-dir';
|
|
65
|
+
} else {
|
|
66
|
+
managedRoot = resolve(join(homeDirectory, DEFAULT_CLAUDE_DIRECTORY_NAME));
|
|
67
|
+
source = 'default-home';
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const mypyIniInstallPath = resolve(join(homeDirectory, MYPY_INI_FILE_NAME));
|
|
72
|
+
return {
|
|
73
|
+
managedRoot,
|
|
74
|
+
source,
|
|
75
|
+
homeDirectory: resolve(homeDirectory),
|
|
76
|
+
manifestFilePath: join(managedRoot, MANIFEST_FILE_NAME),
|
|
77
|
+
mypyIniInstallPath,
|
|
78
|
+
allDeclaredExternalPaths: [mypyIniInstallPath],
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* True when candidatePath is the managed root or a descendant of it.
|
|
84
|
+
* Requires a separator boundary so `.claude-extra` is not inside `.claude`.
|
|
85
|
+
*
|
|
86
|
+
* @param {string} candidatePath
|
|
87
|
+
* @param {string} managedRoot
|
|
88
|
+
* @returns {boolean}
|
|
89
|
+
*/
|
|
90
|
+
export function isPathWithinManagedRoot(candidatePath, managedRoot) {
|
|
91
|
+
if (!candidatePath || !managedRoot) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
const normalizedCandidate = normalizePathForComparison(candidatePath);
|
|
95
|
+
const normalizedRoot = normalizePathForComparison(managedRoot);
|
|
96
|
+
if (normalizedCandidate === normalizedRoot) {
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
const rootWithSeparator = normalizedRoot.endsWith('/')
|
|
100
|
+
? normalizedRoot
|
|
101
|
+
: `${normalizedRoot}/`;
|
|
102
|
+
return normalizedCandidate.startsWith(rootWithSeparator);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* True when a write destination is allowed: inside the managed root or on
|
|
107
|
+
* the declared external allowlist (today: ~/.mypy.ini under the home dir).
|
|
108
|
+
*
|
|
109
|
+
* @param {string} candidatePath
|
|
110
|
+
* @param {InstallRootResolution} resolution
|
|
111
|
+
* @returns {boolean}
|
|
112
|
+
*/
|
|
113
|
+
export function isAllowedInstallDestination(candidatePath, resolution) {
|
|
114
|
+
if (isPathWithinManagedRoot(candidatePath, resolution.managedRoot)) {
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
const normalizedCandidate = normalizePathForComparison(candidatePath);
|
|
118
|
+
return resolution.allDeclaredExternalPaths.some(
|
|
119
|
+
(eachExternalPath) => normalizePathForComparison(eachExternalPath) === normalizedCandidate,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Parse an explicit --target value from argv tokens.
|
|
125
|
+
*
|
|
126
|
+
* @param {string[]} argv
|
|
127
|
+
* @returns {string | null}
|
|
128
|
+
*/
|
|
129
|
+
export function parseExplicitTargetFromArgv(argv) {
|
|
130
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
131
|
+
const token = argv[index];
|
|
132
|
+
if (token === '--target') {
|
|
133
|
+
const value = argv[index + 1];
|
|
134
|
+
if (!value || value.startsWith('--')) {
|
|
135
|
+
throw new Error('--target requires a path argument');
|
|
136
|
+
}
|
|
137
|
+
return value;
|
|
138
|
+
}
|
|
139
|
+
if (token.startsWith('--target=')) {
|
|
140
|
+
const value = token.slice('--target='.length);
|
|
141
|
+
if (!value) {
|
|
142
|
+
throw new Error('--target requires a path argument');
|
|
143
|
+
}
|
|
144
|
+
return value;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* @param {string | null | undefined} maybePath
|
|
152
|
+
* @returns {string | null}
|
|
153
|
+
*/
|
|
154
|
+
function normalizeOptionalPath(maybePath) {
|
|
155
|
+
if (typeof maybePath !== 'string') {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
const trimmed = maybePath.trim();
|
|
159
|
+
if (!trimmed) {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
return resolve(trimmed);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Compare-key for a filesystem path: resolve, forward-slash, case-fold on win32.
|
|
167
|
+
*
|
|
168
|
+
* @param {string} filesystemPath
|
|
169
|
+
* @returns {string}
|
|
170
|
+
*/
|
|
171
|
+
export function normalizePathForComparison(filesystemPath) {
|
|
172
|
+
const resolved = resolve(normalize(filesystemPath));
|
|
173
|
+
let withForwardSlashes = resolved.split(sep).join('/');
|
|
174
|
+
if (process.platform === 'win32') {
|
|
175
|
+
withForwardSlashes = withForwardSlashes.toLowerCase();
|
|
176
|
+
}
|
|
177
|
+
if (withForwardSlashes.length > 1 && withForwardSlashes.endsWith('/')) {
|
|
178
|
+
withForwardSlashes = withForwardSlashes.slice(0, -1);
|
|
179
|
+
}
|
|
180
|
+
return withForwardSlashes;
|
|
181
|
+
}
|
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure install-target selection for single-profile and selected multi-profile runs.
|
|
3
|
+
*
|
|
4
|
+
* Selection finishes before any filesystem mutation. The caller injects the
|
|
5
|
+
* profiles root and the profile-id → directoryName map so tests need no live
|
|
6
|
+
* env or disk fixtures.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { join, resolve, isAbsolute, normalize } from 'node:path';
|
|
10
|
+
import {
|
|
11
|
+
resolveInstallRoot,
|
|
12
|
+
parseExplicitTargetFromArgv,
|
|
13
|
+
normalizePathForComparison,
|
|
14
|
+
} from './resolve-install-root.mjs';
|
|
15
|
+
|
|
16
|
+
export const MAIN_DEFAULT_TARGET_IDENTITY = 'main';
|
|
17
|
+
export const TARGET_IDENTITY_FLAG = '--target-identity';
|
|
18
|
+
export const PROFILE_FLAG = '--profile';
|
|
19
|
+
export const PROFILES_FLAG = '--profiles';
|
|
20
|
+
export const DEFAULT_PROFILES_ROOT_DIRECTORY_NAME = '.claude-profiles';
|
|
21
|
+
export const PROFILES_ROOT_ENVIRONMENT_VARIABLE = 'LLM_SETTINGS_PROFILES_ROOT';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @typedef {{
|
|
25
|
+
* mode: 'main-default' | 'explicit-path' | 'profiles' | 'child-identity',
|
|
26
|
+
* allProfileIds: string[],
|
|
27
|
+
* explicitTarget: string | null,
|
|
28
|
+
* targetIdentity: string | null,
|
|
29
|
+
* }} InstallTargetSelection
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @typedef {{
|
|
34
|
+
* targetIdentity: string,
|
|
35
|
+
* managedRoot: string,
|
|
36
|
+
* source: string,
|
|
37
|
+
* profileId: string | null,
|
|
38
|
+
* }} ResolvedInstallTarget
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Parse target-selection flags from argv. Does not resolve paths.
|
|
43
|
+
*
|
|
44
|
+
* Rejects ambiguous combinations (path target + profile), empty profile lists,
|
|
45
|
+
* and a child hop that still carries profile flags.
|
|
46
|
+
*
|
|
47
|
+
* @param {string[]} argv
|
|
48
|
+
* @returns {InstallTargetSelection}
|
|
49
|
+
*/
|
|
50
|
+
export function parseInstallTargetSelectionFromArgv(argv) {
|
|
51
|
+
const explicitTarget = parseExplicitTargetFromArgv(argv);
|
|
52
|
+
const targetIdentity = parseTargetIdentityFromArgv(argv);
|
|
53
|
+
const allProfileIds = parseProfileIdsFromArgv(argv);
|
|
54
|
+
const hasProfileFlags = allProfileIds.length > 0
|
|
55
|
+
|| argvIncludesFlag(argv, PROFILE_FLAG)
|
|
56
|
+
|| argvIncludesFlag(argv, PROFILES_FLAG);
|
|
57
|
+
|
|
58
|
+
if (targetIdentity && hasProfileFlags) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
'child install hop rejects --profile/--profiles when --target-identity is set',
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
if (explicitTarget && hasProfileFlags) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
'ambiguous targets: use either --target <path> or --profile/--profiles, not both',
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
if (hasProfileFlags && allProfileIds.length === 0) {
|
|
69
|
+
throw new Error('--profile/--profiles requires at least one profile id');
|
|
70
|
+
}
|
|
71
|
+
if (allProfileIds.length > 0) {
|
|
72
|
+
const allDuplicates = findDuplicateStrings(allProfileIds);
|
|
73
|
+
if (allDuplicates.length > 0) {
|
|
74
|
+
throw new Error(`duplicate profile id(s): ${allDuplicates.join(', ')}`);
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
mode: 'profiles',
|
|
78
|
+
allProfileIds,
|
|
79
|
+
explicitTarget: null,
|
|
80
|
+
targetIdentity: null,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
if (targetIdentity) {
|
|
84
|
+
return {
|
|
85
|
+
mode: 'child-identity',
|
|
86
|
+
allProfileIds: [],
|
|
87
|
+
explicitTarget,
|
|
88
|
+
targetIdentity,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
if (explicitTarget) {
|
|
92
|
+
return {
|
|
93
|
+
mode: 'explicit-path',
|
|
94
|
+
allProfileIds: [],
|
|
95
|
+
explicitTarget,
|
|
96
|
+
targetIdentity: null,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
mode: 'main-default',
|
|
101
|
+
allProfileIds: [],
|
|
102
|
+
explicitTarget: null,
|
|
103
|
+
targetIdentity: null,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Resolve the ordered list of install targets from a parsed selection.
|
|
109
|
+
*
|
|
110
|
+
* Dedupes on the resolved managed root path (case-insensitive on Windows) so
|
|
111
|
+
* `--target` and a profile that names the same directory cannot both run.
|
|
112
|
+
* Sanitizes each profile directoryName: absolute paths and `..` segments fail.
|
|
113
|
+
*
|
|
114
|
+
* @param {InstallTargetSelection} selection
|
|
115
|
+
* @param {{
|
|
116
|
+
* homeDirectory: string,
|
|
117
|
+
* environment?: NodeJS.ProcessEnv | Record<string, string | undefined>,
|
|
118
|
+
* profilesRoot?: string | null,
|
|
119
|
+
* directoryNameByProfileId?: Record<string, string>,
|
|
120
|
+
* }} options
|
|
121
|
+
* @returns {ResolvedInstallTarget[]}
|
|
122
|
+
*/
|
|
123
|
+
export function resolveInstallTargets(selection, options) {
|
|
124
|
+
const environment = options.environment ?? {};
|
|
125
|
+
const homeDirectory = options.homeDirectory;
|
|
126
|
+
const directoryNameByProfileId = options.directoryNameByProfileId ?? {};
|
|
127
|
+
|
|
128
|
+
if (selection.mode === 'profiles') {
|
|
129
|
+
const profilesRoot = resolveProfilesRootDirectory({
|
|
130
|
+
homeDirectory,
|
|
131
|
+
environment,
|
|
132
|
+
profilesRoot: options.profilesRoot,
|
|
133
|
+
});
|
|
134
|
+
/** @type {ResolvedInstallTarget[]} */
|
|
135
|
+
const allTargets = [];
|
|
136
|
+
for (const eachProfileId of selection.allProfileIds) {
|
|
137
|
+
const directoryName = directoryNameByProfileId[eachProfileId];
|
|
138
|
+
if (typeof directoryName !== 'string' || !directoryName.trim()) {
|
|
139
|
+
throw new Error(`unknown profile id: ${eachProfileId}`);
|
|
140
|
+
}
|
|
141
|
+
const safeDirectoryName = assertSafeProfileDirectoryName(directoryName);
|
|
142
|
+
const managedRoot = resolve(join(profilesRoot, safeDirectoryName));
|
|
143
|
+
allTargets.push({
|
|
144
|
+
targetIdentity: eachProfileId,
|
|
145
|
+
managedRoot,
|
|
146
|
+
source: 'selected-profile',
|
|
147
|
+
profileId: eachProfileId,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
return dedupeTargetsByManagedRoot(allTargets);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const explicitTarget = selection.mode === 'main-default' ? null : selection.explicitTarget;
|
|
154
|
+
const resolution = resolveInstallRoot({
|
|
155
|
+
homeDirectory,
|
|
156
|
+
environment,
|
|
157
|
+
explicitTarget,
|
|
158
|
+
});
|
|
159
|
+
const isChildIdentity = selection.mode === 'child-identity';
|
|
160
|
+
return [{
|
|
161
|
+
targetIdentity: isChildIdentity
|
|
162
|
+
? (selection.targetIdentity ?? MAIN_DEFAULT_TARGET_IDENTITY)
|
|
163
|
+
: MAIN_DEFAULT_TARGET_IDENTITY,
|
|
164
|
+
managedRoot: resolution.managedRoot,
|
|
165
|
+
source: resolution.source,
|
|
166
|
+
profileId: isChildIdentity ? selection.targetIdentity : null,
|
|
167
|
+
}];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Build the per-target ownership manifest record.
|
|
172
|
+
*
|
|
173
|
+
* @param {{
|
|
174
|
+
* packageName: string,
|
|
175
|
+
* packageVersion: string,
|
|
176
|
+
* targetIdentity: string,
|
|
177
|
+
* managedRoot: string,
|
|
178
|
+
* files: string[],
|
|
179
|
+
* skills?: string[] | null,
|
|
180
|
+
* installedAt?: string,
|
|
181
|
+
* }} parameters
|
|
182
|
+
* @returns {Record<string, unknown>}
|
|
183
|
+
*/
|
|
184
|
+
export function buildTargetManifestRecord(parameters) {
|
|
185
|
+
/** @type {Record<string, unknown>} */
|
|
186
|
+
const manifest = {
|
|
187
|
+
package: parameters.packageName,
|
|
188
|
+
version: parameters.packageVersion,
|
|
189
|
+
installedAt: parameters.installedAt ?? new Date().toISOString(),
|
|
190
|
+
targetIdentity: parameters.targetIdentity,
|
|
191
|
+
managedRoot: parameters.managedRoot,
|
|
192
|
+
files: parameters.files,
|
|
193
|
+
};
|
|
194
|
+
if (parameters.skills) {
|
|
195
|
+
manifest.skills = parameters.skills;
|
|
196
|
+
}
|
|
197
|
+
return manifest;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Strip target-selection flags so group parsing sees only install flags.
|
|
202
|
+
*
|
|
203
|
+
* @param {string[]} argv
|
|
204
|
+
* @returns {string[]}
|
|
205
|
+
*/
|
|
206
|
+
export function stripTargetSelectionFlagsFromArgv(argv) {
|
|
207
|
+
/** @type {string[]} */
|
|
208
|
+
const remaining = [];
|
|
209
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
210
|
+
const token = argv[index];
|
|
211
|
+
if (token === PROFILE_FLAG || token === PROFILES_FLAG || token === TARGET_IDENTITY_FLAG) {
|
|
212
|
+
const value = argv[index + 1];
|
|
213
|
+
if (value && !value.startsWith('--')) {
|
|
214
|
+
index += 1;
|
|
215
|
+
}
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
if (
|
|
219
|
+
token.startsWith(`${PROFILE_FLAG}=`)
|
|
220
|
+
|| token.startsWith(`${PROFILES_FLAG}=`)
|
|
221
|
+
|| token.startsWith(`${TARGET_IDENTITY_FLAG}=`)
|
|
222
|
+
) {
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (token === '--target') {
|
|
226
|
+
const value = argv[index + 1];
|
|
227
|
+
if (value && !value.startsWith('--')) {
|
|
228
|
+
index += 1;
|
|
229
|
+
}
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (token.startsWith('--target=')) {
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
remaining.push(token);
|
|
236
|
+
}
|
|
237
|
+
return remaining;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* @param {{
|
|
242
|
+
* homeDirectory: string,
|
|
243
|
+
* environment?: NodeJS.ProcessEnv | Record<string, string | undefined>,
|
|
244
|
+
* profilesRoot?: string | null,
|
|
245
|
+
* }} options
|
|
246
|
+
* @returns {string}
|
|
247
|
+
*/
|
|
248
|
+
export function resolveProfilesRootDirectory(options) {
|
|
249
|
+
if (typeof options.profilesRoot === 'string' && options.profilesRoot.trim()) {
|
|
250
|
+
return resolve(options.profilesRoot);
|
|
251
|
+
}
|
|
252
|
+
const environment = options.environment ?? {};
|
|
253
|
+
const fromEnvironment = environment[PROFILES_ROOT_ENVIRONMENT_VARIABLE];
|
|
254
|
+
if (typeof fromEnvironment === 'string' && fromEnvironment.trim()) {
|
|
255
|
+
return resolve(fromEnvironment.trim());
|
|
256
|
+
}
|
|
257
|
+
return resolve(join(options.homeDirectory, DEFAULT_PROFILES_ROOT_DIRECTORY_NAME));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Reject absolute and parent-hop directoryName values on every host platform.
|
|
262
|
+
*
|
|
263
|
+
* Host `isAbsolute` plus Windows drive and UNC spellings so a Linux CI host
|
|
264
|
+
* still rejects a profile map that smuggles an absolute Windows path.
|
|
265
|
+
*
|
|
266
|
+
* @param {string} directoryName
|
|
267
|
+
* @returns {string}
|
|
268
|
+
*/
|
|
269
|
+
export function assertSafeProfileDirectoryName(directoryName) {
|
|
270
|
+
const trimmed = directoryName.trim();
|
|
271
|
+
if (!trimmed) {
|
|
272
|
+
throw new Error('profile directoryName must be a non-empty relative name');
|
|
273
|
+
}
|
|
274
|
+
const hasWindowsDriveAbsolute = /^[A-Za-z]:[\\/]/.test(trimmed);
|
|
275
|
+
const hasWindowsUncAbsolute = trimmed.startsWith('\\\\') || trimmed.startsWith('//');
|
|
276
|
+
if (isAbsolute(trimmed) || hasWindowsDriveAbsolute || hasWindowsUncAbsolute) {
|
|
277
|
+
throw new Error(`profile directoryName must be relative, got absolute: ${trimmed}`);
|
|
278
|
+
}
|
|
279
|
+
const allSegments = normalize(trimmed).split(/[/\\]/).filter(Boolean);
|
|
280
|
+
if (allSegments.some((eachSegment) => eachSegment === '..')) {
|
|
281
|
+
throw new Error(`profile directoryName rejects parent segments: ${trimmed}`);
|
|
282
|
+
}
|
|
283
|
+
return trimmed;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* @param {string[]} argv
|
|
288
|
+
* @returns {string[]}
|
|
289
|
+
*/
|
|
290
|
+
function parseProfileIdsFromArgv(argv) {
|
|
291
|
+
/** @type {string[]} */
|
|
292
|
+
const allProfileIds = [];
|
|
293
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
294
|
+
const token = argv[index];
|
|
295
|
+
if (token === PROFILE_FLAG || token === PROFILES_FLAG) {
|
|
296
|
+
const value = argv[index + 1];
|
|
297
|
+
if (!value || value.startsWith('--')) {
|
|
298
|
+
throw new Error(`${token} requires a comma-separated profile id list`);
|
|
299
|
+
}
|
|
300
|
+
allProfileIds.push(...splitProfileIdList(value));
|
|
301
|
+
index += 1;
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
if (token.startsWith(`${PROFILE_FLAG}=`) || token.startsWith(`${PROFILES_FLAG}=`)) {
|
|
305
|
+
const prefix = token.startsWith(`${PROFILE_FLAG}=`)
|
|
306
|
+
? `${PROFILE_FLAG}=`
|
|
307
|
+
: `${PROFILES_FLAG}=`;
|
|
308
|
+
const value = token.slice(prefix.length);
|
|
309
|
+
if (!value) {
|
|
310
|
+
throw new Error(`${prefix.slice(0, -1)} requires a comma-separated profile id list`);
|
|
311
|
+
}
|
|
312
|
+
allProfileIds.push(...splitProfileIdList(value));
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return allProfileIds;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* @param {string[]} argv
|
|
320
|
+
* @returns {string | null}
|
|
321
|
+
*/
|
|
322
|
+
function parseTargetIdentityFromArgv(argv) {
|
|
323
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
324
|
+
const token = argv[index];
|
|
325
|
+
if (token === TARGET_IDENTITY_FLAG) {
|
|
326
|
+
const value = argv[index + 1];
|
|
327
|
+
if (!value || value.startsWith('--')) {
|
|
328
|
+
throw new Error(`${TARGET_IDENTITY_FLAG} requires an identity argument`);
|
|
329
|
+
}
|
|
330
|
+
return value;
|
|
331
|
+
}
|
|
332
|
+
if (token.startsWith(`${TARGET_IDENTITY_FLAG}=`)) {
|
|
333
|
+
const value = token.slice(`${TARGET_IDENTITY_FLAG}=`.length);
|
|
334
|
+
if (!value) {
|
|
335
|
+
throw new Error(`${TARGET_IDENTITY_FLAG} requires an identity argument`);
|
|
336
|
+
}
|
|
337
|
+
return value;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* @param {string} rawList
|
|
345
|
+
* @returns {string[]}
|
|
346
|
+
*/
|
|
347
|
+
function splitProfileIdList(rawList) {
|
|
348
|
+
return rawList
|
|
349
|
+
.split(',')
|
|
350
|
+
.map((eachId) => eachId.trim())
|
|
351
|
+
.filter((eachId) => eachId.length > 0);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* @param {string[]} argv
|
|
356
|
+
* @param {string} flagName
|
|
357
|
+
* @returns {boolean}
|
|
358
|
+
*/
|
|
359
|
+
function argvIncludesFlag(argv, flagName) {
|
|
360
|
+
return argv.some(
|
|
361
|
+
(eachToken) => eachToken === flagName || eachToken.startsWith(`${flagName}=`),
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* @param {string[]} allValues
|
|
367
|
+
* @returns {string[]}
|
|
368
|
+
*/
|
|
369
|
+
function findDuplicateStrings(allValues) {
|
|
370
|
+
const seen = new Set();
|
|
371
|
+
const allDuplicates = new Set();
|
|
372
|
+
for (const eachValue of allValues) {
|
|
373
|
+
if (seen.has(eachValue)) {
|
|
374
|
+
allDuplicates.add(eachValue);
|
|
375
|
+
}
|
|
376
|
+
seen.add(eachValue);
|
|
377
|
+
}
|
|
378
|
+
return [...allDuplicates];
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* @param {ResolvedInstallTarget[]} allTargets
|
|
383
|
+
* @returns {ResolvedInstallTarget[]}
|
|
384
|
+
*/
|
|
385
|
+
function dedupeTargetsByManagedRoot(allTargets) {
|
|
386
|
+
const seenRootByKey = new Map();
|
|
387
|
+
/** @type {ResolvedInstallTarget[]} */
|
|
388
|
+
const uniqueTargets = [];
|
|
389
|
+
for (const eachTarget of allTargets) {
|
|
390
|
+
const rootKey = normalizePathForComparison(eachTarget.managedRoot);
|
|
391
|
+
if (seenRootByKey.has(rootKey)) {
|
|
392
|
+
const priorIdentity = seenRootByKey.get(rootKey);
|
|
393
|
+
throw new Error(
|
|
394
|
+
`duplicate managed root for targets ${priorIdentity} and ${eachTarget.targetIdentity}: ${eachTarget.managedRoot}`,
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
seenRootByKey.set(rootKey, eachTarget.targetIdentity);
|
|
398
|
+
uniqueTargets.push(eachTarget);
|
|
399
|
+
}
|
|
400
|
+
return uniqueTargets;
|
|
401
|
+
}
|
package/commands/CLAUDE.md
CHANGED
|
@@ -11,12 +11,10 @@ Slash-command definitions installed into `~/.claude/commands/` by `bin/install.m
|
|
|
11
11
|
| `hook-log-extract.md` | `/hook-log-extract` | Extracts and formats hook log entries for a session |
|
|
12
12
|
| `hook-log-init.md` | `/hook-log-init` | Initializes the Neon Postgres schema that backs the hook-log extractor (one-time per machine) |
|
|
13
13
|
| `implement.md` | `/implement` | Provides full implementation context to a right-sized engineer in XML format |
|
|
14
|
-
| `initialize.md` | `/initialize` | Bootstraps a new project with standard Claude Code config |
|
|
15
14
|
| `plan.md` | `/plan` | Plans a feature through the `anthropic-plan` skill and workflow |
|
|
16
15
|
| `pr-comments.md` | `/pr-comments` | Fetches and formats PR review comments for response |
|
|
17
16
|
| `review-plan.md` | `/review-plan` | Reviews the current plan packet against code standards |
|
|
18
17
|
| `right-size.md` | `/right-size` | Checks an implementation against the Right-Sized Engineering rules |
|
|
19
|
-
| `stubcheck.md` | `/stubcheck` | Finds stub bodies (`pass`/`...`/`raise NotImplementedError`) in the diff |
|
|
20
18
|
| `sum.md` | `/sum` | Generates a formatted session summary for quick pickup in a new session |
|
|
21
19
|
|
|
22
20
|
## Format
|
|
@@ -8,6 +8,7 @@ Pointer documents to external sources, standard terminology, and internal tool o
|
|
|
8
8
|
|---|---|
|
|
9
9
|
| `dead-code-elimination.md` | External sources and standard terms behind CODE_RULES §9.8 (remove code you orphan): DCE, tree shaking, reachability analysis, and the Lava Flow anti-pattern |
|
|
10
10
|
| `code-review-enforcement.md` | How the code-review gates work: the two required efforts (push at low, PR creation at xhigh), the stamp bound to the branch-surface hash, the single sanctioned minter, the two-layer stamp-directory guard, and the bypass surfaces the gates leave open |
|
|
11
|
+
| `prose-style-enforcement.md` | How `CLAUDE_PROSE_STYLE_ENFORCEMENT` arms opinionated prose gates (default off) while AskUserQuestion lean-block stays always on |
|
|
11
12
|
| `advisor-tool.md` | Canonical consult bones for any stronger reviewer: when to call, hard rule before first write, how to treat advice; maps to the Anthropic advisor tool |
|
|
12
13
|
| `team-advisor-skill.md` | `/team-advisor` map: sole-consumer warm bind, ref index, and how it pairs with `advisor()` |
|
|
13
14
|
|
|
@@ -34,7 +34,7 @@ Your first write, edit, or state-changing shell call on a task must be preceded
|
|
|
34
34
|
|
|
35
35
|
Give the advice serious weight. If a step fails empirically, or primary-source evidence contradicts a claim (the file says X, the paper states Y), adapt. A passing self-test is not evidence the advice is wrong — it is evidence the test does not check what the advice is checking.
|
|
36
36
|
|
|
37
|
-
If your data points one way and the advisor points another: do not silently switch. Surface the conflict in one more call — "I found X, you suggest Y, which constraint breaks the tie?"
|
|
37
|
+
If your data points one way and the advisor points another: do not silently switch. Surface the conflict in one more call — "I found X, you suggest Y, which constraint breaks the tie?" The advisor saw the evidence but may have underweighted it; a reconcile call is cheaper than the wrong branch.
|
|
38
38
|
|
|
39
39
|
## Brevity cue
|
|
40
40
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Prose-style enforcement
|
|
2
|
+
|
|
3
|
+
Opinionated prose gates (heavy-word swaps, hedging, historical state phrasing, intent-only endings, hook prose-detector consistency) are **off by default**.
|
|
4
|
+
|
|
5
|
+
## Opt-in
|
|
6
|
+
|
|
7
|
+
Set `CLAUDE_PROSE_STYLE_ENFORCEMENT` to `1`, `true`, `yes`, or `on` (case and spaces ignored). Any other value, and an unset variable, leave those gates off.
|
|
8
|
+
|
|
9
|
+
The flag is `PROSE_STYLE_ENFORCEMENT_ENABLED` in `hooks/blocking/config/prose_style_enforcement_constants.py`.
|
|
10
|
+
|
|
11
|
+
## Always on
|
|
12
|
+
|
|
13
|
+
Structural AskUserQuestion lean-block validation in `plain_language_blocker` (chat detail, length caps on question blocks) stays active regardless of the flag. Code-rules and security blockers are out of scope for this switch.
|
|
14
|
+
|
|
15
|
+
## Advisory precision measurement (OP-07B)
|
|
16
|
+
|
|
17
|
+
When the flag is off, heavy-word and hedging hits still emit privacy-safe advisory candidates to `~/.claude/logs/prose-matcher-advisory.jsonl` (matcher id, surface, hashed context fingerprint, optional label). Classification (`keep` / `narrow` / `drop` / `advisory`) uses a labeled sample floor of 30 and precision floors 0.7 / 0.4. Matchers below the floor stay advisory. No matcher becomes hard-blocking from historical labels alone. See `hooks/observability/prose_matcher_advisory.py`.
|
|
18
|
+
|
|
19
|
+
## Explicit uncertainty (OP-07C)
|
|
20
|
+
|
|
21
|
+
With enforcement on, a hedge word in a sentence that also labels the claim (`unverified`, `I don't know`, `no source for this claim`, …) passes. A bare hedge in a different sentence still blocks. Detail: `rules/hedging-claims.md`.
|
|
22
|
+
|
|
23
|
+
## Plain-language advisory (OP-07D)
|
|
24
|
+
|
|
25
|
+
Heavy-word matches never hard-deny a Write/Edit or AskUserQuestion. With enforcement on, the hook allows the call and returns a `systemMessage` that names everyday swaps for both AskUserQuestion and `.md` writes. Lean-block structure denials stay hard. Detail: `rules/plain-language.md`.
|
package/hooks/blocking/CLAUDE.md
CHANGED
|
@@ -82,16 +82,16 @@ The check modules it calls are the `code_rules_<concern>.py` files below.
|
|
|
82
82
|
| `destructive_command_blocker.py` | PreToolUse (Bash/PowerShell) | Shell commands with destructive literals (`rm -rf`, `git reset --hard`, etc.) |
|
|
83
83
|
| `docstring_rule_gate_count_blocker.py` | PreToolUse (Write/Edit/MultiEdit) | A stale spelled-out gate-validator count in `docstring-prose-matches-implementation.md` — the "N more gate validators" / "M gated slices" count drifting from the `check_docstring_*` validators the prose names |
|
|
84
84
|
| `duplicate_rmtree_helper_blocker.py` | PreToolUse (Write/Edit) | A local re-definition of the Windows-safe rmtree helper trio (`_strip_read_only_and_retry`, `_force_remove_tree` / `force_rmtree`) in place of importing a shared helper |
|
|
85
|
-
| `eli11_reply_enforcer.py` | Stop | Final replies breaking the `eli11-replies` shape — more than
|
|
85
|
+
| `eli11_reply_enforcer.py` | Stop | Final replies breaking the `eli11-replies` shape — more than 6 bullet lines, more than 2 list lines over 20 words each, or multi-line instructions with no numbered step among the lead lines. Code fences, inline code, blockquotes, table rows, and link targets come off before the reply is judged. |
|
|
86
86
|
| `env_var_table_code_drift_blocker.py` | PreToolUse (Write/Edit/MultiEdit) | A markdown env-var summary table row attributing an environment variable to a code file whose source never references that variable name |
|
|
87
87
|
| `es_exe_path_rewriter.py` | PreToolUse | Rewrites paths referencing `.exe` under the Everything search path |
|
|
88
88
|
| `fable_spawn_gate.py` | PreToolUse (Agent/Task) | An `Agent` or `Task` spawn whose prompt carries no `FABLE-SPAWN-AUTHORIZED` token and whose model field reads `fable` in any letter case — the bare alias, or a delimiter segment of a full model id, so `claude-fable-5` is denied too |
|
|
89
89
|
| `gh_body_arg_blocker.py` | PreToolUse (Bash) | `gh` commands passing `--body`/`-b` directly (requires `--body-file` instead) |
|
|
90
90
|
| `gh_pr_author_enforcer.py` | PreToolUse | Enforces PR author identity rules |
|
|
91
91
|
| `gh_pr_author_restore.py` | PostToolUse | Restores PR author after a tool call |
|
|
92
|
-
| `hedging_language_blocker.py` | Stop | Responses with hedging words (`likely`, `probably`, `appears to`) |
|
|
93
|
-
| `hook_prose_detector_consistency.py` | PreToolUse (Write/Edit) | Hook docstrings/messages that claim a trigger the detector cannot fire on |
|
|
94
|
-
| `intent_only_ending_blocker.py` | Stop | Responses that end on a plan or intent without doing the work |
|
|
92
|
+
| `hedging_language_blocker.py` | Stop | Responses with hedging words (`likely`, `probably`, `appears to`); armed only when `CLAUDE_PROSE_STYLE_ENFORCEMENT` is on (default off) |
|
|
93
|
+
| `hook_prose_detector_consistency.py` | PreToolUse (Write/Edit) | Hook docstrings/messages that claim a trigger the detector cannot fire on; armed only when `CLAUDE_PROSE_STYLE_ENFORCEMENT` is on (default off) |
|
|
94
|
+
| `intent_only_ending_blocker.py` | Stop | Responses that end on a plan or intent without doing the work; armed only when `CLAUDE_PROSE_STYLE_ENFORCEMENT` is on (default off) |
|
|
95
95
|
| `open_questions_in_plans_blocker.py` | PreToolUse (Write/Edit) | Plan documents with unresolved open questions |
|
|
96
96
|
| `nas_ssh_binary_enforcer.py` | PreToolUse (Bash) | A bare `ssh`/`scp`/`sftp` command word targeting the NAS (Git Bash's MSYS ssh stalls on an interactive password prompt), or the full `System32/OpenSSH` binary to that host without `-o BatchMode=yes` |
|
|
97
97
|
| `package_inventory_stale_blocker.py` | PreToolUse (Write) | A new production code file created in a directory whose `README.md`/`CLAUDE.md` inventory (or a parent skill's `SKILL.md` Layout table mapping the `scripts/` subdirectory) names two or more sibling files but no entry for the new file |
|
|
@@ -100,7 +100,7 @@ The check modules it calls are the `code_rules_<concern>.py` files below.
|
|
|
100
100
|
| `pii_prevention_blocker.py` | PreToolUse (Write/Edit/MultiEdit/Bash/PowerShell/MCP GitHub) | Entry hook — content that carries high-confidence personal data or secrets (real emails, home-dir paths, private IPs, credential material) on write, durable GitHub posts, or staged commit paths; resolves the staged-commit repository from the command it gates (via `pii_prevention_blocker_parts`), not the session working directory |
|
|
101
101
|
| `pii_scanner.py` | library | Pure text scanners shared by `pii_prevention_blocker.py` |
|
|
102
102
|
| `piped_pytest_blocker.py` | PreToolUse (Bash) | A pytest run whose output feeds a pipe, where the pipeline reports the exit code of the command on the right |
|
|
103
|
-
| `plain_language_blocker.py` | PreToolUse (Write/Edit/AskUserQuestion) | Heavy or jargon words in user-facing prose |
|
|
103
|
+
| `plain_language_blocker.py` | PreToolUse (Write/Edit/AskUserQuestion) | Heavy or jargon words in user-facing prose when `CLAUDE_PROSE_STYLE_ENFORCEMENT` is on (default off); AskUserQuestion lean-block structure stays always on |
|
|
104
104
|
| `pr_converge_bugteam_enforcer.py` | PreToolUse | Enforces that bugteam runs in parallel with bugbot in pr-converge loops |
|
|
105
105
|
| `pr_description_enforcer.py` | PreToolUse (Bash) | `gh pr create`/`edit`/`comment` bodies that fail the Anthropic claude-code style audit, proof-shaped `gh pr comment` bodies missing proof-of-work parts, and `gh pr ready` while the PR carries no passing proof comment |
|
|
106
106
|
| `precommit_code_rules_gate.py` | PreToolUse (Bash) | Staged changes that fail the CODE_RULES gate at commit time |
|
|
@@ -113,7 +113,7 @@ The check modules it calls are the `code_rules_<concern>.py` files below.
|
|
|
113
113
|
| `session_handoff_blocker.py` | Stop | Responses suggesting a new session mid-task |
|
|
114
114
|
| `shell_substitution_blocker.py` | PreToolUse (Bash) | A command carrying `$(...)`, a live backtick, or `<(...)`/`>(...)` process substitution, which the allowlist matcher cannot descend into |
|
|
115
115
|
| `stale_comment_reference_blocker.py` | PreToolUse (Edit) | An Edit that rewrites a Python code line while keeping the standalone comment directly above it, when that comment names an identifier the rewrite removes from the line |
|
|
116
|
-
| `state_description_blocker.py` | PreToolUse (Write/Edit) | Historical/comparative language in documentation |
|
|
116
|
+
| `state_description_blocker.py` | PreToolUse (Write/Edit) | Historical/comparative language in documentation; armed only when `CLAUDE_PROSE_STYLE_ENFORCEMENT` is on (default off) |
|
|
117
117
|
| `subprocess_budget_completeness.py` | PreToolUse | Subprocess calls missing required budget arguments |
|
|
118
118
|
| `tdd_enforcer.py` | PreToolUse (Write/Edit) | Production code written without a matching failing test |
|
|
119
119
|
| `unscoped_search_blocker.py` | PreToolUse (Bash/PowerShell) | A `find` or recursive listing that walks from the filesystem root, a drive root, bare home, or a network share root |
|