wizz-method 1.14.0 → 1.16.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 (148) hide show
  1. package/package.json +3 -2
  2. package/skills-registry.yaml +4 -0
  3. package/src/bmm-skills/3-solutioning/wizz-architecture/scripts/__pycache__/lint_spine.cpython-313.pyc +0 -0
  4. package/src/bmm-skills/3-solutioning/wizz-architecture/scripts/tests/__pycache__/test_lint_spine.cpython-313-pytest-9.1.1.pyc +0 -0
  5. package/src/bmm-skills/4-implementation/wizz-code-review/SKILL.md +3 -5
  6. package/src/bmm-skills/4-implementation/wizz-code-review/customize.toml +71 -0
  7. package/src/bmm-skills/4-implementation/wizz-code-review/references/claims-check.md +14 -0
  8. package/src/bmm-skills/4-implementation/wizz-code-review/references/deletion-check.md +14 -0
  9. package/src/bmm-skills/4-implementation/wizz-code-review/review-prompts/edge-case-hunter.md +110 -0
  10. package/src/bmm-skills/4-implementation/wizz-code-review/review-prompts/verification-gap.md +113 -0
  11. package/src/bmm-skills/4-implementation/wizz-code-review/steps/step-01-gather-context.md +32 -23
  12. package/src/bmm-skills/4-implementation/wizz-code-review/steps/step-02-review.md +10 -13
  13. package/src/bmm-skills/4-implementation/wizz-code-review/steps/step-03-triage.md +16 -22
  14. package/src/bmm-skills/4-implementation/wizz-code-review/steps/step-04-present.md +4 -2
  15. package/src/bmm-skills/4-implementation/wizz-quick-dev/customize.toml +76 -0
  16. package/src/bmm-skills/4-implementation/wizz-quick-dev/references/claims-check.md +14 -0
  17. package/src/bmm-skills/4-implementation/wizz-quick-dev/references/deletion-check.md +14 -0
  18. package/src/bmm-skills/4-implementation/wizz-quick-dev/review-prompts/edge-case-hunter.md +110 -0
  19. package/src/bmm-skills/4-implementation/wizz-quick-dev/review-prompts/verification-gap.md +113 -0
  20. package/src/bmm-skills/4-implementation/wizz-quick-dev/step-04-review.md +38 -16
  21. package/src/bmm-skills/4-implementation/wizz-retrospective/SKILL.md +56 -1488
  22. package/src/bmm-skills/4-implementation/wizz-retrospective/customize.toml +3 -5
  23. package/src/bmm-skills/4-implementation/wizz-retrospective/references/acceptance-verdict.md +55 -0
  24. package/src/bmm-skills/4-implementation/wizz-retrospective/references/aggregate-views.md +17 -0
  25. package/src/bmm-skills/4-implementation/wizz-retrospective/references/evidence-gathering.md +30 -0
  26. package/src/bmm-skills/4-implementation/wizz-retrospective/references/retro-document.md +84 -0
  27. package/src/bmm-skills/4-implementation/wizz-retrospective/references/team-discussion.md +22 -0
  28. package/src/bmm-skills/4-implementation/wizz-retrospective/scripts/__pycache__/sprint_status.cpython-313.pyc +0 -0
  29. package/src/bmm-skills/4-implementation/wizz-retrospective/scripts/git_evidence.py +304 -0
  30. package/src/bmm-skills/4-implementation/wizz-retrospective/scripts/sprint_status.py +746 -0
  31. package/src/bmm-skills/4-implementation/wizz-retrospective/scripts/tests/__pycache__/test_git_evidence.cpython-313-pytest-9.1.1.pyc +0 -0
  32. package/src/bmm-skills/4-implementation/wizz-retrospective/scripts/tests/__pycache__/test_sprint_status.cpython-313-pytest-9.1.1.pyc +0 -0
  33. package/src/bmm-skills/4-implementation/wizz-retrospective/scripts/tests/fixtures/sprint-status-template.yaml +71 -0
  34. package/src/bmm-skills/4-implementation/wizz-retrospective/scripts/tests/test_git_evidence.py +750 -0
  35. package/src/bmm-skills/4-implementation/wizz-retrospective/scripts/tests/test_sprint_status.py +1579 -0
  36. package/src/bmm-skills/4-implementation/wizz-sprint-planning/SKILL.md +39 -296
  37. package/src/bmm-skills/4-implementation/wizz-sprint-planning/customize.toml +1 -3
  38. package/src/bmm-skills/4-implementation/wizz-sprint-planning/references/fix-sprint-status.md +30 -0
  39. package/src/bmm-skills/4-implementation/wizz-sprint-planning/references/generate-tracking.md +25 -0
  40. package/src/bmm-skills/4-implementation/wizz-sprint-planning/references/readiness-gate.md +20 -0
  41. package/src/bmm-skills/4-implementation/wizz-sprint-planning/references/status-view.md +14 -0
  42. package/src/bmm-skills/4-implementation/wizz-sprint-planning/references/validate.md +10 -0
  43. package/src/bmm-skills/4-implementation/wizz-sprint-planning/scripts/__pycache__/sprint_plan.cpython-313.pyc +0 -0
  44. package/src/bmm-skills/4-implementation/wizz-sprint-planning/scripts/sprint_plan.py +697 -0
  45. package/src/bmm-skills/4-implementation/wizz-sprint-planning/scripts/tests/__pycache__/test_sprint_plan.cpython-313-pytest-9.1.1.pyc +0 -0
  46. package/src/bmm-skills/4-implementation/wizz-sprint-planning/scripts/tests/test_sprint_plan.py +524 -0
  47. package/src/bmm-skills/4-implementation/wizz-sprint-planning/sprint-status-template.yaml +9 -7
  48. package/src/bmm-skills/module-help.csv +3 -3
  49. package/src/core-skills/_shared/handoff-protocol.md +7 -8
  50. package/src/core-skills/module-help.csv +1 -0
  51. package/src/core-skills/wizz-advanced-elicitation/SKILL.md +36 -114
  52. package/src/core-skills/wizz-advanced-elicitation/{methods.csv → assets/methods.csv} +29 -27
  53. package/src/core-skills/wizz-advanced-elicitation/customize.toml +54 -0
  54. package/src/core-skills/wizz-advanced-elicitation/scripts/__pycache__/pick_methods.cpython-313.pyc +0 -0
  55. package/src/core-skills/wizz-advanced-elicitation/scripts/pick_methods.py +233 -0
  56. package/src/core-skills/wizz-advanced-elicitation/scripts/tests/__pycache__/test_pick_methods.cpython-313-pytest-9.1.1.pyc +0 -0
  57. package/src/core-skills/wizz-advanced-elicitation/scripts/tests/test_pick_methods.py +228 -0
  58. package/src/core-skills/wizz-brainstorming/SKILL.md +2 -2
  59. package/src/core-skills/wizz-brainstorming/assets/brain-selector.html +2 -0
  60. package/src/core-skills/wizz-brainstorming/references/converge.md +1 -1
  61. package/src/core-skills/wizz-brainstorming/references/finalize.md +1 -1
  62. package/src/core-skills/wizz-brainstorming/references/headless.md +1 -1
  63. package/src/core-skills/wizz-brainstorming/references/mode-autonomous.md +1 -1
  64. package/src/core-skills/wizz-brainstorming/scripts/__pycache__/brain.cpython-313.pyc +0 -0
  65. package/src/core-skills/wizz-brainstorming/scripts/brain.py +36 -6
  66. package/src/core-skills/wizz-brainstorming/scripts/tests/__pycache__/test_brain.cpython-313-pytest-9.1.1.pyc +0 -0
  67. package/src/core-skills/wizz-brainstorming/scripts/tests/__pycache__/test_brain.cpython-314.pyc +0 -0
  68. package/src/core-skills/wizz-brainstorming/scripts/tests/test_brain.py +24 -2
  69. package/src/core-skills/wizz-customize/scripts/__pycache__/list_customizable_skills.cpython-313.pyc +0 -0
  70. package/src/core-skills/wizz-customize/scripts/tests/__pycache__/test_list_customizable_skills.cpython-313-pytest-9.1.1.pyc +0 -0
  71. package/src/core-skills/wizz-forge-idea/SKILL.md +107 -0
  72. package/src/core-skills/wizz-forge-idea/customize.toml +41 -0
  73. package/src/core-skills/wizz-forge-idea/scripts/__pycache__/resolve_personas.cpython-313.pyc +0 -0
  74. package/src/core-skills/wizz-forge-idea/scripts/__pycache__/resolve_personas.cpython-314.pyc +0 -0
  75. package/src/core-skills/wizz-forge-idea/scripts/resolve_personas.py +275 -0
  76. package/src/core-skills/wizz-forge-idea/scripts/tests/__pycache__/test_resolve_personas.cpython-313-pytest-9.1.1.pyc +0 -0
  77. package/src/core-skills/wizz-forge-idea/scripts/tests/__pycache__/test_resolve_personas.cpython-314.pyc +0 -0
  78. package/src/core-skills/wizz-forge-idea/scripts/tests/test_resolve_personas.py +138 -0
  79. package/src/core-skills/wizz-party-mode/SKILL.md +37 -54
  80. package/src/core-skills/wizz-party-mode/customize.toml +61 -2
  81. package/src/core-skills/wizz-party-mode/references/create-party.md +8 -3
  82. package/src/core-skills/wizz-party-mode/references/mode-agent-team.md +3 -1
  83. package/src/core-skills/wizz-party-mode/references/mode-subagent.md +16 -4
  84. package/src/core-skills/wizz-party-mode/references/party-memory.md +51 -0
  85. package/src/core-skills/wizz-party-mode/scripts/__pycache__/resolve_party.cpython-313.pyc +0 -0
  86. package/src/core-skills/wizz-party-mode/scripts/__pycache__/resolve_party.cpython-314.pyc +0 -0
  87. package/src/core-skills/wizz-party-mode/scripts/resolve_party.py +22 -7
  88. package/src/core-skills/wizz-party-mode/scripts/tests/__pycache__/test_resolve_party.cpython-313-pytest-9.1.1.pyc +0 -0
  89. package/src/core-skills/wizz-party-mode/scripts/tests/__pycache__/test_resolve_party.cpython-314.pyc +0 -0
  90. package/src/core-skills/wizz-party-mode/scripts/tests/{test-resolve_party.py → test_resolve_party.py} +8 -0
  91. package/src/core-skills/wizz-review-edge-case-hunter/SKILL.md +17 -3
  92. package/src/modules/wizz/README.md +1 -1
  93. package/src/modules/wizz/_shared/model-ladder.md +22 -0
  94. package/src/modules/wizz/_shared/token-economy.md +6 -2
  95. package/src/modules/wizz/agents/wizz-ads/customize.toml +1 -1
  96. package/src/modules/wizz/agents/wizz-copy/customize.toml +1 -1
  97. package/src/modules/wizz/agents/wizz-designer/customize.toml +1 -1
  98. package/src/modules/wizz/agents/wizz-growth/customize.toml +11 -1
  99. package/src/modules/wizz/agents/wizz-maestro/customize.toml +1 -1
  100. package/src/modules/wizz/agents/wizz-memoria/customize.toml +1 -1
  101. package/src/modules/wizz/agents/wizz-qa/customize.toml +1 -1
  102. package/src/modules/wizz/agents/wizz-seo/customize.toml +1 -1
  103. package/src/modules/wizz/agents/wizz-social/customize.toml +1 -1
  104. package/src/modules/wizz/overrides/wizz-agent-analyst.toml +1 -1
  105. package/src/modules/wizz/overrides/wizz-agent-architect.toml +1 -1
  106. package/src/modules/wizz/overrides/wizz-agent-dev.toml +1 -1
  107. package/src/modules/wizz/overrides/wizz-agent-pm.toml +1 -1
  108. package/src/modules/wizz/overrides/wizz-agent-tech-writer.toml +1 -1
  109. package/src/modules/wizz/overrides/wizz-agent-ux-designer.toml +1 -1
  110. package/src/modules/wizz/subagents/codex/wizz-exec-haiku.toml +16 -0
  111. package/src/modules/wizz/subagents/codex/wizz-exec-opus.toml +18 -0
  112. package/src/modules/wizz/subagents/codex/wizz-exec-review.toml +17 -0
  113. package/src/modules/wizz/subagents/codex/wizz-exec-sonnet.toml +17 -0
  114. package/src/modules/wizz/subagents/gemini/wizz-exec-haiku.md +15 -0
  115. package/src/modules/wizz/subagents/gemini/wizz-exec-opus.md +17 -0
  116. package/src/modules/wizz/subagents/gemini/wizz-exec-review.md +15 -0
  117. package/src/modules/wizz/subagents/gemini/wizz-exec-sonnet.md +16 -0
  118. package/src/modules/wizz/subagents/opencode/wizz-exec-haiku.md +14 -0
  119. package/src/modules/wizz/subagents/opencode/wizz-exec-opus.md +16 -0
  120. package/src/modules/wizz/subagents/opencode/wizz-exec-review.md +16 -0
  121. package/src/modules/wizz/subagents/opencode/wizz-exec-sonnet.md +15 -0
  122. package/src/modules/wizz/subagents/wizz-exec-opus.md +16 -0
  123. package/src/modules/wizz/subagents/wizz-exec-review.md +15 -0
  124. package/src/scripts/__pycache__/memlog.cpython-313.pyc +0 -0
  125. package/src/scripts/tests/__pycache__/test_memlog.cpython-313-pytest-9.1.1.pyc +0 -0
  126. package/src/scripts/tests/__pycache__/test_resolve_customization.cpython-313-pytest-9.1.1.pyc +0 -0
  127. package/src/skills-lib/pesquisa-de-publico-do-piva/README.md +83 -0
  128. package/src/skills-lib/pesquisa-de-publico-do-piva/SKILL.md +65 -0
  129. package/src/skills-lib/pesquisa-de-publico-do-piva/assets/template.html +414 -0
  130. package/src/skills-lib/pesquisa-de-publico-do-piva/prompts.md +131 -0
  131. package/src/skills-lib/wizz-offer-forge/README.md +13 -0
  132. package/src/skills-lib/wizz-offer-forge/SKILL.md +74 -0
  133. package/src/skills-lib/wizz-offer-forge/references/empilhamento-valor.md +25 -0
  134. package/src/skills-lib/wizz-offer-forge/references/garantia-reversa.md +22 -0
  135. package/src/skills-lib/wizz-offer-forge/references/mecanismo-unico.md +28 -0
  136. package/src/skills-lib/wizz-offer-forge/references/ponte-trafego.md +21 -0
  137. package/src/skills-lib/wizz-router/references/routing-table-flat.md +2 -0
  138. package/tools/installer/core/installer.js +57 -0
  139. package/tools/installer/ide/_config-driven.js +44 -30
  140. package/tools/installer/ide/platform-codes.yaml +42 -3
  141. package/tools/installer/modules/external-manager.js +140 -2
  142. package/tools/installer/modules/official-modules.js +68 -21
  143. package/tools/installer/prompts.js +45 -103
  144. package/tools/installer/ui.js +64 -7
  145. package/wizz-modules.yaml +65 -11
  146. package/src/bmm-skills/4-implementation/wizz-sprint-planning/checklist.md +0 -34
  147. package/src/core-skills/wizz-brainstorming/scripts/memlog.py +0 -202
  148. package/src/core-skills/wizz-brainstorming/scripts/tests/test_memlog.py +0 -265
