@limina-labs/momentum 0.30.2

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 (182) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +214 -0
  3. package/adapters/antigravity/adapter.js +196 -0
  4. package/adapters/antigravity/hooks.json +39 -0
  5. package/adapters/antigravity/instructions/AGENTS.md +638 -0
  6. package/adapters/antigravity/instructions/header.md +3 -0
  7. package/adapters/antigravity/instructions/surfaces.md +153 -0
  8. package/adapters/antigravity/instructions/vars.json +1 -0
  9. package/adapters/antigravity/scripts/antigravity-hook-adapter.sh +147 -0
  10. package/adapters/antigravity/skills/momentum-orient/SKILL.md +29 -0
  11. package/adapters/antigravity/skills/momentum-reviewer-architecture/SKILL.md +39 -0
  12. package/adapters/antigravity/skills/momentum-reviewer-qa/SKILL.md +38 -0
  13. package/adapters/antigravity/skills/momentum-reviewer-security/SKILL.md +42 -0
  14. package/adapters/antigravity/skills/swarm-supervisor/SKILL.md +75 -0
  15. package/adapters/antigravity/workflows/continue.md +39 -0
  16. package/adapters/antigravity/workflows/dispatch.md +49 -0
  17. package/adapters/antigravity/workflows/handoff.md +38 -0
  18. package/adapters/antigravity/workflows/review-code.md +84 -0
  19. package/adapters/antigravity/workflows/scout.md +42 -0
  20. package/adapters/antigravity/workflows/swarm.md +203 -0
  21. package/adapters/claude-code/adapter.js +148 -0
  22. package/adapters/claude-code/commands/continue.md +82 -0
  23. package/adapters/claude-code/commands/dispatch.md +154 -0
  24. package/adapters/claude-code/commands/handoff.md +85 -0
  25. package/adapters/claude-code/commands/review-code.md +194 -0
  26. package/adapters/claude-code/commands/scout.md +119 -0
  27. package/adapters/claude-code/commands/swarm.md +242 -0
  28. package/adapters/claude-code/instructions/header.md +3 -0
  29. package/adapters/claude-code/instructions/vars.json +1 -0
  30. package/adapters/claude-code/settings.json +36 -0
  31. package/adapters/codex/adapter.js +225 -0
  32. package/adapters/codex/agents/momentum-reviewer-architecture.toml +57 -0
  33. package/adapters/codex/agents/momentum-reviewer-qa.toml +52 -0
  34. package/adapters/codex/agents/momentum-reviewer-security.toml +57 -0
  35. package/adapters/codex/agents/swarm-supervisor.toml +76 -0
  36. package/adapters/codex/commands/continue.md +40 -0
  37. package/adapters/codex/commands/dispatch.md +111 -0
  38. package/adapters/codex/commands/handoff.md +57 -0
  39. package/adapters/codex/commands/scout.md +90 -0
  40. package/adapters/codex/commands/swarm.md +334 -0
  41. package/adapters/codex/hooks.json +36 -0
  42. package/adapters/codex/instructions/AGENTS.md +691 -0
  43. package/adapters/codex/instructions/header.md +3 -0
  44. package/adapters/codex/instructions/surfaces.md +206 -0
  45. package/adapters/codex/instructions/vars.json +1 -0
  46. package/adapters/codex/skills/momentum-orient/SKILL.md +29 -0
  47. package/adapters/opencode/adapter.js +224 -0
  48. package/adapters/opencode/agents/momentum-reviewer-architecture.md +54 -0
  49. package/adapters/opencode/agents/momentum-reviewer-qa.md +48 -0
  50. package/adapters/opencode/agents/momentum-reviewer-security.md +48 -0
  51. package/adapters/opencode/agents/swarm-supervisor.md +51 -0
  52. package/adapters/opencode/commands/continue.md +40 -0
  53. package/adapters/opencode/commands/dispatch.md +111 -0
  54. package/adapters/opencode/commands/handoff.md +57 -0
  55. package/adapters/opencode/commands/review-code.md +194 -0
  56. package/adapters/opencode/commands/scout.md +90 -0
  57. package/adapters/opencode/commands/swarm.md +334 -0
  58. package/adapters/opencode/instructions/AGENTS.md +604 -0
  59. package/adapters/opencode/instructions/header.md +3 -0
  60. package/adapters/opencode/instructions/surfaces.md +119 -0
  61. package/adapters/opencode/instructions/vars.json +1 -0
  62. package/adapters/opencode/plugins/momentum.js +219 -0
  63. package/adapters/opencode/skills/momentum-lanes/SKILL.md +30 -0
  64. package/adapters/opencode/skills/momentum-orient/SKILL.md +29 -0
  65. package/adapters/opencode/skills/momentum-track/SKILL.md +28 -0
  66. package/adapters/opencode/skills/momentum-validate/SKILL.md +31 -0
  67. package/bin/antigravity.js +112 -0
  68. package/bin/ecosystem.js +1302 -0
  69. package/bin/lanes.js +300 -0
  70. package/bin/momentum.js +1857 -0
  71. package/bin/okf.js +72 -0
  72. package/bin/orchestration-commands.js +305 -0
  73. package/bin/state-commands.js +218 -0
  74. package/bin/swarm.js +1104 -0
  75. package/bin/waves.js +147 -0
  76. package/core/adapter-capabilities.md +112 -0
  77. package/core/adapter-parity-matrix.md +170 -0
  78. package/core/commands/brainstorm-idea.md +98 -0
  79. package/core/commands/brainstorm-phase.md +129 -0
  80. package/core/commands/complete-phase.md +130 -0
  81. package/core/commands/ecosystem.md +161 -0
  82. package/core/commands/hotfix.md +64 -0
  83. package/core/commands/initiative.md +119 -0
  84. package/core/commands/lanes.md +94 -0
  85. package/core/commands/log.md +45 -0
  86. package/core/commands/migrate.md +70 -0
  87. package/core/commands/review.md +31 -0
  88. package/core/commands/session.md +79 -0
  89. package/core/commands/start-phase.md +161 -0
  90. package/core/commands/start-project.md +142 -0
  91. package/core/commands/sync-docs.md +81 -0
  92. package/core/commands/systematic-debug.md +46 -0
  93. package/core/commands/track.md +54 -0
  94. package/core/commands/validate.md +96 -0
  95. package/core/ecosystem/layout.md +128 -0
  96. package/core/ecosystem/lib/index.js +245 -0
  97. package/core/ecosystem/lib/initiative.js +258 -0
  98. package/core/ecosystem/lib/pointer.js +198 -0
  99. package/core/ecosystem/lib/state.js +295 -0
  100. package/core/ecosystem/schema/ecosystem.schema.json +85 -0
  101. package/core/ecosystem/schema/initiative.schema.json +61 -0
  102. package/core/ecosystem/scripts/session-append.sh +167 -0
  103. package/core/ecosystem/templates/ecosystem-agents.md +79 -0
  104. package/core/ecosystem/templates/ecosystem-claude.md +79 -0
  105. package/core/ecosystem/templates/ecosystem-settings-claude.json +14 -0
  106. package/core/ecosystem/templates/initiative-template.md +50 -0
  107. package/core/engines/subagent-dispatch.md +80 -0
  108. package/core/git-hooks/commit-msg +14 -0
  109. package/core/git-hooks/contract.js +176 -0
  110. package/core/git-hooks/pre-push +15 -0
  111. package/core/git-hooks/run-check.js +160 -0
  112. package/core/instructions/legacy-project-md-hashes.json +12 -0
  113. package/core/instructions/navigation.md +14 -0
  114. package/core/instructions/rules-body.md +454 -0
  115. package/core/lanes/lib/board.js +161 -0
  116. package/core/lanes/lib/land.js +301 -0
  117. package/core/lanes/lib/signals.js +207 -0
  118. package/core/lanes/lib/state.js +325 -0
  119. package/core/lib/frontmatter.js +163 -0
  120. package/core/lib/okf-migrate.js +374 -0
  121. package/core/lib/okf-types.js +61 -0
  122. package/core/lifecycle-contract.md +59 -0
  123. package/core/orchestration/capability-routing.js +120 -0
  124. package/core/orchestration/continue.js +102 -0
  125. package/core/orchestration/dispatch.js +366 -0
  126. package/core/orchestration/events.js +197 -0
  127. package/core/orchestration/handoff.js +317 -0
  128. package/core/orchestration/index.js +50 -0
  129. package/core/orchestration/run-artifact.js +93 -0
  130. package/core/orchestration/scout.js +327 -0
  131. package/core/orchestration/session-log.js +123 -0
  132. package/core/orchestration/tracking.js +180 -0
  133. package/core/orchestration/types.js +131 -0
  134. package/core/scripts/brainstorm-gate.sh +157 -0
  135. package/core/scripts/check-history-reminder.sh +110 -0
  136. package/core/scripts/sessionstart-handoff.sh +180 -0
  137. package/core/specs-templates/CLAUDE.md +484 -0
  138. package/core/specs-templates/README.md +32 -0
  139. package/core/specs-templates/specs/README.md +30 -0
  140. package/core/specs-templates/specs/adhoc/README.md +31 -0
  141. package/core/specs-templates/specs/adhoc/_TEMPLATE.md +29 -0
  142. package/core/specs-templates/specs/architecture/ecosystem.md +214 -0
  143. package/core/specs-templates/specs/backlog/backlog.md +46 -0
  144. package/core/specs-templates/specs/backlog/details/.gitkeep +0 -0
  145. package/core/specs-templates/specs/changelog/.gitkeep +0 -0
  146. package/core/specs-templates/specs/decisions/0000-template.md +33 -0
  147. package/core/specs-templates/specs/decisions/README.md +29 -0
  148. package/core/specs-templates/specs/decisions/impact-map.md +14 -0
  149. package/core/specs-templates/specs/decisions/index.md +4 -0
  150. package/core/specs-templates/specs/index.md +16 -0
  151. package/core/specs-templates/specs/phases/README.md +41 -0
  152. package/core/specs-templates/specs/phases/index.md +6 -0
  153. package/core/specs-templates/specs/planning/roadmap.md +23 -0
  154. package/core/specs-templates/specs/status.md +70 -0
  155. package/core/specs-templates/specs/vision/principles.md +16 -0
  156. package/core/specs-templates/specs/vision/project-charter.md +35 -0
  157. package/core/specs-templates/specs/vision/success-criteria.md +19 -0
  158. package/core/swarm/absorb.js +294 -0
  159. package/core/swarm/conductor.js +499 -0
  160. package/core/swarm/focus.js +151 -0
  161. package/core/swarm/inbox.js +222 -0
  162. package/core/swarm/join.js +138 -0
  163. package/core/swarm/lib/board.js +178 -0
  164. package/core/swarm/lib/brief.js +157 -0
  165. package/core/swarm/lib/git-sha-cache.js +71 -0
  166. package/core/swarm/lib/incremental-log.js +117 -0
  167. package/core/swarm/lib/manifest.js +578 -0
  168. package/core/swarm/lib/pre-merge.js +112 -0
  169. package/core/swarm/lib/saga.js +146 -0
  170. package/core/swarm/lib/sessions.js +83 -0
  171. package/core/swarm/lib/tokens.js +255 -0
  172. package/core/swarm/lib/wave-ordering.js +55 -0
  173. package/core/swarm/schema/board.schema.json +54 -0
  174. package/core/swarm/schema/contract.schema.json +56 -0
  175. package/core/swarm/schema/dispatch-run.schema.json +70 -0
  176. package/core/swarm/schema/manifest.schema.json +234 -0
  177. package/core/swarm/schema/signal.schema.json +61 -0
  178. package/core/swarm/signals.js +296 -0
  179. package/core/swarm/supervise.md +81 -0
  180. package/core/waves/lib/plan-graph.js +175 -0
  181. package/core/waves/lib/waves.js +83 -0
  182. package/package.json +46 -0
