@massa-ai/cursor-plugin 1.29.0 → 1.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cursor-plugin/plugin.json +1 -1
- package/README.md +1 -1
- package/install.sh +219 -82
- package/package.json +1 -1
- package/skills/massa-ai/SKILL.md +5 -3
- package/skills/massa-ai/references/installation.md +3 -3
- package/skills/massa-ai/references/mcp-tools.md +3 -1
- package/skills/massa-ai/workflows/discovery.md +236 -0
- package/skills/massa-ai/workflows/pr-review.md +246 -0
package/README.md
CHANGED
|
@@ -62,7 +62,7 @@ If you are building a VS Code extension for Cursor, you can register the plugin
|
|
|
62
62
|
vscode.cursor.plugins.registerPath("/abs/path/to/apps/cursor-plugin");
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
-
Cursor
|
|
65
|
+
Cursor 3.14 loads user-local plugins from `~/.cursor/plugins/local/<name>/` and also bridges Claude marketplace plugins from `~/.claude`, but subagents are discovered only from the flat `~/.cursor/agents/*.md` (or project `.cursor/agents/`) directory — never from a plugin's `agents/` subtree. `install.sh` therefore copies the specialists to `~/.cursor/agents/`; `hooks/hooks.json` entries are merged into `~/.cursor/hooks.json` pointing at the installed hook binary. Cursor reads no global rules file: the AGENTS.md bootstrap must be pasted into Cursor Settings → Rules or provided per project root. MCP is not bundled — Cursor reads `~/.cursor/mcp.json`, which `scripts/install-agents.sh` owns. The `.cursor-plugin/plugin.json` manifest is optional — Cursor discovers the subdirectories without it, but including one aids marketplace submission later.
|
|
66
66
|
|
|
67
67
|
Use `unregisterPath` to remove:
|
|
68
68
|
|
package/install.sh
CHANGED
|
@@ -11,8 +11,19 @@
|
|
|
11
11
|
# MCP registration is delegated to scripts/install-agents.sh, the single writer
|
|
12
12
|
# of host MCP config. This installer no longer ships a plugin-local mcp.json.
|
|
13
13
|
#
|
|
14
|
+
# Cursor 3.14 also bridges the Claude marketplace registry from ~/.claude and
|
|
15
|
+
# loads any plugin listed there IN ADDITION to a local install — a machine
|
|
16
|
+
# with both would load massa-ai twice and fire every hook twice (AD-017). At
|
|
17
|
+
# USER scope, this installer prefers the bridge: when ~/.claude lists massa-ai
|
|
18
|
+
# as installed and enabled, it skips its own local plugin copy and hook
|
|
19
|
+
# wiring (removing a pre-existing local copy so one run converges), while
|
|
20
|
+
# still writing the flat subagents, harness skills, and MCP registration this
|
|
21
|
+
# installer always owns. Project-scope installs are unaffected — ~/.claude is
|
|
22
|
+
# a user surface, never a project one.
|
|
23
|
+
#
|
|
14
24
|
# Idempotent: re-running is a no-op when owned entries already present.
|
|
15
|
-
# Uninstall removes only ownership-marked entries + the plugin directory
|
|
25
|
+
# Uninstall removes only ownership-marked entries + the plugin directory,
|
|
26
|
+
# regardless of which route (bridge or local) installed them.
|
|
16
27
|
#
|
|
17
28
|
# Usage:
|
|
18
29
|
# apps/cursor-plugin/install.sh # install at user scope (~/.cursor)
|
|
@@ -92,20 +103,80 @@ if [[ "$SCOPE" == "project" ]]; then
|
|
|
92
103
|
else
|
|
93
104
|
CURSOR_DIR="$HOME/.cursor"
|
|
94
105
|
fi
|
|
95
|
-
# Cursor
|
|
96
|
-
#
|
|
97
|
-
#
|
|
98
|
-
#
|
|
99
|
-
#
|
|
100
|
-
# plugin
|
|
101
|
-
#
|
|
102
|
-
#
|
|
106
|
+
# Two verified Cursor 3.14 load surfaces (observed live in "Cursor Plugins"
|
|
107
|
+
# exthost logs, 2026-08-05): user-local plugins ARE loaded from
|
|
108
|
+
# plugins/local/<name>/ ("loadUserLocalPlugin massa-ai loaded"), and Cursor
|
|
109
|
+
# additionally bridges Claude marketplace plugins from ~/.claude
|
|
110
|
+
# ("loadClaudePlugin massa-ai@massa-ai") — so a machine with the Claude
|
|
111
|
+
# plugin installed loads massa-ai twice. Subagents, however, are discovered
|
|
112
|
+
# only from the flat .cursor/agents/*.md directory (cursor.com/docs/subagents
|
|
113
|
+
# — no subdirectories), and there is no global rules file. So: PLUGIN_DIR
|
|
114
|
+
# carries the manifest, hook binary, and command skills; everything else
|
|
115
|
+
# Cursor must SEE goes to its dedicated read paths — $CURSOR_AGENTS_DIR
|
|
116
|
+
# (subagents), $CURSOR_DIR/skills/ (harness skills), $CURSOR_DIR/hooks.json,
|
|
117
|
+
# and $CURSOR_DIR/mcp.json (install-agents.sh).
|
|
103
118
|
PLUGIN_DIR="$CURSOR_DIR/plugins/local/massa-ai"
|
|
119
|
+
# The only directory Cursor discovers subagents from (project scope:
|
|
120
|
+
# ./.cursor/agents/). Flat .md files, real copies, massa-ai- prefix owned.
|
|
121
|
+
CURSOR_AGENTS_DIR="$CURSOR_DIR/agents"
|
|
104
122
|
# Pre-fix installs wrote here; removed on install so the two cannot both be
|
|
105
123
|
# discovered and register duplicate hooks.
|
|
106
124
|
LEGACY_PLUGIN_DIR="$CURSOR_DIR/plugins/massa-ai"
|
|
107
125
|
HOOKS_JSON="$CURSOR_DIR/hooks.json"
|
|
108
126
|
|
|
127
|
+
# ── Claude-bridge detection (PAU-08/09/10) ──────────────────────────────────
|
|
128
|
+
# Probe contract pinned against a read-only capture of this machine's live
|
|
129
|
+
# ~/.claude registry files (2026-08-05) — never invented:
|
|
130
|
+
# - $CLAUDE_PLUGIN_REGISTRY must parse and list a non-empty
|
|
131
|
+
# plugins["massa-ai@massa-ai"] array
|
|
132
|
+
# - $CLAUDE_SETTINGS_JSON's enabledPlugins["massa-ai@massa-ai"] must not be
|
|
133
|
+
# literal false — an absent settings.json, or an absent key, is treated
|
|
134
|
+
# as enabled (matching Claude's own default)
|
|
135
|
+
# - any parse failure on either file → NOT detected (local fallback)
|
|
136
|
+
# User-scope only: ~/.claude is a per-user surface, so a --project plugin
|
|
137
|
+
# install always keeps the local branch regardless of what ~/.claude holds.
|
|
138
|
+
CLAUDE_PLUGIN_REGISTRY="$HOME/.claude/plugins/installed_plugins.json"
|
|
139
|
+
CLAUDE_SETTINGS_JSON="$HOME/.claude/settings.json"
|
|
140
|
+
|
|
141
|
+
claude_bridge_detected() {
|
|
142
|
+
[[ "$SCOPE" == "project" ]] && return 1
|
|
143
|
+
[[ -f "$CLAUDE_PLUGIN_REGISTRY" ]] || return 1
|
|
144
|
+
|
|
145
|
+
local runner=""
|
|
146
|
+
if command -v node &>/dev/null; then runner="node"
|
|
147
|
+
elif command -v bun &>/dev/null; then runner="bun"
|
|
148
|
+
else return 1
|
|
149
|
+
fi
|
|
150
|
+
|
|
151
|
+
"$runner" - "$CLAUDE_PLUGIN_REGISTRY" "$CLAUDE_SETTINGS_JSON" <<'NODE'
|
|
152
|
+
const fs = require("fs");
|
|
153
|
+
const [, , registryFile, settingsFile] = process.argv;
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
const registry = JSON.parse(fs.readFileSync(registryFile, "utf8"));
|
|
157
|
+
const listed = registry && registry.plugins && registry.plugins["massa-ai@massa-ai"];
|
|
158
|
+
if (!Array.isArray(listed) || listed.length === 0) process.exit(1);
|
|
159
|
+
} catch {
|
|
160
|
+
process.exit(1);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
const raw = fs.readFileSync(settingsFile, "utf8");
|
|
165
|
+
if (raw.trim()) {
|
|
166
|
+
const settings = JSON.parse(raw);
|
|
167
|
+
const enabled = settings && settings.enabledPlugins && settings.enabledPlugins["massa-ai@massa-ai"];
|
|
168
|
+
if (enabled === false) process.exit(1);
|
|
169
|
+
}
|
|
170
|
+
} catch (e) {
|
|
171
|
+
// Absent settings.json is treated as enabled (Claude's own default); any
|
|
172
|
+
// other read/parse failure means the probe cannot trust this surface.
|
|
173
|
+
if (e.code !== "ENOENT") process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
process.exit(0);
|
|
177
|
+
NODE
|
|
178
|
+
}
|
|
179
|
+
|
|
109
180
|
# The 7 Cursor events → binary subcommands. The command path uses the
|
|
110
181
|
# INSTALLED plugin dir (not the placeholder), so Cursor invokes the copy.
|
|
111
182
|
# Cursor hooks.json shape: { "version": 1, "hooks": { "<event>": [...] } }
|
|
@@ -377,13 +448,15 @@ record_plugin_version() {
|
|
|
377
448
|
local version installed_at route
|
|
378
449
|
version="$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$SCRIPT_DIR/package.json" | head -n 1)"
|
|
379
450
|
installed_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
380
|
-
# installRoute (
|
|
381
|
-
#
|
|
382
|
-
#
|
|
383
|
-
#
|
|
384
|
-
#
|
|
385
|
-
#
|
|
386
|
-
|
|
451
|
+
# installRoute (T6, design Component 4): installer-owned, engine-read-only.
|
|
452
|
+
# "bridge" when ~/.claude's Claude marketplace plugin is what Cursor
|
|
453
|
+
# actually loads (this run skipped its own local copy); "local" otherwise.
|
|
454
|
+
# The switch engine still never reads it for Cursor (hosts.ts skips Cursor
|
|
455
|
+
# unconditionally: every tier resolves to inherit) — recorded for
|
|
456
|
+
# data-model consistency with the other three hosts, and now also read by
|
|
457
|
+
# the harness sentinel probe (T4/T5), which treats both routes alike (same
|
|
458
|
+
# flat-agents glob). Written on EVERY install path.
|
|
459
|
+
if [[ "$IS_BRIDGE" -eq 1 ]]; then route="bridge"; else route="local"; fi
|
|
387
460
|
|
|
388
461
|
# Tolerant of a corrupt/missing state file (rewrites a minimal valid one —
|
|
389
462
|
# AC-8). A record-write failure warns but never fails the install: the next
|
|
@@ -429,6 +502,19 @@ if [[ "$UNINSTALL" -eq 1 ]]; then
|
|
|
429
502
|
merge_hooks_json "$HOOKS_JSON" "uninstall"
|
|
430
503
|
echo " - removed massa-ai hook entries from $HOOKS_JSON"
|
|
431
504
|
fi
|
|
505
|
+
# Remove the massa-ai-owned subagents from Cursor's discovery directory.
|
|
506
|
+
# Prefix glob only: user-authored agents in the same flat dir survive.
|
|
507
|
+
if [[ -d "$CURSOR_AGENTS_DIR" ]]; then
|
|
508
|
+
removed_agents=0
|
|
509
|
+
for agent in "$CURSOR_AGENTS_DIR/"massa-ai-*.md; do
|
|
510
|
+
[[ -f "$agent" ]] || continue
|
|
511
|
+
rm -f "$agent"
|
|
512
|
+
removed_agents=$((removed_agents + 1))
|
|
513
|
+
done
|
|
514
|
+
if [[ "$removed_agents" -gt 0 ]]; then
|
|
515
|
+
echo " - removed $removed_agents massa-ai subagents from $CURSOR_AGENTS_DIR"
|
|
516
|
+
fi
|
|
517
|
+
fi
|
|
432
518
|
# Remove plugin directory
|
|
433
519
|
if [[ -d "$LEGACY_PLUGIN_DIR" ]]; then
|
|
434
520
|
rm -rf "$LEGACY_PLUGIN_DIR"
|
|
@@ -444,59 +530,101 @@ if [[ "$UNINSTALL" -eq 1 ]]; then
|
|
|
444
530
|
fi
|
|
445
531
|
|
|
446
532
|
# ── Install ──────────────────────────────────────────────────────────────────
|
|
447
|
-
|
|
448
|
-
#
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
rm -rf "$LEGACY_PLUGIN_DIR"
|
|
453
|
-
echo -e " - removed pre-fix plugin copy at $LEGACY_PLUGIN_DIR"
|
|
533
|
+
# PAU-08/09: bridge-preferred, local-fallback. Computed once, used to decide
|
|
534
|
+
# both which artifacts this run writes and what installRoute gets recorded.
|
|
535
|
+
IS_BRIDGE=0
|
|
536
|
+
if claude_bridge_detected; then
|
|
537
|
+
IS_BRIDGE=1
|
|
454
538
|
fi
|
|
455
|
-
mkdir -p "$PLUGIN_DIR/.cursor-plugin" "$PLUGIN_DIR/skills" "$PLUGIN_DIR/hooks" "$PLUGIN_DIR/agents"
|
|
456
|
-
|
|
457
|
-
# Copy manifest
|
|
458
|
-
cp "$SCRIPT_DIR/.cursor-plugin/plugin.json" "$PLUGIN_DIR/.cursor-plugin/plugin.json"
|
|
459
|
-
vecho " + .cursor-plugin/plugin.json"
|
|
460
539
|
|
|
461
|
-
# Copy the 6 host-command skills (each in a subdirectory: skills/<name>/SKILL.md).
|
|
462
|
-
# massa-ai/, persona-router/, and agents/ are the PDO-06 harness bundle, not a
|
|
463
|
-
# Cursor command skill — they are installed separately, into the shared
|
|
464
|
-
# harness skills directory (see "Skills bundling" below), not into this
|
|
465
|
-
# plugin-cache skills/ tree.
|
|
466
540
|
skill_count=0
|
|
467
|
-
|
|
468
|
-
name="$(basename "$(dirname "$src")")"
|
|
469
|
-
case "$name" in
|
|
470
|
-
massa-ai|persona-router|agents) continue ;;
|
|
471
|
-
esac
|
|
472
|
-
mkdir -p "$PLUGIN_DIR/skills/$name"
|
|
473
|
-
cp "$src" "$PLUGIN_DIR/skills/$name/SKILL.md"
|
|
474
|
-
vecho " + skills/$name/SKILL.md"
|
|
475
|
-
skill_count=$((skill_count + 1))
|
|
476
|
-
done
|
|
541
|
+
specialist_count=0
|
|
477
542
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
#
|
|
483
|
-
#
|
|
484
|
-
#
|
|
485
|
-
if [[ -
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
543
|
+
if [[ "$IS_BRIDGE" -eq 1 ]]; then
|
|
544
|
+
vecho "Installing massa-ai Cursor plugin (Claude-bridge route): ~/.claude already lists massa-ai as installed and enabled, so Cursor loads it via the bridge."
|
|
545
|
+
# Converge (PAU-08, spec edge case): a pre-existing local install (current
|
|
546
|
+
# or pre-fix legacy location) must not double-load alongside the bridge, so
|
|
547
|
+
# one run here ends with exactly one load path. Owned hook entries are
|
|
548
|
+
# stripped with the SAME filter merge_hooks_json's "uninstall" mode already
|
|
549
|
+
# uses — the bridge delivers hooks instead.
|
|
550
|
+
if [[ -d "$LEGACY_PLUGIN_DIR" ]]; then
|
|
551
|
+
rm -rf "$LEGACY_PLUGIN_DIR"
|
|
552
|
+
echo -e " - removed pre-fix plugin copy at $LEGACY_PLUGIN_DIR"
|
|
553
|
+
fi
|
|
554
|
+
if [[ -d "$PLUGIN_DIR" ]]; then
|
|
555
|
+
rm -rf "$PLUGIN_DIR"
|
|
556
|
+
echo -e " - removed local plugin copy at $PLUGIN_DIR (Claude bridge already loads massa-ai)"
|
|
557
|
+
fi
|
|
558
|
+
if [[ -f "$HOOKS_JSON" ]]; then
|
|
559
|
+
merge_hooks_json "$HOOKS_JSON" "uninstall"
|
|
560
|
+
echo -e " - removed massa-ai hook entries from $HOOKS_JSON (the bridge delivers hooks instead)"
|
|
561
|
+
fi
|
|
562
|
+
else
|
|
563
|
+
vecho "Installing massa-ai Cursor plugin to: $PLUGIN_DIR"
|
|
564
|
+
# Migration: a pre-fix install left a copy at plugins/massa-ai. Leaving it in
|
|
565
|
+
# place risks Cursor discovering both and firing every hook twice, so it goes
|
|
566
|
+
# before the new location is written.
|
|
567
|
+
if [[ -d "$LEGACY_PLUGIN_DIR" ]]; then
|
|
568
|
+
rm -rf "$LEGACY_PLUGIN_DIR"
|
|
569
|
+
echo -e " - removed pre-fix plugin copy at $LEGACY_PLUGIN_DIR"
|
|
570
|
+
fi
|
|
571
|
+
mkdir -p "$PLUGIN_DIR/.cursor-plugin" "$PLUGIN_DIR/skills" "$PLUGIN_DIR/hooks"
|
|
572
|
+
# Migration: pre-fix installs bundled agents inside the plugin dir, which
|
|
573
|
+
# Cursor never reads. Drop that copy so upgraders converge on the one real
|
|
574
|
+
# discovery surface and a future Cursor plugin scan cannot double-register.
|
|
575
|
+
if [[ -d "$PLUGIN_DIR/agents" ]]; then
|
|
576
|
+
rm -rf "$PLUGIN_DIR/agents"
|
|
577
|
+
echo -e " - removed pre-fix agents copy at $PLUGIN_DIR/agents (Cursor reads $CURSOR_AGENTS_DIR)"
|
|
578
|
+
fi
|
|
579
|
+
|
|
580
|
+
# Copy manifest
|
|
581
|
+
cp "$SCRIPT_DIR/.cursor-plugin/plugin.json" "$PLUGIN_DIR/.cursor-plugin/plugin.json"
|
|
582
|
+
vecho " + .cursor-plugin/plugin.json"
|
|
583
|
+
|
|
584
|
+
# Copy the 6 host-command skills (each in a subdirectory: skills/<name>/SKILL.md).
|
|
585
|
+
# massa-ai/, persona-router/, and agents/ are the PDO-06 harness bundle, not a
|
|
586
|
+
# Cursor command skill — they are installed separately, into the shared
|
|
587
|
+
# harness skills directory (see "Skills bundling" below), not into this
|
|
588
|
+
# plugin-cache skills/ tree.
|
|
589
|
+
for src in "$SCRIPT_DIR/skills/"*/SKILL.md; do
|
|
590
|
+
name="$(basename "$(dirname "$src")")"
|
|
591
|
+
case "$name" in
|
|
592
|
+
massa-ai|persona-router|agents) continue ;;
|
|
593
|
+
esac
|
|
594
|
+
mkdir -p "$PLUGIN_DIR/skills/$name"
|
|
595
|
+
cp "$src" "$PLUGIN_DIR/skills/$name/SKILL.md"
|
|
596
|
+
vecho " + skills/$name/SKILL.md"
|
|
597
|
+
skill_count=$((skill_count + 1))
|
|
598
|
+
done
|
|
599
|
+
|
|
600
|
+
# Copy hooks.json (the placeholder version — installer replaces paths)
|
|
601
|
+
cp "$SCRIPT_DIR/hooks/hooks.json" "$PLUGIN_DIR/hooks/hooks.json"
|
|
602
|
+
vecho " + hooks/hooks.json"
|
|
603
|
+
|
|
604
|
+
# Older installs shipped a plugin-local mcp.json here. Cursor reads
|
|
605
|
+
# ~/.cursor/mcp.json, not the plugin dir, and MCP is now owned by
|
|
606
|
+
# scripts/install-agents.sh — drop the residue so upgraders converge.
|
|
607
|
+
if [[ -f "$PLUGIN_DIR/mcp.json" ]]; then
|
|
608
|
+
rm -f "$PLUGIN_DIR/mcp.json"
|
|
609
|
+
# Not gated by --quiet: this deletes a file in the user's home, so it is a
|
|
610
|
+
# mutation notice rather than per-file chatter.
|
|
611
|
+
echo -e " - removed stale mcp.json (MCP is now registered in ~/.cursor/mcp.json)"
|
|
612
|
+
fi
|
|
490
613
|
fi
|
|
491
614
|
|
|
492
|
-
#
|
|
493
|
-
#
|
|
494
|
-
#
|
|
495
|
-
|
|
615
|
+
# Flat agents and harness skills are written in BOTH branches (PAU-08/09):
|
|
616
|
+
# Cursor discovers subagents only from the flat directory regardless of which
|
|
617
|
+
# plugin-load path is active, and MCP/harness-skills ownership is independent
|
|
618
|
+
# of it too. Prune-then-copy so a specialist deleted from the bundle cannot
|
|
619
|
+
# linger installed. All of them, navigator included, are generated from
|
|
620
|
+
# skills/agents/*/SKILL.md and owned by the massa-ai- name prefix (CRS-04) —
|
|
621
|
+
# user-authored agents are untouched.
|
|
622
|
+
mkdir -p "$CURSOR_AGENTS_DIR"
|
|
623
|
+
rm -f "$CURSOR_AGENTS_DIR/"massa-ai-*.md
|
|
496
624
|
for src in "$SCRIPT_DIR/agents/"massa-ai-*.md; do
|
|
497
625
|
[[ -f "$src" ]] || continue
|
|
498
626
|
name="$(basename "$src")"
|
|
499
|
-
cp "$src" "$
|
|
627
|
+
cp "$src" "$CURSOR_AGENTS_DIR/$name"
|
|
500
628
|
vecho " + $name"
|
|
501
629
|
specialist_count=$((specialist_count + 1))
|
|
502
630
|
done
|
|
@@ -504,31 +632,33 @@ vecho " + ${specialist_count} subagent specialists (generated from skills/agent
|
|
|
504
632
|
|
|
505
633
|
# Skills bundling (PDO-08, 09): install massa-ai/persona-router into the
|
|
506
634
|
# shared harness skills directory, unless scripts/install-skills.sh already
|
|
507
|
-
# owns it for this platform.
|
|
635
|
+
# owns it for this platform. Runs in both branches (same reasoning as above).
|
|
508
636
|
vecho ""
|
|
509
637
|
install_bundled_skills
|
|
510
638
|
|
|
511
|
-
|
|
512
|
-
#
|
|
513
|
-
#
|
|
514
|
-
#
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
639
|
+
if [[ "$IS_BRIDGE" -ne 1 ]]; then
|
|
640
|
+
# Copy the real hooks/massa-ai-hook this plugin already ships (T14/PDO-14: a
|
|
641
|
+
# generated real file, no longer a symlink into apps/claude-plugin/ — that
|
|
642
|
+
# path does not exist in a registry tarball install, where $REPO_ROOT is not
|
|
643
|
+
# this monorepo. $CLAUDE_PLUGIN_BIN is kept only as a repo-checkout fallback.
|
|
644
|
+
if [[ -f "$SCRIPT_DIR/hooks/massa-ai-hook" ]]; then
|
|
645
|
+
cp "$SCRIPT_DIR/hooks/massa-ai-hook" "$PLUGIN_DIR/hooks/massa-ai-hook"
|
|
646
|
+
chmod +x "$PLUGIN_DIR/hooks/massa-ai-hook"
|
|
647
|
+
vecho " + hooks/massa-ai-hook"
|
|
648
|
+
elif [[ -f "$CLAUDE_PLUGIN_BIN" ]]; then
|
|
649
|
+
ln -sfn "$CLAUDE_PLUGIN_BIN" "$PLUGIN_DIR/hooks/massa-ai-hook"
|
|
650
|
+
vecho " + hooks/massa-ai-hook → $CLAUDE_PLUGIN_BIN"
|
|
651
|
+
else
|
|
652
|
+
echo " ⚠ Warning: no massa-ai-hook binary found" >&2
|
|
653
|
+
echo " Hooks will not fire until the binary is available." >&2
|
|
654
|
+
fi
|
|
526
655
|
|
|
527
|
-
# Merge hooks.json (array-append, backup, idempotent)
|
|
528
|
-
vecho ""
|
|
529
|
-
vecho "Merging hooks into $HOOKS_JSON..."
|
|
530
|
-
merge_hooks_json "$HOOKS_JSON" "install"
|
|
531
|
-
vecho " + 7 massa-ai hook events wired (array-append, user hooks preserved)"
|
|
656
|
+
# Merge hooks.json (array-append, backup, idempotent)
|
|
657
|
+
vecho ""
|
|
658
|
+
vecho "Merging hooks into $HOOKS_JSON..."
|
|
659
|
+
merge_hooks_json "$HOOKS_JSON" "install"
|
|
660
|
+
vecho " + 7 massa-ai hook events wired (array-append, user hooks preserved)"
|
|
661
|
+
fi
|
|
532
662
|
|
|
533
663
|
# ── MCP registration (delegated) ─────────────────────────────────────────────
|
|
534
664
|
# scripts/install-agents.sh is the single writer of host MCP config. It writes
|
|
@@ -552,10 +682,17 @@ fi
|
|
|
552
682
|
|
|
553
683
|
# Summary line in quiet mode
|
|
554
684
|
if [ "${MASSA_AI_VERBOSE:-0}" != "1" ]; then
|
|
555
|
-
|
|
685
|
+
if [[ "$IS_BRIDGE" -eq 1 ]]; then
|
|
686
|
+
ok "cursor plugin: using the Claude bridge (${specialist_count} specialists) — hooks delivered by the bridge, not installed locally"
|
|
687
|
+
else
|
|
688
|
+
ok "cursor plugin installed (${skill_count} skills, ${specialist_count} specialists, 7 hooks)"
|
|
689
|
+
fi
|
|
556
690
|
else
|
|
557
691
|
vecho ""
|
|
558
692
|
vecho "Done. Restart Cursor to pick up the plugin."
|
|
693
|
+
if [[ "$IS_BRIDGE" -eq 1 ]]; then
|
|
694
|
+
vecho "💡 massa-ai loads via the Claude bridge (~/.claude) — local plugin/hook install skipped."
|
|
695
|
+
fi
|
|
559
696
|
vecho "💡 MCP is registered in ~/.cursor/mcp.json by scripts/install-agents.sh (single writer)."
|
|
560
697
|
fi
|
|
561
698
|
|
package/package.json
CHANGED
package/skills/massa-ai/SKILL.md
CHANGED
|
@@ -46,7 +46,7 @@ Before reading any massa-ai file:
|
|
|
46
46
|
Never pass `workflowSessionId` in that field. Use `synapse_task_begin`/`synapse_task_end`
|
|
47
47
|
for task envelopes and `synapse_prefetch` to warm the buffer on file open.
|
|
48
48
|
- Prefer the shared v2 retrieval order; fall back gracefully if the massa-ai
|
|
49
|
-
server or Synapse is unavailable. The full tool surface includes
|
|
49
|
+
server or Synapse is unavailable. The full tool surface includes 54 tools
|
|
50
50
|
(see `references/mcp-tools.md`): indexing, search, symbol graph
|
|
51
51
|
(`trace_path`, `impact_analysis`, `get_architecture`), memory CRUD
|
|
52
52
|
(`remember`, `recall`, `memory_update`, `memory_delete`), checkpoints
|
|
@@ -166,6 +166,8 @@ current context already contains it.
|
|
|
166
166
|
| `commit` | draft or create safe Conventional Commits with Jira branch prefixes and audit report exclusions | `workflows/commit.md` |
|
|
167
167
|
| `the-fool` | direct challenge, red-team, pre-mortem, evidence audit | `workflows/the-fool.md` |
|
|
168
168
|
| `judge-with-debate` | standalone multi-judge debate evaluation of user-supplied artifacts | `workflows/judge-with-debate.md` |
|
|
169
|
+
| `pr-review` | review a hosted GitHub PR / GitLab MR and post findings via `gh`/`glab` | `workflows/pr-review.md` |
|
|
170
|
+
| `discovery` | product brainstorming / problem-space thinking partner | `workflows/discovery.md` |
|
|
169
171
|
| `to-prd` | turn the current conversation into a PRD without a new interview | `workflows/to-prd.md` |
|
|
170
172
|
| `skill-architect` | design and build a new skill through structured conversation | `workflows/skill-architect.md` |
|
|
171
173
|
| `furps-refinement` | FURPS+ refinement of a PRD and/or ADR before implementation, with The Fool pre-validation and DoR coverage | `workflows/refinement/furps-refinement.md` |
|
|
@@ -181,8 +183,8 @@ Deterministic routing precedence, first match wins:
|
|
|
181
183
|
|
|
182
184
|
1. **Explicit route:** user names a massa-ai workflow, report family, saved finding type, or asks for a direct challenge.
|
|
183
185
|
2. **Requested artifact:** ADR, RFC, TDD, Jira ticket, commit, session guide, audit report, implementation audit report, mobile Figma report, FURPS refinement report, PRD synthesized from the current conversation -> `to-prd` (explicit request only; refining an existing PRD stays `furps-refinement`), or new SKILL.md / skill design -> `skill-architect`.
|
|
184
|
-
3. **Target type:** broken behavior/error -> `debug`; saved audit finding -> matching `*-fix`; implementation scope review -> `implementation-audit`; Maestro E2E/device automation target -> `maestro`, `maestro-audit`, or child-only `maestro-fix` before generic tests workflows; security/privacy/auth finding -> security workflow; tests/flakes/coverage finding -> tests workflow; supplied Figma/screenshot mobile UI design -> `design`; mobile Figma compare/audit -> `mobile-figma-audit`; saved `MFM-*` findings -> `mobile-figma-fix`.
|
|
185
|
-
4. **Primary verb:** create/add/implement -> `feature` unless the concrete target is new Maestro flow work, which routes to `maestro`; restructure without behavior change -> `refactor`; inspect/understand only -> `exploration`; record selected decision -> `adr`; compare open options -> `rfc`; design settled implementation -> `tdd`; refine/quality-check an existing PRD or ADR document (not implementation auditing) -> `furps-refinement`.
|
|
186
|
+
3. **Target type:** broken behavior/error -> `debug`; hosted PR/MR reference (number or URL) to review with posted findings -> `pr-review` (local working diff stays with audit routes); saved audit finding -> matching `*-fix`; implementation scope review -> `implementation-audit`; Maestro E2E/device automation target -> `maestro`, `maestro-audit`, or child-only `maestro-fix` before generic tests workflows; security/privacy/auth finding -> security workflow; tests/flakes/coverage finding -> tests workflow; supplied Figma/screenshot mobile UI design -> `design`; mobile Figma compare/audit -> `mobile-figma-audit`; saved `MFM-*` findings -> `mobile-figma-fix`.
|
|
187
|
+
4. **Primary verb:** create/add/implement -> `feature` unless the concrete target is new Maestro flow work, which routes to `maestro`; restructure without behavior change -> `refactor`; inspect/understand only -> `exploration`; brainstorm/explore a product problem, idea, or direction with no concrete code target -> `discovery`; record selected decision -> `adr`; compare open options -> `rfc`; design settled implementation -> `tdd`; refine/quality-check an existing PRD or ADR document (not implementation auditing) -> `furps-refinement`.
|
|
186
188
|
5. **Risk domain escalation:** migrations, irreversible operations, auth/privacy, cross-service contracts, public compatibility, or work over 10 files routes to `spec-driven` unless the user explicitly requests a narrower workflow and accepts the containment.
|
|
187
189
|
6. **General fallback:** use `general` only after a one-line General fallback preflight names the specialized workflow considered, rejected reason, and why fallback does not change verification or mutation behavior.
|
|
188
190
|
|
|
@@ -100,9 +100,9 @@ behavior through MCP declarations and non-destructive runtime probes.
|
|
|
100
100
|
- Preferred: `bash scripts/install-agents.sh --agent <host>` — the single writer
|
|
101
101
|
of host MCP config (Claude Code, Claude Desktop, Codex, Cursor, OpenCode).
|
|
102
102
|
Plugin installers call it; nothing else writes an MCP entry.
|
|
103
|
-
- OpenCode:
|
|
104
|
-
|
|
105
|
-
|
|
103
|
+
- OpenCode: `@massa-ai/opencode-plugin` is hooks-only (AD-017) and delegates
|
|
104
|
+
MCP registration to `install-agents.sh --agent opencode` on every install,
|
|
105
|
+
same as every other host — no separate MCP step needed.
|
|
106
106
|
- VS Code/Antigravity: `.vscode/mcp.json` or `./scripts/setup-vscode.sh`.
|
|
107
107
|
- Docker: run the `mcp` service through `docker compose run --rm -i mcp`.
|
|
108
108
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# massa-ai Tool Contracts (
|
|
1
|
+
# massa-ai Tool Contracts (54 Tools)
|
|
2
2
|
|
|
3
3
|
Load when exact MCP schemas, REST fallbacks, response modes, or
|
|
4
4
|
polling rules are needed. Prefer the active tool declaration over copied
|
|
@@ -138,6 +138,8 @@ depends on exact content.
|
|
|
138
138
|
|---|---|---|
|
|
139
139
|
| `rename_project` | Rename a project identity transactionally | Req: `sourceProjectId`, `targetProjectId`. Default `dryRun=true`; apply with `dryRun=false` + `operationId` + `expectedPlanHash`. Administrative, not workflow-recurring. |
|
|
140
140
|
| `merge_projects` | Merge one project identity into another | Req: `sourceProjectId`, `targetProjectId`. Same dryRun/planHash contract as `rename_project`. Administrative, not workflow-recurring. |
|
|
141
|
+
| `profile_list` | List shipped model profiles and each detected host's current profile + bundle version | No required args; optional `host` to scope to one. Offline — reads on-disk variant directories only, never the registry. |
|
|
142
|
+
| `profile_set` | Switch the installed massa-ai agents to a registry model profile | Req: `profile`. Optional `host` (default: every detected, supported host), `dryRun` (default `false`) to preview without writing. Local trust model — mutates installed agent files on the machine the MCP server runs on; a host session restart is required to take effect. |
|
|
141
143
|
|
|
142
144
|
## Retrieval Order
|
|
143
145
|
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: discovery
|
|
3
|
+
description: "Product discovery and brainstorming workflow — act as a sharp product thinking partner to explore problem spaces, generate and stress-test ideas, and spar on strategy before anything is spec-ready, ending with a mandatory offer to synthesize the conversation into a PRD via the to-prd workflow. Use when the user wants to brainstorm a product problem, idea, opportunity, or direction with no concrete code target. Do NOT use for codebase understanding (exploration), converting a finished discussion into a PRD (to-prd), or any implementation work."
|
|
4
|
+
license: Apache-2.0
|
|
5
|
+
metadata:
|
|
6
|
+
version: "1.0.0"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
### Discovery (Product Brainstorming)
|
|
10
|
+
|
|
11
|
+
Attribution: adapted from the `product-brainstorming` skill in
|
|
12
|
+
`anthropics/knowledge-work-plugins` (Apache-2.0). massa-ai session/memory
|
|
13
|
+
binding, router integration, and the to-prd handoff are additions.
|
|
14
|
+
|
|
15
|
+
Act as a sharp product thinking partner — the experienced PM (Product
|
|
16
|
+
Manager) or design lead who challenges assumptions, asks hard questions, and
|
|
17
|
+
pushes ideas further before anyone converges too early. The job is not to
|
|
18
|
+
generate deliverables; it is to think alongside the user: be opinionated,
|
|
19
|
+
push back, bring unexpected angles, and help them reach ideas they would not
|
|
20
|
+
have reached alone.
|
|
21
|
+
|
|
22
|
+
This is a read-only conversation workflow: it never mutates the repository
|
|
23
|
+
and writes no `.specs/` artifacts. Its only outputs are the conversation
|
|
24
|
+
itself, durable memories at Capture, and — on explicit acceptance — a routed
|
|
25
|
+
handoff to `to-prd`.
|
|
26
|
+
|
|
27
|
+
Load `references/project-context.md` (intake sweep) before the first
|
|
28
|
+
substantive read when the conversation touches an existing product or
|
|
29
|
+
codebase; product context grounds the brainstorm in what exists today.
|
|
30
|
+
|
|
31
|
+
## Session And Memory
|
|
32
|
+
|
|
33
|
+
- `workflowSessionId`: `discovery-<entity>` (e.g., `discovery-onboarding-dropoff`),
|
|
34
|
+
stable for the whole conversation.
|
|
35
|
+
- Start with a budgeted `recall` (limit ≤ 3, minImportance ≥ 0.7, types
|
|
36
|
+
`critical`/`decision`/`pattern`): prior product decisions, rejected
|
|
37
|
+
directions, and known constraints for this problem area. Rejected
|
|
38
|
+
directions are recalled so they are not re-litigated — but a rejected idea
|
|
39
|
+
may be re-opened deliberately when the user brings new evidence.
|
|
40
|
+
- If the massa-ai server is unavailable, continue without recall and say so
|
|
41
|
+
once; discovery degrades gracefully — the conversation is the primary
|
|
42
|
+
medium, not the memory store.
|
|
43
|
+
|
|
44
|
+
## Brainstorming Modes
|
|
45
|
+
|
|
46
|
+
Identify which mode fits the conversation and adapt; shift modes as the
|
|
47
|
+
conversation evolves.
|
|
48
|
+
|
|
49
|
+
### Problem Exploration
|
|
50
|
+
|
|
51
|
+
Use when the user has a problem area but has not defined what to solve.
|
|
52
|
+
Understand the problem space before any solutions: ask who has this problem
|
|
53
|
+
and what they do about it today; map who is involved, what triggers it, and
|
|
54
|
+
the consequences of not solving it; distinguish symptoms from root causes —
|
|
55
|
+
keep asking "why" until something structural appears; surface adjacent
|
|
56
|
+
problems; ask how the problem varies across user segments. Strong questions:
|
|
57
|
+
"What happens if we do nothing — who suffers and how?", "Who has solved a
|
|
58
|
+
version of this in a different context?", "Is this a problem of awareness,
|
|
59
|
+
ability, or motivation?"
|
|
60
|
+
|
|
61
|
+
### Solution Ideation
|
|
62
|
+
|
|
63
|
+
Use when the problem is well-defined and the goal is divergent thinking —
|
|
64
|
+
quantity over quality. Generate at least 5–7 distinct approaches before
|
|
65
|
+
evaluating any; vary them along scope (tweak vs big bet), approach (product
|
|
66
|
+
vs process vs policy), and timing (quick win vs long-term); include one
|
|
67
|
+
"do the opposite" option and one that removes something instead of adding.
|
|
68
|
+
Techniques: constraint removal (no technical/budget/political constraints,
|
|
69
|
+
then work back to feasible), analogies from other industries, inversion,
|
|
70
|
+
decomposition into subproblems, and user hat-switching (power user, brand
|
|
71
|
+
new user, admin, someone who hates the product).
|
|
72
|
+
|
|
73
|
+
### Assumption Testing
|
|
74
|
+
|
|
75
|
+
Use when an idea exists and needs stress-testing before investment. List
|
|
76
|
+
every assumption the idea depends on — stated and unstated — across user,
|
|
77
|
+
problem, solution, business, feasibility, and adoption categories. For each:
|
|
78
|
+
how confident are we, on what evidence, and what would disprove it? Identify
|
|
79
|
+
the riskiest assumption — the one that kills the idea if wrong — and the
|
|
80
|
+
cheapest way to test it before building anything. Argue the strongest
|
|
81
|
+
possible case against the idea.
|
|
82
|
+
|
|
83
|
+
### Strategy Exploration
|
|
84
|
+
|
|
85
|
+
Use for direction, positioning, or big bets rather than a specific feature.
|
|
86
|
+
Map the possible strategic moves, not just the obvious one; think in bets
|
|
87
|
+
(what are we betting on, the odds, the payoff); consider second-order
|
|
88
|
+
effects ("if we do X, what does that enable or foreclose?"); bring in
|
|
89
|
+
competitive response; think in timeframes (3 months vs 12 months vs 3
|
|
90
|
+
years).
|
|
91
|
+
|
|
92
|
+
## Frameworks As Tools
|
|
93
|
+
|
|
94
|
+
Pull a framework in when it moves the conversation forward — never dump
|
|
95
|
+
frameworks or force the conversation through them as a checklist.
|
|
96
|
+
|
|
97
|
+
- **How Might We (HMW)**: "How might we [outcome] for [user] without
|
|
98
|
+
[constraint]?" Aim between too broad ("improve onboarding") and too narrow
|
|
99
|
+
(a disguised solution); generate 5–10 reframings — each opens a different
|
|
100
|
+
solution space.
|
|
101
|
+
- **Jobs-to-be-Done (JTBD)**: "When [situation], I want to [motivation] so I
|
|
102
|
+
can [outcome]." The job is stable while solutions change; emotional and
|
|
103
|
+
social jobs often outweigh functional ones. Ask "what did they fire to
|
|
104
|
+
hire this?" — it reveals the real competitive set.
|
|
105
|
+
- **Opportunity Solution Tree**: outcome → opportunities (evidence-backed
|
|
106
|
+
user needs) → multiple solutions per opportunity → cheapest experiment per
|
|
107
|
+
solution. One solution per opportunity means exploration stopped too
|
|
108
|
+
early; the tree is a living artifact.
|
|
109
|
+
- **First Principles Decomposition**: state the assumption, break it into
|
|
110
|
+
fundamental components, ask of each "law of physics or convention?", then
|
|
111
|
+
rebuild from only the fundamentals. Use when the team is stuck in
|
|
112
|
+
incremental thinking.
|
|
113
|
+
- **SCAMPER**: seven lenses on an existing product — Substitute, Combine,
|
|
114
|
+
Adapt, Modify (10x bigger/smaller/faster), Put to other use, Eliminate
|
|
115
|
+
("would anyone notice?"), Reverse (flip the sequence or the default).
|
|
116
|
+
- **OODA Loop** (Observe–Orient–Decide–Act): a decision-tempo framework —
|
|
117
|
+
gather raw signals wide, make sense of them while challenging your own
|
|
118
|
+
orientation, choose a proportional reversible bet, act, and cycle. Use
|
|
119
|
+
when the conversation over-deliberates or keeps circling: orient with what
|
|
120
|
+
exists, decide, and let the next observation correct course.
|
|
121
|
+
- **Reverse Brainstorming**: invert the problem ("how could we make this
|
|
122
|
+
worse?"), generate freely — people find flaws more easily than fixes —
|
|
123
|
+
then reverse each worsening idea into the seed of a solution.
|
|
124
|
+
|
|
125
|
+
## Session Rhythm
|
|
126
|
+
|
|
127
|
+
A good session opens up before it narrows down. Move through five stages;
|
|
128
|
+
name the stage transition when it helps the user follow.
|
|
129
|
+
|
|
130
|
+
1. **Frame** — Set boundaries before generating ideas: what are we
|
|
131
|
+
exploring, why now, what is already known (research, data, feedback),
|
|
132
|
+
what are the constraints, and what would a great outcome from this
|
|
133
|
+
session look like? A poorly framed brainstorm produces ideas that connect
|
|
134
|
+
to nothing.
|
|
135
|
+
2. **Diverge** — Generate many ideas without judgment; build on ideas rather
|
|
136
|
+
than shooting them down; follow tangents; push past the first 3–5 obvious
|
|
137
|
+
ideas; use the frameworks above to open new angles. Do not evaluate
|
|
138
|
+
feasibility here — that kills divergent thinking.
|
|
139
|
+
3. **Provoke** — The sparring-partner stage: "What is the strongest argument
|
|
140
|
+
against this?", "Who would hate this and why?", "What are we not
|
|
141
|
+
seeing?", "What if the opposite were true?", "What is the 10x more
|
|
142
|
+
ambitious version?"
|
|
143
|
+
4. **Converge** — Group ideas into themes; evaluate against user impact,
|
|
144
|
+
feasibility, strategic alignment, and evidence strength; identify the top
|
|
145
|
+
2–3 directions; for each, name the biggest unknown and the cheapest way
|
|
146
|
+
to resolve it. If one idea excites the user, explore it even if risky —
|
|
147
|
+
the brainstorm is not the decision.
|
|
148
|
+
5. **Capture** — Mandatory; a brainstorm with no capture never happened.
|
|
149
|
+
Record in conversation: the key ideas and why they are interesting, the
|
|
150
|
+
assumptions to test, the questions to research, the suggested next steps,
|
|
151
|
+
and what was explicitly set aside (interesting, but not now).
|
|
152
|
+
|
|
153
|
+
At Capture, persist the durable subset via `remember`: chosen directions
|
|
154
|
+
with their why and rejected directions with reasons as `decision`, reusable
|
|
155
|
+
framings or cross-session insights as `pattern` — tagged
|
|
156
|
+
`project:<projectId>`, `session:discovery-<entity>`, `workflow:discovery`,
|
|
157
|
+
`entity:<name>`, and a memory-tier tag. Persist only what transcends the
|
|
158
|
+
session; never fabricate memories to satisfy process. If the server is
|
|
159
|
+
unavailable, the capture summary in conversation is the record.
|
|
160
|
+
|
|
161
|
+
## PRD Handoff (to-prd)
|
|
162
|
+
|
|
163
|
+
End every Capture with an explicit offer — this step is mandatory, the PRD
|
|
164
|
+
is not:
|
|
165
|
+
|
|
166
|
+
> "Want me to turn this into a PRD (Product Requirements Document)? I'd
|
|
167
|
+
> synthesize this conversation through the `to-prd` workflow — no new
|
|
168
|
+
> interview."
|
|
169
|
+
|
|
170
|
+
- **Accepted** → route to `workflows/to-prd.md`. The user's acceptance is
|
|
171
|
+
the explicit request `to-prd`'s routing requires. Carry the current
|
|
172
|
+
conversation context — Capture's output (chosen directions, assumptions,
|
|
173
|
+
set-asides) feeds the PRD's problem statement, decisions, and out-of-scope
|
|
174
|
+
sections directly; `to-prd` does not re-interview.
|
|
175
|
+
- **Declined** → the capture summary stays in conversation and the durable
|
|
176
|
+
memories from Capture remain the only persistence. Offer nothing else.
|
|
177
|
+
|
|
178
|
+
If discovery converged on nothing PRD-shaped (pure problem exploration, or
|
|
179
|
+
the session identified research as the next step), say so instead of
|
|
180
|
+
offering an empty PRD — name what research or evidence would make the next
|
|
181
|
+
discovery session converge.
|
|
182
|
+
|
|
183
|
+
## Thinking-Partner Conduct
|
|
184
|
+
|
|
185
|
+
Do:
|
|
186
|
+
|
|
187
|
+
- **Be opinionated.** "I think approach B is stronger because…" beats a
|
|
188
|
+
pro/con list.
|
|
189
|
+
- **Challenge constructively.** "That assumes X — are we confident?", not
|
|
190
|
+
"that will not work."
|
|
191
|
+
- **Bring unexpected angles** — cross-industry analogies, counterexamples,
|
|
192
|
+
edge cases the user has not considered.
|
|
193
|
+
- **Match energy.** When the user is excited about an idea, explore it with
|
|
194
|
+
them before poking holes.
|
|
195
|
+
- **Ask the next question.** When the user finishes a thought, push
|
|
196
|
+
further: "and then what happens?"
|
|
197
|
+
- **Name the pattern.** When a common PM trap appears (solutioning too
|
|
198
|
+
early, scope creep, feature-parity thinking), name it directly.
|
|
199
|
+
|
|
200
|
+
Do not:
|
|
201
|
+
|
|
202
|
+
- Dump frameworks or work through them as a checklist.
|
|
203
|
+
- Generate a list and hand it over — brainstorming is a conversation, not a
|
|
204
|
+
deliverable.
|
|
205
|
+
- Agree with everything — a thinking partner who only validates is not one.
|
|
206
|
+
- Evaluate feasibility during divergence.
|
|
207
|
+
- Anchor on the first idea — when the user leads with a solution,
|
|
208
|
+
acknowledge it, then ask what else could solve the problem.
|
|
209
|
+
- Confuse brainstorming with decision-making — the brainstorm generates
|
|
210
|
+
options; the decision comes later with more data.
|
|
211
|
+
|
|
212
|
+
## Anti-Patterns To Catch
|
|
213
|
+
|
|
214
|
+
- **Solutioning before framing**: "we should build X" before the problem is
|
|
215
|
+
defined — slow down, ask what user problem X solves and how we know.
|
|
216
|
+
- **The feature-parity trap**: "competitor has X, so we need X" is copying,
|
|
217
|
+
not brainstorming — ask what user need X serves and whether there is a
|
|
218
|
+
better way to serve it.
|
|
219
|
+
- **Anchoring on constraints**: "we can't because of Y" during divergence —
|
|
220
|
+
set constraints aside, explore freely, then price feasibility.
|
|
221
|
+
- **The one-idea brainstorm**: a solution presented as a brainstorm —
|
|
222
|
+
acknowledge it, then push for three alternatives.
|
|
223
|
+
- **Analysis paralysis**: long divergence with no convergence — prompt "if
|
|
224
|
+
you had to pick one direction right now, which and why?"
|
|
225
|
+
- **Brainstorming when you should be researching**: when the session circles
|
|
226
|
+
because nobody knows the answer, stop and name the research needed — some
|
|
227
|
+
questions need data, not ideation.
|
|
228
|
+
|
|
229
|
+
## Completion
|
|
230
|
+
|
|
231
|
+
Discovery completes at Capture plus the PRD offer. Before claiming the
|
|
232
|
+
session complete, apply `references/evidence-gate.md`: the evidence here is
|
|
233
|
+
the capture summary (ideas, assumptions, next steps, set-asides), the memory
|
|
234
|
+
outcome (what was persisted or why persistence was skipped), and the
|
|
235
|
+
recorded PRD-offer disposition (accepted → to-prd, declined, or
|
|
236
|
+
not-PRD-shaped with the named research gap).
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pr-review
|
|
3
|
+
description: "Explicit-route workflow to review a hosted GitHub Pull Request or GitLab Merge Request across six dimensions — security, requirements, test coverage, architecture, regression, performance — using massa-ai roster subagents, then post inline comments plus one consolidated summary through the host CLI (gh or glab). Use when the user says review PR 128, review this MR, or code review this pull request. Do NOT use for local working-diff review (audit workflows), creating PRs, replying to review comments, or fixing CI."
|
|
4
|
+
license: CC-BY-4.0
|
|
5
|
+
metadata:
|
|
6
|
+
version: "1.0.0"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
Attribution: adapted from the `pr-review` skill by github.com/augusto-dmh
|
|
10
|
+
(TLC skills catalog), licensed CC-BY-4.0. Host abstraction (GitLab support),
|
|
11
|
+
massa-ai roster dispatches, memory/index/`.specs/` integration, and channel
|
|
12
|
+
discipline are this repository's additions; repository contracts win on any
|
|
13
|
+
conflict with the base.
|
|
14
|
+
|
|
15
|
+
### PR Review
|
|
16
|
+
|
|
17
|
+
Use when the user explicitly asks to review a hosted PR (Pull Request, GitHub) or
|
|
18
|
+
MR (Merge Request, GitLab) — "review PR 128", "review this MR", "check pull request
|
|
19
|
+
42". Explicit route only: never auto-trigger during coding. Local working-tree diff
|
|
20
|
+
review stays with the audit workflows and `massa-ai-reviewer`; this workflow exists
|
|
21
|
+
to **post findings back to the host**.
|
|
22
|
+
|
|
23
|
+
Load `references/project-context.md` (intake sweep) before the first substantive
|
|
24
|
+
read. Resolve `projectId` and `workflowSessionId` = `pr-review-<number>` per the
|
|
25
|
+
Core Contract, and run a budgeted `recall` (limit ≤ 3, minImportance ≥ 0.7) for
|
|
26
|
+
prior review conventions and known regression patterns.
|
|
27
|
+
|
|
28
|
+
## Execution Contract (non-negotiable)
|
|
29
|
+
|
|
30
|
+
1. **Orchestration-only.** The main agent never authors a review finding. It
|
|
31
|
+
gathers context, dispatches the review subagents, dedupes their returned
|
|
32
|
+
findings, and posts. Doing the review inline — even for a small diff — is a
|
|
33
|
+
failure of this workflow.
|
|
34
|
+
2. **Comment-only, never destructive.** Forbidden in every circumstance:
|
|
35
|
+
`gh pr review --approve`, `gh pr review --request-changes`, `gh pr merge`,
|
|
36
|
+
`glab mr approve`, `glab mr revoke`, `glab mr merge`, and the raw
|
|
37
|
+
`POST …/approve` / `POST …/unapprove` endpoints. Posting notes or discussions
|
|
38
|
+
never approves — keep it that way. Never modify repository files.
|
|
39
|
+
3. **Subagents never touch the host.** Review subagents are read-only and
|
|
40
|
+
host-agnostic: they receive the diff and context in their packet and return
|
|
41
|
+
findings in their reply block. Only the orchestrator executes `gh`/`glab`.
|
|
42
|
+
4. **File-body posting.** Every multiline body is written to a temp file and
|
|
43
|
+
posted with the host's file-body mechanism (`--body-file` / `-F body=@file`).
|
|
44
|
+
Inlining a multiline `--body` string is the protocol's most common failure.
|
|
45
|
+
5. **Ask, never guess.** No PR/MR reference in the request → ask for it. Host CLI
|
|
46
|
+
cannot resolve the reference → stop and surface the CLI error output.
|
|
47
|
+
|
|
48
|
+
## Step 1 — Initialize
|
|
49
|
+
|
|
50
|
+
### 1a. Resolve the host
|
|
51
|
+
|
|
52
|
+
Order: explicit user statement > CLI probe > git remote host. Probe with
|
|
53
|
+
`gh repo view` / `glab repo view` **exit status** (glab's no-remote error text is
|
|
54
|
+
not a documented stable string — never match on the message). Both probes fail →
|
|
55
|
+
stop and report which CLI is missing or unauthenticated (`gh auth status` /
|
|
56
|
+
`glab auth status`). Both succeed (mirrored repo) → ask the user which host to
|
|
57
|
+
review on; a posted comment is outward-facing. Record `HOST ∈ {github, gitlab}`.
|
|
58
|
+
|
|
59
|
+
### 1b. PR/MR context (via the command map below)
|
|
60
|
+
|
|
61
|
+
Resolve repository identity, then fetch: title + body/description + source
|
|
62
|
+
branch, the head anchor (`{SHA}` on GitHub; the full `diff_refs`
|
|
63
|
+
`{base_sha, head_sha, start_sha}` triple on GitLab), the full diff, and the
|
|
64
|
+
changed-file list. Then load the existing inline-comment inventory as
|
|
65
|
+
`{id, path, line, body}` records — **page to completion** (GitLab discussions
|
|
66
|
+
default to 20 per page; pin `per_page=100` and loop) — used for dedupe,
|
|
67
|
+
`[RESOLVED]` replies, and threading.
|
|
68
|
+
|
|
69
|
+
### 1c. Project discovery (the adaptive spine)
|
|
70
|
+
|
|
71
|
+
Probe the repository once and record a DISCOVERY MAP passed verbatim to every
|
|
72
|
+
subagent. Prefer evidence the project states over guesses; mark absences `none`.
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
TEST: <command CI actually runs> | globs: <...> | unit vs e2e: <split | none>
|
|
76
|
+
REQS: tracker=<GH #42 | Jira KEY-123 | GitLab #42 | none> ; specs=<paths | none>
|
|
77
|
+
CONVENTIONS: <doc/skill paths that state rules | none-found>
|
|
78
|
+
REVIEW_SKILLS: <project-local review skill paths | none>
|
|
79
|
+
INDEX: <massa-ai retrieval state: fresh | stale | unavailable — CLI fallback>
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
- **TEST**: the CI workflow config is authoritative; manifests are fallback.
|
|
83
|
+
- **REQS Track A (tracker)**: ticket key from branch name or PR/MR body —
|
|
84
|
+
`gh issue view {N} --json title,body` / `glab issue view {N} --output json`;
|
|
85
|
+
Jira only through an already-configured Atlassian MCP (never invent a host).
|
|
86
|
+
- **REQS Track B (in-repo)**: `.specs/project/FEATURES.json` and
|
|
87
|
+
`.specs/features/<slug>/{spec,tasks}.md` acceptance criteria matched by branch,
|
|
88
|
+
ticket, or feature stem; then `docs/`, ADR/RFC directories, `*-spec.md`.
|
|
89
|
+
- **CONVENTIONS/REVIEW_SKILLS**: `CONTRIBUTING*`, `ARCHITECTURE*`, `AGENTS.md`,
|
|
90
|
+
`CLAUDE.md`, `docs/**` convention files, `.claude/skills/`, `.cursor/skills/`.
|
|
91
|
+
- **INDEX**: `list_projects` freshness first; when fresh, `project_map` or
|
|
92
|
+
`get_architecture` for orientation and `impact_analysis` over the PR/MR diff
|
|
93
|
+
for centrality-ranked hotspots; `search` under `references/synapse-policy.md`
|
|
94
|
+
when two or more related searches are planned. Index results are leads until
|
|
95
|
+
confirmed against the diff — never evidence on their own. Server or index
|
|
96
|
+
unavailable → record it and continue per `references/graceful-degradation.md`.
|
|
97
|
+
|
|
98
|
+
## Host Command Map
|
|
99
|
+
|
|
100
|
+
The orchestrator reads every host operation from this table. `{REPO}`/`{PR}` are
|
|
101
|
+
GitHub coordinates; `{MR}` is the GitLab IID; `:id` is glab's project placeholder
|
|
102
|
+
(resolved from the current repo's remote — 8 placeholders are documented:
|
|
103
|
+
`:branch :fullpath :group :id :namespace :repo :user :username`).
|
|
104
|
+
|
|
105
|
+
| Operation | GitHub (`gh`) | GitLab (`glab`) |
|
|
106
|
+
| --- | --- | --- |
|
|
107
|
+
| Identity | `gh repo view --json nameWithOwner -q .nameWithOwner` → `{REPO}` | `glab repo view --output json --jq .path_with_namespace` (project id: `--jq .id`) |
|
|
108
|
+
| Metadata | `gh pr view {PR} --json title,body,headRefName,headRefOid` → `{SHA}` | `glab mr view {MR} --output json` → `title`, `description`, `source_branch`, `sha`, `diff_refs.{base_sha,head_sha,start_sha}` |
|
|
109
|
+
| Full diff | `gh pr diff {PR}` | `glab mr diff {MR} --raw` |
|
|
110
|
+
| Changed files | `gh pr diff {PR} --name-only` | `glab api "projects/:id/merge_requests/{MR}/diffs?per_page=100&page={N}"` → `new_path`/`old_path`, page to completion |
|
|
111
|
+
| Existing comments | `gh api repos/{REPO}/pulls/{PR}/comments` | `glab api "projects/:id/merge_requests/{MR}/discussions?per_page=100&page={N}"` → note `id`, `position.new_path`, `position.new_line`, `body` |
|
|
112
|
+
| Inline comment | `gh api repos/{REPO}/pulls/{PR}/comments -F body=@body.md -f commit_id={SHA} -f path={path} -F line={N} -f side=RIGHT` | `glab api --method POST "projects/:id/merge_requests/{MR}/discussions" -F body=@body.md -f "position[position_type]=text" -f "position[base_sha]={base}" -f "position[head_sha]={head}" -f "position[start_sha]={start}" -f "position[new_path]={path}" -f "position[old_path]={old}" -F "position[new_line]={N}"` |
|
|
113
|
+
| Thread reply | `gh api repos/{REPO}/pulls/{PR}/comments/{COMMENT_ID}/replies -F body=@body.md` | `glab api --method POST "projects/:id/merge_requests/{MR}/discussions/{DISCUSSION_ID}/notes" -F body=@body.md` |
|
|
114
|
+
| Summary | `gh pr review {PR} --comment --body-file summary.md` | `glab api --method POST "projects/:id/merge_requests/{MR}/notes" -F body=@summary.md` |
|
|
115
|
+
|
|
116
|
+
Anchoring and flag semantics (load-bearing, verified against official docs):
|
|
117
|
+
|
|
118
|
+
- **GitHub `line={N}`** is the 1-based line number in the **head file** on side
|
|
119
|
+
`RIGHT` — count from the hunk header across added and context lines. A
|
|
120
|
+
diff-relative offset returns 422 or lands on the wrong line.
|
|
121
|
+
- **GitLab added line** ⇒ send `position[new_line]` and **omit** `old_line`
|
|
122
|
+
(removed line: the reverse; context line: both). `new_path` **and** `old_path`
|
|
123
|
+
are both required for `position_type=text` — take `old_path` from the `/diffs`
|
|
124
|
+
inventory, never assume it equals `new_path` (renames break that).
|
|
125
|
+
- **`-F`/`--field` expands `@file` and infers types on both CLIs; `-f`/
|
|
126
|
+
`--raw-field` does neither** — `-f body=@body.md` posts the literal string
|
|
127
|
+
`@body.md`. Use `-F` for bodies and line numbers, `-f` for plain strings.
|
|
128
|
+
glab's `-F` also switches the default method to POST.
|
|
129
|
+
- `glab mr note create` has experimental inline flags (`--file`, `--line`,
|
|
130
|
+
`--reply`) — GitLab marks them "might be unstable or removed at any time"; the
|
|
131
|
+
stable `glab api` paths above are the contract. A plain summary may also use
|
|
132
|
+
`glab mr note create {MR} < summary.md` (body from stdin).
|
|
133
|
+
|
|
134
|
+
## Step 2 — Dispatch the review (two waves)
|
|
135
|
+
|
|
136
|
+
Six dimensions run as read-only roster dispatches under
|
|
137
|
+
`references/agent-orchestration.md` (wave cap 4 → wave 1 = rows 1–4, wave 2 =
|
|
138
|
+
rows 5–6). Each packet carries: the dimension row below, the DISCOVERY MAP, the
|
|
139
|
+
PR/MR intent (title/body/branch), the existing-comment inventory, the diff
|
|
140
|
+
trimmed to hunks relevant to the dimension per `references/context-firewall.md`,
|
|
141
|
+
the severity labels, and the reply contract.
|
|
142
|
+
|
|
143
|
+
| # | Dimension | Agent | Packet delta (lens / scope) | Marker `{type}` |
|
|
144
|
+
| --- | --- | --- | --- | --- |
|
|
145
|
+
| 1 | Security | `massa-ai-audit-specialist` | `lens: security` — secrets, authn/authz on new endpoints, injection, unsafe deserialization, PII in logs, permissive CORS, leaking payload fields | `security` |
|
|
146
|
+
| 2 | Requirements & DoD (Definition of Done) | `massa-ai-audit-specialist` | `lens: requirements` — score merged Track A + Track B criteria against the diff, evidence-or-zero: ✅ implemented (`path:line`) / 🟡 partial / ❌ missing; no source ⇒ report "requirements verification skipped" | `requirements` |
|
|
147
|
+
| 3 | Architecture & conventions | `massa-ai-audit-specialist` | `lens: architecture` — extract every explicit rule from the profile's CONVENTIONS/REVIEW_SKILLS docs into a numbered matrix, grade each changed file PASS/VIOLATION/N/A; no docs ⇒ minimal generic boundary sweep, stated | `architecture` |
|
|
148
|
+
| 4 | Performance | `massa-ai-audit-specialist` | `lens: performance` — only issues clearly visible in the diff: N+1 queries, unbounded fetches, per-row lazy I/O, sequential awaits of independent calls, loop-invariant recomputation, unbatched writes | `performance` |
|
|
149
|
+
| 5 | Test coverage | `massa-ai-audit-specialist` | `lens: performance`, scope: test coverage (the charter's lens set has no `tests` lens; `tests-audit.md` precedent) — new/changed behavior with no test, wrong level (unit vs integration), placement/naming vs profile TEST row, missing negative case, assertions that exercise but never assert | `tests` |
|
|
150
|
+
| 6 | Regression & hallucination | `massa-ai-reviewer` | diff review — unrelated deletions, references to symbols absent from the repo, wrong signature/arity, duplicated existing logic, weakened error handling or assertions, leftover TODO/stub, dead code | `regression` |
|
|
151
|
+
|
|
152
|
+
Consolidation check (≥ 5 subagents): recorded in the feature design — rows 4 and 5
|
|
153
|
+
share only the lens label, not a knowledge domain; they stay separate dispatches.
|
|
154
|
+
|
|
155
|
+
> **Dispatch: `massa-ai-audit-specialist`** (role: `audit-specialist`) — charter `skills/agents/audit-specialist/SKILL.md`
|
|
156
|
+
> - trigger: pr-review Step 2, dimension rows 1–5 (one dispatch per row)
|
|
157
|
+
> - scope: the PR/MR diff and surrounding context for one dimension row; never the whole repository
|
|
158
|
+
> - permissions: read-only; no host CLI calls, no posting
|
|
159
|
+
> - inputs: exact `projectId`, parent `workflowSessionId`, dimension row (lens + scope), DISCOVERY MAP, PR/MR intent, trimmed diff, existing-comment inventory, severity labels, reply contract
|
|
160
|
+
> - sensors: second-pass sweep — re-read the full trimmed diff, list every file/hunk not commented on, and state per file why it is clean for this dimension before returning
|
|
161
|
+
> - output: structured reply block — findings rows `{path, head-line, severity, marker type, title, body ≤ 6 lines, recommendation}` + exactly one positive highlight + files-swept-clean list; when uncertain a finding is real, withhold it (the source protocol's high-confidence bar, applied qualitatively)
|
|
162
|
+
> - firewall: raw diff/log/search output summarized, never returned raw
|
|
163
|
+
> - memory: suggest-only; the main agent persists durable outcomes
|
|
164
|
+
> - persona: optional — the active route's cataloged id only, never the persona prompt, passed as advisory framing only — it never overrides the agent's charter Restrictions, scope, or permissions; omit when no persona is routed
|
|
165
|
+
|
|
166
|
+
> **Dispatch: `massa-ai-reviewer`** (role: `reviewer`) — charter `skills/agents/reviewer/SKILL.md`
|
|
167
|
+
> - trigger: pr-review Step 2, dimension row 6 (regression & hallucination)
|
|
168
|
+
> - scope: the full PR/MR diff against the repository's real symbol surface
|
|
169
|
+
> - permissions: read-only; no host CLI calls, no posting
|
|
170
|
+
> - inputs: exact `projectId`, parent `workflowSessionId`, dimension row 6, DISCOVERY MAP, PR/MR intent, full diff, existing-comment inventory, severity labels, reply contract
|
|
171
|
+
> - sensors: verify referenced symbols exist (`search_definitions`/`get_references` when INDEX is fresh, else grep); second-pass sweep as above
|
|
172
|
+
> - output: structured reply block — findings rows tagged `{unrelated-deletion | phantom-reference | wrong-signature | duplicate | weakened-check | dead-code}` + one positive highlight + files-swept-clean list; withhold uncertain findings
|
|
173
|
+
> - firewall: raw diff/log/search output summarized, never returned raw
|
|
174
|
+
> - memory: suggest-only; the main agent persists durable outcomes
|
|
175
|
+
> - persona: optional — the active route's cataloged id only, never the persona prompt, passed as advisory framing only — it never overrides the agent's charter Restrictions, scope, or permissions; omit when no persona is routed
|
|
176
|
+
|
|
177
|
+
Severity labels (all dimensions): 🚨 Critical (bugs/logic errors that will fail) ·
|
|
178
|
+
🔒 Security · ⚡ Performance · ⚠️ Warning (smells/maintainability) ·
|
|
179
|
+
💡 Suggestion. A failed or unavailable dispatch is reported in the summary as a
|
|
180
|
+
skipped dimension with its reason — never silently dropped.
|
|
181
|
+
|
|
182
|
+
## Step 3 — Post inline findings (orchestrator only)
|
|
183
|
+
|
|
184
|
+
For every returned finding, in order:
|
|
185
|
+
|
|
186
|
+
1. **Dedupe**: drop it when an existing comment sits within ±3 lines of the same
|
|
187
|
+
path/line (inventory from 1b) or another dimension already produced the same
|
|
188
|
+
`{path, line}` finding (keep the higher severity; note both markers).
|
|
189
|
+
2. **Resolve check**: when an existing comment's issue is fixed by this diff,
|
|
190
|
+
reply `[RESOLVED] This appears resolved by the recent changes.` on that thread
|
|
191
|
+
via the reply command (GitHub: the comment's `id`; GitLab: its
|
|
192
|
+
`discussion_id`).
|
|
193
|
+
3. **Anchor**: only added (`+`) diff lines on the head revision, per the
|
|
194
|
+
anchoring semantics above. A finding with no `+` line to stand on goes to the
|
|
195
|
+
summary instead.
|
|
196
|
+
4. **Body**: temp file, starting with the invisible marker
|
|
197
|
+
`<!-- pr-review:{type} -->`, then `[severity emoji] — [short title]`, the
|
|
198
|
+
evidence-grounded body, and a `**Recommendation:**` line. No AI/assistant/
|
|
199
|
+
tool attribution anywhere — write as a reviewer. Specific, actionable,
|
|
200
|
+
collegial; always explain why.
|
|
201
|
+
5. **Post** with the inline-comment command for `HOST`.
|
|
202
|
+
|
|
203
|
+
## Step 4 — Consolidated summary
|
|
204
|
+
|
|
205
|
+
Assemble from the reply blocks (no extra subagent) and post one summary via the
|
|
206
|
+
summary command:
|
|
207
|
+
|
|
208
|
+
```markdown
|
|
209
|
+
## 📋 PR Review Summary
|
|
210
|
+
|
|
211
|
+
| | |
|
|
212
|
+
|---|---|
|
|
213
|
+
| **Host / target** | {github PR #N | gitlab MR !N} @ {head sha} |
|
|
214
|
+
| **Dimensions** | 6 (Security · Requirements & DoD · Tests · Architecture · Regression · Performance) |
|
|
215
|
+
| **Detected runner** | {TEST row | none found} |
|
|
216
|
+
| **Requirements source** | {tracker / spec paths / none} |
|
|
217
|
+
| **Project refs loaded** | {CONVENTIONS + REVIEW_SKILLS rows} |
|
|
218
|
+
| **Findings** | {N} across {M} files |
|
|
219
|
+
|
|
220
|
+
### 🔒 Security ({N}) / 🚨 Critical ({N}) / ⚡ Performance ({N}) / ⚠️ Warnings ({N}) / 💡 Suggestions ({N})
|
|
221
|
+
- [`path/file:L42`] Finding title — one line each, grouped by severity
|
|
222
|
+
|
|
223
|
+
### 📋 Requirements
|
|
224
|
+
{✅/🟡/❌ rows from dimension 2, with `path:line` evidence}
|
|
225
|
+
|
|
226
|
+
### 🔍 Files with no findings
|
|
227
|
+
- `path` — swept clean by {dimensions} (omit section when every logic file got a comment; config/lock/declaration files excluded)
|
|
228
|
+
|
|
229
|
+
### ✅ Highlights
|
|
230
|
+
- one per dimension
|
|
231
|
+
|
|
232
|
+
> See inline comments for details. {Skipped dimensions/sensors with reasons, if any.}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Zero findings overall → post "✅ No issues found across all review dimensions."
|
|
236
|
+
with the metadata table intact.
|
|
237
|
+
|
|
238
|
+
## Completion
|
|
239
|
+
|
|
240
|
+
- Emit Conversation Feedback status updates at wave boundaries when that policy
|
|
241
|
+
is active; expand every abbreviation on first use in user-facing output.
|
|
242
|
+
- Persist durable outcomes only (recurring review pattern, confirmed project
|
|
243
|
+
convention) with the required memory tags; do not fabricate memories.
|
|
244
|
+
- Close with `references/evidence-gate.md`: counts posted vs deduped vs withheld,
|
|
245
|
+
skipped dimensions/sensors with reasons, and the summary URL/reference.
|
|
246
|
+
<!-- validator anchors: comment-only | added (+) diff lines | page to completion | two waves -->
|