@@ -0,0 +1,275 @@
1
+ #!/usr/bin/env python3
2
+ # /// script
3
+ # requires-python = ">=3.11"
4
+ # ///
5
+ """Resolve the personas and parties the forge can bring into the room.
6
+
7
+ The forge cross-examines witnesses: the installed Wizz agents, plus any
8
+ custom personas and party groups the user has authored for `wizz-party-mode`.
9
+ This surfaces all of them in one shot so the orchestrator never has to ask
10
+ "who's available?" — it just intermixes whoever fits the branch, alongside
11
+ any persona the user names on the fly.
12
+
13
+ What it returns (JSON, stdout):
14
+ * agents — the installed Wizz roster: the default room, always present.
15
+ * members — extra custom personas in the pool (party_members the user
16
+ defined that aren't already an installed slot).
17
+ * parties — the user's named party groups, members resolved to brief
18
+ entries; open-cast groups (scene names a pool, no roster)
19
+ are flagged.
20
+ * default_party — the group id pinned as party-mode's default, if any.
21
+
22
+ Discovery is best-effort and never blocks the forge. The installed roster
23
+ comes from the core resolver; custom personas/parties come from
24
+ `wizz-party-mode`'s resolved customization when that skill is found beside
25
+ this one, else from the user's override TOMLs read directly. Anything that
26
+ can't be resolved is simply omitted and flagged, never fatal.
27
+
28
+ Stdlib only (Python 3.11+ for tomllib).
29
+
30
+ resolve_personas.py --project-root P --skill S
31
+ """
32
+
33
+ import argparse
34
+ import json
35
+ import subprocess
36
+ import sys
37
+ from pathlib import Path
38
+
39
+ try:
40
+ import tomllib
41
+ except ImportError: # pragma: no cover - guarded for <3.11
42
+ sys.stderr.write("error: Python 3.11+ is required (stdlib `tomllib`).\n")
43
+ sys.exit(3)
44
+
45
+ PARTY_SKILL = "wizz-party-mode"
46
+
47
+
48
+ def _run_json(cmd):
49
+ """Run a resolver script and parse its JSON stdout. None on any failure."""
50
+ try:
51
+ out = subprocess.run(
52
+ cmd, capture_output=True, text=True, encoding="utf-8", timeout=60
53
+ )
54
+ except (OSError, subprocess.SubprocessError):
55
+ return None
56
+ if out.returncode != 0 or not out.stdout.strip():
57
+ return None
58
+ try:
59
+ return json.loads(out.stdout)
60
+ except json.JSONDecodeError:
61
+ return None
62
+
63
+
64
+ def _load_toml(path: Path):
65
+ if not path.exists():
66
+ return {}
67
+ try:
68
+ with path.open("rb") as f:
69
+ data = tomllib.load(f)
70
+ return data if isinstance(data, dict) else {}
71
+ except (OSError, tomllib.TOMLDecodeError):
72
+ return {}
73
+
74
+
75
+ def load_agents(project_root: Path):
76
+ """Installed Wizz agents as {code: entry}. (dict, resolved_ok).
77
+
78
+ The core resolver may emit agents as a dict keyed by code or as an array
79
+ of tables (depending on how the layers merged); normalize both to a dict.
80
+ """
81
+ script = project_root / "_wizz" / "scripts" / "resolve_config.py"
82
+ data = _run_json([sys.executable, str(script), "--project-root", str(project_root), "--key", "agents"])
83
+ if data is None:
84
+ return {}, False
85
+ agents = data.get("agents", {}) or {}
86
+ if isinstance(agents, list):
87
+ agents = {a["code"]: a for a in agents if isinstance(a, dict) and a.get("code")}
88
+ elif not isinstance(agents, dict):
89
+ agents = {}
90
+ return agents, True
91
+
92
+
93
+ def find_party_skill(project_root: Path, skill_root: Path):
94
+ """Locate the installed wizz-party-mode skill dir, or None.
95
+
96
+ Skills install as siblings, so the party skill is almost always next to
97
+ this one. A couple of common install roots cover the rest.
98
+ """
99
+ candidates = [
100
+ skill_root.parent / PARTY_SKILL,
101
+ project_root / ".claude" / "skills" / PARTY_SKILL,
102
+ project_root / "_wizz" / "skills" / PARTY_SKILL,
103
+ ]
104
+ for c in candidates:
105
+ if (c / "customize.toml").exists():
106
+ return c
107
+ return None
108
+
109
+
110
+ def load_party_workflow(project_root: Path, party_skill: Path):
111
+ """Merged [workflow] table for wizz-party-mode (base + user overrides)."""
112
+ resolver = project_root / "_wizz" / "scripts" / "resolve_customization.py"
113
+ data = _run_json([sys.executable, str(resolver), "--skill", str(party_skill), "--key", "workflow"])
114
+ if data is not None and isinstance(data.get("workflow"), dict):
115
+ return data["workflow"]
116
+ # Fallback: base customize.toml directly, no override merge.
117
+ wf = _load_toml(party_skill / "customize.toml").get("workflow", {})
118
+ return wf if isinstance(wf, dict) else {}
119
+
120
+
121
+ def load_party_overrides(project_root: Path):
122
+ """Custom personas/parties when party-mode itself isn't installed.
123
+
124
+ Reads only the user's override TOMLs (team then personal, personal wins on
125
+ scalars). No base roster exists in this path, so a shallow merge is enough.
126
+ """
127
+ custom = project_root / "_wizz" / "custom"
128
+ team = _load_toml(custom / f"{PARTY_SKILL}.toml").get("workflow", {})
129
+ user = _load_toml(custom / f"{PARTY_SKILL}.user.toml").get("workflow", {})
130
+ team = team if isinstance(team, dict) else {}
131
+ user = user if isinstance(user, dict) else {}
132
+ merged = dict(team)
133
+ for key, val in user.items():
134
+ if isinstance(val, list) and isinstance(merged.get(key), list):
135
+ merged[key] = merged[key] + val
136
+ else:
137
+ merged[key] = val
138
+ return merged
139
+
140
+
141
+ def _alias(code: str) -> str:
142
+ """Short alias for an installed agent code: wizz-agent-analyst -> analyst."""
143
+ for prefix in ("wizz-agent-", "wizz-"):
144
+ if code.startswith(prefix):
145
+ return code[len(prefix):]
146
+ return code
147
+
148
+
149
+ def build_pool(agents: dict, party_members: list):
150
+ """One pool keyed by code; custom members override matching installed slots.
151
+
152
+ Returns (pool, index, installed_codes, custom_codes):
153
+ * installed_codes — the default room (installed agents, overrides applied
154
+ in place); custom-only additions stay in the pool but don't crowd it.
155
+ * custom_codes — pure-custom personas (no installed slot), the extra
156
+ faces the forge can summon by name or via a party group.
157
+ """
158
+ pool, index, installed_codes, custom_codes = {}, {}, [], []
159
+
160
+ def register(code, entry):
161
+ pool[code] = entry
162
+ index[code] = code
163
+ index[code.lower()] = code
164
+ index[_alias(code).lower()] = code
165
+ name = entry.get("name")
166
+ if name:
167
+ key = name.lower()
168
+ # A custom rename must not hijack another agent's name lookup.
169
+ if index.get(key, code) == code:
170
+ index[key] = code
171
+
172
+ for code, info in (agents or {}).items():
173
+ register(code, {
174
+ "code": code,
175
+ "name": info.get("name", code),
176
+ "icon": info.get("icon", ""),
177
+ "title": info.get("title", ""),
178
+ "description": info.get("description", ""),
179
+ "source": "installed",
180
+ })
181
+ installed_codes.append(code)
182
+
183
+ for m in (party_members if isinstance(party_members, list) else []):
184
+ if not isinstance(m, dict):
185
+ continue
186
+ code = m.get("code")
187
+ if not code:
188
+ continue
189
+ canonical = index.get(code) or index.get(code.lower()) or code
190
+ was_installed = canonical in pool
191
+ # Start from the installed entry so fields the override omits
192
+ # (icon, title, description) survive.
193
+ entry = dict(pool.get(canonical, {}))
194
+ entry.update({"code": canonical, "source": "custom"})
195
+ for field in ("name", "icon", "title", "persona", "capabilities", "model"):
196
+ if m.get(field) is not None:
197
+ entry[field] = m[field]
198
+ entry.setdefault("name", canonical)
199
+ register(canonical, entry)
200
+ if not was_installed:
201
+ custom_codes.append(canonical)
202
+
203
+ return pool, index, installed_codes, custom_codes
204
+
205
+
206
+ def _brief(entry):
207
+ """The slim card the orchestrator needs to cast a persona."""
208
+ out = {k: entry[k] for k in ("code", "name", "icon", "title", "source") if entry.get(k)}
209
+ for k in ("description", "persona", "capabilities", "model"):
210
+ if entry.get(k):
211
+ out[k] = entry[k]
212
+ return out
213
+
214
+
215
+ def resolve_parties(groups, pool, index):
216
+ out = []
217
+ for g in groups or []:
218
+ if not isinstance(g, dict) or not g.get("id"):
219
+ continue
220
+ raw = g.get("members", []) or []
221
+ members = []
222
+ for t in raw:
223
+ key = t if isinstance(t, str) else str(t)
224
+ code = index.get(key) or index.get(key.lower())
225
+ if code in pool:
226
+ members.append(_brief(pool[code]))
227
+ party = {"id": g["id"], "name": g.get("name", g["id"]), "members": members}
228
+ if g.get("scene"):
229
+ party["scene"] = g["scene"]
230
+ if not raw:
231
+ party["open_cast"] = True
232
+ out.append(party)
233
+ return out
234
+
235
+
236
+ def main():
237
+ ap = argparse.ArgumentParser(description="Resolve forge personas and parties.")
238
+ ap.add_argument("--project-root", required=True)
239
+ ap.add_argument("--skill", required=True, help="Path to the wizz-forge-idea skill dir")
240
+ args = ap.parse_args()
241
+
242
+ project_root = Path(args.project_root).resolve()
243
+ skill_root = Path(args.skill).resolve()
244
+
245
+ agents, agents_ok = load_agents(project_root)
246
+
247
+ party_skill = find_party_skill(project_root, skill_root)
248
+ if party_skill is not None:
249
+ workflow = load_party_workflow(project_root, party_skill)
250
+ else:
251
+ workflow = load_party_overrides(project_root)
252
+
253
+ pool, index, installed_codes, custom_codes = build_pool(
254
+ agents, workflow.get("party_members", []))
255
+ parties = resolve_parties(workflow.get("party_groups", []), pool, index)
256
+
257
+ _emit({
258
+ "agents": [_brief(pool[c]) for c in installed_codes],
259
+ "members": [_brief(pool[c]) for c in custom_codes],
260
+ "parties": parties,
261
+ "default_party": workflow.get("default_party", "") or "",
262
+ "party_mode_found": party_skill is not None,
263
+ "agents_resolved": agents_ok,
264
+ })
265
+
266
+
267
+ def _emit(obj):
268
+ reconfigure = getattr(sys.stdout, "reconfigure", None)
269
+ if reconfigure is not None:
270
+ reconfigure(encoding="utf-8")
271
+ sys.stdout.write(json.dumps(obj, indent=2, ensure_ascii=False) + "\n")
272
+
273
+
274
+ if __name__ == "__main__":
275
+ main()
@@ -0,0 +1,138 @@
1
+ #!/usr/bin/env python3
2
+ # /// script
3
+ # requires-python = ">=3.11"
4
+ # ///
5
+ """Unit tests for resolve_personas.py — pool merge, alias, party resolution."""
6
+
7
+ import sys
8
+ import unittest
9
+ from pathlib import Path
10
+
11
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
12
+ import resolve_personas as rp # noqa: E402
13
+
14
+ AGENTS = {
15
+ "wizz-agent-analyst": {"name": "Mary", "icon": "📊", "title": "Analyst"},
16
+ "wizz-agent-pm": {"name": "John", "icon": "📋", "title": "PM"},
17
+ }
18
+
19
+
20
+ class TestAlias(unittest.TestCase):
21
+ def test_strips_known_prefixes(self):
22
+ self.assertEqual(rp._alias("wizz-agent-analyst"), "analyst")
23
+ self.assertEqual(rp._alias("wizz-foo"), "foo")
24
+
25
+ def test_passes_through_unprefixed(self):
26
+ self.assertEqual(rp._alias("morpheus"), "morpheus")
27
+
28
+
29
+ class TestBuildPool(unittest.TestCase):
30
+ def test_installed_become_default_room_indexed_every_way(self):
31
+ pool, idx, installed, custom = rp.build_pool(AGENTS, [])
32
+ self.assertEqual(installed, ["wizz-agent-analyst", "wizz-agent-pm"])
33
+ self.assertEqual(custom, [])
34
+ self.assertEqual(idx["analyst"], "wizz-agent-analyst") # alias
35
+ self.assertEqual(idx["mary"], "wizz-agent-analyst") # name (ci)
36
+ self.assertEqual(pool["wizz-agent-analyst"]["source"], "installed")
37
+
38
+ def test_pure_custom_member_stays_out_of_default_room(self):
39
+ pool, _, installed, custom = rp.build_pool(
40
+ AGENTS, [{"code": "morpheus", "name": "Morpheus", "persona": "riddles"}])
41
+ self.assertEqual(custom, ["morpheus"])
42
+ self.assertNotIn("morpheus", installed)
43
+ self.assertEqual(pool["morpheus"]["persona"], "riddles")
44
+
45
+ def test_custom_override_lands_on_installed_slot_not_a_new_face(self):
46
+ pool, _, installed, custom = rp.build_pool(
47
+ AGENTS, [{"code": "analyst", "name": "Mary-Custom", "persona": "p"}])
48
+ self.assertNotIn("analyst", pool)
49
+ self.assertEqual(custom, []) # an override is not a new face
50
+ self.assertEqual(pool["wizz-agent-analyst"]["source"], "custom")
51
+ self.assertEqual(pool["wizz-agent-analyst"]["name"], "Mary-Custom")
52
+
53
+ def test_member_without_code_skipped(self):
54
+ pool, _, _, custom = rp.build_pool(AGENTS, [{"name": "Nameless"}])
55
+ self.assertEqual(custom, [])
56
+ self.assertEqual(set(pool), {"wizz-agent-analyst", "wizz-agent-pm"})
57
+
58
+ def test_custom_rename_does_not_hijack_another_agents_name(self):
59
+ # Override the analyst slot, renaming it to "John" — the PM's name.
60
+ # The PM's name lookup must survive (last-writer-wins would corrupt it).
61
+ _, idx, _, _ = rp.build_pool(AGENTS, [{"code": "analyst", "name": "John"}])
62
+ self.assertEqual(idx["john"], "wizz-agent-pm")
63
+
64
+ def test_brief_carries_model_and_capabilities(self):
65
+ pool, _, _, _ = rp.build_pool(
66
+ AGENTS, [{"code": "neo", "name": "Neo", "model": "opus", "capabilities": ["x"]}])
67
+ brief = rp._brief(pool["neo"])
68
+ self.assertEqual(brief["model"], "opus")
69
+ self.assertEqual(brief["capabilities"], ["x"])
70
+
71
+ def test_non_list_party_members_is_safe(self):
72
+ pool, _, installed, custom = rp.build_pool(AGENTS, "not-a-list")
73
+ self.assertEqual(custom, [])
74
+ self.assertEqual(set(pool), {"wizz-agent-analyst", "wizz-agent-pm"})
75
+
76
+
77
+ class TestResolveParties(unittest.TestCase):
78
+ def setUp(self):
79
+ self.pool, self.idx, _, _ = rp.build_pool(
80
+ AGENTS, [{"code": "shark", "name": "Marcus", "title": "CFO"}])
81
+
82
+ def test_resolves_members_by_alias_and_custom_code(self):
83
+ parties = rp.resolve_parties(
84
+ [{"id": "tank", "name": "Tank", "scene": "hostile",
85
+ "members": ["shark", "analyst"]}], self.pool, self.idx)
86
+ self.assertEqual(len(parties), 1)
87
+ self.assertEqual([m["name"] for m in parties[0]["members"]], ["Marcus", "Mary"])
88
+ self.assertEqual(parties[0]["scene"], "hostile")
89
+
90
+ def test_unknown_member_dropped_silently(self):
91
+ parties = rp.resolve_parties(
92
+ [{"id": "g", "members": ["analyst", "ghost"]}], self.pool, self.idx)
93
+ self.assertEqual([m["name"] for m in parties[0]["members"]], ["Mary"])
94
+
95
+ def test_member_resolution_is_case_insensitive(self):
96
+ # A TOML author naturally writes "Analyst"/"Shark"; the filter accepts
97
+ # them via the lowercase index, so resolution must too (no KeyError).
98
+ parties = rp.resolve_parties(
99
+ [{"id": "g", "members": ["Analyst", "Shark"]}], self.pool, self.idx)
100
+ self.assertEqual([m["name"] for m in parties[0]["members"]], ["Mary", "Marcus"])
101
+
102
+ def test_non_string_member_does_not_crash(self):
103
+ # Malformed members (int, list) must drop silently, never raise.
104
+ parties = rp.resolve_parties(
105
+ [{"id": "g", "members": [123, ["x"], "analyst"]}], self.pool, self.idx)
106
+ self.assertEqual([m["name"] for m in parties[0]["members"]], ["Mary"])
107
+
108
+ def test_open_cast_group_flagged(self):
109
+ parties = rp.resolve_parties(
110
+ [{"id": "rebels", "name": "Rebels", "scene": "the Ghost"}], self.pool, self.idx)
111
+ self.assertTrue(parties[0]["open_cast"])
112
+ self.assertEqual(parties[0]["members"], [])
113
+
114
+ def test_group_without_id_skipped(self):
115
+ self.assertEqual(rp.resolve_parties([{"name": "no id"}], self.pool, self.idx), [])
116
+
117
+
118
+ class TestOverrideMergeFallback(unittest.TestCase):
119
+ """When party-mode isn't installed, user override TOMLs are read directly."""
120
+
121
+ def test_arrays_append_scalars_override(self):
122
+ import tempfile, os
123
+ with tempfile.TemporaryDirectory() as d:
124
+ custom = Path(d) / "_wizz" / "custom"
125
+ custom.mkdir(parents=True)
126
+ (custom / "wizz-party-mode.toml").write_text(
127
+ '[workflow]\ndefault_party = "a"\n'
128
+ '[[workflow.party_members]]\ncode = "x"\nname = "X"\n')
129
+ (custom / "wizz-party-mode.user.toml").write_text(
130
+ '[workflow]\ndefault_party = "b"\n'
131
+ '[[workflow.party_members]]\ncode = "y"\nname = "Y"\n')
132
+ wf = rp.load_party_overrides(Path(d))
133
+ self.assertEqual(wf["default_party"], "b") # personal wins
134
+ self.assertEqual([m["code"] for m in wf["party_members"]], ["x", "y"]) # appended
135
+
136
+
137
+ if __name__ == "__main__":
138
+ unittest.main()
@@ -1,79 +1,62 @@
1
1
  ---
