claude-dev-env 2.0.1 → 2.1.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.
Files changed (127) hide show
  1. package/CLAUDE.md +1 -1
  2. package/agents/clean-coder.md +31 -1
  3. package/audit-rubrics/prompts/category-e-dead-code.md +2 -2
  4. package/bin/install.test.mjs +2 -2
  5. package/docs/CLAUDE.md +6 -3
  6. package/docs/CODE_RULES.md +5 -1
  7. package/docs/agent-spawn-protocol.md +39 -0
  8. package/docs/nas-ssh-invocation.md +23 -0
  9. package/docs/worker-completion-gate.md +33 -0
  10. package/hooks/blocking/CLAUDE.md +1 -0
  11. package/hooks/blocking/code_rules_dead_module_constant.py +11 -5
  12. package/hooks/blocking/code_rules_shared.py +126 -47
  13. package/hooks/blocking/config/CLAUDE.md +2 -0
  14. package/hooks/blocking/config/verified_commit_constants.py +8 -1
  15. package/hooks/blocking/config/verified_commit_context_constants.py +21 -0
  16. package/hooks/blocking/config/verified_commit_gate_output_constants.py +14 -0
  17. package/hooks/blocking/conftest.py +36 -30
  18. package/hooks/blocking/convergence_gate_blocker.py +76 -8
  19. package/hooks/blocking/plain_language_blocker.py +8 -0
  20. package/hooks/blocking/state_description_blocker.py +4 -1
  21. package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +58 -0
  22. package/hooks/blocking/test_code_rules_enforcer_dead_module_constant_alias.py +133 -0
  23. package/hooks/blocking/test_code_rules_shared.py +120 -20
  24. package/hooks/blocking/test_convergence_gate_blocker.py +146 -0
  25. package/hooks/blocking/test_plain_language_blocker.py +15 -0
  26. package/hooks/blocking/test_state_description_blocker.py +15 -0
  27. package/hooks/blocking/test_verification_verdict_store.py +12 -0
  28. package/hooks/blocking/test_verified_commit_config_bootstrap.py +18 -0
  29. package/hooks/blocking/test_verified_commit_docs_delta.py +176 -0
  30. package/hooks/blocking/test_verified_commit_gate_additional_context.py +134 -0
  31. package/hooks/blocking/tests/test_pii_prevention_blocker.py +0 -1
  32. package/hooks/blocking/tests/test_verified_commit_gate.py +41 -0
  33. package/hooks/blocking/verified_commit_config_bootstrap.py +22 -10
  34. package/hooks/blocking/verified_commit_gate.py +113 -568
  35. package/hooks/blocking/verified_commit_gate_parts/CLAUDE.md +28 -0
  36. package/hooks/blocking/verified_commit_gate_parts/__init__.py +1 -0
  37. package/hooks/blocking/verified_commit_gate_parts/command_tokenization.py +174 -0
  38. package/hooks/blocking/verified_commit_gate_parts/deny_payload.py +53 -0
  39. package/hooks/blocking/verified_commit_gate_parts/deny_reason.py +80 -0
  40. package/hooks/blocking/verified_commit_gate_parts/directory_resolution.py +170 -0
  41. package/hooks/blocking/verified_commit_gate_parts/gated_invocations.py +205 -0
  42. package/hooks/blocking/verified_commit_gate_parts/tests/conftest.py +10 -0
  43. package/hooks/blocking/verified_commit_gate_parts/tests/test_command_tokenization.py +94 -0
  44. package/hooks/blocking/verified_commit_gate_parts/tests/test_deny_payload.py +17 -0
  45. package/hooks/blocking/verified_commit_gate_parts/tests/test_deny_reason.py +38 -0
  46. package/hooks/blocking/verified_commit_gate_parts/tests/test_directory_resolution.py +71 -0
  47. package/hooks/blocking/verified_commit_gate_parts/tests/test_gated_invocations.py +61 -0
  48. package/hooks/hooks_constants/CLAUDE.md +4 -1
  49. package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -0
  50. package/hooks/hooks_constants/convergence_gate_blocker_constants.py +36 -0
  51. package/hooks/hooks_constants/dead_module_constant_constants.py +8 -0
  52. package/hooks/hooks_constants/harness_scratchpad_constants.py +10 -9
  53. package/hooks/hooks_constants/mypy_integration_constants.py +16 -0
  54. package/hooks/hooks_constants/pii_prevention_constants.py +1 -0
  55. package/hooks/validators/mypy_integration.py +145 -24
  56. package/hooks/validators/python_antipattern_checks.py +16 -0
  57. package/hooks/validators/run_all_validators.py +9 -3
  58. package/hooks/validators/test_mypy_integration.py +154 -0
  59. package/hooks/validators/test_python_antipattern_checks.py +112 -1
  60. package/hooks/validators/test_run_all_validators_pretooluse.py +16 -0
  61. package/package.json +1 -1
  62. package/rules/CLAUDE.md +3 -3
  63. package/rules/agent-spawn-protocol.md +5 -43
  64. package/rules/code-standards.md +1 -36
  65. package/rules/env-var-table-code-drift.md +2 -21
  66. package/rules/hook-prose-matches-detector.md +5 -16
  67. package/rules/nas-ssh-invocation.md +3 -15
  68. package/rules/no-historical-clutter.md +7 -49
  69. package/rules/no-inline-destructive-literals.md +3 -5
  70. package/rules/package-inventory-stale-entry.md +7 -32
  71. package/rules/re-stage-before-commit.md +6 -23
  72. package/rules/shell-invocation-policy.md +1 -1
  73. package/rules/vault-context.md +3 -3
  74. package/rules/verified-commit-gate-skip.md +28 -0
  75. package/rules/workers-done-before-complete.md +2 -30
  76. package/scripts/claude_chain_runner.py +39 -3
  77. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +3 -0
  78. package/scripts/test_claude_chain_runner.py +112 -0
  79. package/scripts/test_grok_headless_runner.py +0 -1
  80. package/skills/auditing-claude-config/CLAUDE.md +2 -1
  81. package/skills/auditing-claude-config/SKILL.md +114 -176
  82. package/skills/auditing-claude-config/reference/probe-hook.md +74 -0
  83. package/skills/autoconverge/CLAUDE.md +1 -0
  84. package/skills/autoconverge/SKILL.md +310 -346
  85. package/skills/autoconverge/reference/CLAUDE.md +1 -0
  86. package/skills/autoconverge/reference/copilot-findings.md +51 -0
  87. package/skills/closeout/SKILL.md +7 -9
  88. package/skills/copilot-finding-triage/SKILL.md +5 -7
  89. package/skills/copilot-review/CLAUDE.md +3 -2
  90. package/skills/copilot-review/SKILL.md +119 -155
  91. package/skills/copilot-review/templates/subagent-prompt.md +49 -0
  92. package/skills/everything-search/SKILL.md +7 -1
  93. package/skills/fresh-branch/CLAUDE.md +6 -9
  94. package/skills/fresh-branch/SKILL.md +84 -33
  95. package/skills/fresh-branch/scripts/create_fresh_branch.py +445 -0
  96. package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/__init__.py +1 -0
  97. package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +74 -0
  98. package/skills/fresh-branch/scripts/test_create_fresh_branch.py +670 -0
  99. package/skills/orchestrator/SKILL.md +161 -147
  100. package/skills/pr-converge/SKILL.md +277 -422
  101. package/skills/pr-converge/reference/CLAUDE.md +1 -0
  102. package/skills/pr-converge/reference/progress-checklist.md +168 -0
  103. package/skills/pr-converge/test_step5_host_branch.py +8 -6
  104. package/skills/pr-loop-cloud-transport/SKILL.md +1 -1
  105. package/skills/privacy-hygiene/SKILL.md +68 -115
  106. package/skills/privacy-hygiene/reference/sweep-procedure.md +62 -0
  107. package/skills/session-log/CLAUDE.md +2 -1
  108. package/skills/session-log/SKILL.md +4 -26
  109. package/skills/session-log/templates/frontmatter.md +40 -0
  110. package/skills/skill-builder/CLAUDE.md +8 -7
  111. package/skills/skill-builder/SKILL.md +26 -11
  112. package/skills/skill-builder/references/CLAUDE.md +3 -1
  113. package/skills/skill-builder/references/delegation-map.md +21 -12
  114. package/skills/skill-builder/references/description-field.md +9 -11
  115. package/skills/skill-builder/references/deterministic-elements.md +218 -0
  116. package/skills/skill-builder/references/self-audit-checklist.md +62 -45
  117. package/skills/skill-builder/references/skill-modularity.md +8 -9
  118. package/skills/skill-builder/templates/CLAUDE.md +2 -2
  119. package/skills/skill-builder/templates/gap-analysis.md +15 -0
  120. package/skills/skill-builder/workflows/CLAUDE.md +5 -5
  121. package/skills/skill-builder/workflows/improve-skill.md +18 -9
  122. package/skills/skill-builder/workflows/new-skill.md +23 -15
  123. package/skills/skill-builder/workflows/polish-skill.md +28 -21
  124. package/system-prompts/CLAUDE.md +3 -3
  125. package/docs/agents-md-alignment-plan.md +0 -123
  126. package/docs/emotion-informed-prompt-design.md +0 -362
  127. package/rules/es-exe-file-search.md +0 -17
