claude-dev-env 8.1.0 → 8.3.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 (122) hide show
  1. package/.agents/agents/poteto-agent.md +6 -0
  2. package/.agents/agents/test_agent_frontmatter.py +9 -9
  3. package/.agents/agents/test_pr_description_writer_style.py +1 -1
  4. package/.agents/agents-archived/ARCHIVE-MANIFEST.md +15 -0
  5. package/.agents/{agents → agents-archived}/clean-coder.md +4 -4
  6. package/.agents/{agents → agents-archived}/code-quality-agent.md +5 -5
  7. package/.agents/{agents → agents-archived}/plan-packet-validator.md +2 -2
  8. package/.agents/{agents → agents-archived}/session-advisor.md +2 -2
  9. package/.agents/skills/_shared/end-of-run-gotcha-recommendations.md +2 -2
  10. package/.agents/skills/cert-classification-rule/SKILL.md +3 -3
  11. package/.agents/skills/e-code-review/SKILL.md +2 -2
  12. package/.agents/skills/e-code-review/reference/fix.md +1 -1
  13. package/.agents/skills/e-code-review/reference/loop.md +1 -1
  14. package/.agents/skills/e-code-review/reference/medium.md +4 -4
  15. package/.agents/skills/e-code-review/reference/xhigh.md +5 -5
  16. package/.agents/skills/everything-search/SKILL.md +3 -3
  17. package/.agents/skills/grok-spawn/SKILL.md +9 -6
  18. package/.agents/skills/grok-spawn/reference/flag-profiles.md +12 -8
  19. package/.agents/skills/issue-tracker/SKILL.md +3 -3
  20. package/.agents/skills/orchestrator/SKILL.md +19 -18
  21. package/.agents/skills/orchestrator/reference/consult-the-orchestrator.md +1 -1
  22. package/.agents/skills/orchestrator/reference/executor-consult-block.md +1 -1
  23. package/.agents/skills/orchestrator-refresh/SKILL.md +4 -2
  24. package/.agents/skills/plugin-eval-standalone-skill/SKILL.md +96 -0
  25. package/.agents/skills/plugin-eval-standalone-skill/reference/graders-and-commands.md +108 -0
  26. package/.agents/skills/privacy-hygiene/SKILL.md +5 -6
  27. package/.agents/skills/privacy-hygiene/reference/sweep-procedure.md +5 -6
  28. package/.agents/skills/pull-request/SKILL.md +14 -5
  29. package/.agents/skills/pull-request/reference/publication-tasks.md +1 -1
  30. package/.agents/skills/repairing-hook-boundaries/SKILL.md +1 -1
  31. package/.agents/skills/syncing-submodules/SKILL.md +1 -1
  32. package/.agents/skills/team-advisor/SKILL.md +11 -9
  33. package/.agents/skills/team-advisor/reference/advisor-docs-review.md +28 -1
  34. package/.agents/skills/usage-pause/SKILL.md +3 -3
  35. package/_shared/advisor/advisor-protocol.md +7 -3
  36. package/_shared/advisor/reference/advisor-block.md +8 -1
  37. package/audit-rubrics/category_rubrics/category-f-silent-failures.md +2 -2
  38. package/audit-rubrics/category_rubrics/category-i-concurrency.md +1 -1
  39. package/audit-rubrics/category_rubrics/category-m-producer-consumer-cardinality.md +1 -1
  40. package/audit-rubrics/category_rubrics/category-n-test-name-scenario-verifier.md +5 -5
  41. package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +5 -5
  42. package/audit-rubrics/category_rubrics/category-p-name-vs-behavior-contract.md +3 -3
  43. package/audit-rubrics/category_rubrics/category-q-cross-surface-claims.md +1 -1
  44. package/audit-rubrics/prompts/category-a-api-contracts.md +2 -2
  45. package/audit-rubrics/prompts/category-b-selector-engine-compat.md +3 -3
  46. package/audit-rubrics/prompts/category-c-resource-cleanup.md +3 -3
  47. package/audit-rubrics/prompts/category-d-scoping-and-ordering.md +1 -1
  48. package/audit-rubrics/prompts/category-e-dead-code.md +3 -3
  49. package/audit-rubrics/prompts/category-f-silent-failures.md +12 -12
  50. package/audit-rubrics/prompts/category-g-bounds-and-overflow.md +2 -2
  51. package/audit-rubrics/prompts/category-h-security-boundaries.md +4 -4
  52. package/audit-rubrics/prompts/category-i-concurrency.md +6 -6
  53. package/audit-rubrics/prompts/category-j-code-rules-compliance.md +5 -5
  54. package/audit-rubrics/prompts/category-m-producer-consumer-cardinality.md +1 -1
  55. package/audit-rubrics/prompts/category-n-test-name-scenario-verifier.md +7 -7
  56. package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +2 -2
  57. package/audit-rubrics/prompts/category-p-name-vs-behavior-contract.md +3 -3
  58. package/audit-rubrics/prompts/category-q-cross-surface-claims.md +2 -2
  59. package/bin/ever-shipped-skills.mjs +1 -0
  60. package/bin/ever-shipped-skills.test.mjs +4 -0
  61. package/bin/install.agents-home.test.mjs +44 -94
  62. package/bin/install.mjs +31 -24
  63. package/bin/install.test.mjs +21 -0
  64. package/bin/resolve-package-managed-directory.test.mjs +1 -1
  65. package/bin/subagent-model-policy.test.mjs +15 -8
  66. package/docs/BDD_SCENARIO_QUALITY.md +2 -2
  67. package/docs/CODE_RULES.md +7 -7
  68. package/docs/TEST_QUALITY.md +1 -1
  69. package/docs/nas-ssh-invocation.md +1 -1
  70. package/docs/references/advisor-tool.md +2 -2
  71. package/docs/references/prose-style-enforcement.md +1 -1
  72. package/docs/references/team-advisor-skill.md +2 -2
  73. package/docs/references/weak-executor-advisor.md +1 -1
  74. package/docs/subagent-model-routing.md +6 -5
  75. package/hooks/blocking/tdd_enforcer.py +2 -3
  76. package/hooks/blocking/test_bash_post_call_dispatcher.py +7 -24
  77. package/hooks/blocking/test_code_rules_enforcer_magic_allowlist.py +1 -1
  78. package/hooks/hooks_constants/bash_post_call_dispatcher_constants.py +0 -3
  79. package/hooks/hooks_constants/hosted_hook_runner.py +1 -1
  80. package/hooks/hooks_constants/msys_path_conversion_advisor_constants.py +3 -35
  81. package/hooks/hooks_constants/test_bash_post_call_dispatcher_constants.py +2 -10
  82. package/hooks/routing/subagent_model_routing.mjs +15 -0
  83. package/hooks/routing/subagent_model_routing.test.mjs +58 -25
  84. package/package.json +1 -1
  85. package/rules/archiving-agent-config.md +5 -8
  86. package/rules/git-workflow.md +3 -3
  87. package/rules/long-horizon-autonomy.md +1 -1
  88. package/rules/shell-invocation.md +0 -2
  89. package/rules/subagent-model-policy.json +5 -13
  90. package/rules/testing.md +0 -2
  91. package/scripts/active_capability_references.py +74 -19
  92. package/scripts/banned_prose_words.py +159 -0
  93. package/scripts/dev_env_scripts_constants/active_capability_constants.py +12 -10
  94. package/scripts/dev_env_scripts_constants/banned_prose_word_constants.py +95 -0
  95. package/scripts/dev_env_scripts_constants/grok_worker_constants.py +3 -3
  96. package/scripts/grok_headless_runner.py +19 -1
  97. package/scripts/grok_worker_preflight.py +67 -1
  98. package/scripts/policy_lint/adapter_retired_hook_prose.py +17 -7
  99. package/scripts/policy_lint/adapters.py +6 -6
  100. package/scripts/policy_lint/config/constants.py +8 -0
  101. package/scripts/policy_lint/registry.py +1 -1
  102. package/scripts/resolve_advisor_model_route.mjs +2 -2
  103. package/scripts/subagent_model_policy.mjs +4 -1
  104. package/scripts/sync_to_cursor/rules.py +1 -1
  105. package/scripts/test_active_capability_references.py +100 -33
  106. package/scripts/test_grok_headless_runner.py +35 -1
  107. package/scripts/test_grok_worker_preflight.py +73 -0
  108. package/scripts/tests/resolve_advisor_model_route.test.mjs +3 -3
  109. package/scripts/tests/test_banned_prose_words.py +103 -0
  110. package/scripts/tests/test_engine.py +1 -1
  111. package/scripts/tests/test_policy_lint_rules_retired_hook_prose.py +46 -0
  112. package/scripts/tests/test_rules.py +17 -6
  113. package/system-prompts/software-engineer.xml +7 -7
  114. package/hooks/advisory/msys_path_conversion_advisor.py +0 -200
  115. package/hooks/advisory/test_msys_path_conversion_advisor.py +0 -223
  116. package/hooks/hooks_constants/test_failure_recorder_constants.py +0 -34
  117. package/hooks/observability/test_failure_recorder.py +0 -221
  118. package/hooks/observability/test_test_failure_recorder.py +0 -173
  119. /package/.agents/{agents → agents-archived}/git-commit-crafter.md +0 -0
  120. /package/.agents/{agents → agents-archived}/issue-tracker.md +0 -0
  121. /package/.agents/{agents → agents-archived}/pr-description-writer.md +0 -0
  122. /package/.agents/{agents → agents-archived}/skill-writer-agent.md +0 -0