2
2
  name: wizz-party-mode
3
- description: 'Orchestrates lively group discussions between installed WIZZ agents or custom personas, and helps author custom parties. Use when the user requests party mode, a roundtable, or multiple agent perspectives — or wants to create/configure a party, define personas, or build an AI focus-group panel.'
3
+ description: 'Orchestrates lively group discussions between installed WIZZ agents or custom personas, and helps author custom parties. Use when the user requests party mode, a roundtable, or multiple agent perspectives — or wants to create/configure a party, define personas, or build an AI focus-group panel'
4
4
  ---
5
5
 
6
6
  # Party Mode
7
7
 
8
- Run a round-table where WIZZ agents talk to each other, and to the user, like a real group of distinct people in conversation. Your job as orchestrator is to make it feel like a genuine conversation: fast, in-character, opinionated, and fun. Everything below is an objective, not a script. Use whatever mechanism your model and harness make available to hit it.
8
+ Run a round-table where these agents talk to each other and to the user like real, distinct people in conversation. You're the orchestrator.
9
9
 
10
- **Two intents.** Usually the user wants to *run* a party — that's everything below. If instead they want to *create or configure* one — invent a cast, add a persona, distill customer data into a focus-group panel, set a default, or **edit an existing custom party** (retune a member, add someone to a group) — load `references/create-party.md` and follow it. Detect which from how they invoke the skill; when it's unclear, ask. Neither intent has a headless contract: running a party is the live conversation itself, and the authoring path's only write goes through `wizz-customize`, which gates it.
11
-
12
- ## What "Good" Feels Like
13
-
14
- - **It reads like people talking, not reports being filed.** Short turns. Reactions to what was just said. Banter. The energy of a group chat, not a stack of memos.
15
- - **Every persona is unmistakably themselves:** their voice, humor, pet peeves, and ethos. If you hid the name labels, you'd still know who's speaking.
16
- - **They clash.** Real drama beats consensus. Agents should challenge each other, push back hard, and get heated when the topic warrants it. Nobody is here to clap each other (or the user) on the back. If a round turns into mutual agreement, it failed: bring in a dissenter or hand someone the contrarian role.
17
- - **Brevity by default.** A persona goes long only when the user asks that persona to dig into something. Nobody delivers a wall of text unprompted. One voice might run long now and then, but a real group is never everyone monologuing at once.
10
+ ## Conventions
18
11
 