@@ -0,0 +1,131 @@
1
+ 'use strict';
2
+
3
+ // Structured result types for orchestration primitives.
4
+ //
5
+ // JSDoc-based — momentum is zero-dependency Node.js, no TypeScript at
6
+ // build time. Helpers below provide light runtime shape validation
7
+ // used by primitive implementations + tests.
8
+
9
+ /**
10
+ * @typedef {object} Finding
11
+ * @property {'discovery'|'decision'|'note'} kind
12
+ * @property {string} title Short human-readable title
13
+ * @property {string} [detail] Multi-sentence description
14
+ * @property {string} [recommendedBacklogPriority] P0|P1|P2|P3 if a backlog entry is warranted
15
+ * @property {string} [recommendedBacklogType] bug|tech-debt|enhancement|feature
16
+ * @property {string[]} [filesTouched]
17
+ */
18
+
19
+ /**
20
+ * @typedef {object} ScoutResult
21
+ * @property {string} repo target repo path (absolute or relative to originating CWD)
22
+ * @property {string} prompt user-provided scope prompt
23
+ * @property {string} summary structured summary answering the prompt
24
+ * @property {Finding[]} findings notable findings surfaced during the scout
25
+ * @property {string[]} filesRead files inspected
26
+ * @property {number} duration wall-clock milliseconds
27
+ * @property {string} runArtifactPath absolute path to scout-NNN.md artifact
28
+ * @property {string} [sessionLogRef] YYYY-MM-DD reference into ecosystem session log
29
+ * @property {object} [error] present only when scout failed
30
+ */
31
+
32
+ /**
33
+ * @typedef {object} PerRepoResult
34
+ * @property {string} repo
35
+ * @property {string} prompt per-repo tailored prompt
36
+ * @property {string} [summary]
37
+ * @property {Finding[]} [findings]
38
+ * @property {string[]} [filesRead]
39
+ * @property {number} [duration]
40
+ * @property {object} [error] present on failure
41
+ */
42
+
43
+ /**
44
+ * @typedef {object} DispatchResult
45
+ * @property {string[]} repos
46
+ * @property {string} userIntent
47
+ * @property {'parallel'|'sequential'} mode
48
+ * @property {string[]} modeNotes degraded-mode labels surfaced to the user
49
+ * @property {PerRepoResult[]} perRepoResults
50
+ * @property {PerRepoResult[]} failures sub-agents that crashed; their entries also appear in perRepoResults with `error`
51
+ * @property {string} synthesis top-level answer to userIntent
52
+ * @property {number} duration
53
+ * @property {string} runArtifactPath
54
+ * @property {string} [sessionLogRef]
55
+ */
56
+
57
+ /**
58
+ * @typedef {object} HandoffBlock
59
+ * @property {string} fromRepo
60
+ * @property {string} toRepo
61
+ * @property {string} summary
62
+ * @property {string[]} decisions
63
+ * @property {string[]} filesTouched
64
+ * @property {string[]} verificationCommands
65
+ * @property {string[]} openQuestions
66
+ * @property {string} originatingPhase e.g. "phase-11-orchestration-handover"
67
+ * @property {string} originatingHistoryRef e.g. "[DECISION] 2026-06-07 — Handoff #042"
68
+ * @property {string} inboxPath absolute path to .momentum/inbox/handoff-NNN.md
69
+ * @property {string} handoffId monotonic id, e.g. "042"
70
+ * @property {string} createdAt ISO timestamp
71
+ */
72
+
73
+ // ── Runtime shape validation (light) ────────────────────────────────────────
74
+ //
75
+ // These helpers exist to catch obvious shape mistakes in tests and at
76
+ // runtime — not as a full type system. Validation is best-effort.
77
+
78
+ function assertString(value, name) {
79
+ if (typeof value !== 'string') {
80
+ throw new TypeError(`orchestration/types: ${name} must be a string, got ${typeof value}`);
81
+ }
82
+ }
83
+
84
+ function assertArray(value, name) {
85
+ if (!Array.isArray(value)) {
86
+ throw new TypeError(`orchestration/types: ${name} must be an array, got ${typeof value}`);
87
+ }
88
+ }
89
+
90
+ function validateScoutResult(result) {
91
+ assertString(result.repo, 'ScoutResult.repo');
92
+ assertString(result.prompt, 'ScoutResult.prompt');
93
+ assertString(result.summary, 'ScoutResult.summary');
94
+ assertArray(result.findings, 'ScoutResult.findings');
95
+ assertArray(result.filesRead, 'ScoutResult.filesRead');
96
+ if (typeof result.duration !== 'number') {
97
+ throw new TypeError('ScoutResult.duration must be a number');
98
+ }
99
+ return result;
100
+ }
101
+
102
+ function validateDispatchResult(result) {
103
+ assertArray(result.repos, 'DispatchResult.repos');
104
+ assertString(result.userIntent, 'DispatchResult.userIntent');
105
+ if (result.mode !== 'parallel' && result.mode !== 'sequential') {
106
+ throw new TypeError(`DispatchResult.mode must be 'parallel'|'sequential', got ${result.mode}`);
107
+ }
108
+ assertArray(result.modeNotes, 'DispatchResult.modeNotes');
109
+ assertArray(result.perRepoResults, 'DispatchResult.perRepoResults');
110
+ assertArray(result.failures, 'DispatchResult.failures');
111
+ assertString(result.synthesis, 'DispatchResult.synthesis');
112
+ return result;
113
+ }
114
+
115
+ function validateHandoffBlock(block) {
116
+ assertString(block.fromRepo, 'HandoffBlock.fromRepo');
117
+ assertString(block.toRepo, 'HandoffBlock.toRepo');
118
+ assertString(block.summary, 'HandoffBlock.summary');
119
+ assertArray(block.decisions, 'HandoffBlock.decisions');
120
+ assertArray(block.filesTouched, 'HandoffBlock.filesTouched');
121
+ assertArray(block.verificationCommands, 'HandoffBlock.verificationCommands');
122
+ assertArray(block.openQuestions, 'HandoffBlock.openQuestions');
123
+ assertString(block.handoffId, 'HandoffBlock.handoffId');
124
+ return block;
125
+ }
126
+
127
+ module.exports = {
128
+ validateScoutResult,
129
+ validateDispatchResult,
130
+ validateHandoffBlock,
131
+ };
@@ -0,0 +1,157 @@
1
+ #!/usr/bin/env bash
2
+ # brainstorm-gate.sh — PreToolUse hook (shared across Claude Code + Codex + Antigravity)
3
+ #
4
+ # Blocks write-class tool calls targeting paths under specs/ while a
5
+ # brainstorm session is active (indicated by the .momentum/brainstorm-active
6
+ # sentinel file).
7
+ #
8
+ # Phase 16 Rework: promoted from adapters/claude-code/scripts/ to
9
+ # core/scripts/ and generalized to handle all three adapters' native tool
10
+ # names and payload shapes.
11
+ #
12
+ # Tool names matched per platform:
13
+ # Claude Code: Write, Edit, MultiEdit
14
+ # Codex: apply_patch, Bash
15
+ # Antigravity: run_command, view_file, *write*, apply_patch
16
+ #
17
+ # Project root resolution (in order):
18
+ # 1. MOMENTUM_PROJECT_DIR (explicit override)
19
+ # 2. CLAUDE_PROJECT_DIR (Claude Code sets this)
20
+ # 3. PWD (Codex + Antigravity hooks run with cwd = session root)
21
+ #
22
+ # Exit codes:
23
+ # 0 — allow the tool call (default; also used for fail-open paths)
24
+ # 2 — block the tool call (stderr is shown to the model)
25
+
26
+ set -euo pipefail
27
+
28
+ # ---------------------------------------------------------------------------
29
+ # 1. Read hook input (JSON on stdin)
30
+ # ---------------------------------------------------------------------------
31
+ input=$(cat)
32
+
33
+ # ---------------------------------------------------------------------------
34
+ # 2. Extract tool_name + file path from payload.
35
+ #
36
+ # Payload shapes we handle:
37
+ # - Claude Code: {tool_name, tool_input: {file_path: "..."}}
38
+ # - Codex apply_patch: {tool_name: "apply_patch", tool_input: {input: "*** Update File: <path>\n..."}}
39
+ # - Codex Bash: {tool_name: "Bash", tool_input: {command: "..."}}
40
+ # - Antigravity: {tool_name, tool_input: {path: "...", command?: "..."}}
41
+ #
42
+ # Strategy: try jq first (best precision), fall back to grep/sed.
43
+ # Extract `file_path` (Claude / Antigravity) OR `path` (Antigravity) OR
44
+ # the first `*** Update File:` / `*** Add File:` line from `input` (Codex apply_patch).
45
+ # ---------------------------------------------------------------------------
46
+ if command -v jq >/dev/null 2>&1; then
47
+ tool_name=$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null || true)
48
+ file_path=$(printf '%s' "$input" | jq -r '.tool_input.file_path // empty' 2>/dev/null || true)
49
+ if [[ -z "${file_path:-}" ]]; then
50
+ # Antigravity uses `path` instead of `file_path` for some tools
51
+ file_path=$(printf '%s' "$input" | jq -r '.tool_input.path // empty' 2>/dev/null || true)
52
+ fi
53
+ if [[ -z "${file_path:-}" ]]; then
54
+ # Codex apply_patch — extract from input
55
+ patch_input=$(printf '%s' "$input" | jq -r '.tool_input.input // empty' 2>/dev/null || true)
56
+ if [[ -n "$patch_input" ]]; then
57
+ file_path=$(printf '%s' "$patch_input" \
58
+ | grep -oE '\*\*\* (Update|Add|Delete) File: [^ ]+' \
59
+ | head -1 \
60
+ | sed -E 's/\*\*\* (Update|Add|Delete) File: //' || true)
61
+ fi
62
+ fi
63
+ if [[ -z "${file_path:-}" ]]; then
64
+ # Last resort — look for shell command targeting specs/
65
+ cmd=$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null || true)
66
+ if [[ -n "$cmd" ]]; then
67
+ # Match: `> specs/path`, `>> specs/path`, `echo ... specs/path`, etc.
68
+ file_path=$(printf '%s' "$cmd" \
69
+ | grep -oE '(specs/[^[:space:]"'"'"'\\]+)' \
70
+ | head -1 || true)
71
+ fi
72
+ fi
73
+ else
74
+ # No jq — regex fallbacks. Brittle but adequate for canonical shapes.
75
+ tool_name=$(printf '%s' "$input" \
76
+ | grep -oE '"tool_name"[[:space:]]*:[[:space:]]*"[^"]*"' \
77
+ | head -1 \
78
+ | sed -E 's/.*:[[:space:]]*"([^"]+)"$/\1/' || true)
79
+ file_path=$(printf '%s' "$input" \
80
+ | grep -oE '"file_path"[[:space:]]*:[[:space:]]*"[^"]*"' \
81
+ | head -1 \
82
+ | sed -E 's/.*:[[:space:]]*"([^"]+)"$/\1/' || true)
83
+ if [[ -z "${file_path:-}" ]]; then
84
+ file_path=$(printf '%s' "$input" \
85
+ | grep -oE '"path"[[:space:]]*:[[:space:]]*"[^"]*"' \
86
+ | head -1 \
87
+ | sed -E 's/.*:[[:space:]]*"([^"]+)"$/\1/' || true)
88
+ fi
89
+ if [[ -z "${file_path:-}" ]]; then
90
+ # Codex apply_patch path extraction (string-mode escapes)
91
+ file_path=$(printf '%s' "$input" \
92
+ | grep -oE '\*\*\* (Update|Add|Delete) File: [^\\"]+' \
93
+ | head -1 \
94
+ | sed -E 's/\*\*\* (Update|Add|Delete) File: //' || true)
95
+ fi
96
+ fi
97
+
98
+ # ---------------------------------------------------------------------------
99
+ # 3. Allow non-write tool calls
100
+ # Match Claude Code (Write/Edit/MultiEdit), Codex (apply_patch/Bash),
101
+ # Antigravity (run_command/view_file/*write*/apply_patch).
102
+ # ---------------------------------------------------------------------------
103
+ case "${tool_name:-}" in
104
+ Write|Edit|MultiEdit) ;; # Claude Code
105
+ apply_patch|Bash) ;; # Codex (per docs: canonical tool_name is "Bash" for shell)
106
+ run_command|view_file) ;; # Antigravity (matcher includes these)
107
+ *write*) ;; # Antigravity wildcard
108
+ *) exit 0 ;;
109
+ esac
110
+
111
+ # ---------------------------------------------------------------------------
112
+ # 4. Resolve project root.
113
+ # MOMENTUM_PROJECT_DIR (override) → CLAUDE_PROJECT_DIR → PWD.
114
+ # ---------------------------------------------------------------------------
115
+ project_dir="${MOMENTUM_PROJECT_DIR:-${CLAUDE_PROJECT_DIR:-${PWD:-}}}"
116
+ if [[ -z "${project_dir}" || ! -d "${project_dir}" ]]; then
117
+ exit 0
118
+ fi
119
+
120
+ # ---------------------------------------------------------------------------
121
+ # 5. Sentinel check — if the brainstorm-active sentinel is missing, allow.
122
+ # ---------------------------------------------------------------------------
123
+ sentinel="${project_dir}/.momentum/brainstorm-active"
124
+ if [[ ! -e "$sentinel" ]]; then
125
+ exit 0
126
+ fi
127
+
128
+ # ---------------------------------------------------------------------------
129
+ # 6. Path check — only block writes under specs/.
130
+ # ---------------------------------------------------------------------------
131
+ if [[ -z "${file_path:-}" ]]; then
132
+ exit 0 # fail-open if path missing
133
+ fi
134
+
135
+ # Resolve relative paths against project_dir
136
+ case "$file_path" in
137
+ /*) abs_path="$file_path" ;;
138
+ *) abs_path="${project_dir}/${file_path}" ;;
139
+ esac
140
+
141
+ specs_dir="${project_dir}/specs/"
142
+ case "$abs_path" in
143
+ "${specs_dir}"*)
144
+ cat >&2 <<EOF
145
+ [brainstorm-gate] Blocked: cannot write to specs/ during active brainstorm.
146
+ [brainstorm-gate] Path: ${file_path}
147
+ [brainstorm-gate]
148
+ [brainstorm-gate] The conversation IS the draft. Get explicit user approval, then:
149
+ [brainstorm-gate] rm .momentum/brainstorm-active
150
+ [brainstorm-gate] and retry the write.
151
+ EOF
152
+ exit 2
153
+ ;;
154
+ *)
155
+ exit 0
156
+ ;;
157
+ esac
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env bash
2
+ # Read-only hook: reminds Claude to log history when significant files
3
+ # change, AND best-effort appends to today's ecosystem session log when
4
+ # this repo is a registered member.
5
+
6
+ set -euo pipefail
7
+
8
+ INPUT=$(cat)
9
+ TOOL_NAME=$(echo "$INPUT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('tool_name',''))" 2>/dev/null || echo "")
10
+ FILE_PATH=$(echo "$INPUT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('tool_input',{}).get('file_path',''))" 2>/dev/null || echo "")
11
+ TOOL_COMMAND=$(echo "$INPUT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('tool_input',{}).get('command',''))" 2>/dev/null || echo "")
12
+
13
+ # ── Phase-history reminder (file edits) ──────────────────────────────────
14
+
15
+ if [ -n "$FILE_PATH" ]; then
16
+ SIGNIFICANT=false
17
+ case "$FILE_PATH" in
18
+ specs/decisions/*.md) SIGNIFICANT=true ;;
19
+ specs/phases/*/tasks.md) SIGNIFICANT=true ;;
20
+ specs/adhoc/*/record.md) SIGNIFICANT=true ;;
21
+ specs/backlog/backlog.md) SIGNIFICANT=true ;;
22
+ specs/status.md) SIGNIFICANT=true ;;
23
+ specs/architecture/*.md) SIGNIFICANT=true ;;
24
+ esac
25
+
26
+ if [ "$SIGNIFICANT" = "true" ]; then
27
+ # ── Lane binding (Rule 15): resolve the phase from the current branch.
28
+ # phase-* branch with a matching specs/phases/<branch>/ dir wins;
29
+ # non-phase branch → ad-hoc sink; detached HEAD or no match → fall back
30
+ # to the status.md Active Phase table (first row with an existing dir).
31
+ BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || echo "")
32
+ PHASE_DIR=""
33
+ PHASE_SOURCE=""
34
+ if [ -n "$BRANCH" ]; then
35
+ case "$BRANCH" in
36
+ phase-*)
37
+ if [ -d "specs/phases/$BRANCH" ]; then
38
+ PHASE_DIR="specs/phases/$BRANCH"
39
+ PHASE_SOURCE="branch"
40
+ fi
41
+ ;;
42
+ *)
43
+ PHASE_SOURCE="adhoc"
44
+ ;;
45
+ esac
46
+ fi
47
+ if [ -z "$PHASE_DIR" ] && [ "$PHASE_SOURCE" != "adhoc" ] && [ -f "specs/status.md" ]; then
48
+ while IFS= read -r candidate; do
49
+ if [ -d "specs/phases/$candidate" ]; then
50
+ PHASE_DIR="specs/phases/$candidate"
51
+ PHASE_SOURCE="status"
52
+ break
53
+ fi
54
+ done < <(awk '/^## Active Phase/{f=1;next} /^## /{f=0} f' specs/status.md \
55
+ | grep -E '^\|' | grep -oE '\`phase-[A-Za-z0-9._-]+\`' | tr -d '\`' || true)
56
+ fi
57
+
58
+ if [ "$PHASE_SOURCE" = "branch" ]; then
59
+ echo "HISTORY REMINDER: '$FILE_PATH' was modified — if this reflects a decision, scope change, or discovery, append an entry to your lane's history file ($PHASE_DIR/history.md — the phase bound to branch '$BRANCH', Rule 15)."
60
+ elif [ "$PHASE_SOURCE" = "status" ]; then
61
+ echo "HISTORY REMINDER: '$FILE_PATH' was modified — if this reflects a decision, scope change, or discovery, append an entry to the active phase history ($PHASE_DIR/history.md — resolved from the status.md Active Phase table; the current branch does not bind to a phase directory)."
62
+ elif [ "$PHASE_SOURCE" = "adhoc" ]; then
63
+ echo "HISTORY REMINDER: '$FILE_PATH' was modified on non-phase branch '$BRANCH' — if this reflects a decision, scope change, or discovery, append an entry to the ad-hoc record (specs/adhoc/<id>/record.md) or specs/adhoc/history.md."
64
+ else
65
+ echo "HISTORY REMINDER: '$FILE_PATH' was modified — if this reflects a decision, scope change, or discovery, append an entry to the active phase history file (specs/phases/<active-phase>/history.md), or — when no phase is active — to the ad-hoc record (specs/adhoc/<id>/record.md) or specs/adhoc/history.md."
66
+ fi
67
+ fi
68
+ fi
69
+
70
+ # ── Ecosystem session-log auto-append (best-effort) ─────────────────────
71
+ # When this hook fires after a `git commit` or `gh pr ...` Bash command,
72
+ # record one line to today's session log if this repo is a registered
73
+ # ecosystem member. Silent no-op otherwise (no ecosystem, no member id,
74
+ # no helper script — all cases just exit cleanly).
75
+
76
+ session_append() {
77
+ local script_dir
78
+ script_dir=$(dirname "$(readlink -f "$0" 2>/dev/null || echo "$0")")
79
+ local session_script=""
80
+ # Try common locations: peer ecosystem/scripts/ dir,
81
+ # in-repo nested copy, or sibling.
82
+ for candidate in \
83
+ "$script_dir/../ecosystem/scripts/session-append.sh" \
84
+ "$script_dir/ecosystem/scripts/session-append.sh" \
85
+ "$script_dir/session-append.sh"; do
86
+ if [ -x "$candidate" ]; then
87
+ session_script="$candidate"
88
+ break
89
+ fi
90
+ done
91
+ [ -z "$session_script" ] && return 0
92
+ "$session_script" "$@" 2>/dev/null || true
93
+ }
94
+
95
+ if [ "$TOOL_NAME" = "Bash" ] && [ -n "$TOOL_COMMAND" ]; then
96
+ case "$TOOL_COMMAND" in
97
+ *"git commit"*)
98
+ sha=$(git rev-parse --short HEAD 2>/dev/null || echo "")
99
+ subject=$(git log -1 --pretty=%s 2>/dev/null || echo "")
100
+ if [ -n "$sha" ] && [ -n "$subject" ]; then
101
+ session_append "commit" "$subject" "$sha"
102
+ fi
103
+ ;;
104
+ *"gh pr merge"*|*"gh pr create"*)
105
+ session_append "pr" "$(echo "$TOOL_COMMAND" | head -c 80)" ""
106
+ ;;
107
+ esac
108
+ fi
109
+
110
+ exit 0
@@ -0,0 +1,180 @@
1
+ #!/usr/bin/env bash
2
+ # SessionStart hook — momentum context + handoff auto-greet.
3
+ #
4
+ # Two banners, printed in this order to stderr:
5
+ #
6
+ # 1. Ecosystem context (ENH-033, Phase 15) — when CWD is reachable
7
+ # from an ecosystem.json (parent walk + sibling scan), one or two
8
+ # lines naming the ecosystem, member count, and active initiative.
9
+ #
10
+ # 2. Handoff inbox — when `.momentum/inbox/handoff-*.md` exists, one
11
+ # line per pending handoff. Final line prompts the user
12
+ # `Read now? [y/skip]`; on `y` the script exits 10 to signal the
13
+ # adapter that the next agent action should be `/continue` (or
14
+ # `momentum continue`). On any other input or non-TTY runs, exits
15
+ # 0 silently — the handoff stays in the inbox until the user runs
16
+ # `/continue` themselves.
17
+ #
18
+ # Both banners are silent when not relevant. Total cost target <100ms.
19
+ #
20
+ # Adapter wiring:
21
+ # - Claude Code: .claude/settings.json SessionStart entry → this script
22
+ # - Codex: .codex/hooks.json SessionStart entry → this script
23
+ # - Antigravity: no SessionStart hook surface; banners ship via
24
+ # primary instruction text (see overlay).
25
+
26
+ set -eu
27
+
28
+ # ─────────────────────────────────────────────────────────────────────────────
29
+ # Banner 1 — Ecosystem context (ENH-033)
30
+ # ─────────────────────────────────────────────────────────────────────────────
31
+ #
32
+ # Mirror the parent-walk + sibling-scan algorithm from
33
+ # core/ecosystem/scripts/session-append.sh so what the session log
34
+ # considers "the reachable ecosystem" is what the SessionStart banner
35
+ # announces.
36
+
37
+ find_ecosystem_root() {
38
+ local current="$PWD"
39
+ local depth=0
40
+ local max_depth="${MOMENTUM_MAX_PARENT_WALK:-5}"
41
+ case "$max_depth" in
42
+ ''|*[!0-9]*) max_depth=5 ;;
43
+ esac
44
+ while [ $depth -le $max_depth ]; do
45
+ # Same-directory check (caller might already be in ecosystem root)
46
+ if [ -f "$current/ecosystem.json" ]; then
47
+ echo "$current"
48
+ return 0
49
+ fi
50
+ # Sibling check
51
+ local parent
52
+ parent=$(dirname "$current")
53
+ if [ "$parent" = "$current" ]; then return 1; fi
54
+ for sibling in "$parent"/*; do
55
+ if [ -d "$sibling" ] && [ -f "$sibling/ecosystem.json" ]; then
56
+ echo "$sibling"
57
+ return 0
58
+ fi
59
+ done
60
+ current="$parent"
61
+ depth=$((depth + 1))
62
+ done
63
+ return 1
64
+ }
65
+
66
+ # Best-effort: silent on any failure so a broken ecosystem.json never
67
+ # blocks the session.
68
+ ECO_ROOT="$(find_ecosystem_root 2>/dev/null || true)"
69
+
70
+ if [ -n "${ECO_ROOT:-}" ] && [ -f "$ECO_ROOT/ecosystem.json" ]; then
71
+ # Extract name + member count. Prefer python3 for JSON parsing; fall
72
+ # back to grep/sed when python3 isn't available (rare but possible).
73
+ if command -v python3 >/dev/null 2>&1; then
74
+ eco_summary=$(python3 - "$ECO_ROOT/ecosystem.json" <<'PY' 2>/dev/null || echo ""
75
+ import sys, json
76
+ try:
77
+ with open(sys.argv[1]) as f:
78
+ m = json.load(f)
79
+ name = m.get("name", "(unnamed)")
80
+ members = len(m.get("members", []) or [])
81
+ print(f"{name}\t{members}")
82
+ except Exception:
83
+ pass
84
+ PY
85
+ )
86
+ else
87
+ # Crude fallback — extract `"name": "..."` and count `"id":` entries.
88
+ eco_name=$(sed -nE 's/.*"name"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/p' "$ECO_ROOT/ecosystem.json" | head -1)
89
+ eco_members=$(grep -cE '"id"[[:space:]]*:' "$ECO_ROOT/ecosystem.json" || echo 0)
90
+ eco_summary="${eco_name:-(unnamed)} ${eco_members}"
91
+ fi
92
+
93
+ if [ -n "${eco_summary:-}" ]; then
94
+ eco_name=$(printf '%s' "$eco_summary" | cut -f1)
95
+ eco_members=$(printf '%s' "$eco_summary" | cut -f2)
96
+ if [ "$eco_members" = "1" ]; then
97
+ plural=""
98
+ else
99
+ plural="s"
100
+ fi
101
+ printf '▸ Ecosystem: %s (%s member%s)\n' "$eco_name" "$eco_members" "$plural" >&2
102
+
103
+ # Active initiative line (only when set).
104
+ if [ -f "$ECO_ROOT/.state/active-initiative" ]; then
105
+ active=$(tr -d '\n' < "$ECO_ROOT/.state/active-initiative" 2>/dev/null | head -c 200)
106
+ if [ -n "$active" ]; then
107
+ printf '▸ Active initiative: %s\n' "$active" >&2
108
+ fi
109
+ fi
110
+ fi
111
+ fi
112
+
113
+ # ─────────────────────────────────────────────────────────────────────────────
114
+ # Banner 2 — Handoff inbox (existing behaviour)
115
+ # ─────────────────────────────────────────────────────────────────────────────
116
+
117
+ INBOX_DIR=".momentum/inbox"
118
+
119
+ # No inbox dir → no pending handoffs.
120
+ [ -d "$INBOX_DIR" ] || exit 0
121
+
122
+ # Walk the inbox and collect pending handoff files.
123
+ shopt -s nullglob 2>/dev/null || true
124
+ pending=( "$INBOX_DIR"/handoff-*.md )
125
+ if [ "${#pending[@]}" -eq 0 ]; then
126
+ exit 0
127
+ fi
128
+
129
+ count="${#pending[@]}"
130
+ if [ "$count" -eq 1 ]; then
131
+ plural=""
132
+ else
133
+ plural="s"
134
+ fi
135
+ banner_lines=()
136
+ banner_lines+=( "▸ ${count} pending handoff${plural}:" )
137
+
138
+ for f in "${pending[@]}"; do
139
+ base=$(basename "$f" .md)
140
+ # Pull a one-line summary from the inbox file. Use awk to grab the
141
+ # line after `## Summary` heading. Fallback to filename if missing.
142
+ summary=$(awk '/^## Summary$/{getline; print; exit}' "$f" 2>/dev/null || true)
143
+ [ -z "$summary" ] && summary="(no summary)"
144
+ from=$(awk -F': *' '/^\*\*fromRepo:\*\*/ {print $2; exit}' "$f" 2>/dev/null || true)
145
+ if [ -n "$from" ]; then
146
+ from_label=$(basename "$from")
147
+ else
148
+ from_label="(unknown)"
149
+ fi
150
+ banner_lines+=( " ▸ ${base} from ${from_label}: ${summary}" )
151
+ done
152
+
153
+ banner_lines+=( "▸ Read now? [y/skip] (or run: /continue / momentum continue)" )
154
+
155
+ # Emit the banner. Use stderr so the agent's primary stdout stays
156
+ # clean for normal output; adapters that route hook output anywhere
157
+ # will still surface it.
158
+ for line in "${banner_lines[@]}"; do
159
+ printf '%s\n' "$line" >&2
160
+ done
161
+
162
+ # If stdin is not a TTY (CI, agent harness without interactive surface)
163
+ # just exit 0 — the banner has informed the user (or downstream
164
+ # adapter) without forcing a blocking read.
165
+ if [ ! -t 0 ]; then
166
+ exit 0
167
+ fi
168
+
169
+ # Read a single character with a short timeout so we don't hang an
170
+ # unattended session start.
171
+ ans=""
172
+ if IFS= read -t 5 -r -n 1 ans 2>/dev/null; then
173
+ : # read succeeded (ans may be empty if user hit Enter)
174
+ fi
175
+ printf '\n' >&2
176
+
177
+ case "$ans" in
178
+ y|Y) exit 10 ;;
179
+ *) exit 0 ;;
180
+ esac