package/CLAUDE.md CHANGED
@@ -43,7 +43,7 @@ Run every multi-step code task in two phases:
43
43
  1. **Coders** — one coder agent per scoped assignment writes the code. A coder that hits a decision it can't reasonably solve consults the advisor (see beginning of this file).
44
44
  2. **Verification** — when the coders finish, the main session spawns the `code-verifier` agent in a fresh context, but you must first verify that their work is based on upstream's origin main (aka: the commit live on github). It derives and runs the checks itself rather than trusting coder reports: the task's named gates, tests against baselines recorded before the coders ran, and a two-way diff-vs-assignment reading (every task item maps to a hunk, every hunk maps to a task item, nothing missing). A finding must cite a failing command or a named task item. Before it emits the verdict, it puts the draft through one strongest-tier validation subagent — selected per the advisor protocol's host detection and tier ladder — that tries to refute it, and it re-checks and corrects any part the validator overturns. Source: the fresh-context review step in Claude Code best practices (https://code.claude.com/docs/en/best-practices) — the agent doing the work isn't the one grading it.
45
45
 
46
- Repair agents run only on reported findings; the verifier re-checks after each repair. Work lands (commit, push, draft PR) only on a clean verdict — enforced by the `verified_commit_gate` hook, which blocks `git commit`/`git push` unless a hook-minted verdict covers the current branch diff. The one exemption is mechanical, not discretionary: a diff whose every changed file is non-code or has an unchanged Python AST once docstrings are stripped (docs, docstrings, comments).
46
+ Repair agents run only on reported findings; the verifier re-checks after each repair. Work lands (commit, push, draft PR) only on a clean verdict — enforced by the `verified_commit_gate` hook, which blocks `git commit`/`git push` unless a hook-minted verdict covers the current branch diff. One exemption is mechanical, not discretionary: a diff whose every changed file is non-code or has an unchanged Python AST once docstrings are stripped (docs, docstrings, comments). One escape hatch is manual and narrow: appending `# verify-skip` as a trailing shell comment (outside every quoted region) to the blocked commit or push command bypasses the gate for that one command — allowed only when the branch surface is the same code a code-verifier already passed clean and the gate is blocking on a verdict that doesn't cover it (an unminted verdict, staging churn, a reverted concurrent write); any real code change since the clean verdict runs a fresh verification instead. Full rule: `~/.claude/rules/verified-commit-gate-skip.md`.
47
47
 
48
48
  ## Sub-agent Output Validation
49
49
 
@@ -2,7 +2,7 @@
2
2
  name: clean-coder
3
3
  description: "Use PROACTIVELY for ALL code generation — feature development, bug fixes, refactoring, hook creation, automation scripts, and any task that produces code. Internalizes CODE_RULES.md and the 8-dimension readability standard so thoroughly that /check finds zero issues. The definitive code-writing agent."
4
4
  model: opus
5
- tools: Read, Write, Edit, Bash, Grep, Glob, Task, Skill
5
+ tools: Read, Write, Edit, Bash, Grep, Glob, Task, Skill, SendMessage
6
6
  color: green
7
7
  ---
8
8
 
@@ -389,6 +389,36 @@ BEFORE writing:
389
389
  [12] One abstraction level throughout?