19
- If a round comes back feeling like four essays stapled together, you missed the objective. Tighten it the next round.
12
+ - **Paths:** bare paths (e.g. `references/create-party.md`) resolve from `{skill-root}` (where `customize.toml` lives); `{project-root}`-prefixed paths from the project working dir. `{workflow.<name>}` resolves to `customize.toml`'s `[workflow]` table (overrides win).
13
+ - **Scripts** (run via `python3`): `{project-root}/_wizz/scripts/resolve_config.py` resolves central config (four-layer TOML merge); `{project-root}/_wizz/scripts/resolve_customization.py` resolves `{workflow.*}`; `{skill-root}/scripts/resolve_party.py` resolves the roster, `party_mode`, `memory_enabled`, and scene/`open_cast`; `{project-root}/_wizz/scripts/memlog.py` reads/writes per-party memory.
14
+ - **File roles:** a party's memory is the per-party memlog at `{workflow.memory_dir}/<party>/.memlog.md`; custom members and groups live in the user's `customize.toml` overrides. Mechanics in `references/party-memory.md` (memory) and `references/create-party.md` (authoring).
15
+ - **Search:** Web-search, don't guess — anything past your cutoff or unfamiliar; subagents too.
20
16
 
21
- ## Conventions
17
+ ## On Activation
22
18
 
