@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.
Files changed (180) hide show
  1. package/.claude-plugin/plugin.json +18 -0
  2. package/README.md +74 -0
  3. package/agents/massa-ai-architecture-specialist.md +65 -0
  4. package/agents/massa-ai-audit-specialist.md +81 -0
  5. package/agents/massa-ai-builder.md +67 -0
  6. package/agents/massa-ai-context-curator.md +67 -0
  7. package/agents/massa-ai-documentation-agent.md +65 -0
  8. package/agents/massa-ai-furps-analyst.md +71 -0
  9. package/agents/massa-ai-investigator.md +68 -0
  10. package/agents/massa-ai-mobile-specialist.md +82 -0
  11. package/agents/massa-ai-navigator.md +75 -0
  12. package/agents/massa-ai-plan-critic.md +90 -0
  13. package/agents/massa-ai-planner.md +65 -0
  14. package/agents/massa-ai-requirements-analyst.md +64 -0
  15. package/agents/massa-ai-reviewer.md +66 -0
  16. package/agents/massa-ai-test-engineer.md +66 -0
  17. package/agents/massa-ai-verification-agent.md +65 -0
  18. package/commands/def.md +17 -0
  19. package/commands/find.md +19 -0
  20. package/commands/graph.md +16 -0
  21. package/commands/index.md +19 -0
  22. package/commands/map.md +24 -0
  23. package/commands/status.md +15 -0
  24. package/hooks/README.md +52 -0
  25. package/hooks/_pin.sh +64 -0
  26. package/hooks/_post.sh +77 -0
  27. package/hooks/hooks.json +54 -0
  28. package/hooks/massa-ai-hook.ts +298 -0
  29. package/hooks/post-tool-use.sh +4 -0
  30. package/hooks/pre-compact.sh +70 -0
  31. package/hooks/session-start.sh +5 -0
  32. package/hooks/stop.sh +4 -0
  33. package/hooks/user-prompt-submit.sh +4 -0
  34. package/install.sh +435 -0
  35. package/package.json +32 -0
  36. package/skills/agents/architecture-specialist/SKILL.md +69 -0
  37. package/skills/agents/audit-specialist/SKILL.md +85 -0
  38. package/skills/agents/builder/SKILL.md +71 -0
  39. package/skills/agents/context-curator/SKILL.md +71 -0
  40. package/skills/agents/documentation-agent/SKILL.md +69 -0
  41. package/skills/agents/furps-analyst/SKILL.md +74 -0
  42. package/skills/agents/investigator/SKILL.md +72 -0
  43. package/skills/agents/mobile-specialist/SKILL.md +86 -0
  44. package/skills/agents/navigator/SKILL.md +79 -0
  45. package/skills/agents/plan-critic/SKILL.md +93 -0
  46. package/skills/agents/planner/SKILL.md +69 -0
  47. package/skills/agents/requirements-analyst/SKILL.md +68 -0
  48. package/skills/agents/reviewer/SKILL.md +70 -0
  49. package/skills/agents/test-engineer/SKILL.md +70 -0
  50. package/skills/agents/verification-agent/SKILL.md +69 -0
  51. package/skills/massa-ai/SKILL.md +315 -0
  52. package/skills/massa-ai/personas/README.md +35 -0
  53. package/skills/massa-ai/personas/ai-native-nodejs-cli-architect.md +76 -0
  54. package/skills/massa-ai/personas/catalog.json +157 -0
  55. package/skills/massa-ai/personas/context-skill-harness-engineer-architect.md +74 -0
  56. package/skills/massa-ai/personas/product-manager.md +67 -0
  57. package/skills/massa-ai/personas/senior-mobile-engineer.md +74 -0
  58. package/skills/massa-ai/personas/senior-mobile-qa-automation-engineer.md +75 -0
  59. package/skills/massa-ai/references/adr-authoring.md +189 -0
  60. package/skills/massa-ai/references/agent-orchestration.md +221 -0
  61. package/skills/massa-ai/references/architecture-coupling-lens.md +239 -0
  62. package/skills/massa-ai/references/architecture-deepening-lens.md +136 -0
  63. package/skills/massa-ai/references/architecture-domain-lens.md +186 -0
  64. package/skills/massa-ai/references/architecture-lenses.md +108 -0
  65. package/skills/massa-ai/references/audit-report-io.md +459 -0
  66. package/skills/massa-ai/references/audit-scope.md +103 -0
  67. package/skills/massa-ai/references/code-annotation.md +111 -0
  68. package/skills/massa-ai/references/codebase-investigation.md +96 -0
  69. package/skills/massa-ai/references/context-firewall.md +62 -0
  70. package/skills/massa-ai/references/conversation-feedback.md +104 -0
  71. package/skills/massa-ai/references/debug-diagnosis-loop.md +140 -0
  72. package/skills/massa-ai/references/decision-engine.md +73 -0
  73. package/skills/massa-ai/references/evidence-gate.md +53 -0
  74. package/skills/massa-ai/references/furps/analyst-role.md +49 -0
  75. package/skills/massa-ai/references/furps/checklist.md +92 -0
  76. package/skills/massa-ai/references/furps/intake.md +104 -0
  77. package/skills/massa-ai/references/furps/report-contract.md +140 -0
  78. package/skills/massa-ai/references/hook-enforcement.md +137 -0
  79. package/skills/massa-ai/references/implementation-delivery.md +101 -0
  80. package/skills/massa-ai/references/installation.md +110 -0
  81. package/skills/massa-ai/references/lessons.md +119 -0
  82. package/skills/massa-ai/references/maestro/artifacts-reports.md +69 -0
  83. package/skills/massa-ai/references/maestro/cli-device.md +65 -0
  84. package/skills/massa-ai/references/maestro/cloud.md +67 -0
  85. package/skills/massa-ai/references/maestro/config-env-output.md +76 -0
  86. package/skills/massa-ai/references/maestro/fact-ledger.md +71 -0
  87. package/skills/massa-ai/references/maestro/js-scripting.md +70 -0
  88. package/skills/massa-ai/references/maestro/mcp.md +59 -0
  89. package/skills/massa-ai/references/maestro/patterns.md +96 -0
  90. package/skills/massa-ai/references/maestro/selectors.md +91 -0
  91. package/skills/massa-ai/references/maestro/workspace-execution.md +81 -0
  92. package/skills/massa-ai/references/maestro/yaml-commands.md +203 -0
  93. package/skills/massa-ai/references/maestro.md +47 -0
  94. package/skills/massa-ai/references/mcp-tools.md +296 -0
  95. package/skills/massa-ai/references/memory-policy.md +103 -0
  96. package/skills/massa-ai/references/mobile-context.md +113 -0
  97. package/skills/massa-ai/references/mobile-diagnosis.md +106 -0
  98. package/skills/massa-ai/references/mobile-figma-matcher/ATTRIBUTION.md +5 -0
  99. package/skills/massa-ai/references/mobile-figma-matcher/android-compose.md +13 -0
  100. package/skills/massa-ai/references/mobile-figma-matcher/android-views.md +13 -0
  101. package/skills/massa-ai/references/mobile-figma-matcher/core.md +117 -0
  102. package/skills/massa-ai/references/mobile-figma-matcher/ios-swiftui.md +12 -0
  103. package/skills/massa-ai/references/mobile-figma-matcher/ios-uikit.md +12 -0
  104. package/skills/massa-ai/references/mobile-figma-matcher/kmp-compose-multiplatform.md +14 -0
  105. package/skills/massa-ai/references/mobile-figma-matcher/repository-detection.md +77 -0
  106. package/skills/massa-ai/references/naming-standards.md +47 -0
  107. package/skills/massa-ai/references/pr-task-fix.md +80 -0
  108. package/skills/massa-ai/references/project-context.md +76 -0
  109. package/skills/massa-ai/references/rfc/ATTRIBUTION.md +5 -0
  110. package/skills/massa-ai/references/rfc/discovery-and-sizing.md +120 -0
  111. package/skills/massa-ai/references/rfc/document-contract.md +85 -0
  112. package/skills/massa-ai/references/rfc/quality-and-lifecycle.md +101 -0
  113. package/skills/massa-ai/references/root-cause-scripts.md +97 -0
  114. package/skills/massa-ai/references/spec-driven/artifact-store.md +98 -0
  115. package/skills/massa-ai/references/spec-driven/code-analysis.md +119 -0
  116. package/skills/massa-ai/references/spec-driven/coding-principles.md +80 -0
  117. package/skills/massa-ai/references/spec-driven/context-limits.md +64 -0
  118. package/skills/massa-ai/references/spec-driven/design.md +257 -0
  119. package/skills/massa-ai/references/spec-driven/discuss.md +182 -0
  120. package/skills/massa-ai/references/spec-driven/execute.md +471 -0
  121. package/skills/massa-ai/references/spec-driven/lessons.md +5 -0
  122. package/skills/massa-ai/references/spec-driven/memory.md +214 -0
  123. package/skills/massa-ai/references/spec-driven/specify.md +283 -0
  124. package/skills/massa-ai/references/spec-driven/sub-agents.md +151 -0
  125. package/skills/massa-ai/references/spec-driven/tasks.md +494 -0
  126. package/skills/massa-ai/references/spec-driven/validate.md +397 -0
  127. package/skills/massa-ai/references/subagent-design.md +132 -0
  128. package/skills/massa-ai/references/synapse-policy.md +160 -0
  129. package/skills/massa-ai/references/tdd/calibrated-examples.md +54 -0
  130. package/skills/massa-ai/references/tdd/discovery-and-sizing.md +83 -0
  131. package/skills/massa-ai/references/tdd/document-contract.md +136 -0
  132. package/skills/massa-ai/references/tdd/quality-and-lifecycle.md +83 -0
  133. package/skills/massa-ai/references/the-fool/cognitive-bias-inventory.md +103 -0
  134. package/skills/massa-ai/references/the-fool/dialectic-synthesis.md +170 -0
  135. package/skills/massa-ai/references/the-fool/evidence-audit.md +202 -0
  136. package/skills/massa-ai/references/the-fool/mode-selection-guide.md +113 -0
  137. package/skills/massa-ai/references/the-fool/pre-mortem-analysis.md +200 -0
  138. package/skills/massa-ai/references/the-fool/red-team-adversarial.md +206 -0
  139. package/skills/massa-ai/references/the-fool/socratic-questioning.md +153 -0
  140. package/skills/massa-ai/references/ticket/atlassian-fix.md +130 -0
  141. package/skills/massa-ai/references/ticket/intake-and-sources.md +65 -0
  142. package/skills/massa-ai/references/ticket/templates-and-quality.md +129 -0
  143. package/skills/massa-ai/references/verification-ladder.md +62 -0
  144. package/skills/massa-ai/scripts/lessons.py +590 -0
  145. package/skills/massa-ai/workflows/adr.md +33 -0
  146. package/skills/massa-ai/workflows/architecture/architecture-audit.md +125 -0
  147. package/skills/massa-ai/workflows/architecture/architecture-fix.md +110 -0
  148. package/skills/massa-ai/workflows/bugs/bugs-audit.md +113 -0
  149. package/skills/massa-ai/workflows/bugs/bugs-fix.md +97 -0
  150. package/skills/massa-ai/workflows/code-quality/code-quality-audit.md +154 -0
  151. package/skills/massa-ai/workflows/code-quality/code-quality-fix.md +99 -0
  152. package/skills/massa-ai/workflows/commit.md +61 -0
  153. package/skills/massa-ai/workflows/debug.md +86 -0
  154. package/skills/massa-ai/workflows/design.md +54 -0
  155. package/skills/massa-ai/workflows/exploration.md +119 -0
  156. package/skills/massa-ai/workflows/feature.md +52 -0
  157. package/skills/massa-ai/workflows/general.md +46 -0
  158. package/skills/massa-ai/workflows/implementation/implementation-audit.md +87 -0
  159. package/skills/massa-ai/workflows/implementation/implementation-fix.md +90 -0
  160. package/skills/massa-ai/workflows/long-session.md +44 -0
  161. package/skills/massa-ai/workflows/maestro/maestro-audit.md +56 -0
  162. package/skills/massa-ai/workflows/maestro/maestro-fix.md +74 -0
  163. package/skills/massa-ai/workflows/maestro/maestro.md +68 -0
  164. package/skills/massa-ai/workflows/mobile-figma/mobile-figma-audit.md +68 -0
  165. package/skills/massa-ai/workflows/mobile-figma/mobile-figma-fix.md +74 -0
  166. package/skills/massa-ai/workflows/onboarding.md +23 -0
  167. package/skills/massa-ai/workflows/refactor.md +47 -0
  168. package/skills/massa-ai/workflows/refinement/furps-refinement.md +81 -0
  169. package/skills/massa-ai/workflows/requirements/requirements-audit.md +114 -0
  170. package/skills/massa-ai/workflows/requirements/requirements-fix.md +93 -0
  171. package/skills/massa-ai/workflows/rfc.md +55 -0
  172. package/skills/massa-ai/workflows/security/security-audit.md +113 -0
  173. package/skills/massa-ai/workflows/security/security-fix.md +97 -0
  174. package/skills/massa-ai/workflows/spec-driven.md +217 -0
  175. package/skills/massa-ai/workflows/tdd.md +71 -0
  176. package/skills/massa-ai/workflows/tests/tests-audit.md +114 -0
  177. package/skills/massa-ai/workflows/tests/tests-fix.md +96 -0
  178. package/skills/massa-ai/workflows/the-fool.md +82 -0
  179. package/skills/massa-ai/workflows/ticket.md +42 -0
  180. 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
@@ -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,4 @@
1
+ #!/bin/sh
2
+ # Claude Code PostToolUse hook → massa-ai observation (event: post-tool-use).
3
+ EVENT="post-tool-use"
4
+ . "$(dirname "$0")/_post.sh"
@@ -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
@@ -0,0 +1,5 @@
1
+ #!/bin/sh
2
+ # Claude Code SessionStart hook → massa-ai observation (event: session-start).
3
+ # Wire in .claude/settings.json under hooks.SessionStart.
4
+ EVENT="session-start"
5
+ . "$(dirname "$0")/_post.sh"
package/hooks/stop.sh ADDED
@@ -0,0 +1,4 @@
1
+ #!/bin/sh
2
+ # Claude Code Stop hook → massa-ai observation (event: session-end).
3
+ EVENT="session-end"
4
+ . "$(dirname "$0")/_post.sh"
@@ -0,0 +1,4 @@
1
+ #!/bin/sh
2
+ # Claude Code UserPromptSubmit hook → massa-ai observation (event: user-prompt).
3
+ EVENT="user-prompt"
4
+ . "$(dirname "$0")/_post.sh"