390
390
  ```
391
391
 
392
+ ## Pre-Check Loop — Validate Candidate Content Before Each Write
393
+
394
+ The write gate reports every violation in one denial, so a blocked write is a poor place to find a rule. Run the checker on the candidate content first, read the verdict, fix each line, and re-run until the verdict is clean. Then call Write or Edit, and the write lands on the first try.
395
+
396
+ ### The two local surfaces
397
+
398
+ - **`code_rules_enforcer.py --check <candidate> [--as <target>]`** — the full CODE_RULES verdict on one complete candidate file. Stage the candidate at any path and point `--as` at the real destination. The `--as` target drives every path-based decision (test-file detection, `config/` exemption, hook-infrastructure exemption), so a candidate staged in a temp folder is judged as if it sat at its destination. Exit `0` is clean or an exempt target; `1` is one or more violations, each on its own line, or an unreadable candidate; `2` is a malformed flag sequence.
399
+ - **`run_all_validators.py`** — the validator suite over the git-changed files: Ruff, Mypy, type safety, magic values, abbreviations, and more. `--pre-tool-use` runs the same suite as a gate on one proposed write from stdin. `--health` reports whether the suite runs.
400
+
401
+ Point `--as` at the file's real production path. When `--as` names an exempt path, the check returns `0` for a candidate that carries violations, so a wrong target hides the verdict you want.
402
+
403
+ ### The loop
404
+
405
+ 1. Build the full candidate content.
406
+ 2. Stage it at a temp path with the Write tool or the PowerShell tool.
407
+ 3. Run `code_rules_enforcer.py --check <staged path> --as <real destination>`.
408
+ 4. Read every reported line and fix each one in the candidate.
409
+ 5. Re-run step 3. A fix can raise a fresh violation: a magic number pulled to a module constant needs the `ALL_` collection prefix, and that rename can leave the source constant with no reader; a long function split to meet the length target can push the file past the length advisory. Loop steps 3 through 5 until the verdict is clean.
410
+ 6. Call Write or Edit with the clean content.
411
+
412
+ Re-running the checker after each fix catches the rework a single pass misses, so the write clears the gate one time.
413
+
414
+ ### Windows shell shape
415
+
416
+ Create every multi-line script with the PowerShell tool or the Write tool. A bash heredoc on Windows strips backslashes from paths and garbles multi-line Python, so a heredoc-built script fails in ways its source text hides. For a wait-for-condition step, drive a Monitor until-loop; a `sleep && tail` poll chain blocks the turn and misses the moment the condition flips.
417
+
418
+ ### Mining session transcripts
419
+
420
+ When reading transcripts under the user projects directory, pass explicit file paths to the reader, or search through Python or `Select-String`. A `.gitignore` holding `*` in that tree makes ripgrep and Grep directory recursion return zero matches with no error, so a recursive pattern reads as a clean miss even when files match.
421
+
392
422
  ## Constants Protocol
393
423
 
394
424
  Decision tree before writing any constant:
@@ -66,7 +66,7 @@ Inline the artifact under this section using the section types defined in the ch
66
66
  **E8. Stub / placeholder code without TODO**
67
67
  - Distinguish real-behavior `pass` / `continue` / empty-handler bodies (intentional swallowing of expected exceptions, no-op branches that exist to satisfy a contract) from scaffolding stubs.
68
68
  - Real-behavior bodies do NOT require a TODO; the audit must state the rationale (e.g., "rmdir race with concurrent writer is intentionally swallowed").
69
- - Scaffolding bodies (`pass`, `...`, `raise NotImplementedError`, empty `else { }`, single-statement `return None` placeholders) without a `# TODO` comment ARE Category E findings under the project's "Document Temporary Code" rule.
69
+ - Scaffolding bodies (`pass`, `...`, `raise NotImplementedError`, empty `else { }`, single-statement `return None` placeholders) without a `# TODO` comment ARE Category E findings under the project's `CODE_RULES.md` "COMMENT PRESERVATION" rule.
70
70
  - Adversarial probes for proof-of-absence: (a) any empty brace block in PowerShell / TypeScript / Go (`{ }` with no statements)? (b) any function whose entire body is `pass` / `return` / `return None`? (c) any branch that exits cleanly only because the surrounding loop is no-op for an empty input — is the no-op intentional or a placeholder?
71
71
 
72
72
  **E9. Constants-module exports with no importer**
@@ -165,7 +165,7 @@ ID prefix: `find`.
165
165
  - `sweep_empty_dirs.py` line 28: the second `except OSError: continue` similarly is intended behavior (skip the directory whose ctime is unreadable), not a stub.
166
166
  - No `...` literal anywhere in the four files.
167
167
  - No `raise NotImplementedError` anywhere.
168
- - No `# TODO` markers in the diff — the project's own rule (`code-standards.md` → "Document Temporary Code") requires TODOs only for scaffolding/placeholder code. The two `pass`/`continue` bodies above are production behavior, not scaffolding.
168
+ - No `# TODO` markers in the diff — the project's own rule (`CODE_RULES.md` → "COMMENT PRESERVATION") requires TODOs only for scaffolding/placeholder code. The two `pass`/`continue` bodies above are production behavior, not scaffolding.
169
169
  - Adversarial probes for proof-of-absence: (a) does the PowerShell script have an empty `else { }` or empty branch body? — scan lines 14-71 for any `{ }` with no statements between the braces. (b) does any function body consist of a single `pass` or `return` with no work done? — every function body in this PR performs at least one statement. (c) does the `Status` branch (lines 14-31) exit cleanly even when `$task.Triggers` is empty? — the `foreach` loop at line 26 is a no-op for an empty collection, which is correct behavior, not a stub.
170
170
 
171
171
  **E9. Constants-module exports with no importer**
@@ -803,13 +803,13 @@ test('managedHookScriptRelativePathsFromSourceRoots unions managed scripts acros
803
803
  hooks: { Stop: [{ matcher: '', hooks: [{ command: 'python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/code_rules_enforcer.py' }] }] },
804
804
  });
805
805
  writeHooksJsonAtRoot(dependencyRoot, {
806
- hooks: { PreToolUse: [{ matcher: 'Bash', hooks: [{ command: 'python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/pwsh_enforcer.py' }] }] },
806
+ hooks: { PreToolUse: [{ matcher: 'Bash', hooks: [{ command: 'python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/example_hook.py' }] }] },
807
807
  });
