@massa-ai/claude-plugin 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +18 -0
- package/README.md +74 -0
- package/agents/massa-ai-architecture-specialist.md +65 -0
- package/agents/massa-ai-audit-specialist.md +81 -0
- package/agents/massa-ai-builder.md +67 -0
- package/agents/massa-ai-context-curator.md +67 -0
- package/agents/massa-ai-documentation-agent.md +65 -0
- package/agents/massa-ai-furps-analyst.md +71 -0
- package/agents/massa-ai-investigator.md +68 -0
- package/agents/massa-ai-mobile-specialist.md +82 -0
- package/agents/massa-ai-navigator.md +75 -0
- package/agents/massa-ai-plan-critic.md +90 -0
- package/agents/massa-ai-planner.md +65 -0
- package/agents/massa-ai-requirements-analyst.md +64 -0
- package/agents/massa-ai-reviewer.md +66 -0
- package/agents/massa-ai-test-engineer.md +66 -0
- package/agents/massa-ai-verification-agent.md +65 -0
- package/commands/def.md +17 -0
- package/commands/find.md +19 -0
- package/commands/graph.md +16 -0
- package/commands/index.md +19 -0
- package/commands/map.md +24 -0
- package/commands/status.md +15 -0
- package/hooks/README.md +52 -0
- package/hooks/_pin.sh +64 -0
- package/hooks/_post.sh +77 -0
- package/hooks/hooks.json +54 -0
- package/hooks/massa-ai-hook.ts +298 -0
- package/hooks/post-tool-use.sh +4 -0
- package/hooks/pre-compact.sh +70 -0
- package/hooks/session-start.sh +5 -0
- package/hooks/stop.sh +4 -0
- package/hooks/user-prompt-submit.sh +4 -0
- package/install.sh +435 -0
- package/package.json +32 -0
- package/skills/agents/architecture-specialist/SKILL.md +69 -0
- package/skills/agents/audit-specialist/SKILL.md +85 -0
- package/skills/agents/builder/SKILL.md +71 -0
- package/skills/agents/context-curator/SKILL.md +71 -0
- package/skills/agents/documentation-agent/SKILL.md +69 -0
- package/skills/agents/furps-analyst/SKILL.md +74 -0
- package/skills/agents/investigator/SKILL.md +72 -0
- package/skills/agents/mobile-specialist/SKILL.md +86 -0
- package/skills/agents/navigator/SKILL.md +79 -0
- package/skills/agents/plan-critic/SKILL.md +93 -0
- package/skills/agents/planner/SKILL.md +69 -0
- package/skills/agents/requirements-analyst/SKILL.md +68 -0
- package/skills/agents/reviewer/SKILL.md +70 -0
- package/skills/agents/test-engineer/SKILL.md +70 -0
- package/skills/agents/verification-agent/SKILL.md +69 -0
- package/skills/massa-ai/SKILL.md +315 -0
- package/skills/massa-ai/personas/README.md +35 -0
- package/skills/massa-ai/personas/ai-native-nodejs-cli-architect.md +76 -0
- package/skills/massa-ai/personas/catalog.json +157 -0
- package/skills/massa-ai/personas/context-skill-harness-engineer-architect.md +74 -0
- package/skills/massa-ai/personas/product-manager.md +67 -0
- package/skills/massa-ai/personas/senior-mobile-engineer.md +74 -0
- package/skills/massa-ai/personas/senior-mobile-qa-automation-engineer.md +75 -0
- package/skills/massa-ai/references/adr-authoring.md +189 -0
- package/skills/massa-ai/references/agent-orchestration.md +221 -0
- package/skills/massa-ai/references/architecture-coupling-lens.md +239 -0
- package/skills/massa-ai/references/architecture-deepening-lens.md +136 -0
- package/skills/massa-ai/references/architecture-domain-lens.md +186 -0
- package/skills/massa-ai/references/architecture-lenses.md +108 -0
- package/skills/massa-ai/references/audit-report-io.md +459 -0
- package/skills/massa-ai/references/audit-scope.md +103 -0
- package/skills/massa-ai/references/code-annotation.md +111 -0
- package/skills/massa-ai/references/codebase-investigation.md +96 -0
- package/skills/massa-ai/references/context-firewall.md +62 -0
- package/skills/massa-ai/references/conversation-feedback.md +104 -0
- package/skills/massa-ai/references/debug-diagnosis-loop.md +140 -0
- package/skills/massa-ai/references/decision-engine.md +73 -0
- package/skills/massa-ai/references/evidence-gate.md +53 -0
- package/skills/massa-ai/references/furps/analyst-role.md +49 -0
- package/skills/massa-ai/references/furps/checklist.md +92 -0
- package/skills/massa-ai/references/furps/intake.md +104 -0
- package/skills/massa-ai/references/furps/report-contract.md +140 -0
- package/skills/massa-ai/references/hook-enforcement.md +137 -0
- package/skills/massa-ai/references/implementation-delivery.md +101 -0
- package/skills/massa-ai/references/installation.md +110 -0
- package/skills/massa-ai/references/lessons.md +119 -0
- package/skills/massa-ai/references/maestro/artifacts-reports.md +69 -0
- package/skills/massa-ai/references/maestro/cli-device.md +65 -0
- package/skills/massa-ai/references/maestro/cloud.md +67 -0
- package/skills/massa-ai/references/maestro/config-env-output.md +76 -0
- package/skills/massa-ai/references/maestro/fact-ledger.md +71 -0
- package/skills/massa-ai/references/maestro/js-scripting.md +70 -0
- package/skills/massa-ai/references/maestro/mcp.md +59 -0
- package/skills/massa-ai/references/maestro/patterns.md +96 -0
- package/skills/massa-ai/references/maestro/selectors.md +91 -0
- package/skills/massa-ai/references/maestro/workspace-execution.md +81 -0
- package/skills/massa-ai/references/maestro/yaml-commands.md +203 -0
- package/skills/massa-ai/references/maestro.md +47 -0
- package/skills/massa-ai/references/mcp-tools.md +296 -0
- package/skills/massa-ai/references/memory-policy.md +103 -0
- package/skills/massa-ai/references/mobile-context.md +113 -0
- package/skills/massa-ai/references/mobile-diagnosis.md +106 -0
- package/skills/massa-ai/references/mobile-figma-matcher/ATTRIBUTION.md +5 -0
- package/skills/massa-ai/references/mobile-figma-matcher/android-compose.md +13 -0
- package/skills/massa-ai/references/mobile-figma-matcher/android-views.md +13 -0
- package/skills/massa-ai/references/mobile-figma-matcher/core.md +117 -0
- package/skills/massa-ai/references/mobile-figma-matcher/ios-swiftui.md +12 -0
- package/skills/massa-ai/references/mobile-figma-matcher/ios-uikit.md +12 -0
- package/skills/massa-ai/references/mobile-figma-matcher/kmp-compose-multiplatform.md +14 -0
- package/skills/massa-ai/references/mobile-figma-matcher/repository-detection.md +77 -0
- package/skills/massa-ai/references/naming-standards.md +47 -0
- package/skills/massa-ai/references/pr-task-fix.md +80 -0
- package/skills/massa-ai/references/project-context.md +76 -0
- package/skills/massa-ai/references/rfc/ATTRIBUTION.md +5 -0
- package/skills/massa-ai/references/rfc/discovery-and-sizing.md +120 -0
- package/skills/massa-ai/references/rfc/document-contract.md +85 -0
- package/skills/massa-ai/references/rfc/quality-and-lifecycle.md +101 -0
- package/skills/massa-ai/references/root-cause-scripts.md +97 -0
- package/skills/massa-ai/references/spec-driven/artifact-store.md +98 -0
- package/skills/massa-ai/references/spec-driven/code-analysis.md +119 -0
- package/skills/massa-ai/references/spec-driven/coding-principles.md +80 -0
- package/skills/massa-ai/references/spec-driven/context-limits.md +64 -0
- package/skills/massa-ai/references/spec-driven/design.md +257 -0
- package/skills/massa-ai/references/spec-driven/discuss.md +182 -0
- package/skills/massa-ai/references/spec-driven/execute.md +471 -0
- package/skills/massa-ai/references/spec-driven/lessons.md +5 -0
- package/skills/massa-ai/references/spec-driven/memory.md +214 -0
- package/skills/massa-ai/references/spec-driven/specify.md +283 -0
- package/skills/massa-ai/references/spec-driven/sub-agents.md +151 -0
- package/skills/massa-ai/references/spec-driven/tasks.md +494 -0
- package/skills/massa-ai/references/spec-driven/validate.md +397 -0
- package/skills/massa-ai/references/subagent-design.md +132 -0
- package/skills/massa-ai/references/synapse-policy.md +160 -0
- package/skills/massa-ai/references/tdd/calibrated-examples.md +54 -0
- package/skills/massa-ai/references/tdd/discovery-and-sizing.md +83 -0
- package/skills/massa-ai/references/tdd/document-contract.md +136 -0
- package/skills/massa-ai/references/tdd/quality-and-lifecycle.md +83 -0
- package/skills/massa-ai/references/the-fool/cognitive-bias-inventory.md +103 -0
- package/skills/massa-ai/references/the-fool/dialectic-synthesis.md +170 -0
- package/skills/massa-ai/references/the-fool/evidence-audit.md +202 -0
- package/skills/massa-ai/references/the-fool/mode-selection-guide.md +113 -0
- package/skills/massa-ai/references/the-fool/pre-mortem-analysis.md +200 -0
- package/skills/massa-ai/references/the-fool/red-team-adversarial.md +206 -0
- package/skills/massa-ai/references/the-fool/socratic-questioning.md +153 -0
- package/skills/massa-ai/references/ticket/atlassian-fix.md +130 -0
- package/skills/massa-ai/references/ticket/intake-and-sources.md +65 -0
- package/skills/massa-ai/references/ticket/templates-and-quality.md +129 -0
- package/skills/massa-ai/references/verification-ladder.md +62 -0
- package/skills/massa-ai/scripts/lessons.py +590 -0
- package/skills/massa-ai/workflows/adr.md +33 -0
- package/skills/massa-ai/workflows/architecture/architecture-audit.md +125 -0
- package/skills/massa-ai/workflows/architecture/architecture-fix.md +110 -0
- package/skills/massa-ai/workflows/bugs/bugs-audit.md +113 -0
- package/skills/massa-ai/workflows/bugs/bugs-fix.md +97 -0
- package/skills/massa-ai/workflows/code-quality/code-quality-audit.md +154 -0
- package/skills/massa-ai/workflows/code-quality/code-quality-fix.md +99 -0
- package/skills/massa-ai/workflows/commit.md +61 -0
- package/skills/massa-ai/workflows/debug.md +86 -0
- package/skills/massa-ai/workflows/design.md +54 -0
- package/skills/massa-ai/workflows/exploration.md +119 -0
- package/skills/massa-ai/workflows/feature.md +52 -0
- package/skills/massa-ai/workflows/general.md +46 -0
- package/skills/massa-ai/workflows/implementation/implementation-audit.md +87 -0
- package/skills/massa-ai/workflows/implementation/implementation-fix.md +90 -0
- package/skills/massa-ai/workflows/long-session.md +44 -0
- package/skills/massa-ai/workflows/maestro/maestro-audit.md +56 -0
- package/skills/massa-ai/workflows/maestro/maestro-fix.md +74 -0
- package/skills/massa-ai/workflows/maestro/maestro.md +68 -0
- package/skills/massa-ai/workflows/mobile-figma/mobile-figma-audit.md +68 -0
- package/skills/massa-ai/workflows/mobile-figma/mobile-figma-fix.md +74 -0
- package/skills/massa-ai/workflows/onboarding.md +23 -0
- package/skills/massa-ai/workflows/refactor.md +47 -0
- package/skills/massa-ai/workflows/refinement/furps-refinement.md +81 -0
- package/skills/massa-ai/workflows/requirements/requirements-audit.md +114 -0
- package/skills/massa-ai/workflows/requirements/requirements-fix.md +93 -0
- package/skills/massa-ai/workflows/rfc.md +55 -0
- package/skills/massa-ai/workflows/security/security-audit.md +113 -0
- package/skills/massa-ai/workflows/security/security-fix.md +97 -0
- package/skills/massa-ai/workflows/spec-driven.md +217 -0
- package/skills/massa-ai/workflows/tdd.md +71 -0
- package/skills/massa-ai/workflows/tests/tests-audit.md +114 -0
- package/skills/massa-ai/workflows/tests/tests-fix.md +96 -0
- package/skills/massa-ai/workflows/the-fool.md +82 -0
- package/skills/massa-ai/workflows/ticket.md +42 -0
- package/skills/persona-router/SKILL.md +158 -0
package/hooks/_pin.sh
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Shared helper: per-session project id pinning for massa-ai hook scripts.
|
|
3
|
+
#
|
|
4
|
+
# Source this AFTER stdin capture (single-read constraint — plan-critic C3),
|
|
5
|
+
# never before: this helper runs no stdin reads, but callers must have already
|
|
6
|
+
# consumed the hook payload so the POST body stays intact.
|
|
7
|
+
#
|
|
8
|
+
# massa_ai_pin_project_id <session_id> <cwd>
|
|
9
|
+
#
|
|
10
|
+
# echoes the pinned project id. First event of a session computes the id
|
|
11
|
+
# (MASSA_AI_PROJECT_ID env override > git toplevel basename > cwd basename)
|
|
12
|
+
# and writes the pin file; later events of the same session read it back, so a
|
|
13
|
+
# hook fired from a subdirectory keeps the session's original attribution.
|
|
14
|
+
# Without a session id there is nothing to key on — the computed id is echoed
|
|
15
|
+
# without any pin I/O. Silent-degrade: git absence or pin-file failure falls
|
|
16
|
+
# back to the computed id; this helper never fails the caller.
|
|
17
|
+
#
|
|
18
|
+
# Pin files live under ${TMPDIR:-/tmp}/massa-ai-hooks/<sanitized-session_id>.
|
|
19
|
+
|
|
20
|
+
massa_ai_pin_project_id() {
|
|
21
|
+
_mpin_session="$1"
|
|
22
|
+
_mpin_cwd="$2"
|
|
23
|
+
|
|
24
|
+
# Existing pin wins: read it back without recomputing. Sanitization is a
|
|
25
|
+
# lossy but filesystem-safe encoding (distinct ids can collide; real Claude
|
|
26
|
+
# session ids are UUIDs, so this is accepted). "." / ".." are skipped: they
|
|
27
|
+
# name directories, never valid pin files.
|
|
28
|
+
if [ -n "$_mpin_session" ]; then
|
|
29
|
+
_mpin_dir="${TMPDIR:-/tmp}/massa-ai-hooks"
|
|
30
|
+
_mpin_safe=$(printf '%s' "$_mpin_session" | tr -c 'A-Za-z0-9._-' '_')
|
|
31
|
+
case "$_mpin_safe" in
|
|
32
|
+
.|..) _mpin_file="" ;;
|
|
33
|
+
*) _mpin_file="$_mpin_dir/$_mpin_safe" ;;
|
|
34
|
+
esac
|
|
35
|
+
if [ -n "$_mpin_file" ] && [ -f "$_mpin_file" ]; then
|
|
36
|
+
_mpin_pinned=$(cat "$_mpin_file" 2>/dev/null)
|
|
37
|
+
if [ -n "$_mpin_pinned" ]; then
|
|
38
|
+
printf '%s' "$_mpin_pinned"
|
|
39
|
+
return 0
|
|
40
|
+
fi
|
|
41
|
+
fi
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
# Compute the candidate id: env override > git toplevel basename > cwd basename.
|
|
45
|
+
_mpin_computed="${MASSA_AI_PROJECT_ID:-}"
|
|
46
|
+
if [ -z "$_mpin_computed" ]; then
|
|
47
|
+
_mpin_root=$(git -C "$_mpin_cwd" rev-parse --show-toplevel 2>/dev/null) || _mpin_root=""
|
|
48
|
+
if [ -n "$_mpin_root" ]; then
|
|
49
|
+
_mpin_computed=$(basename "$_mpin_root")
|
|
50
|
+
else
|
|
51
|
+
_mpin_computed=$(basename "$_mpin_cwd")
|
|
52
|
+
fi
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
# Pin for later events of this session (best effort). stderr is redirected
|
|
56
|
+
# before stdout so a redirect-open failure (e.g. unwritable dir) stays silent.
|
|
57
|
+
if [ -n "$_mpin_session" ] && [ -n "$_mpin_file" ]; then
|
|
58
|
+
mkdir -p "$_mpin_dir" 2>/dev/null || true
|
|
59
|
+
printf '%s' "$_mpin_computed" 2>/dev/null > "$_mpin_file" || true
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
printf '%s' "$_mpin_computed"
|
|
63
|
+
return 0
|
|
64
|
+
}
|
package/hooks/_post.sh
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Shared helper: POST a lifecycle observation to the massa-ai hook endpoint.
|
|
3
|
+
# Source this from the per-hook scripts after setting:
|
|
4
|
+
# EVENT — the lifecycle event kind
|
|
5
|
+
# PROJECT_ID — the projectId (from env or cwd basename)
|
|
6
|
+
# The Claude Code hook payload is read from stdin as JSON and forwarded as the
|
|
7
|
+
# observation `payload`. The session id is lifted from the payload to the
|
|
8
|
+
# top-level `sessionId` field so rows are session-scoped (NULL when absent).
|
|
9
|
+
# Silent-degrade: never blocks the agent (exit 0, no stdout).
|
|
10
|
+
#
|
|
11
|
+
# Env:
|
|
12
|
+
# MASSA_AI_API_BASE — API base URL (default http://localhost:3333)
|
|
13
|
+
# MASSA_AI_API_KEY — optional API key (x-api-key header)
|
|
14
|
+
# MASSA_AI_PROJECT_ID — optional explicit projectId (else git toplevel
|
|
15
|
+
# basename, else cwd basename; pinned per session)
|
|
16
|
+
|
|
17
|
+
_massa_ai_base="${MASSA_AI_API_BASE:-http://localhost:3333}"
|
|
18
|
+
_massa_ai_url="$_massa_ai_base/api/v1/hook"
|
|
19
|
+
|
|
20
|
+
# Read the Claude Code hook payload from stdin (JSON) — may be empty.
|
|
21
|
+
_massa_ai_raw=""
|
|
22
|
+
if [ -t 0 ]; then
|
|
23
|
+
_massa_ai_raw=""
|
|
24
|
+
else
|
|
25
|
+
_massa_ai_raw=$(cat)
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
# Lift the session id to the top-level `sessionId` field. Claude Code sends
|
|
29
|
+
# "session_id"; accept "sessionId" too. jq preferred, sed fallback otherwise.
|
|
30
|
+
_massa_ai_session=""
|
|
31
|
+
if [ -n "$_massa_ai_raw" ] && command -v jq >/dev/null 2>&1; then
|
|
32
|
+
_massa_ai_session=$(printf '%s' "$_massa_ai_raw" | jq -r '.session_id // .sessionId // ""' 2>/dev/null)
|
|
33
|
+
elif [ -n "$_massa_ai_raw" ]; then
|
|
34
|
+
_massa_ai_session=$(printf '%s' "$_massa_ai_raw" | sed -n 's/.*"session_id"[: ]*"*\([^",}]*\).*/\1/p' 2>/dev/null)
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
# Whitespace-stripped copy for the emptiness check + payload embed. When there
|
|
38
|
+
# is no payload (interactive TTY, empty stdin, or whitespace only), skip the
|
|
39
|
+
# POST entirely: the server rejects an empty payload object
|
|
40
|
+
# ("payload must be a non-empty object" → 400), and a fire-and-forget hook
|
|
41
|
+
# has nothing useful to record without one. Exit 0 so the agent is never blocked.
|
|
42
|
+
_massa_ai_stdin=$(printf '%s' "$_massa_ai_raw" | tr -d '[:space:]')
|
|
43
|
+
[ -z "$_massa_ai_stdin" ] && exit 0
|
|
44
|
+
|
|
45
|
+
# Resolve the project id through the per-session pin (M45/HAR-04). This runs
|
|
46
|
+
# AFTER the stdin capture above — the pin helper never touches stdin, and the
|
|
47
|
+
# POST body must stay intact (single-read constraint). A missing helper falls
|
|
48
|
+
# back to today's env/basename behavior so silent-degrade is preserved.
|
|
49
|
+
_massa_ai_pin_lib="$(dirname "$0")/_pin.sh"
|
|
50
|
+
if [ -f "$_massa_ai_pin_lib" ]; then
|
|
51
|
+
. "$_massa_ai_pin_lib"
|
|
52
|
+
_massa_ai_project=$(massa_ai_pin_project_id "$_massa_ai_session" "$PWD")
|
|
53
|
+
else
|
|
54
|
+
_massa_ai_project="${MASSA_AI_PROJECT_ID:-$(basename "$PWD")}"
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
# Conditionally include the top-level sessionId field. Omit it entirely when
|
|
58
|
+
# absent so the server stores NULL (preserves prior behavior for payloads
|
|
59
|
+
# without a session id, rather than storing an empty string).
|
|
60
|
+
_massa_ai_session_field=""
|
|
61
|
+
if [ -n "$_massa_ai_session" ]; then
|
|
62
|
+
_massa_ai_session_field=$(printf ',"sessionId":"%s"' "$_massa_ai_session")
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
# Build the observation body. We trust the host payload is already valid JSON
|
|
66
|
+
# (Claude Code emits JSON), so we only need to drop it verbatim into payload.
|
|
67
|
+
_massa_ai_body=$(printf '{"event":"%s","projectId":"%s"%s,"payload":%s}' \
|
|
68
|
+
"$EVENT" "$_massa_ai_project" "$_massa_ai_session_field" "$_massa_ai_stdin")
|
|
69
|
+
|
|
70
|
+
# Silent-degrade: if curl is missing or the endpoint is down, just exit 0.
|
|
71
|
+
command -v curl >/dev/null 2>&1 || exit 0
|
|
72
|
+
curl -sS -m 2 -o /dev/null \
|
|
73
|
+
-H "Content-Type: application/json" \
|
|
74
|
+
${MASSA_AI_API_KEY:+-H "x-api-key: $MASSA_AI_API_KEY"} \
|
|
75
|
+
-X POST "$_massa_ai_url" \
|
|
76
|
+
--data "$_massa_ai_body" >/dev/null 2>&1 || true
|
|
77
|
+
exit 0
|
package/hooks/hooks.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"SessionStart": [
|
|
4
|
+
{
|
|
5
|
+
"hooks": [
|
|
6
|
+
{
|
|
7
|
+
"type": "command",
|
|
8
|
+
"command": "bun run \"${CLAUDE_PLUGIN_ROOT}/hooks/massa-ai-hook.ts\" session-start"
|
|
9
|
+
}
|
|
10
|
+
]
|
|
11
|
+
}
|
|
12
|
+
],
|
|
13
|
+
"UserPromptSubmit": [
|
|
14
|
+
{
|
|
15
|
+
"hooks": [
|
|
16
|
+
{
|
|
17
|
+
"type": "command",
|
|
18
|
+
"command": "bun run \"${CLAUDE_PLUGIN_ROOT}/hooks/massa-ai-hook.ts\" user-prompt-submit"
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
],
|
|
23
|
+
"PostToolUse": [
|
|
24
|
+
{
|
|
25
|
+
"hooks": [
|
|
26
|
+
{
|
|
27
|
+
"type": "command",
|
|
28
|
+
"command": "bun run \"${CLAUDE_PLUGIN_ROOT}/hooks/massa-ai-hook.ts\" post-tool-use"
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
],
|
|
33
|
+
"PreCompact": [
|
|
34
|
+
{
|
|
35
|
+
"hooks": [
|
|
36
|
+
{
|
|
37
|
+
"type": "command",
|
|
38
|
+
"command": "bun run \"${CLAUDE_PLUGIN_ROOT}/hooks/massa-ai-hook.ts\" pre-compact"
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
}
|
|
42
|
+
],
|
|
43
|
+
"Stop": [
|
|
44
|
+
{
|
|
45
|
+
"hooks": [
|
|
46
|
+
{
|
|
47
|
+
"type": "command",
|
|
48
|
+
"command": "bun run \"${CLAUDE_PLUGIN_ROOT}/hooks/massa-ai-hook.ts\" stop"
|
|
49
|
+
}
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* massa-ai-hook — single typed Bun binary replacing 7 shell hook scripts.
|
|
4
|
+
*
|
|
5
|
+
* Wave 6 N30 (T20): reads JSON from stdin (Bun-native, no jq), resolves the
|
|
6
|
+
* project id through the per-session pin, and POSTs a lifecycle observation
|
|
7
|
+
* to the massa-ai hook endpoint. Silent-degrade: never blocks the agent
|
|
8
|
+
* (exit 0, no stdout).
|
|
9
|
+
*
|
|
10
|
+
* Subcommands (Claude Code hook event types):
|
|
11
|
+
* session-start → event: "session-start"
|
|
12
|
+
* user-prompt-submit → event: "user-prompt"
|
|
13
|
+
* post-tool-use → event: "post-tool-use"
|
|
14
|
+
* pre-compact → SPECIAL: TWO POSTs (observation + compact-snapshot)
|
|
15
|
+
* stop → event: "session-end"
|
|
16
|
+
*
|
|
17
|
+
* CRITICAL (pre-mortem F3): pre-compact does TWO POSTs:
|
|
18
|
+
* (1) observation to /api/v1/hook (3s timeout, observation body
|
|
19
|
+
* {event, projectId, sessionId, cwd, payload})
|
|
20
|
+
* (2) snapshot to /api/v1/hook/compact-snapshot (5s timeout, snapshot body
|
|
21
|
+
* {sessionId, projectId, persist, cwd})
|
|
22
|
+
* All other subcommands: single POST to /api/v1/hook (2s timeout).
|
|
23
|
+
*
|
|
24
|
+
* Pin resolution order (ported from _pin.sh):
|
|
25
|
+
* existing pin → env (MASSA_AI_PROJECT_ID) → git toplevel basename → cwd basename
|
|
26
|
+
*
|
|
27
|
+
* Terminal stdin (no pipe) → exit 0, no POST (same as shell [ -t 0 ] check).
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { spawnSync } from "child_process";
|
|
31
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, fstatSync } from "fs";
|
|
32
|
+
import path from "path";
|
|
33
|
+
|
|
34
|
+
// ── Event type mapping ──────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
export const EVENT_MAP: Record<string, string> = {
|
|
37
|
+
"session-start": "session-start",
|
|
38
|
+
"user-prompt-submit": "user-prompt",
|
|
39
|
+
"pre-tool-use": "pre-tool-use",
|
|
40
|
+
"post-tool-use": "post-tool-use",
|
|
41
|
+
"pre-compact": "pre-compact",
|
|
42
|
+
stop: "session-end",
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// ── Pin resolution (ported from _pin.sh) ─────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
export function sanitizeSessionId(sessionId: string): string {
|
|
48
|
+
return sessionId.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function getPinDir(): string {
|
|
52
|
+
return `${process.env.TMPDIR || "/tmp"}/massa-ai-hooks`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function getPinFile(sessionId: string): string | null {
|
|
56
|
+
if (!sessionId) return null;
|
|
57
|
+
const safe = sanitizeSessionId(sessionId);
|
|
58
|
+
if (safe === "." || safe === "..") return null;
|
|
59
|
+
return path.join(getPinDir(), safe);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function resolveProjectId(sessionId: string, cwd: string): string {
|
|
63
|
+
// 1. Existing pin wins
|
|
64
|
+
if (sessionId) {
|
|
65
|
+
const pinFile = getPinFile(sessionId);
|
|
66
|
+
if (pinFile) {
|
|
67
|
+
try {
|
|
68
|
+
if (existsSync(pinFile)) {
|
|
69
|
+
const pinned = readFileSync(pinFile, "utf8").trim();
|
|
70
|
+
if (pinned) return pinned;
|
|
71
|
+
}
|
|
72
|
+
} catch {
|
|
73
|
+
// fall through to compute
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 2. Compute: env override > git toplevel basename > cwd basename
|
|
79
|
+
let computed = process.env.MASSA_AI_PROJECT_ID || "";
|
|
80
|
+
if (!computed) {
|
|
81
|
+
try {
|
|
82
|
+
const result = spawnSync("git", ["-C", cwd, "rev-parse", "--show-toplevel"], {
|
|
83
|
+
encoding: "utf8",
|
|
84
|
+
timeout: 2000,
|
|
85
|
+
});
|
|
86
|
+
const root = result.stdout?.trim();
|
|
87
|
+
if (root) {
|
|
88
|
+
computed = path.basename(root);
|
|
89
|
+
}
|
|
90
|
+
} catch {
|
|
91
|
+
// git not available
|
|
92
|
+
}
|
|
93
|
+
if (!computed) {
|
|
94
|
+
computed = path.basename(cwd);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 3. Write pin for later events of this session (best effort)
|
|
99
|
+
if (sessionId) {
|
|
100
|
+
const pinFile = getPinFile(sessionId);
|
|
101
|
+
if (pinFile) {
|
|
102
|
+
try {
|
|
103
|
+
mkdirSync(getPinDir(), { recursive: true });
|
|
104
|
+
writeFileSync(pinFile, computed, { encoding: "utf8" });
|
|
105
|
+
} catch {
|
|
106
|
+
// silent-degrade: pin write failure is non-fatal
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return computed;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ── Stdin reading ───────────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
export function readStdin(): string {
|
|
117
|
+
// Terminal stdin (no pipe) → exit 0, no POST (same as shell [ -t 0 ] check)
|
|
118
|
+
try {
|
|
119
|
+
const stats = fstatSync(0);
|
|
120
|
+
if (stats && stats.isCharacterDevice()) {
|
|
121
|
+
return ""; // terminal stdin → no payload
|
|
122
|
+
}
|
|
123
|
+
} catch {
|
|
124
|
+
// stat failed — assume no stdin available
|
|
125
|
+
return "";
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
return readFileSync(0, "utf8");
|
|
130
|
+
} catch {
|
|
131
|
+
return "";
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ── POST helper ─────────────────────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
export function postObservation(
|
|
138
|
+
url: string,
|
|
139
|
+
body: Record<string, unknown>,
|
|
140
|
+
timeoutMs: number,
|
|
141
|
+
hookType: string = "unknown",
|
|
142
|
+
): Promise<void> {
|
|
143
|
+
// Fire-and-forget with bounded timeout; never rejects (silent-degrade).
|
|
144
|
+
// Returned promise lets main() await POSTs before process.exit so the
|
|
145
|
+
// event loop doesn't tear down pending sockets mid-flight. The caller's
|
|
146
|
+
// exit is still bounded by timeoutMs — never blocks the agent longer than
|
|
147
|
+
// the configured POST timeout.
|
|
148
|
+
try {
|
|
149
|
+
const headers: Record<string, string> = {
|
|
150
|
+
"Content-Type": "application/json",
|
|
151
|
+
};
|
|
152
|
+
const apiKey = process.env.MASSA_AI_API_KEY;
|
|
153
|
+
if (apiKey) {
|
|
154
|
+
headers["x-api-key"] = apiKey;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const startMs = Date.now();
|
|
158
|
+
|
|
159
|
+
return fetch(url, {
|
|
160
|
+
method: "POST",
|
|
161
|
+
headers,
|
|
162
|
+
body: JSON.stringify(body),
|
|
163
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
164
|
+
})
|
|
165
|
+
.then(() => {
|
|
166
|
+
// Breadcrumb-on-fire (W7-13): log to stderr when POST takes >80% of deadline.
|
|
167
|
+
const elapsedMs = Date.now() - startMs;
|
|
168
|
+
const pct = (elapsedMs / timeoutMs) * 100;
|
|
169
|
+
if (pct > 80) {
|
|
170
|
+
process.stderr.write(
|
|
171
|
+
JSON.stringify({
|
|
172
|
+
type: "breadcrumb",
|
|
173
|
+
hook: hookType,
|
|
174
|
+
elapsed: elapsedMs,
|
|
175
|
+
deadline: timeoutMs,
|
|
176
|
+
pct: Math.round(pct * 100) / 100,
|
|
177
|
+
}) + "\n",
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
// response body discarded; fire-and-forget
|
|
181
|
+
})
|
|
182
|
+
.catch((err) => {
|
|
183
|
+
// Breadcrumb-on-fire (W7-13): log deadline-on-fire on timeout.
|
|
184
|
+
const elapsedMs = Date.now() - startMs;
|
|
185
|
+
const isTimeout =
|
|
186
|
+
err instanceof DOMException &&
|
|
187
|
+
(err.name === "TimeoutError" || err.name === "AbortError");
|
|
188
|
+
if (isTimeout) {
|
|
189
|
+
process.stderr.write(
|
|
190
|
+
JSON.stringify({
|
|
191
|
+
type: "deadline-on-fire",
|
|
192
|
+
hook: hookType,
|
|
193
|
+
elapsed: elapsedMs,
|
|
194
|
+
deadline: timeoutMs,
|
|
195
|
+
reason: "timeout",
|
|
196
|
+
}) + "\n",
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
// silent-degrade: network failure is non-fatal
|
|
200
|
+
});
|
|
201
|
+
} catch {
|
|
202
|
+
// silent-degrade: fetch unavailable or immediate error is non-fatal
|
|
203
|
+
return Promise.resolve();
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ── Main ────────────────────────────────────────────────────────────────────
|
|
208
|
+
|
|
209
|
+
export async function main(stdinInput?: string): Promise<void> {
|
|
210
|
+
const subcommand = process.argv[2];
|
|
211
|
+
if (!subcommand || !EVENT_MAP[subcommand]) {
|
|
212
|
+
// Unknown or missing subcommand → exit 0 (silent-degrade)
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const rawStdin = stdinInput ?? readStdin();
|
|
217
|
+
// Whitespace-stripped check: empty payload → no POST (server rejects empty payload)
|
|
218
|
+
const stdinStripped = rawStdin.trim();
|
|
219
|
+
if (!stdinStripped) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Parse JSON (Bun-native, no jq). Malformed JSON → exit 0, no POST.
|
|
224
|
+
let payload: Record<string, unknown>;
|
|
225
|
+
try {
|
|
226
|
+
const parsed = JSON.parse(stdinStripped);
|
|
227
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
payload = parsed as Record<string, unknown>;
|
|
231
|
+
} catch {
|
|
232
|
+
// Malformed JSON → exit 0, no POST (same as shell jq failure)
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const event = EVENT_MAP[subcommand]!;
|
|
237
|
+
const cwd = process.cwd();
|
|
238
|
+
|
|
239
|
+
// Extract session id from payload (Claude Code sends "session_id"; accept "sessionId" too)
|
|
240
|
+
const sessionId =
|
|
241
|
+
(typeof payload.session_id === "string" ? payload.session_id : "") ||
|
|
242
|
+
(typeof payload.sessionId === "string" ? payload.sessionId : "") ||
|
|
243
|
+
"unknown";
|
|
244
|
+
|
|
245
|
+
const projectId = resolveProjectId(sessionId, cwd);
|
|
246
|
+
|
|
247
|
+
const baseUrl = process.env.MASSA_AI_API_BASE || "http://localhost:3333";
|
|
248
|
+
const hookUrl = `${baseUrl}/api/v1/hook`;
|
|
249
|
+
|
|
250
|
+
if (subcommand === "pre-compact") {
|
|
251
|
+
// CRITICAL (F3): pre-compact does TWO POSTs
|
|
252
|
+
// 1. Observation to /api/v1/hook (3s timeout, observation body)
|
|
253
|
+
const obsBody = {
|
|
254
|
+
event,
|
|
255
|
+
projectId,
|
|
256
|
+
sessionId,
|
|
257
|
+
cwd,
|
|
258
|
+
payload,
|
|
259
|
+
};
|
|
260
|
+
const obsP = postObservation(hookUrl, obsBody, 3000, subcommand);
|
|
261
|
+
|
|
262
|
+
// 2. Snapshot to /api/v1/hook/compact-snapshot (5s timeout, snapshot body)
|
|
263
|
+
const snapshotUrl = `${baseUrl}/api/v1/hook/compact-snapshot`;
|
|
264
|
+
const snapBody = {
|
|
265
|
+
sessionId,
|
|
266
|
+
projectId,
|
|
267
|
+
persist: true,
|
|
268
|
+
cwd,
|
|
269
|
+
};
|
|
270
|
+
const snapP = postObservation(snapshotUrl, snapBody, 5000, "compact-snapshot");
|
|
271
|
+
|
|
272
|
+
// Await both POSTs (bounded by their timeouts) before exit so the event
|
|
273
|
+
// loop doesn't tear down pending sockets. Silent-degrade: any rejection
|
|
274
|
+
// is swallowed inside postObservation; exit is still 0.
|
|
275
|
+
await Promise.all([obsP, snapP]);
|
|
276
|
+
} else {
|
|
277
|
+
// All other subcommands: single POST to /api/v1/hook (2s timeout, observation body)
|
|
278
|
+
// The body carries projectId (caller), sessionId, and cwd so the server-
|
|
279
|
+
// side AttributionResolver can run its explicit→sticky→containment→
|
|
280
|
+
// verbatim chain (AC6). The binary does NOT replicate that logic; it
|
|
281
|
+
// sends the raw inputs for server-side resolution.
|
|
282
|
+
const sessionField = sessionId && sessionId !== "unknown" ? { sessionId } : {};
|
|
283
|
+
const obsBody = {
|
|
284
|
+
event,
|
|
285
|
+
projectId,
|
|
286
|
+
...sessionField,
|
|
287
|
+
cwd,
|
|
288
|
+
payload,
|
|
289
|
+
};
|
|
290
|
+
await postObservation(hookUrl, obsBody, 2000, subcommand);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Always exit 0 (silent-degrade: never blocks the agent)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (import.meta.main) {
|
|
297
|
+
main().catch(() => process.exit(0));
|
|
298
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Claude Code PreCompact hook → massa-ai compaction snapshot.
|
|
3
|
+
#
|
|
4
|
+
# Emits a `pre-compact` lifecycle observation AND triggers the compact_snapshot
|
|
5
|
+
# endpoint to build a bounded, reference-based table-of-contents snapshot of
|
|
6
|
+
# the current session's observations. The snapshot provides zero-loss session
|
|
7
|
+
# continuity: raw events stay in the store; the snapshot just points to them.
|
|
8
|
+
#
|
|
9
|
+
# Claude Code passes the session id in the hook payload JSON on stdin.
|
|
10
|
+
# Silent-degrade: never blocks the agent (exit 0, no stdout).
|
|
11
|
+
|
|
12
|
+
_massa_ai_base="${MASSA_AI_API_BASE:-http://localhost:3333}"
|
|
13
|
+
|
|
14
|
+
# Read the Claude Code hook payload from stdin (JSON) — may be empty.
|
|
15
|
+
_massa_ai_stdin=""
|
|
16
|
+
if [ -t 0 ]; then
|
|
17
|
+
_massa_ai_stdin="{}"
|
|
18
|
+
else
|
|
19
|
+
_massa_ai_stdin=$(cat)
|
|
20
|
+
fi
|
|
21
|
+
[ -z "$_massa_ai_stdin" ] && _massa_ai_stdin="{}"
|
|
22
|
+
|
|
23
|
+
# Extract session id from the payload (Claude Code provides "session_id").
|
|
24
|
+
# Use a tiny sed/grep fallback if jq is unavailable.
|
|
25
|
+
_massa_ai_session=""
|
|
26
|
+
if command -v jq >/dev/null 2>&1; then
|
|
27
|
+
_massa_ai_session=$(printf '%s' "$_massa_ai_stdin" | jq -r '.session_id // .sessionId // ""' 2>/dev/null)
|
|
28
|
+
else
|
|
29
|
+
_massa_ai_session=$(printf '%s' "$_massa_ai_stdin" | sed -n 's/.*"session_id"[: ]*"*\([^",}]*\).*/\1/p' 2>/dev/null)
|
|
30
|
+
fi
|
|
31
|
+
# Resolve the project id through the per-session pin (M45/HAR-04) — AFTER the
|
|
32
|
+
# stdin capture above (single-read constraint; the pin helper never reads
|
|
33
|
+
# stdin). A missing helper falls back to today's env/basename behavior.
|
|
34
|
+
_massa_ai_pin_lib="$(dirname "$0")/_pin.sh"
|
|
35
|
+
if [ -f "$_massa_ai_pin_lib" ]; then
|
|
36
|
+
. "$_massa_ai_pin_lib"
|
|
37
|
+
_massa_ai_project=$(massa_ai_pin_project_id "$_massa_ai_session" "$PWD")
|
|
38
|
+
else
|
|
39
|
+
_massa_ai_project="${MASSA_AI_PROJECT_ID:-$(basename "$PWD")}"
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
[ -z "$_massa_ai_session" ] && _massa_ai_session="unknown"
|
|
43
|
+
|
|
44
|
+
# 1. Emit the pre-compact lifecycle observation (backward-compatible with the
|
|
45
|
+
# existing hook ingestion pipeline). cwd travels on the wire so the server
|
|
46
|
+
# attributes both this obs and the compact-snapshot consistently (M45/HAR-01
|
|
47
|
+
# sibling-divergence guard).
|
|
48
|
+
_massa_ai_obs_body=$(printf '{"event":"pre-compact","projectId":"%s","sessionId":"%s","cwd":"%s","payload":%s}' \
|
|
49
|
+
"$_massa_ai_project" "$_massa_ai_session" "$PWD" "$_massa_ai_stdin")
|
|
50
|
+
|
|
51
|
+
command -v curl >/dev/null 2>&1 || exit 0
|
|
52
|
+
curl -sS -m 3 -o /dev/null \
|
|
53
|
+
-H "Content-Type: application/json" \
|
|
54
|
+
${MASSA_AI_API_KEY:+-H "x-api-key: $MASSA_AI_API_KEY"} \
|
|
55
|
+
-X POST "$_massa_ai_base/api/v1/hook" \
|
|
56
|
+
--data "$_massa_ai_obs_body" >/dev/null 2>&1 || true
|
|
57
|
+
|
|
58
|
+
# 2. Trigger the compact_snapshot endpoint to build + persist the snapshot.
|
|
59
|
+
# cwd travels on the wire so server-side attribution containment can map the
|
|
60
|
+
# session to its registered workspace (M45/HAR-01).
|
|
61
|
+
_massa_ai_snap_body=$(printf '{"sessionId":"%s","projectId":"%s","persist":true,"cwd":"%s"}' \
|
|
62
|
+
"$_massa_ai_session" "$_massa_ai_project" "$PWD")
|
|
63
|
+
|
|
64
|
+
curl -sS -m 5 -o /dev/null \
|
|
65
|
+
-H "Content-Type: application/json" \
|
|
66
|
+
${MASSA_AI_API_KEY:+-H "x-api-key: $MASSA_AI_API_KEY"} \
|
|
67
|
+
-X POST "$_massa_ai_base/api/v1/hook/compact-snapshot" \
|
|
68
|
+
--data "$_massa_ai_snap_body" >/dev/null 2>&1 || true
|
|
69
|
+
|
|
70
|
+
exit 0
|
package/hooks/stop.sh
ADDED