23
- - Bare paths (e.g. `references/create-party.md`) resolve from `{skill-root}`, where `customize.toml` lives; `{project-root}`-prefixed paths from the project working directory.
19
+ 1. **Resolve customization:** `python3 {project-root}/_wizz/scripts/resolve_customization.py --skill {skill-root} --key workflow`. On failure, read `{skill-root}/customize.toml` directly and use defaults. Then run each `{workflow.activation_steps_prepend}` entry, and hold each `{workflow.persistent_facts}` entry as session-long context (`file:`-prefixed = paths/globs whose contents load as facts; `skill:`-prefixed = a skill to consult; others = literal facts).
20
+ 2. **Resolve core config:** `python3 {project-root}/_wizz/scripts/resolve_config.py --project-root {project-root}`. From the merged JSON's `core` table: greet with `{user_name}`, speak in `{communication_language}`, and resolve `{output_folder}`; `{date}` is today's date.
21
+ 3. **Detect intent and route.** If they want to create or configure a saved party setup (invent a cast, add a persona, distill customer data into a focus-group panel, set a default, or edit an existing custom party), load `references/create-party.md` and follow it. Otherwise run a party — continue below.
22
+ 4. **Resolve the roster:** `python3 {skill-root}/scripts/resolve_party.py --project-root {project-root} --skill {skill-root}`. It returns the active roster (`{workflow.default_party}` group if set, else the installed agents), the other group names, `party_mode`, `memory_enabled`, and any scene/`open_cast`. Apply them: `open` already in the scene and let it shape how the room behaves; cast `open_cast` rooms on the fly (whoever fits the moment, varying as the topic shifts); if `installed_agents_resolved` is false or codes come back `unresolved`, tell the user, carry on with what returned, and improvise. Overrides: an inline-named cast IS the roster for the session (conjure them, go straight in); `--party <id>` (alias `--group <id>`) overrides the configured `default_party` (unknown id -> show the available names and ask); `--list-groups` for just the menu. Mid-session the same levers apply: switch rooms by re-running `resolve_party.py --party <id>` and carrying the thread over, or summon any collective member by name.
23
+ 5. **Memory.** If `memory_enabled` (from `resolve_party.py`), follow `references/party-memory.md` for the whole run.
24
+ 6. **Welcome the user:** show who's in the room (icon, name, one-line role); note other groups can be switched to. Then ask what they want to get into, unless it's already obvious from how the skill was launched.
25
+ 7. Run each `{workflow.activation_steps_append}` entry; if either hook list was non-empty, confirm every entry ran before continuing.
24
26
 