808
808
 
809
809
  const relativePaths = managedHookScriptRelativePathsFromSourceRoots([builtinRoot, dependencyRoot]);
810
810
 
811
811
  assert.ok(relativePaths.has('blocking/code_rules_enforcer.py'));
812
- assert.ok(relativePaths.has('blocking/pwsh_enforcer.py'));
812
+ assert.ok(relativePaths.has('blocking/example_hook.py'));
813
813
  for (const foldedPath of FOLDED_HOOK_RELATIVE_PATHS) {
814
814
  assert.ok(relativePaths.has(foldedPath), `folded hook ${foldedPath} must always be in the managed set`);
815
815
  }
package/docs/CLAUDE.md CHANGED
@@ -14,8 +14,9 @@ Reference documentation installed into `~/.claude/docs/` by `bin/install.mjs`. T
14
14
  | `PR_DESCRIPTION_GUIDE.md` | Authoritative reference for the `pr-description-writer` agent; PR body shapes derived from a 120-PR Anthropic corpus |
15
15
  | `DJANGO_PATTERNS.md` | Django-specific coding patterns |
16
16
  | `REACT_PATTERNS.md` | React-specific coding patterns |
17
- | `emotion-informed-prompt-design.md` | Guidance for emotionally-aware prompt authoring |
18
- | `agents-md-alignment-plan.md` | Alignment between `AGENTS.md` review-tool rules and the local hook enforcement layer |
17
+ | `agent-spawn-protocol.md` | Full agent-spawn protocol behind the `rules/agent-spawn-protocol.md` kernel: context-sufficiency check, `/prompt-generator` prompt crafting, and the spawn step |
18
+ | `nas-ssh-invocation.md` | Full NAS ssh policy behind the `rules/nas-ssh-invocation.md` kernel: the OpenSSH binary form, config sources, and hook enforcement |
19
+ | `worker-completion-gate.md` | Full worker-completion gate behind the `rules/workers-done-before-complete.md` kernel: the checklist, examples, and run-state records |
19
20
 
20
21
  ## Subdirectory
21
22
 
@@ -25,4 +26,6 @@ Reference documentation installed into `~/.claude/docs/` by `bin/install.mjs`. T
25
26
 
26
27
  ## Load pattern
27
28
 
28
- Rules reference these docs with `@~/.claude/docs/<file>.md` to pull them into context only when needed, rather than loading every doc on every session start.
29
+ A rule points to a doc with the path wrapped in backticks, such as `@~/.claude/docs/<file>.md`. The backticks make it a plain pointer: Claude Code reads the doc only when a rule, skill, or agent opens it, so the doc stays out of session-start context. The same path without backticks expands into context at launch when it sits in a file that loads at session start.
30
+
31
+ The `InstructionsLoaded` hook confirms this: a bare `@`-import fires an `include` load event; a backtick-wrapped path fires none.
@@ -8,6 +8,8 @@ Compact reference for agents. ⚡ marks rules enforced by `code_rules_enforcer.p
8
8
 
9
9
  **Keep existing comments in place.** Only evaluate comments on lines you are actively changing. Do not add new inline comments in production code — write self-documenting code. Docstrings (module/class/function) are always allowed. Test files are exempt. The hook treats the two directions differently: adding a new inline comment blocks the edit, while removing an existing comment prints a stderr advisory and lets the edit through.
10
10
 
11
+ Scaffolding and placeholder code carry a `TODO:` comment naming the permanent implementation and why the code is temporary — the documented exception to the no-new-comments rule.
12
+
11
13
  ---
12
14
 
13
15
  ## CORE PRINCIPLES
@@ -16,6 +18,7 @@ Compact reference for agents. ⚡ marks rules enforced by `code_rules_enforcer.p
16
18
  - **Centralized configuration** — every constant lives in ONE place (`config/`).
17
19
  - **Reuse before create** — search first, import second, create last.
18
20
  - **Encapsulation enables cleaner naming** — `isMaxLevel(level)` > `level >= MAXIMUM_LEVEL`.
21
+ - **Construction logic lives in the model** — path/URL building, formatting, and transformations belong on the model or service that owns the data; a string pattern built at two or more call sites moves to a method there.
19
22
 
20
23
  ---
21
24
 
@@ -37,7 +40,7 @@ Before writing ANY constant: search `config/` for the exact value → semantic m
37
40
 
38
41
  ## 5. NO ABBREVIATIONS
39
42
 
40
- Full words only (`context`, not `ctx`). Exceptions: `i`/`j`/`k` in loops, `e` for exception. Naming patterns: loop vars `each_*`; booleans `is_/has_/should_/can_/was_/did_`; collections `all_*`; maps `X_by_Y`; preposition params (`from_path=`, `to=`, `into=`). Banned names: `result`, `data`, `output`, `response`, `value`, `item`, `temp`. Banned prefixes: `handle`, `process`, `manage`, `do`.
43
+ Full words only (`context`, not `ctx`). Exceptions: `i`/`j`/`k` in loops, `e` for exception. Naming patterns: loop vars `each_*`; booleans `is_/has_/should_/can_/was_/did_`; collections `all_*`; maps `X_by_Y`; preposition params (`from_path=`, `to=`, `into=`). Banned names: `result`, `data`, `output`, `response`, `value`, `item`, `temp`. Banned prefixes: `handle`, `process`, `manage`, `do`. Name a component for what it IS — `Overlay`, `Validator`, `InvoicePreview`.
41
44
 
42
45
  ---
43
46
 
@@ -55,6 +58,7 @@ Advisory only, never blocking: soft advisory at >= 400 lines, strong nudge at >=
55
58
 
56
59
  **Simple > Clever. Functions > Classes. Concrete > Abstract.**
57
60
  Never: ABC for single impl, DI frameworks, factory for single type. Always: functions when no state, concrete classes, simple imports.
61
+ Parameters follow YAGNI: add an optional parameter when a caller varies the value; when every call site passes the same value, make it required or inline the constant. Remove parameters no caller passes and no body reads.
58
62
 
59
63
  ## 7.5 SOLID PRINCIPLES
60
64
 
