claude-dev-env 2.8.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 +8 -2
- 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 +62 -562
- package/agents/code-quality-agent.md +10 -2
- package/agents/code-verifier.md +1 -1
- package/agents/test_agent_frontmatter.py +58 -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/CODE_RULES.md +4 -2
- package/docs/references/CLAUDE.md +3 -2
- package/docs/references/advisor-tool.md +44 -6
- package/docs/references/prose-style-enforcement.md +25 -0
- package/docs/references/team-advisor-skill.md +14 -8
- 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/code_rules_path_utils_constants.py +1 -0
- 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 +15 -0
- package/package.json +5 -2
- package/rules/CLAUDE.md +1 -0
- package/rules/code-standards.md +33 -7
- package/rules/durable-post-artifacts.md +2 -2
- package/rules/eli11-replies.md +7 -2
- 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 +13 -2
- 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 +3 -1
- 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/grok_run_ledger_constants.py +50 -0
- package/scripts/dev_env_scripts_constants/grok_worker_constants.py +104 -0
- package/scripts/dev_env_scripts_constants/verify_installable_package_constants.py +116 -0
- package/scripts/grok_patch_artifacts.py +123 -0
- package/scripts/grok_run_ledger.py +318 -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 +556 -9
- 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_grok_patch_artifacts.py +82 -0
- package/scripts/test_grok_run_ledger.py +116 -0
- package/scripts/test_resolve_worker_spawn.py +2 -2
- package/scripts/test_spawn_grok_batch.py +295 -0
- 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 +6 -2
- package/skills/_shared/CLAUDE.md +37 -4
- package/skills/_shared/advisor/CLAUDE.md +9 -0
- package/skills/_shared/advisor/advisor-protocol.md +5 -0
- package/skills/_shared/advisor/scripts/README.md +9 -0
- package/skills/_shared/end-of-run-gotcha-recommendations.md +156 -0
- package/skills/_shared/pr-loop/CLAUDE.md +18 -1
- package/skills/_shared/pr-loop/audit-contract.md +5 -0
- package/skills/_shared/pr-loop/audit-reply-template.md +5 -0
- package/skills/_shared/pr-loop/code-rules-gate.md +5 -0
- package/skills/_shared/pr-loop/fix-protocol.md +5 -0
- package/skills/_shared/pr-loop/gh-payloads.md +5 -0
- package/skills/_shared/pr-loop/post-audit-thread-contract.md +5 -0
- package/skills/_shared/pr-loop/precatch-rubric.md +5 -0
- package/skills/_shared/pr-loop/scripts/CLAUDE.md +9 -1
- package/skills/_shared/pr-loop/scripts/RUNTIME_SCRIPTS.md +29 -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/_shared/pr-loop/state-schema.md +5 -0
- package/skills/_shared/pr-loop/worker-spawn.md +5 -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 +29 -3
- 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/reference/runner-selection.md +40 -0
- 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/__init__.py +1 -0
- package/skills/e-code-review/scripts/e_code_review_scripts_constants/finding_pipeline_constants.py +49 -0
- package/skills/e-code-review/scripts/e_code_review_scripts_constants/grok_code_review_constants.py +55 -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 +230 -0
- 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 +289 -0
- package/skills/grok-spawn/SKILL.md +5 -0
- package/skills/orchestrator/SKILL.md +5 -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/skills/task-build/reference/tool-routing.md +3 -0
- package/skills/team-advisor/SKILL.md +23 -44
- package/system-prompts/software-engineer.xml +17 -5
- package/commands/initialize.md +0 -90
- package/commands/stubcheck.md +0 -88
- package/skills/test_markdown_link_integrity.py +0 -107
|
@@ -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
|
package/docs/CODE_RULES.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# Code Rules Reference
|
|
2
2
|
|
|
3
|
-
Compact
|
|
3
|
+
Compact **projection** of the repository-root canonical policy [`AGENTS.md`](../../../AGENTS.md). When this file and `AGENTS.md` disagree, **`AGENTS.md` wins** — update this projection in the same change.
|
|
4
|
+
|
|
5
|
+
⚡ marks rules enforced by hand-maintained `code_rules_enforcer.py` — the hook blocks the Write/Edit and returns the corrective detail at violation time, so this document lists those rules by name only. Session policy (question routing, task tracking) lives in `rules/*.md`; see [`code-standards.md`](../rules/code-standards.md).
|
|
4
6
|
|
|
5
7
|
---
|
|
6
8
|
|
|
@@ -46,7 +48,7 @@ Full words only (`context`, not `ctx`). Exceptions: `i`/`j`/`k` in loops, `e` fo
|
|
|
46
48
|
|
|
47
49
|
## 6. COMPLETE TYPE HINTS
|
|
48
50
|
|
|
49
|
-
ALL parameters typed, ALL returns typed. No `Any
|
|
51
|
+
ALL parameters typed, ALL returns typed. No `Any`. A `# type: ignore` is allowed only with a second trailing `#` justification of at least five characters (e.g. `# type: ignore[misc] # stubs missing in foo library`); bare ignores are blocked by `code_rules_enforcer.py`. Prefer fixing the type over an ignore when a real annotation is available.
|
|
50
52
|
|
|
51
53
|
## 6.5 FILE LENGTH GUIDANCE
|
|
52
54
|
|
|
@@ -8,8 +8,9 @@ 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
|
-
| `
|
|
12
|
-
| `
|
|
11
|
+
| `prose-style-enforcement.md` | How `CLAUDE_PROSE_STYLE_ENFORCEMENT` arms opinionated prose gates (default off) while AskUserQuestion lean-block stays always on |
|
|
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 |
|
|
13
|
+
| `team-advisor-skill.md` | `/team-advisor` map: sole-consumer warm bind, ref index, and how it pairs with `advisor()` |
|
|
13
14
|
|
|
14
15
|
## Role
|
|
15
16
|
|
|
@@ -1,13 +1,51 @@
|
|
|
1
1
|
# Advisor Tool
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Canonical consult timing and weight for any stronger-reviewer path: the native `advisor()` tool, `/team-advisor`, and the shared warm advisor.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Source bones: [Anthropic Advisor tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/advisor-tool) (Suggested system prompt for coding tasks). API shape, model pairs, cost, and caching live there. This file carries only the call rules a session needs every time.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## What it is
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
`advisor()` is a no-parameter review call. The platform forwards the full conversation (task, tool calls, results) to a stronger model. The executor continues with that guidance.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
When `advisor()` is absent, use `/team-advisor` (see `team-advisor-skill.md`).
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
## When to call
|
|
14
|
+
|
|
15
|
+
Call **before substantive work** — before writing, before locking an interpretation, before building on an assumption.
|
|
16
|
+
|
|
17
|
+
If the task needs orientation first (find files, fetch a source, see what exists), do that, then call. Orientation is not substantive work. Writing, editing, and declaring an answer are.
|
|
18
|
+
|
|
19
|
+
Also call:
|
|
20
|
+
|
|
21
|
+
- **When you believe the task is complete.** Before this call, make the deliverable durable: write the file, save the result, commit the change. The call takes time; if the session ends during it, a durable result survives and an unwritten one does not.
|
|
22
|
+
- **When stuck** — errors recur, approach does not converge, results do not fit.
|
|
23
|
+
- **When considering a change of approach.**
|
|
24
|
+
|
|
25
|
+
On tasks longer than a few steps, call at least once before committing to an approach and once before declaring done. On short reactive tasks where the next action is dictated by tool output you just read, you do not need repeated calls — most value is on the first call, before the approach hardens.
|
|
26
|
+
|
|
27
|
+
Call for design, architecture, and risk questions where you will not touch a file. If the response would be analysis or a recommendation with no other tool calls, call first. That judgment is where a second opinion is highest value. Simple factual lookups and arithmetic do not need a call.
|
|
28
|
+
|
|
29
|
+
## Hard rule
|
|
30
|
+
|
|
31
|
+
Your first write, edit, or state-changing shell call on a task must be preceded by an advisor call in the same or an earlier turn. Read-only orientation (`ls`, `cat`, `grep`, `find`, and harness equivalents) is not state-changing. This is a checkpoint, not a difficulty judgment. It applies to one-line edits too.
|
|
32
|
+
|
|
33
|
+
## How to treat advice
|
|
34
|
+
|
|
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
|
+
|
|
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
|
+
|
|
39
|
+
## Brevity cue
|
|
40
|
+
|
|
41
|
+
When the consult path supports a free-text brief, append:
|
|
42
|
+
|
|
43
|
+
`(Advisor: please keep your guidance under 80 words — I need a focused starting point, not a comprehensive plan.)`
|
|
44
|
+
|
|
45
|
+
## Related
|
|
46
|
+
|
|
47
|
+
| Doc | Holds |
|
|
48
|
+
|---|---|
|
|
49
|
+
| [Anthropic Advisor tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/advisor-tool) | API shape, model pairs, cost, caching, full best practices |
|
|
50
|
+
| `team-advisor-skill.md` | Standing warm advisor when `advisor()` is missing |
|
|
51
|
+
| `~/.claude/_shared/advisor/advisor-protocol.md` | Host bind, floor walk, lifecycle, executor paste blocks |
|