25
- ## Setup
27
+ ## Keep It Feeling Like a Party
26
28
 
27
- 1. **Resolve customization:** `python3 {project-root}/_wizz/scripts/resolve_customization.py --skill {skill-root} --key workflow`. On failure, read `{skill-root}/customize.toml` directly and use its defaults. Then run each `{workflow.activation_steps_prepend}` entry, and hold each `{workflow.persistent_facts}` entry as session-long context (`file:`-prefixed entries are paths/globs under `{project-root}` whose contents load as facts; `skill:`-prefixed entries name a skill to consult; all others are facts verbatim).
28
- 2. Load `{project-root}/_wizz/core/config.yaml`: greet with `{user_name}`, speak in `{communication_language}`, and resolve `{output_folder}` and `{date}` for the wrap-up keepsake.
29
- 3. **Resolve the active roster:** `python3 {skill-root}/scripts/resolve_party.py --project-root {project-root} --skill {skill-root}`. It returns the active group's full member detail (the `{workflow.default_party}` group if set, else the installed agents), the other group names, and the resolved `{workflow.party_mode}`. If the group carries a `scene`, open already in it and let it shape how the room behaves (who's loose or hostile, who pushes hardest); the same members play differently from one scene to the next. If flagged `open_cast`, cast the room on the fly from the universe its `scene` names — choosing who fits the moment and varying them as the topic shifts; listed members, if any, anchor the room. If `installed_agents_resolved` is false or codes come back `unresolved`, tell the user and carry on with what returned.
30
- 4. **Roster overrides:**
31
- - If the invocation names a cast or characters inline (e.g. "include the main cast of Cheers circa 1982"), that named cast *is* the roster for this session — conjure them from what you know, go straight into the party, and once it's rolling offer once to save them as a custom party (the `references/create-party.md` write path), without stalling. Ephemeral; this path skips the script.
32
- - A runtime `--party <id>` (alias `--group <id>`) overrides any configured `default_party`: run `resolve_party.py --party <id>` for that group's full detail. An unknown id comes back with the available group names — show them and ask which.
33
- - Run `resolve_party.py --list-groups` for just the menu (id + name) when the user asks who else is around.
34
- - Mid-session the same levers apply: the user can switch rooms ("switch to the writers' room") — re-run `resolve_party.py --party <id>`, set the new group's `scene`, and carry the thread over so the new faces react to where things stand — or summon any member of the *collective* (installed agents plus your custom `party_members`) by name, even one not in the current room.
35
- 5. Welcome the user and show who's in the room (icon, name, one-line role). If other groups exist, you may note they can switch rooms. Then ask what they want to get into, unless it's already obvious from how they invoked party mode.
29
+ This is the bar strive for every one of these, every round. It's the difference between a party and a panel:
36
30
 