@@ -0,0 +1,39 @@
1
+ # Agent Spawn Protocol
2
+
3
+ Full protocol behind the always-on `rules/agent-spawn-protocol.md` kernel. It applies before any Agent or Task tool invocation — Explore, implementation, research, or team subagents.
4
+
5
+ ## Step 1: Context sufficiency check
6
+
7
+ Before writing any agent prompt, confirm you can answer all of these:
8
+
9
+ - What specific files, directories, or areas of the codebase are involved?
10
+ - What constraints apply — patterns to follow, things to leave untouched, boundaries?
11
+ - What does success look like — expected output, acceptance criteria?
12
+ - Is the task unambiguous enough to delegate?
13
+
14
+ When any answer is "I don't know", investigate first (read files, search code) or ask the user. Do not spawn with incomplete context.
15
+
16
+ ## Step 2: Craft the prompt with /prompt-generator
17
+
18
+ Run the `/prompt-generator` skill to produce a structured prompt. Feed it:
19
+
20
+ - The task description and goal
21
+ - Target files and directories found in Step 1
22
+ - Constraints and boundaries
23
+ - Expected output format
24
+ - Acceptance criteria
25
+
26
+ The skill asks one to three clarifying questions when information is missing — this is the built-in context verification. Use the skill's output as the agent's `prompt` parameter.
27
+
28
+ ## Step 3: Spawn the agent
29
+
30
+ Pass the structured prompt from Step 2 to the Agent or Task tool.
31
+
32
+ ## Why
33
+
34
+ An agent that receives a vague prompt wastes tokens exploring in circles, produces code that misses constraints, and needs a second pass. A small investment in prompt quality through `/prompt-generator` saves a long agent failure. This holds for Explore agents, which waste context on unfocused searches, and for execution agents, which write wrong code.
35
+
36
+ ## Relationship to other rules
37
+
38
+ - `conservative-action.md` gates acting when intent is ambiguous. This protocol extends that: an ambiguous task goes to investigation or a user question first, never straight to a subagent.
39
+ - Project-specific rules or `~/.claude/CLAUDE.md` may decide whether to use subagents at all; this protocol governs how to craft the prompt once you delegate.
@@ -0,0 +1,23 @@
1
+ # NAS SSH Invocation Policy
2
+
3
+ Full detail behind the always-on `rules/nas-ssh-invocation.md` kernel. It applies to any `ssh`, `scp`, or `sftp` command against the NAS.
4
+
5
+ ## Why the Windows OpenSSH binary
6
+
7
+ Git Bash's MSYS `ssh` reads `~/.ssh/id_ed25519` as world-readable through its ACL mapping, rejects the key as bad permissions, offers no key, and falls back to an interactive password prompt. In an unattended session no one answers that prompt, so the session hangs. The `System32/OpenSSH` binary authenticates the same key without a prompt.
8
+
9
+ ## The form to use
10
+
11
+ ```
12
+ "/c/Windows/System32/OpenSSH/ssh.exe" -o BatchMode=yes -o ConnectTimeout=10 -p 22 operator@nas.example.local "<cmd>"
13
+ ```
14
+
15
+ `scp` and `sftp` take the matching `System32/OpenSSH` binary, `-o BatchMode=yes`, and the same port (`-P` for `scp`).
16
+
17
+ The host, ssh user, and port come from the `CLAUDE_NAS_*` environment variables or `~/.claude/local-identity.json`; the committed examples show placeholders (`nas.example.local`, `operator`, `22`).
18
+
19
+ `-o BatchMode=yes` is required, not optional: it turns a key-authentication failure into a loud non-zero exit rather than a silent password prompt, so an auth regression surfaces as an error you can read.
20
+
21
+ ## Enforcement
22
+
23
+ `nas_ssh_binary_enforcer.py` (PreToolUse on Bash) denies a bare `ssh`/`scp`/`sftp` command word aimed at the NAS host and points at the full-binary form. It also denies the full `System32/OpenSSH` binary to that host when the command omits `-o BatchMode=yes`. Commands to any other host, and commands that mention the address without an ssh-family command word, pass.
@@ -0,0 +1,33 @@
1
+ # Worker Completion Gate
2
+
3
+ Full detail behind the always-on `rules/workers-done-before-complete.md` kernel. It applies before marking any task `completed` when the task spawned workers — subagents, workflow agents, or background shells.
4
+
5
+ ## The gate
6
+
7
+ A task reaches `completed` only when two things hold: every worker it spawned has finished, and each worker's result is merged into run state — `state.json`, `pr-converge-state.json`, the task list, or whatever record the task keeps. A worker that still runs, or one that finished but whose output never landed in run state, leaves the task open.
8
+
9
+ List the live workers before you mark the task complete. When a worker is dead or hung, that is a finding to record and report, not a result to drop in silence. A step that waits on workers ends its turn `in_progress` with a wakeup scheduled, so the run picks the workers back up rather than closing the task without them.
10
+
11
+ ## Checklist before marking complete
12
+
13
+ | Check | Action |
14
+ |---|---|
15
+ | Are any spawned workers still running? | List them; when yes, stay `in_progress` and schedule a wakeup. |
16
+ | Did every finished worker return a result? | Read each result; a dead or hung worker is a finding to report. |
17
+ | Is each result merged into run state? | Write it to `state.json` or the task list before closing. |
18
+ | Does the task's own goal now hold? | Confirm against the merged state, not a worker's self-report. |
19
+
20
+ Mark `completed` only when every row passes.
21
+
22
+ ## Examples
23
+
24
+ **Wrong:** Marking the audit task complete while two bugteam workers still run in the background.
25
+ **Right:** List the workers, see two still running, keep the task `in_progress`, and schedule a wakeup to collect them.
26
+
27
+ **Wrong:** A worker crashes; the task closes as complete because the other workers finished.
28
+ **Right:** Record the crashed worker as a finding, report it, and hold the task open until its work is covered.
29
+
30
+ ## Relationship to other rules
31
+
32
+ - `long-horizon-autonomy` covers acting on what you have and not ending a turn on a promise. This gate names the specific completion condition: workers finished and their results merged.
33
+ - `skills/pr-converge/reference/state-schema.md` defines the run-state records a worker's result lands in before the task closes.
@@ -8,6 +8,7 @@ PreToolUse hooks that deny (block) tool calls when a rule is violated. The main
8
8
  |---|---|