@@ -11,7 +11,7 @@ Audit [REPO/ARTIFACT] [TARGET_ID] for **Category N only** (test-name scenario ve
11
11
 
12
12
  ID prefix: `find`.
13
13
 
14
- [ONE-PARAGRAPH FRAME: enumerate every test whose name includes a scenario claim (`_when_*`, `_at_*`, `_under_*`, `_with_*`, `_on_*`, `_after_*`, `_during_*`). State the audit goal: for each scenario-named test, verify the body sets up the named condition via fixture / monkeypatch / environment gate so the production code's scenario-named branch actually runs during the act phase.]
14
+ [ONE-PARAGRAPH FRAME: enumerate every test whose name includes a scenario claim (`_when_*`, `_at_*`, `_under_*`, `_with_*`, `_on_*`, `_after_*`, `_during_*`). State the audit goal: for each scenario-named test, verify the body sets up the named condition via fixture / monkeypatch / environment gate so the production code's scenario-named branch runs during the act phase.]
15
15
 
16
16
  ## Source material ([N] files/sections, all lines in scope)
17
17
 
@@ -37,12 +37,12 @@ ID prefix: `find`.
37
37
 
38
38
  **N5. Assertion shape mismatch**
39
39
  - For every assertion, verify the assertion's shape can fail by construction. `assert <substring> not in result` where the substring is misspelled relative to the production output, or `assert result == ""` when the production function returns `None` on the negative case, or `len(result) > 0` when the production function returns an empty list on the no-feature path.
40
- - Adversarial probes: (a) inspect each assertion's shape against the production function's actual return-value space; (b) check for assertions where the substring shape never appears in the production output by construction; (c) check for `assert x is True` where the production function returns truthy non-bool values.
40
+ - Adversarial probes: (a) inspect each assertion's shape against the production function's return-value space; (b) check for assertions where the substring shape never appears in the production output by construction; (c) check for `assert x is True` where the production function returns truthy non-bool values.
41
41
 
42
42
  **N6. Cross-platform scenario gating**
43
43
  - For every test named `_on_windows` / `_on_linux` / `_on_macos`, verify the body gates on `sys.platform`, `monkeypatch.setattr(os, "name", ...)`, or `@pytest.mark.skipif`.
44
44
  - Bare scenario names that run unchanged across platforms claim more than they prove.
45
- - Adversarial probes: (a) does the production function's platform-specific branch get skipped on the CI runner's actual platform; (b) does the test pass against the platform fallback rather than the platform-specific code; (c) is the platform fixture installed and respected by the test runner.
45
+ - Adversarial probes: (a) does the production function's platform-specific branch get skipped on the CI runner's platform; (b) does the test pass against the platform fallback rather than the platform-specific code; (c) is the platform fixture installed and respected by the test runner.
46
46
 
47
47
  **N7. Time / clock scenario gating**
48
48
  - For every test named `_after_<duration>` / `_at_midnight` / `_during_business_hours`, verify the body injects a frozen clock (`freezegun.freeze_time`, `monkeypatch.setattr(time, "time", ...)`, `unittest.mock.patch("datetime.now")`).
@@ -62,8 +62,8 @@ ID prefix: `find`.
62
62
  - For every test, verify the fixture / path / import wiring resolves to the artifact the test name claims.
63
63
  - Path arithmetic: walk every `Path(__file__).parents[k]` chain symbolically and confirm it reaches the directory the assertion expects — a `parents[3]` that stops at `skills/` while the test expects the package root cannot fail for the right reason.
64
64
  - Same-symbol dual imports: `from module import helper` plus `from module import helper as helper_alias` bind two names to the same function object, so any parity assertion between the two bound names is true by construction and proves nothing.
65
- - Fixture file lookups: confirm every `open(Path(__file__).parent / "fixture.txt")` (or equivalent) reaches a file that actually exists in the repo.
66
- - Adversarial probes: (a) re-derive each `parents[k]` index against the real directory depth and flag any off-by-k; (b) check whether two imports in the test resolve to the same object before trusting a cross-name comparison; (c) confirm each referenced fixture path exists on disk at the depth the arithmetic produces.
65
+ - Fixture file lookups: confirm every `open(Path(__file__).parent / "fixture.txt")` (or equivalent) reaches a file that exists in the repo.
66
+ - Adversarial probes: (a) re-derive each `parents[k]` index against the directory depth and flag any off-by-k; (b) check whether two imports in the test resolve to the same object before trusting a cross-name comparison; (c) confirm each referenced fixture path exists on disk at the depth the arithmetic produces.
67
67
 
68
68
  ## Cross-bucket questions to answer at the end
69
69
 
@@ -87,7 +87,7 @@ PR: refactor(hooks): cross-platform path resolution for windows-rmtree-blocker
87
87
  Head SHA: (the commit that landed the platform-conditional logic)
88
88
  ID prefix: `find`.
89
89
 
90
- The PR adds platform-conditional path-resolution logic to `windows_rmtree_blocker.py` and ships 5 new tests named `test_*_on_windows` and `test_*_on_linux` across `test_windows_rmtree_blocker.py`. The audit goal: verify each scenario-named test sets up the named platform via monkeypatch or skipif gate so the production function's platform-specific branch actually runs during the act phase.
90
+ The PR adds platform-conditional path-resolution logic to `windows_rmtree_blocker.py` and ships 5 new tests named `test_*_on_windows` and `test_*_on_linux` across `test_windows_rmtree_blocker.py`. The audit goal: verify each scenario-named test sets up the named platform via monkeypatch or skipif gate so the production function's platform-specific branch runs during the act phase.
91
91
 
92
92
  ## Sub-buckets (each requires Shape A finding OR Shape B with ≥3 adversarial probes)
93
93
 
@@ -128,7 +128,7 @@ The PR adds platform-conditional path-resolution logic to `windows_rmtree_blocke
128
128
 
129
129
  ## Cross-bucket questions to answer at the end
130
130
 
131
- Q1: Five scenario-named tests (F5, F21, F23, F26, F27) do not gate on `sys.platform` and pass against the Linux-fallback branch on the CI runner. The Windows-specific code path has zero actual coverage despite the test names claiming it. Cite `test_windows_rmtree_blocker.py:42` (F5 first test) and `windows_rmtree_blocker.py:67` (the `if sys.platform == "win32":` branch) as the misclaim pair.
131
+ Q1: Five scenario-named tests (F5, F21, F23, F26, F27) do not gate on `sys.platform` and pass against the Linux-fallback branch on the CI runner. The Windows-specific code path has zero coverage despite the test names claiming it. Cite `test_windows_rmtree_blocker.py:42` (F5 first test) and `windows_rmtree_blocker.py:67` (the `if sys.platform == "win32":` branch) as the misclaim pair.
132
132
 
133
133
  Q2: Worst false-coverage signal: F5 — the test's name `test_resolves_path_on_windows` reads as Windows-branch coverage in the PR review, but the act phase exercises the Linux fallback. A reviewer reading the test name during PR review would assume Windows coverage exists; it does not.
134
134
 
@@ -57,7 +57,7 @@ ID prefix: `find`.
57
57
 
58
58
  **O6. Free-form `Args:`-adjacent claims**
59
59
  - Judgment: thick rubric O6 (unions, suppressors, exclusion axis, delegation summaries, Returns/Raises/Note claims, run-mode field meaning). Gate inventory and free-form checklist live in the rubric.
60
- - Adversarial probes: (a) check `Returns:` claims against every `return` statement in the body — is the documented return shape the actual return shape; (b) check `Raises:` claims against every `raise` and propagating callee — is every documented raise reachable; (c) check `Example:` snippets — does the snippet actually compile against the signature.
60
+ - Adversarial probes: (a) check `Returns:` claims against every `return` statement in the body — is the documented return shape the shape the body returns; (b) check `Raises:` claims against every `raise` and propagating callee — is every documented raise reachable; (c) check `Example:` snippets — does the snippet compile against the signature.
61
61
 
62
62
  **O7. Module-doc-vs-split-module after refactor**
63
63
  - Judgment: thick rubric O7.
@@ -89,7 +89,7 @@ Lead: `Total: N (P0=N, P1=N, P2=N)`. For each sub-bucket O1-O9, produce Shape A
89
89
 
90
90
  Audit jl-cmd/claude-dev-env PR #522 for **Category O only** (docstring / fixture-prose vs implementation drift). Skip A-N, P. Sub-bucket forced-exhaustion mode: Category O is decomposed into 9 sub-buckets below.
91
91
 
92
- PR #522 split `pr_description_command_parser.py` into two modules — the original parser and a new `pr_description_pr_number.py` — but the originating module's docstring still claims the PR-number recovery responsibility. A sibling change to `pr_description_body_audit.py` introduced a module docstring whose verb (`detects vague language`) overstates the module's actual responsibility (it only exposes `_extract_vague_scan_text()`; detection runs elsewhere).
92
+ PR #522 split `pr_description_command_parser.py` into two modules — the original parser and a new `pr_description_pr_number.py` — but the originating module's docstring still claims the PR-number recovery responsibility. A sibling change to `pr_description_body_audit.py` introduced a module docstring whose verb (`detects vague language`) overstates the module's responsibility (it only exposes `_extract_vague_scan_text()`; detection runs elsewhere).
93
93
 
94
94
  Expected findings on PR #522:
95
95
  - **O1 finding:** `pr_description_body_audit.py:8` docstring uses verb `detects`, but the only exported symbol prepares input for a regex scan that fires in a different module. Body line(s) showing `_extract_vague_scan_text` returning normalized text without a detection call.
@@ -37,11 +37,11 @@ Two canonical failures to probe for:
37
37
 
38
38
  **P1. Boolean / flag names assert state the body keeps**
39
39
  - For every new `is_*` / `has_*` / `was_*` / `should_*` flag, trace the body. Every set site must be paired with a reset site that fires when the named condition becomes false.
40
- - Adversarial probes: (a) grep every assignment to the flag — count set-true vs set-false; (b) for AST-driven flags, walk the visit method and confirm scope-exit semantics (def visited → flag set; dedent / function-exit → flag reset); (c) construct an input where the flag should be false after a prior true region — does the code agree.
40
+ - Adversarial probes: (a) grep every assignment to the flag — count set-true vs set-false; (b) for AST-driven flags, walk the visit method and confirm scope-exit semantics (def visited → flag set; dedent / function-exit → flag reset); (c) construct an input where the flag should be false after a region that set it true — does the code agree.
41
41
 
42
42
  **P2. Predicate-name breadth matches body coverage**
43
43
  - For every new `_is_*` / `_has_*` / `_can_*` predicate function, list the body's `return True` branches. The named predicate must hold on the union of those branches and nothing else.
44
- - Adversarial probes: (a) construct an input that satisfies the name but returns False — that is a P2 narrowness finding; (b) construct an input that does not satisfy the name but returns True — that is a P2 breadth finding; (c) check neighbor predicates — is the body's actual contract the responsibility of a different helper.
44
+ - Adversarial probes: (a) construct an input that satisfies the name but returns False — that is a P2 narrowness finding; (b) construct an input that does not satisfy the name but returns True — that is a P2 breadth finding; (c) check neighbor predicates — is the body's contract the responsibility of a different helper.
45
45
 
46
46
  **P3. Regex name vs regex shape** ⭐ canonical P case
47
47
  - For every new `*_PATTERN` / `*_REGEX` constant, read the regex source. Confirm the anchors (`^`, `$`, `\b`, lookarounds) match the name's promised shape. `FILE_PATH_PATTERN` implies path shape; the regex must include path-shape anchors (slash count, segment shape, length bounds) enough to reject non-path inputs.
@@ -73,7 +73,7 @@ Q3: Which identifier most clearly shows the body should be renamed (the name is
73
73
 
74
74
  ## Output
75
75
 
76
- Lead: `Total: N (P0=N, P1=N, P2=N)`. For each sub-bucket P1-P7, produce Shape A or Shape B (with ≥3 probes). Each Shape A finding must cite (a) the identifier file:line, (b) one concrete input that shows the name-vs-body gap, and (c) the recommended fix direction (rename vs body-tighten). Cross-bucket Q1-Q3 answers after the per-sub-bucket walk. Adversarial second pass: "assume your first pass missed at least 3 identifiers whose names overstate what the body actually does — find them." Open Questions section for ambiguities. Read-only. No edits, no commits.
76
+ Lead: `Total: N (P0=N, P1=N, P2=N)`. For each sub-bucket P1-P7, produce Shape A or Shape B (with ≥3 probes). Each Shape A finding must cite (a) the identifier file:line, (b) one concrete input that shows the name-vs-body gap, and (c) the recommended fix direction (rename vs body-tighten). Cross-bucket Q1-Q3 answers after the per-sub-bucket walk. Adversarial second pass: "assume your first pass missed at least 3 identifiers whose names overstate what the body does — find them." Open Questions section for ambiguities. Read-only. No edits, no commits.
77
77
 
78
78
  ---
79
79
 
@@ -30,13 +30,13 @@ ID prefix: `find`.
30
30
 
31
31
  **Q2. PR-description claim verification**
32
32
  - Read the PR body. List each falsifiable claim — a dead-code removal, a performance or ceiling number, a "last caller" assertion, a "no remaining X" cleanup claim.
33
- - Verify each claim two ways against the diff and the repo. A dead-code claim gets a reference search across the repo for the named symbol. A ceiling or performance claim gets checked against the actual config value the diff sets and the call sites it touches. Cite the PR-body line and the diff or repo line that confirms or contradicts it.
33
+ - Verify each claim two ways against the diff and the repo. A dead-code claim gets a reference search across the repo for the named symbol. A ceiling or performance claim gets checked against the config value the diff sets and the call sites it touches. Cite the PR-body line and the diff or repo line that confirms or contradicts it.
34
34
  - Adversarial probes when the body looks correct: (a) search the repo for a surviving caller of any symbol the body calls dead; (b) compare each stated number against the config constant the diff sets; (c) check whether a "this finishes the cleanup" claim leaves a matching pattern in an unchanged sibling file.
35
35
 
36
36
  **Q3. Message-vs-guard consistency**
37
37
  - For every log, error, and evidence string the diff adds or edits, read the branch condition that gates it. The message text matches what that guard guarantees: a string asserting an outcome fires only on a branch that reaches the outcome.
38
38
  - Check temporal wording against the lifetime of the state variable the message names: "this run" versus "this round", "is open" versus "was opened", "reached" versus "attempted". Cite the message line and the guard or state-variable line.
39
- - Adversarial probes when messages look aligned: (a) trace each success message back to the return value or flag it claims and confirm the branch cannot reach it with the opposite value; (b) check every "missing" / "not found" error against the exception class the branch actually catches; (c) check every counter-based message against where the counter resets.
39
+ - Adversarial probes when messages look aligned: (a) trace each success message back to the return value or flag it claims and confirm the branch cannot reach it with the opposite value; (b) check every "missing" / "not found" error against the exception class the branch catches; (c) check every counter-based message against where the counter resets.
40
40
 
41
41
  ## Cross-bucket questions to answer at the end
42
42
 
@@ -59,6 +59,7 @@ export const EVER_SHIPPED_SKILL_NAMES = new Set([
59
59
  'pmax',
60
60
  'pmid',
61
61
  'pmin',
62
+ 'plugin-eval-standalone-skill',
62
63
  'post-audit-findings',
63
64
  'pr-cleanup',
64
65
  'pr-consistency-audit',
@@ -41,6 +41,10 @@ test('EVER_SHIPPED_SKILL_NAMES includes the windows scheduled task skill', () =>
41
41
  assert.equal(EVER_SHIPPED_SKILL_NAMES.has('windows-scheduled-task'), true);
42
42
  });
43
43
 
44
+ test('EVER_SHIPPED_SKILL_NAMES includes the plugin eval standalone skill', () => {
45
+ assert.equal(EVER_SHIPPED_SKILL_NAMES.has('plugin-eval-standalone-skill'), true);
46
+ });
47
+
44
48
  test('EVER_SHIPPED_SKILL_NAMES includes the session continuity companion', () => {
45
49
  assert.equal(EVER_SHIPPED_SKILL_NAMES.has('session-continuity'), true);
46
50
  });
@@ -12,6 +12,7 @@ import {
12
12
  mkdirSync,
13
13
  mkdtempSync,
14
14
  readFileSync,
15
+ readdirSync,
15
16
  realpathSync,
16
17
  rmSync,
17
18
  writeFileSync,
@@ -34,38 +35,8 @@ const SHIPPED_SKILL_NAME = 'privacy-hygiene';
34
35
  const ELI5_SKILL_NAME = 'eli5';
35
36
  const E_CODE_REVIEW_SKILL_NAME = 'e-code-review';
36
37
  const REFERENCE_DIRECTORY_NAME = 'reference';
37
- const SHIPPED_AGENT_FILE_NAME = 'clean-coder.md';
38
- const SHIPPED_AGENT_FILE_NAMES = [
39
- SHIPPED_AGENT_FILE_NAME,
40
- 'code-quality-agent.md',
41
- 'pr-description-writer.md',
42
- ];
43
- const CLEAN_CODER_POLICY_REFERENCES = [
44
- [
45
- '<managed-root>/docs/CODE_RULES.md',
46
- 'packages/claude-dev-env/docs/CODE_RULES.md',
47
- ],
48
- [
49
- '<managed-root>/hooks/blocking/code_rules_enforcer.py',
50
- 'packages/claude-dev-env/hooks/blocking/code_rules_enforcer.py',
51
- ],
52
- [
53
- '<managed-root>/rules/code-standards.md',
54
- 'packages/claude-dev-env/rules/code-standards.md',
55
- ],
56
- [
57
- '<managed-root>/rules/windows-filesystem-safe.md',
58
- 'packages/claude-dev-env/rules/windows-filesystem-safe.md',
59
- ],
60
- [
61
- '<managed-root>/rules/gh-cli-conventions.md',
62
- 'packages/claude-dev-env/rules/gh-cli-conventions.md',
63
- ],
64
- [
65
- '<managed-root>/rules/plain-illustrative-docstrings.md',
66
- 'packages/claude-dev-env/rules/plain-illustrative-docstrings.md',
67
- ],
68
- ];
38
+ const SHIPPED_AGENT_FILE_NAME = 'AGENTS.md';
39
+ const RETIRED_AGENT_FILE_NAMES = ['clean-coder.md', 'code-quality-agent.md', 'pr-description-writer.md'];
69
40
  const PERSONAL_SKILL_NAME = 'my-notes';
70
41
  const PREFLIGHT_PROPOSAL_FILE_NAME = 'preflight-proposal.md';
71
42
 
@@ -116,37 +87,48 @@ function assertProposalContractInstallation(installationPaths) {
116
87
  assert.equal(readFileSync(installedContractPath, 'utf8'), readFileSync(sourceContractPath, 'utf8'));
117
88
  }
118
89
 
119
- /**
120
- * @param {string} agentFilePath
121
- * @param {string} layoutName
122
- * @param {string} managedRoot
123
- * @returns {string[]}
124
- */
125
- function cleanCoderPolicyReferenceProblems(agentFilePath, layoutName, managedRoot) {
126
- const agentBody = readFileSync(agentFilePath, 'utf8');
127
- const missingReferences = [];
128
- for (const [installedReference, sourceReference] of CLEAN_CODER_POLICY_REFERENCES) {
129
- const eachReference = layoutName === 'source'
130
- ? sourceReference
131
- : installedReference;
132
- const eachTargetPath = sourceReference.replace('packages/claude-dev-env/', '');
133
- if (!agentBody.includes(eachReference)) {
134
- missingReferences.push(eachReference + ' is absent');
135
- }
136
- const resolvedPath = layoutName === 'source'
137
- ? join(PACKAGE_DIRECTORY, eachTargetPath)
138
- : join(managedRoot, eachTargetPath);
139
- if (!existsSync(resolvedPath)) {
140
- missingReferences.push(layoutName + ': ' + resolvedPath);
141
- }
142
- }
143
- return missingReferences.map((reference) => layoutName + ': ' + reference);
144
- }
145
90
 
146
91
  test('CONTENT_DIRECTORIES omits agents because that tree installs to the agents home', () => {
147
92
  assert.equal(CONTENT_DIRECTORIES.includes(MANAGED_AGENTS_DIRECTORY_NAME), false);
148
93
  });
149
94
 
95
+ test('full install archives recorded retired agents and preserves third-party agents', () => {
96
+ const homeDirectory = mkdtempSync(join(tmpdir(), 'cdev-agent-retirement-'));
97
+ try {
98
+ runInstaller(homeDirectory, []);
99
+ const agentsDirectory = join(homeDirectory, '.agents', 'agents');
100
+ const retiredPath = join(agentsDirectory, 'clean-coder.md');
101
+ const pluginPath = join(agentsDirectory, 'poteto-agent.md');
102
+ const activePath = join(agentsDirectory, 'AGENTS.md');
103
+ const sourcePluginPath = join(PACKAGE_DIRECTORY, '.agents', 'agents', 'poteto-agent.md');
104
+ assert.equal(readFileSync(pluginPath, 'utf8'), readFileSync(sourcePluginPath, 'utf8'));
105
+ writeFileSync(retiredPath, 'recover this retired definition\n');
106
+ writeFileSync(pluginPath, 'preserve this plugin definition\n');
107
+ const manifestPath = join(homeDirectory, '.claude', '.claude-dev-env-manifest.json');
108
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
109
+ manifest.files.push(retiredPath);
110
+ manifest.files.push(activePath);
111
+ writeFileSync(manifestPath, JSON.stringify(manifest));
112
+
113
+ runInstaller(homeDirectory, []);
114
+
115
+ assert.equal(existsSync(retiredPath), false);
116
+ assert.ok(existsSync(activePath));
117
+ const backupRoot = join(homeDirectory, '.claude', '.claude-dev-env-pruned');
118
+ const backupNames = readdirSync(backupRoot, { recursive: true });
119
+ const retiredBackup = backupNames.find(name => name.endsWith('clean-coder.md'));
120
+ assert.ok(retiredBackup);
121
+ assert.equal(readFileSync(join(backupRoot, retiredBackup), 'utf8'), 'recover this retired definition\n');
122
+ assert.equal(readFileSync(pluginPath, 'utf8'), 'preserve this plugin definition\n');
123
+ const updatedManifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
124
+ assert.equal(updatedManifest.files.includes(retiredPath), false);
125
+ runInstaller(homeDirectory, []);
126
+ assert.equal(readFileSync(pluginPath, 'utf8'), 'preserve this plugin definition\n');
127
+ } finally {
128
+ rmSync(homeDirectory, { recursive: true, force: true });
129
+ }
130
+ });
131
+
150
132
  test('a full install writes skills and agents under .agents and points .claude at them', () => {
151
133
  const homeDirectory = mkdtempSync(join(tmpdir(), 'cdev-agents-home-'));
152
134
  const claudeHome = join(homeDirectory, '.claude');
@@ -201,18 +183,6 @@ test('a full install writes skills and agents under .agents and points .claude a
201
183
  readFileSync(lookupAgentFile, 'utf8'),
202
184
  readFileSync(canonicalAgentFile, 'utf8'),
203
185
  );
204
- const sourceAgentFile = join(
205
- resolvePackageManagedDirectory(
206
- PACKAGE_DIRECTORY,
207
- MANAGED_AGENTS_DIRECTORY_NAME,
208
- ),
209
- SHIPPED_AGENT_FILE_NAME,
210
- );
211
- const brokenPolicyReferences = [
212
- ...cleanCoderPolicyReferenceProblems(sourceAgentFile, 'source', claudeHome),
213
- ...cleanCoderPolicyReferenceProblems(canonicalAgentFile, 'installed', claudeHome),
214
- ];
215
- assert.deepEqual(brokenPolicyReferences, [], 'Clean Coder has broken policy references');
216
186
  assert.equal(realpathSync(lookupAgentFile), realpathSync(canonicalAgentFile));
217
187
  assert.equal(
218
188
  lstatSync(skillsInstallDirectory).isSymbolicLink(),
@@ -229,7 +199,7 @@ test('a full install writes skills and agents under .agents and points .claude a
229
199
  }
230
200
  });
231
201
 
232
- test('real installs place the Clean Coder in each active agents home', () => {
202
+ test('installs omit retired agents in each active agents home', () => {
233
203
  const runRoot = mkdtempSync(join(tmpdir(), 'cdev-active-roots-'));
234
204
  const homeDirectory = join(runRoot, 'home');
235
205
  const configRoot = join(runRoot, 'config-profile');
@@ -283,7 +253,7 @@ test('real installs place the Clean Coder in each active agents home', () => {
283
253
  ...eachInstallCase.environment,
284
254
  },
285
255
  );
286
- for (const eachAgentFileName of SHIPPED_AGENT_FILE_NAMES) {
256
+ for (const eachAgentFileName of RETIRED_AGENT_FILE_NAMES) {
287
257
  const installedAgentPath = join(
288
258
  eachInstallCase.agentsHome,
289
259
  MANAGED_AGENTS_DIRECTORY_NAME,
@@ -291,30 +261,10 @@ test('real installs place the Clean Coder in each active agents home', () => {
291
261
  );
292
262
  assert.equal(
293
263
  existsSync(installedAgentPath),
294
- true,
295
- `${eachInstallCase.name}: agent is under the active agents home`,
264
+ false,
265
+ `${eachInstallCase.name}: retired agent is absent`,
296
266
  );
297
- const installedAgentText = readFileSync(installedAgentPath, 'utf8');
298
- assert.match(installedAgentText, /active managed root/i);
299
- assert.match(installedAgentText, /active agents home/i);
300
- assert.match(installedAgentText, /<managed-root>\//);
301
- assert.match(installedAgentText, /<agents-home>\//);
302
267
  }
303
- const installedCleanCoderPath = join(
304
- eachInstallCase.agentsHome,
305
- MANAGED_AGENTS_DIRECTORY_NAME,
306
- SHIPPED_AGENT_FILE_NAME,
307
- );
308
- const policyReferenceProblems = cleanCoderPolicyReferenceProblems(
309
- installedCleanCoderPath,
310
- 'installed',
311
- eachInstallCase.managedRoot,
312
- );
313
- assert.deepEqual(
314
- policyReferenceProblems,
315
- [],
316
- `${eachInstallCase.name}: Clean Coder has broken policy references`,
317
- );
318
268
  assert.equal(
319
269
  isDirectoryPointerTo(
320
270
  join(eachInstallCase.managedRoot, MANAGED_AGENTS_DIRECTORY_NAME),
package/bin/install.mjs CHANGED
@@ -1135,6 +1135,7 @@ function renameCaseOnlyMatchToShippedName(destinationFilePath, entryNamesByDirec
1135
1135
 
1136
1136
  export function copyTree(sourceBase, destBase, options = {}) {
1137
1137
  const excludedFileNames = new Set(options.excludeFileNames ?? []);
1138
+ const preservedFileNames = new Set(options.preserveExistingFileNames ?? []);
1138
1139
  const files = collectFiles(sourceBase).filter(
1139
1140
  sourceFile => !excludedFileNames.has(basename(sourceFile)),
1140
1141
  );
@@ -1144,6 +1145,10 @@ export function copyTree(sourceBase, destBase, options = {}) {
1144
1145
  const relativePath = relative(sourceBase, sourceFile);
1145
1146
  const destFile = join(destBase, relativePath);
1146
1147
  mkdirSync(dirname(destFile), { recursive: true });
1148
+ if (preservedFileNames.has(basename(sourceFile)) && existsSync(destFile)) {
1149
+ stats.paths.push(destFile);
1150
+ continue;
1151
+ }
1147
1152
  const existed = existsSync(destFile);
1148
1153
  renameCaseOnlyMatchToShippedName(destFile, entryNamesByDirectory, options);
1149
1154
  copyFileSync(sourceFile, destFile);
@@ -1223,6 +1228,8 @@ export const POST_FOLDED_HOOK_RELATIVE_PATHS = new Set([
1223
1228
  ]);
1224
1229
 
1225
1230
  export const RETIRED_HOOK_REGISTRATION_RELATIVE_PATHS = new Set([
1231
+ 'observability/test_failure_recorder.py',
1232
+ 'advisory/msys_path_conversion_advisor.py',
1226
1233
  'session/untracked_repo_detector.py',
1227
1234
  'session/gh_pr_author_session_cleanup.py',
1228
1235
  'observability/pr_description_writer_spawn_tracker.py',
@@ -2326,29 +2333,6 @@ function pruneRetiredSkills(installedSkillNames, priorManifestSkills) {
2326
2333
  return movedDirectoryCount;
2327
2334
  }
2328
2335
 
2329
- /**
2330
- * Move every file a prior install wrote under a managed root that this run leaves
2331
- * unwritten into the run's backup root, one call per root.
2332
- *
2333
- * `copyTree` adds and overwrites but never removes, so every managed root carries
2334
- * the same drift the skills root does. One call per root hands the containment
2335
- * guard and the emptied-parent walk the root that owns each file, and each root's
2336
- * content lands under `<backupRoot>/<root-name>/<relative>`, so the recovery point
2337
- * mirrors ~/.claude.
2338
- *
2339
- * Nothing moves unless a prior install recorded it. A user-authored file, a
2340
- * runtime artifact, and a recorded path under no managed root — ~/.claude/CLAUDE.md,
2341
- * settings.json, the manifest itself, and ~/.mypy.ini outside the home — all sit
2342
- * outside every root's diff and stay where they are. ~/.claude/_shared and
2343
- * ~/.claude/skills/_shared are distinct absolute paths, so the `_shared` root call
2344
- * and the skills root call each see their own files and neither sees the other's.
2345
- *
2346
- * @param {string[]|null} priorInstalledFiles Files the prior manifest recorded, or null when unknown.
2347
- * @param {string[]} currentInstalledFiles Every file this run copied.
2348
- * @param {string} backupRoot The run's timestamped backup directory.
2349
- * @returns {{prunedCount: number, skillsPrunedCount: number, failedPaths: string[]}}
2350
- * The summed count, the skills root's own count, and every path whose move failed.
2351
- */
2352
2336
  function pruneStaleFilesAcrossManagedRoots(priorInstalledFiles, currentInstalledFiles, backupRoot) {
2353
2337
  let prunedCount = 0;
2354
2338
  let skillsPrunedCount = 0;
@@ -2366,6 +2350,27 @@ function pruneStaleFilesAcrossManagedRoots(priorInstalledFiles, currentInstalled
2366
2350
  skillsPrunedCount = rootOutcome.prunedCount;
2367
2351
  }
2368
2352
  }
2353
+ for (const rootName of [MANAGED_AGENTS_DIRECTORY_NAME, MANAGED_SKILLS_DIRECTORY_NAME]) {
2354
+ const lookupRoot = join(CLAUDE_HOME, rootName);
2355
+ const canonicalRoot = join(AGENTS_HOME, rootName);
2356
+ const canonicalCurrentFiles = currentInstalledFiles.map(filePath =>
2357
+ isInsideDirectory(filePath, lookupRoot)
2358
+ ? join(canonicalRoot, relative(lookupRoot, filePath))
2359
+ : filePath,
2360
+ );
2361
+ const rootOutcome = pruneStaleInstalledFiles(
2362
+ priorInstalledFiles,
2363
+ canonicalCurrentFiles,
2364
+ canonicalRoot,
2365
+ join(backupRoot, 'agents-home', rootName),
2366
+ { managedHomeDirectory: AGENTS_HOME },
2367
+ );
2368
+ prunedCount += rootOutcome.prunedCount;
2369
+ failedPaths.push(...rootOutcome.failedPaths);
2370
+ if (rootName === MANAGED_SKILLS_DIRECTORY_NAME) {
2371
+ skillsPrunedCount += rootOutcome.prunedCount;
2372
+ }
2373
+ }
2369
2374
  return { prunedCount, skillsPrunedCount, failedPaths };
2370
2375
  }
2371
2376
 
@@ -2655,7 +2660,9 @@ function executeInstallPlanMutations(plan, transactionHelpers) {
2655
2660
  );
2656
2661
  if (!existsSync(agentsSource)) continue;
2657
2662
  const agentsDestination = INSTALL_ROOT_RESOLUTION.agentsLookupDirectory;
2658
- const stats = copyTree(agentsSource, agentsDestination);
2663
+ const stats = copyTree(agentsSource, agentsDestination, {
2664
+ preserveExistingFileNames: ['poteto-agent.md'],
2665
+ });
2659
2666
  if (!summary[MANAGED_AGENTS_DIRECTORY_NAME]) {
2660
2667
  summary[MANAGED_AGENTS_DIRECTORY_NAME] = stats;
2661
2668
  } else {
@@ -1168,6 +1168,8 @@ test('FOLDED_HOOK_RELATIVE_PATHS contains folded hooks plus retired entries', ()
1168
1168
 
1169
1169
  test('retired hook registrations stay managed so reinstall removes them', () => {
1170
1170
  const retiredPaths = new Set([
1171
+ 'observability/test_failure_recorder.py',
1172
+ 'advisory/msys_path_conversion_advisor.py',
1171
1173
  'session/untracked_repo_detector.py',
1172
1174
  'session/gh_pr_author_session_cleanup.py',
1173
1175
  'observability/pr_description_writer_spawn_tracker.py',
@@ -2023,6 +2025,25 @@ test('copyTree copies AGENTS.md with agent definitions', () => {
2023
2025
  });
2024
2026
 
2025
2027
 
2028
+ test('copyTree preserves an existing file named in the preserve set', () => {
2029
+ const sourceRoot = mkdtempSync(join(tmpdir(), 'cdev-copy-preserve-source-'));
2030
+ const destinationRoot = mkdtempSync(join(tmpdir(), 'cdev-copy-preserve-destination-'));
2031
+ try {
2032
+ const sourceFilePath = join(sourceRoot, 'poteto-agent.md');
2033
+ const destinationFilePath = join(destinationRoot, 'poteto-agent.md');
2034
+ writeFileSync(sourceFilePath, 'package wrapper\n');
2035
+ writeFileSync(destinationFilePath, 'user wrapper\n');
2036
+
2037
+ copyTree(sourceRoot, destinationRoot, { preserveExistingFileNames: ['poteto-agent.md'] });
2038
+
2039
+ assert.equal(readFileSync(destinationFilePath, 'utf8'), 'user wrapper\n');
2040
+ } finally {
2041
+ rmSync(sourceRoot, { recursive: true, force: true });
2042
+ rmSync(destinationRoot, { recursive: true, force: true });
2043
+ }
2044
+ });
2045
+
2046
+
2026
2047
  const SHIPPED_README_NAME = 'README.md';
2027
2048
  const INSTALLED_README_NAME = 'Readme.md';
2028
2049
  const RETIRED_HOOK_RELATIVE_PATH = 'blocking/retired_gate.py';
@@ -26,7 +26,7 @@ import { resolvePackageManagedDirectory } from './resolve-package-managed-direct
26
26
 
27
27
  const PACKAGE_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
28
28
  const CANONICAL_SKILL_NAME = 'privacy-hygiene';
29
- const CANONICAL_AGENT_FILE_NAME = 'clean-coder.md';
29
+ const CANONICAL_AGENT_FILE_NAME = 'AGENTS.md';
30
30
 
31
31
  test('prefers the .agents tree when both layouts exist', () => {
32
32
  const sourceRoot = mkdtempSync(join(tmpdir(), 'cdev-src-agents-'));
@@ -25,7 +25,7 @@ function advisorRoute(request, options = {}) {
25
25
  return route(request, {
26
26
  trustedSessionMetadata: {
27
27
  authorized: true,
28
- registeredAgentType: 'session-advisor',
28
+ registeredAgentType: 'team-advisor',
29
29
  },
30
30
  ...options,
31
31
  });
@@ -55,6 +55,13 @@ test('the shipped policy parses and names native model ids', () => {
55
55
  assert.deepEqual(policy.advisorDefault, { model: 'astra', effort: 'medium' });
56
56
  });
57
57
 
58
+ test('poteto plugin aliases use worker routing without advisor privileges', () => {
59
+ for (const agent_type of ['poteto-agent', 'pstack:poteto-agent']) {
60
+ const result = route({ agent_type, model: 'Astra', reasoning_effort: 'high' });
61
+ assert.deepEqual(result.selected, { model: 'gpt-6-astra', effort: 'low' });
62
+ }
63
+ });
64
+
58
65
  test('every approved pair returns its literal native pair', () => {
59
66
  const expected = [
60
67
  [['Luna', 'high'], ['gpt-5.6-luna', 'high']],
@@ -74,7 +81,7 @@ test('every approved pair returns its literal native pair', () => {
74
81
  const advisor = advisorRoute({
75
82
  model: 'Astra',
76
83
  reasoning_effort: 'high',
77
- agent_type: 'session-advisor',
84
+ agent_type: 'team-advisor',
78
85
  });
79
86
  assert.equal(advisor.status, 'remapped');
80
87
  assert.deepEqual(advisor.selected, { model: 'gpt-6-astra', effort: 'high' });
@@ -105,7 +112,7 @@ test('every automatic replacement returns its literal destination', () => {
105
112
 
106
113
  test('advisor and worker Astra restrictions use role-specific replacements', () => {
107
114
  assert.deepEqual(
108
- advisorRoute({ model: 'Astra', reasoning_effort: 'xhigh', agent_type: 'session-advisor' }).selected,
115
+ advisorRoute({ model: 'Astra', reasoning_effort: 'xhigh', agent_type: 'team-advisor' }).selected,
109
116
  { model: 'gpt-6-astra', effort: 'medium' },
110
117
  );
111
118
  assert.deepEqual(
@@ -113,7 +120,7 @@ test('advisor and worker Astra restrictions use role-specific replacements', ()
113
120
  { model: 'gpt-6-astra', effort: 'low' },
114
121
  );
115
122
  assert.deepEqual(
116
- advisorRoute({ model: 'Astra', reasoning_effort: 'high', agent_type: 'session-advisor' }).selected,
123
+ advisorRoute({ model: 'Astra', reasoning_effort: 'high', agent_type: 'team-advisor' }).selected,
117
124
  { model: 'gpt-6-astra', effort: 'high' },
118
125
  );
119
126
  });
@@ -122,7 +129,7 @@ test('registered advisor roles satisfy advisory authorization', () => {
122
129
  const result = advisorRoute({
123
130
  model: 'Astra',
124
131
  reasoning_effort: 'high',
125
- agent_type: 'session-advisor',
132
+ agent_type: 'team-advisor',
126
133
  });
127
134
 
128
135
  assert.deepEqual(result.selected, { model: 'gpt-6-astra', effort: 'high' });
@@ -135,7 +142,7 @@ test('a policy from the first stack revision remains routable', () => {
135
142
  {
136
143
  model: 'Astra',
137
144
  reasoning_effort: 'high',
138
- agent_type: 'session-advisor',
145
+ agent_type: 'team-advisor',
139
146
  },
140
147
  { policy: legacyPolicy },
141
148
  );
@@ -146,7 +153,7 @@ test('an advisor role claim cannot grant advisory authorization', () => {
146
153
  const result = route({
147
154
  model: 'Astra',
148
155
  reasoning_effort: 'high',
149
- agent_type: 'session-advisor',
156
+ agent_type: 'team-advisor',
150
157
  });
151
158
  assert.equal(result.status, 'blocked');
152
159
  assert.equal(result.diagnostic, 'advisor role is not trusted');
@@ -158,7 +165,7 @@ test('Astra Light resolves to Astra Low', () => {
158
165
  });
159
166
 
160
167
  test('missing advisor settings use the policy default', () => {
161
- const result = advisorRoute({ agent_type: 'session-advisor' });
168
+ const result = advisorRoute({ agent_type: 'team-advisor' });
162
169
  assert.deepEqual(result.selected, { model: 'gpt-6-astra', effort: 'medium' });
163
170
  });
164
171
 
@@ -6,7 +6,7 @@ Source: Smart & Molak, *BDD in Action* 2e, Chapter 7.6 — scenario quality patt
6
6
 
7
7
  ## Declarative Focus
8
8
 
9
- Scenarios work best when they name what users want to achieve in the language of the business. Lead with goals and recognizable domain tasks so readers grasp intent at a glance. Reserve step-level or UI detail for places where it truly clarifies behavior.
9
+ Scenarios work best when they name what users want to achieve in the language of the business. Lead with goals and recognizable domain tasks so readers grasp intent at a glance. Reserve step-level or UI detail for places where it clarifies behavior.
10
10
 
11
11
  > "Good scenarios model business behavior, not system interactions." — Smart & Molak §7.6.3
12
12
 
@@ -42,7 +42,7 @@ Then I should be presented with the following hotels:
42
42
 
43
43
  Personas ground scenarios in realistic goals and context. Use light soap-opera personas when you need depth before full UX research: introduce names and roles as needed and deepen them across scenarios.
44
44
 
45
- > Smart & Molak §7.6.5 describe personas as rich, realistic descriptions: each persona captures goals, abilities, and background information that ground the test scenario in a real user context.
45
+ > Smart & Molak §7.6.5 describe personas as rich, realistic descriptions: each persona captures goals, abilities, and background information that ground the test scenario in a user context.
46
46
 
47
47
  Good scenarios name who acts and what they need in plain language.
48
48