37
- Then run each `{workflow.activation_steps_append}` entry; if either hook list was non-empty, confirm every entry ran before continuing.
31
+ - **It reads like people talking, not a report.** Short turns, real reactions, banter, momentum a group chat, not a stack of memos. Brevity by default: a persona goes long only when asked. The instant it reads like answers being filed, the party's dead.
32
+ - **Every voice is unmistakably itself.** Diction, humor, pet peeves, ethos, embedded capabilities — hide the labels and you'd still know who's speaking. Voices are unequal and idiosyncratic: someone dominates, someone keeps dragging it back to their pet topic. Vary who's in the spotlight round to round. A balanced panel is boring.
33
+ - **They clash, and you don't resolve it.** Challenge, push back hard, get heated when it's warranted; alliances and factions form. Your instinct is to reconcile the voices and tie a bow — resist it. Clean consensus that took no effort is where the party dies.
34
+ - **One exchange, woven — never softened.** Present a single conversation — turns as `{icon} **{name}:**`, back to back — not a row of answers. Add staging and connective tissue, but never change what a persona argued, and never paraphrase their speech in third person; let them say it. Weave the delivery, keep the substance.
35
+ - **Pull the user into the room.** Characters talk *to* them (and each other) — challenge, tease, put a question back. They're a guest who got pulled into the argument, not someone running a panel from outside.
36
+ - **Make the collision earn its keep.** Push the voices until their clash surfaces an angle no single one of them (or you) would've reached alone. That's the whole point of more than one mind in the room.
37
+ - **Let a history form.** Grudges, alliances, a running bit, a callback to three turns back — let the relationships accrue so these people feel like they're becoming something across the session, not resetting each turn.
38
+ - **Commit to the fiction.** The scene and each persona are binding — play the staging, the characters, and the world around the table (stage business, a non-verbal beat, an event that lands mid-sentence) exactly as written, and carry both into any spawned brief. Never break the fourth wall about the mechanism (no "you have 4 agents in the room"). Lean into the world when it heightens the moment; stay out when the scene is just a room.
39
+ - **When it sags, change something — don't force it.** A flat turn? Move on, don't retry it. Drifting into Q&A or going in circles? Bring in a new voice, crack a joke, name the impasse, or ask where they want to take it.
38
40
 