9
9
  | `config/` | Shared constants for the verified-commit gate family (`verified_commit_constants.py`) |
10
10
  | `tdd_enforcer_parts/` | Concern modules the `tdd_enforcer.py` entry hook wires together: path classification, content analysis, candidate-path resolution, freshness, git-tracking restore detection, decisions, and constants |
11
+ | `verified_commit_gate_parts/` | Concern modules the `verified_commit_gate.py` entry hook wires together: command tokenization, directory-change resolution, gated git-invocation resolution, deny-reason resolution, and deny-payload assembly |
11
12
  | `claude_md_orphan_file_blocker_parts/` | Concern modules the `claude_md_orphan_file_blocker.py` entry hook wires together: reference extraction, subtree scan, scan plan, decision, and constants |
12
13
  | `package_inventory_stale_blocker_parts/` | Concern modules the `package_inventory_stale_blocker.py` entry hook wires together: inventory detection, decision, and constants |
13
14
  | `inventory_intent_records/` | The shared per-session pending-intent store both inventory blockers read to break the file/row add-order deadlock |
@@ -74,6 +74,7 @@ from hooks_constants.dead_module_constant_constants import ( # noqa: E402
74
74
  CONFIG_DIRECTORY_SEGMENT,
75
75
  CONSTANTS_MODULE_SUFFIX,
76
76
  DEAD_MODULE_CONSTANT_GUIDANCE,
77
+ DEAD_MODULE_CONSTANT_RETRY_GUIDANCE,
77
78
  DUNDER_ALL_NAME,
78
79
  DUNDER_INIT_FILENAME,
79
80
  GIT_DIRECTORY_NAME,
@@ -204,9 +205,11 @@ def _referenced_names_in_source(
204
205
  """Return every name a module references — imported, read, or re-exported.
205
206
 
206
207
  Collects imported binding names, ``from`` import member names, name
207
- references, attribute roots, and (when ``collect_string_literals`` is set)
208
- string literals, so a name listed in an ``__all__`` literal or named in a
209
- string annotation counts as a reference. A module that fails to parse
208
+ references, both the root and the member name of each attribute access (so
209
+ ``module.CONSTANT`` counts ``CONSTANT`` as read), and (when
210
+ ``collect_string_literals`` is set) string literals, so a name listed in an
211
+ ``__all__`` literal or named in a string annotation counts as a reference. A
212
+ module that fails to parse
210
213
  contributes no names. With ``load_only`` set, only ``Load``-context names
211
214
  count, so a constant's own assignment target in the module being judged does
212
215
  not count as a reference to itself.
@@ -234,6 +237,8 @@ def _referenced_names_in_source(
234
237
  if load_only and not isinstance(each_node.ctx, ast.Load):
235
238
  continue
236
239
  referenced_names.add(each_node.id)
240
+ elif isinstance(each_node, ast.Attribute):
241
+ referenced_names.add(each_node.attr)
237
242
  elif isinstance(each_node, ast.Import | ast.ImportFrom):
238
243
  for each_alias in each_node.names:
239
244
  referenced_names.add(each_alias.asname or each_alias.name)
@@ -631,8 +636,9 @@ def check_dead_module_constants(
631
636
  if each_name in all_referenced_names:
632
637
  continue
633
638
  issues.append(
634
- f"Line {each_line}: module-level constant {each_name!r}"
635
- f" - {DEAD_MODULE_CONSTANT_GUIDANCE}"
639
+ f"Line {each_line}: module-level constant {each_name!r} in"
640
+ f" {written_path.name} - {DEAD_MODULE_CONSTANT_GUIDANCE}"
641
+ f" {DEAD_MODULE_CONSTANT_RETRY_GUIDANCE}"
636
642
  )
637
643
  if len(issues) >= MAX_DEAD_MODULE_CONSTANT_ISSUES:
638
644
  break
@@ -31,11 +31,11 @@ from hooks_constants.code_rules_enforcer_constants import ( # noqa: E402
31
31
  STRICT_TEST_FILE_BASENAME_PATTERN,
32
32
  )
33
33
  from hooks_constants.harness_scratchpad_constants import ( # noqa: E402
34
+ CLAUDE_SESSION_ID_ENVIRONMENT_VARIABLE_NAME,
34
35
  HARNESS_SCRATCHPAD_LEAF_DIRECTORY_NAME,
35
- HARNESS_SCRATCHPAD_PATH_SEPARATOR_REPLACEMENT,
36
+ HARNESS_SCRATCHPAD_USER_DIRECTORY_NAME,
36
37
  HARNESS_SCRATCHPAD_USER_DIRECTORY_PREFIX,
37
38
  HOOK_PAYLOAD_SESSION_ID_KEY,
38
- HOOK_PAYLOAD_WORKING_DIRECTORY_KEY,
39
39
  )
40
40
  from hooks_constants.unused_module_import_constants import ( # noqa: E402
41
41
  TYPE_CHECKING_IDENTIFIER,
@@ -275,78 +275,157 @@ def is_ephemeral_script_path(file_path: str) -> bool:
275
275
  return False
276
276
 
277
277
 
278
- def _resolve_session_scratchpad_root(hook_payload: dict) -> str | None:
279
- """Rebuild the harness session scratchpad directory from a PreToolUse payload.
278
+ def _session_id_for_scratchpad(hook_payload: dict) -> str:
279
+ """Return the session id from the payload, or the harness environment value.
280
+
281
+ Args:
282
+ hook_payload: The PreToolUse payload that may carry ``session_id``.
283
+
284
+ Returns:
285
+ The payload session id when present, otherwise the
286
+ ``CLAUDE_CODE_SESSION_ID`` environment value, or an empty string when
287
+ neither source carries one.
288
+ """
289
+ payload_session_id = hook_payload.get(HOOK_PAYLOAD_SESSION_ID_KEY, "")
290
+ if isinstance(payload_session_id, str) and payload_session_id:
291
+ return payload_session_id
292
+ return os.environ.get(CLAUDE_SESSION_ID_ENVIRONMENT_VARIABLE_NAME, "")
280
293
 
281
- ::
282
294
 
283
- payload.cwd = /home/user/project payload.session_id = 5f2c...
284
- | |
285
- <tempdir>/claude-<uid>/-home-user-project/5f2c.../scratchpad
286
- | |
287
- os.getuid() cwd with each "/" turned to "-"
295
+ def _is_harness_user_directory(directory_name: str) -> bool:
296
+ """Return whether a path component is the harness user directory.
288
297
 
289
- No environment variable carries this path, so it is rebuilt from the three
290
- signals a hook can read: the POSIX user id and the ``cwd`` and ``session_id``
291
- fields the harness puts in every PreToolUse payload. The rebuilt directory
292
- is returned only when it exists on disk, so a wrong guess or a non-POSIX
293
- platform yields None and the gates keep full enforcement.
298
+ The harness names this directory ``claude`` on Windows and ``claude-<uid>``
299
+ on POSIX, so both the exact name and the prefixed form count.
294
300
 
295
301
  Args:
296
- hook_payload: The PreToolUse payload carrying ``cwd`` and ``session_id``.
302
+ directory_name: A single path component below the temp-directory root.
297
303
 
298
304
  Returns:
299
- The scratchpad directory path when it exists on disk, else None.
305
+ True when the component is the harness user directory.
300
306
  """
301
- get_user_id = getattr(os, "getuid", None)
302
- if get_user_id is None:
303
- return None
304
- session_id = hook_payload.get(HOOK_PAYLOAD_SESSION_ID_KEY, "")
305
- working_directory = hook_payload.get(HOOK_PAYLOAD_WORKING_DIRECTORY_KEY, "")
306
- if not isinstance(session_id, str) or not session_id:
307
- return None
308
- if not isinstance(working_directory, str) or not working_directory:
309
- return None
310
- mangled_working_directory = working_directory.replace("\\", "/").replace(
311
- "/", HARNESS_SCRATCHPAD_PATH_SEPARATOR_REPLACEMENT
312
- )
313
- user_directory_name = f"{HARNESS_SCRATCHPAD_USER_DIRECTORY_PREFIX}{get_user_id()}"
314
- scratchpad_root = os.path.join(
315
- tempfile.gettempdir(),
316
- user_directory_name,
317
- mangled_working_directory,
318
- session_id,
319
- HARNESS_SCRATCHPAD_LEAF_DIRECTORY_NAME,
307
+ return (
308
+ directory_name == HARNESS_SCRATCHPAD_USER_DIRECTORY_NAME
309
+ or directory_name.startswith(HARNESS_SCRATCHPAD_USER_DIRECTORY_PREFIX)
320
310
  )
321
- if not os.path.isdir(scratchpad_root):
311
+
312
+
313
+ def _relative_parts_under_temp_root(
314
+ real_target: str, real_temp_root: str
315
+ ) -> tuple[str, ...]:
316
+ """Return real_target's path components below real_temp_root, or empty when outside.
317
+
318
+ Args:
319
+ real_target: The resolved candidate path.
320
+ real_temp_root: The resolved temp-directory root.
321
+
322
+ Returns:
323
+ The path components between the temp root and the target, or an empty
324
+ tuple when the target sits outside the temp directory or on another
325
+ drive.
326
+ """
327
+ try:
328
+ relative_path = os.path.relpath(real_target, real_temp_root)
329
+ except ValueError:
330
+ return ()
331
+ all_relative_parts = Path(relative_path).parts
332
+ if all_relative_parts and all_relative_parts[0] == os.path.pardir:
333
+ return ()
334
+ return all_relative_parts
335
+
336
+
337
+ def _existing_scratchpad_root(
338
+ all_relative_parts: tuple[str, ...], real_temp_root: str, session_id: str
339
+ ) -> str | None:
340
+ """Return the on-disk scratchpad root matching the harness shape, or None.
341
+
342
+ ::
343
+
344
+ <temp-root>/<user-dir>/<mangled-cwd>/<session-id>/scratchpad/<file>
345
+ | | |
346
+ claude user dir session id leaf name
347
+
348
+ Args:
349
+ all_relative_parts: The target's path components below the temp root.
350
+ real_temp_root: The resolved temp-directory root.
351
+ session_id: The session id the scratchpad's parent segment must equal.
352
+
353
+ Returns:
354
+ The scratchpad directory path when the shape matches and that directory
355
+ exists on disk, otherwise None.
356
+ """
357
+ if not all_relative_parts or not _is_harness_user_directory(all_relative_parts[0]):
322
358
  return None
323
- return scratchpad_root
359
+ for each_session_index in range(2, len(all_relative_parts) - 1):
360
+ leaf_index = each_session_index + 1
361
+ if all_relative_parts[each_session_index] != session_id:
362
+ continue
363
+ if all_relative_parts[leaf_index] != HARNESS_SCRATCHPAD_LEAF_DIRECTORY_NAME:
364
+ continue
365
+ scratchpad_root = os.path.join(
366
+ real_temp_root, *all_relative_parts[: leaf_index + 1]
367
+ )
368
+ if os.path.isdir(scratchpad_root):
369
+ return scratchpad_root
370
+ return None
324
371
 
325
372
 
326
373
  def is_under_session_scratchpad(file_path: str, hook_payload: dict) -> bool:
327
374
  """Return True when file_path resolves under the harness session scratchpad.
328
375
 
329
376
  One-off scripts written to the session scratchpad are throwaway tooling
330
- outside every repo, so the TDD and CODE_RULES gates skip them. Both
331
- file_path and the rebuilt scratchpad root are resolved through the real
332
- filesystem (symlinks followed) before the containment test, so a symlink
333
- into the scratchpad exempts and a symlink out of it does not.
377
+ outside every repo, so the TDD and CODE_RULES gates skip them.
378
+
379
+ The match keys on the session id — from the payload, or the
380
+ ``CLAUDE_CODE_SESSION_ID`` environment variable and on the temp-directory
381
+ path shape ``<user-dir>/<mangled-cwd>/<session-id>/scratchpad``, so it holds
382
+ on Windows and POSIX alike. file_path resolves through the real filesystem
383
+ (symlinks followed) before the shape test, and the scratchpad directory must
384
+ exist on disk, so a crafted path that never existed keeps full enforcement.
334
385
 
335
386
  Args:
336
387
  file_path: The path the write targets.
337
- hook_payload: The PreToolUse payload carrying ``cwd`` and ``session_id``.
388
+ hook_payload: The PreToolUse payload carrying the session id.
338
389
 
339
390
  Returns:
340
391
  True when file_path's real path sits at or under the session scratchpad.
341
392
  """
342
393
  if not file_path:
343
394
  return False
344
- scratchpad_root = _resolve_session_scratchpad_root(hook_payload)
345
- if scratchpad_root is None:
395
+ session_id = _session_id_for_scratchpad(hook_payload)
396
+ if not session_id:
346
397
  return False
347
398
  real_target = os.path.realpath(file_path)
348
- real_root = os.path.realpath(scratchpad_root)
349
- return real_target == real_root or real_target.startswith(real_root + os.sep)
399
+ real_temp_root = os.path.realpath(tempfile.gettempdir())
400
+ all_relative_parts = _relative_parts_under_temp_root(real_target, real_temp_root)
401
+ return (
402
+ _existing_scratchpad_root(all_relative_parts, real_temp_root, session_id)
403
+ is not None
404
+ )
405
+
406
+
407
+ def is_ephemeral_path(file_path: str, hook_payload: dict | None = None) -> bool:
408
+ """Return True when file_path is a throwaway scratch path exempt from repo gates.
409
+
410
+ Combines the two throwaway-path families a repo gate skips: the root-anchored
411
+ ephemeral scratch directories (``/tmp`` and ``$CLAUDE_JOB_DIR/tmp``) and the
412
+ harness session scratchpad. The session scratchpad match reads the session id
413
+ from the payload when one is supplied, and from the harness environment
414
+ variable otherwise, so a caller that holds no payload still gets the match.
415
+ One call answers path exemption for both families, so a gate that skips
416
+ throwaway paths need not repeat the two checks itself.
417
+
418
+ Args:
419
+ file_path: The candidate path to classify.
420
+ hook_payload: The PreToolUse payload carrying the session id, or None to
421
+ read the session id from the environment alone.
422
+
423
+ Returns:
424
+ True when the path is ephemeral scratch or under the session scratchpad.
425
+ """
426
+ if is_ephemeral_script_path(file_path):
427
+ return True
428
+ return is_under_session_scratchpad(file_path, hook_payload or {})
350
429
 
351
430
 
352
431
  def is_migration_file(file_path: str) -> bool:
@@ -12,6 +12,8 @@ A Python package that holds shared constants for the verified-commit gate family
12
12
  |---|---|
13
13
  | `__init__.py` | Declares this as a regular package (not a namespace package) so it resolves first on `sys.path` |
14
14
  | `verified_commit_constants.py` | All tunables for the gate: directory names, regex patterns for detecting verdict paths and obfuscation attempts, timeout values, git subcommand sets, bypass marker, and corrective messages |
15
+ | `verified_commit_context_constants.py` | `VERIFY_SKIP_ADDITIONAL_CONTEXT`, the ``# verify-skip`` usage rule the gate attaches to a deny payload's `additionalContext` |
16
+ | `verified_commit_gate_output_constants.py` | `PRE_TOOL_USE_HOOK_EVENT_NAME` and `DENY_PERMISSION_DECISION` (the deny payload's event name and decision string), `GATE_HOOK_MODULE_NAME` (the gate's own module name for block logging), and `REGEX_ALTERNATION_SEPARATOR` (the `\|` join for the directory-change verb alternation in `gated_invocations.py`) |
15
17
 
16
18
  ## Key constants in `verified_commit_constants.py`
17
19
 
@@ -131,7 +131,14 @@ CORRECTIVE_MESSAGE = (
131
131
  "and recorded baselines; when it finishes with a clean verdict the "
132
132
  "SubagentStop hook mints the verdict and this command will pass. Any "
133
133
  "file change after verification invalidates the verdict, so verify "
134
- "last. Exempt automatically: docs/image files, pytest test files, and "
134
+ "last. The verdict binds to the exact surface content (every changed "
135
+ "and untracked file's bytes), not to a work-tree path, so a commit "
136
+ "from a different work tree passes only when its surface is "
137
+ "byte-identical; a work tree carrying any extra or later edit has a "
138
+ "surface no verdict covers. To land this change, run this command "
139
+ "from the work tree that holds the verified surface, or verify this "
140
+ "surface here. Exempt automatically: docs/image files, pytest test "
141
+ "files, and "
135
142
  "Python files whose docstring- and comment-stripped AST is unchanged "
136
143
  "(comment-only edits in non-Python files are not exempt)."
137
144
  )
@@ -0,0 +1,21 @@
1
+ """The additionalContext text the verified-commit gate attaches to a deny.
2
+
3
+ The gate injects this next to the blocked tool result so the agent learns,
4
+ at the moment of the block, when the '# verify-skip' marker is legitimate
5
+ and when a fresh verification is required instead.
6
+ """
7
+
8
+ VERIFY_SKIP_ADDITIONAL_CONTEXT = (
9
+ "The verified-commit gate honors a one-command escape hatch: appending the "
10
+ "marker '# verify-skip' as a trailing shell comment (outside every quoted "
11
+ "region) to the blocked command bypasses the gate for that command only. "
12
+ "It is allowed only when the branch surface is the "
13
+ "same code a code-verifier already passed clean and the gate is "
14
+ "blocking on a verdict that does not cover it (an unminted verdict, "
15
+ "staging churn, or a reverted concurrent write) — confirm the verified "
16
+ "suite still passes and the diff holds nothing beyond the clean verdict "
17
+ "before using it. Any real code change since that clean verdict, a "
18
+ "first verification, or unrepaired findings all require a fresh "
19
+ "verification by the code-verifier agent instead. Full rule: "
20
+ "~/.claude/rules/verified-commit-gate-skip.md."
21
+ )
@@ -0,0 +1,14 @@
1
+ """Output-shape constants for the verified-commit gate's deny payload.
2
+
3
+ Shared by ``verified_commit_gate.py`` and its ``verified_commit_gate_parts``
4
+ package so the PreToolUse event name, the deny decision string, the hook's
5
+ own module name for block logging, and the regex-alternation join separator
6
+ live in one place.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ PRE_TOOL_USE_HOOK_EVENT_NAME = "PreToolUse"
12
+ DENY_PERMISSION_DECISION = "deny"
13
+ GATE_HOOK_MODULE_NAME = "verified_commit_gate.py"
14
+ REGEX_ALTERNATION_SEPARATOR = "|"