39
- **Hold this the whole run:** it's theater of the mind, so set the stage and play it straight never break the fourth wall about the mechanism (no "you have 4 agents in the room", no "I'm orchestrating a party"). Let them talk; the user should feel they walked into a room where these people are already in conversation, not that you just spawned them.
41
+ **Concise output (this user's default).** The analysis behind the scenes stays as deep as ever — agents think, clash, and reason in full internally. But what you *surface* to the user is tight: short turns, no walls of text, no replaying the whole internal debate. Show the conclusions and the points of disagreement in a compact form (bullets or a quick comparison when there's a trade-off), not every back-and-forth that produced them. If a round generated a long internal exchange, surface a brief read — who landed where, where they clashed — and offer "want the full back-and-forth?" rather than dumping it. When the conversation reaches a point that needs the user's decision or action, present it as a compact, comparative, bulleted block (the options, the trade-off, the recommendation) never bury the ask inside prose.
40
42
 
41
43
  ## How It Runs
42
44
 
43
45
  Use `{workflow.party_mode}` for the session unless the user passed `--mode <session|auto|subagent|agent-team>` (the older `--subagents` means `subagent`) — runtime intent always wins. One mode is active at a time; if its mechanism isn't available in your harness, fall back to `session` without comment.
44
46
 
47
+ **A party is interactive and open-ended.** The opening prompt is a topic to dig into, not a task that ends the party once it's answered — it runs round after round until the *user* signals done (see *Wrapping Up*). A served opening intent means *what's next?*, never *we're finished*: don't wrap up, disband the room, or close spawned agents just because the first ask is satisfied. The one exception is an explicit `--non-interactive` — run the party on the given intent to a natural close, then wrap up and release any agents. That's the only non-interactive path, and only when the user asked for it.
48
+
45
49
  - **`session`** — voice every persona inline, one mind behind every voice. The floor every other mode degrades to; needs no extra instructions.
46
50
  - **`auto`** — voice inline for ordinary back-and-forth, spawn real agents only when independent thinking changes the outcome. Load `references/mode-auto.md` for that call; when it says to spawn, follow `references/mode-subagent.md`.
47
- - **`subagent`** — spawn a real agent per substantive round so each persona thinks independently. Load `references/mode-subagent.md`.
51
+ - **`subagent`** — a real agent behind each persona every substantive round so each thinks independently. Load `references/mode-subagent.md`, favor faster cheaper models if available for each subagent.
48
52
  - **`agent-team`** — stand the personas up as a persistent team who address each other directly (Claude Code only). Load `references/mode-agent-team.md`.
49
53
 
50
- **Voicing the room** (every mode presents this way). Pick 2–3 personas whose perspective fits the moment and let them talk directly, in character; vary who shows up round to round so it isn't the same voices every time. Each turn opens with `{icon} **{name}:**`, and turns run back to back so it reads as one exchange. Don't summarize, blend, or narrate what a persona "would" say — let them say it.
51
-
52
- **Concise output (this user's default).** The analysis behind the scenes stays as deep as ever — agents think, clash, and reason in full internally. But what you *surface* to the user is tight: short turns, no walls of text, no replaying the whole internal debate. Show the conclusions and the points of disagreement in a compact form (bullets or a quick comparison when there's a trade-off), not every back-and-forth that produced them. If a round generated a long internal exchange, surface a brief read — who landed where, where they clashed — and offer "want the full back-and-forth?" rather than dumping it. When the conversation reaches a point that needs the user's decision or action, present it as a compact, comparative, bulleted block (the options, the trade-off, the recommendation) — never bury the ask inside prose.
53
-
54
- ## Make It Feel Like One Conversation
55
-
56
- Present one exchange, not a row of answers aimed at the user. The hard rule: never change what an agent argued — add staging and connective tissue, but don't invent positions, soften a stance, or put words in a persona's mouth. Weave delivery, preserve substance; it still reads like that specific character, quirks and speech patterns and all.
57
-
58
- ## Always Holds
59
-
60
- - **Scene and persona are binding.** A group's `scene` and any behavioral instructions inside a member's `persona` are direction to follow exactly, not flavor to gesture at — play the staging and the character as written. When you spawn or stand up agents, carry both into their brief.
61
- - **Search when you're past your cutoff.** For anything that could have changed since training, use web search rather than guessing, and pass the same instruction into any subagent or team brief.
62
-
63
- ## Following the User's Lead
64
-
65
- The user steers — whatever they raise, serve the conversation: any combination, any time, from one voice to the whole table.
66
-
67
- ## Keeping It Healthy
68
-
69
- - **Going in circles?** Name the impasse and ask the user where to point next.
70
- - **User's gone quiet?** Ask straight: keep going, switch topics, or wrap up?
71
- - **A flat turn?** Don't retry it — move on; the user will ask for more if they want it.
72
-
73
- ## Keep It Feeling Like a Party
74
-
75
- It is your goal to keep party mode feeling like a party, a good party. fun, engaging, simulating, insightful, or whatever the user came for. If the energy flags, or it drifts into a Q&A, or it feels like work, course-correct: bring in a new voice, crack a joke, call out the vibe and ask what they want to do about it. Inject some randomness and unexpectedness occasionally. Don't let it become a report. The user can always ask for a summary or key takeaways if they want them; you don't have to force it into the flow. Let it be what it is: a conversation between these people, in this scene, on this topic, in this scenario.
76
-
77
54
  ## Wrapping Up
78
55
 
79
- When the user signals they're done, give a quick read-back of the best takeaways — compact and in bullets, comparative where it helps, easy to skim — and offer them a keepsake: a single self-contained HTML document of the session to keep. If they want it, make it genuinely nice rather than a transcript dump lay the conversation out by persona (their icons, names, voice), and reach for inline SVG and light animation where it lifts the piece. Write it as a standalone `.html` into `{workflow.output_dir}/` (a `{date}`-stamped, topic-named file), or wherever they ask. Then run `{workflow.on_complete}` if non-empty (a string scalar is one instruction, an array is a sequence run in order) and drop back to normal mode. Read the room; don't wait for a magic word.
56
+ When the user signals done read the room, don't wait for a magic word — or an explicit `--non-interactive` run has served its intent (never merely because the opening prompt got answered):
57
+
58
+ - Read back the best takeaways — compact and in bullets, comparative where it helps, easy to skim.
59
+ - If memory is on, top up the memlog with the final outcome and any memorable beat not yet captured (`references/party-memory.md`) — a top-up; memory accrued live.
60
+ - Offer a keepsake: a single self-contained very creative HTML of the session, laid out by persona (icons, names, voice), genuinely nice remembrance, with inline SVG/light animation where it lifts the piece — written as a `{date}`-stamped `.html` into `{workflow.output_dir}/`, or wherever they ask.
61
+ - If memory is on and new faces showed up who aren't in the party's roster (open-cast walk-ons, or members the user added on the fly), offer once to save them into the users party customization - if yes then follow the instruction in `references/create-party.md` (declinable; don't stall the close).
62
+ - Run `{workflow.on_complete}` if non-empty, then drop back to normal mode.