@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.
- package/LICENSE +21 -0
- package/README.md +214 -0
- package/adapters/antigravity/adapter.js +196 -0
- package/adapters/antigravity/hooks.json +39 -0
- package/adapters/antigravity/instructions/AGENTS.md +638 -0
- package/adapters/antigravity/instructions/header.md +3 -0
- package/adapters/antigravity/instructions/surfaces.md +153 -0
- package/adapters/antigravity/instructions/vars.json +1 -0
- package/adapters/antigravity/scripts/antigravity-hook-adapter.sh +147 -0
- package/adapters/antigravity/skills/momentum-orient/SKILL.md +29 -0
- package/adapters/antigravity/skills/momentum-reviewer-architecture/SKILL.md +39 -0
- package/adapters/antigravity/skills/momentum-reviewer-qa/SKILL.md +38 -0
- package/adapters/antigravity/skills/momentum-reviewer-security/SKILL.md +42 -0
- package/adapters/antigravity/skills/swarm-supervisor/SKILL.md +75 -0
- package/adapters/antigravity/workflows/continue.md +39 -0
- package/adapters/antigravity/workflows/dispatch.md +49 -0
- package/adapters/antigravity/workflows/handoff.md +38 -0
- package/adapters/antigravity/workflows/review-code.md +84 -0
- package/adapters/antigravity/workflows/scout.md +42 -0
- package/adapters/antigravity/workflows/swarm.md +203 -0
- package/adapters/claude-code/adapter.js +148 -0
- package/adapters/claude-code/commands/continue.md +82 -0
- package/adapters/claude-code/commands/dispatch.md +154 -0
- package/adapters/claude-code/commands/handoff.md +85 -0
- package/adapters/claude-code/commands/review-code.md +194 -0
- package/adapters/claude-code/commands/scout.md +119 -0
- package/adapters/claude-code/commands/swarm.md +242 -0
- package/adapters/claude-code/instructions/header.md +3 -0
- package/adapters/claude-code/instructions/vars.json +1 -0
- package/adapters/claude-code/settings.json +36 -0
- package/adapters/codex/adapter.js +225 -0
- package/adapters/codex/agents/momentum-reviewer-architecture.toml +57 -0
- package/adapters/codex/agents/momentum-reviewer-qa.toml +52 -0
- package/adapters/codex/agents/momentum-reviewer-security.toml +57 -0
- package/adapters/codex/agents/swarm-supervisor.toml +76 -0
- package/adapters/codex/commands/continue.md +40 -0
- package/adapters/codex/commands/dispatch.md +111 -0
- package/adapters/codex/commands/handoff.md +57 -0
- package/adapters/codex/commands/scout.md +90 -0
- package/adapters/codex/commands/swarm.md +334 -0
- package/adapters/codex/hooks.json +36 -0
- package/adapters/codex/instructions/AGENTS.md +691 -0
- package/adapters/codex/instructions/header.md +3 -0
- package/adapters/codex/instructions/surfaces.md +206 -0
- package/adapters/codex/instructions/vars.json +1 -0
- package/adapters/codex/skills/momentum-orient/SKILL.md +29 -0
- package/adapters/opencode/adapter.js +224 -0
- package/adapters/opencode/agents/momentum-reviewer-architecture.md +54 -0
- package/adapters/opencode/agents/momentum-reviewer-qa.md +48 -0
- package/adapters/opencode/agents/momentum-reviewer-security.md +48 -0
- package/adapters/opencode/agents/swarm-supervisor.md +51 -0
- package/adapters/opencode/commands/continue.md +40 -0
- package/adapters/opencode/commands/dispatch.md +111 -0
- package/adapters/opencode/commands/handoff.md +57 -0
- package/adapters/opencode/commands/review-code.md +194 -0
- package/adapters/opencode/commands/scout.md +90 -0
- package/adapters/opencode/commands/swarm.md +334 -0
- package/adapters/opencode/instructions/AGENTS.md +604 -0
- package/adapters/opencode/instructions/header.md +3 -0
- package/adapters/opencode/instructions/surfaces.md +119 -0
- package/adapters/opencode/instructions/vars.json +1 -0
- package/adapters/opencode/plugins/momentum.js +219 -0
- package/adapters/opencode/skills/momentum-lanes/SKILL.md +30 -0
- package/adapters/opencode/skills/momentum-orient/SKILL.md +29 -0
- package/adapters/opencode/skills/momentum-track/SKILL.md +28 -0
- package/adapters/opencode/skills/momentum-validate/SKILL.md +31 -0
- package/bin/antigravity.js +112 -0
- package/bin/ecosystem.js +1302 -0
- package/bin/lanes.js +300 -0
- package/bin/momentum.js +1857 -0
- package/bin/okf.js +72 -0
- package/bin/orchestration-commands.js +305 -0
- package/bin/state-commands.js +218 -0
- package/bin/swarm.js +1104 -0
- package/bin/waves.js +147 -0
- package/core/adapter-capabilities.md +112 -0
- package/core/adapter-parity-matrix.md +170 -0
- package/core/commands/brainstorm-idea.md +98 -0
- package/core/commands/brainstorm-phase.md +129 -0
- package/core/commands/complete-phase.md +130 -0
- package/core/commands/ecosystem.md +161 -0
- package/core/commands/hotfix.md +64 -0
- package/core/commands/initiative.md +119 -0
- package/core/commands/lanes.md +94 -0
- package/core/commands/log.md +45 -0
- package/core/commands/migrate.md +70 -0
- package/core/commands/review.md +31 -0
- package/core/commands/session.md +79 -0
- package/core/commands/start-phase.md +161 -0
- package/core/commands/start-project.md +142 -0
- package/core/commands/sync-docs.md +81 -0
- package/core/commands/systematic-debug.md +46 -0
- package/core/commands/track.md +54 -0
- package/core/commands/validate.md +96 -0
- package/core/ecosystem/layout.md +128 -0
- package/core/ecosystem/lib/index.js +245 -0
- package/core/ecosystem/lib/initiative.js +258 -0
- package/core/ecosystem/lib/pointer.js +198 -0
- package/core/ecosystem/lib/state.js +295 -0
- package/core/ecosystem/schema/ecosystem.schema.json +85 -0
- package/core/ecosystem/schema/initiative.schema.json +61 -0
- package/core/ecosystem/scripts/session-append.sh +167 -0
- package/core/ecosystem/templates/ecosystem-agents.md +79 -0
- package/core/ecosystem/templates/ecosystem-claude.md +79 -0
- package/core/ecosystem/templates/ecosystem-settings-claude.json +14 -0
- package/core/ecosystem/templates/initiative-template.md +50 -0
- package/core/engines/subagent-dispatch.md +80 -0
- package/core/git-hooks/commit-msg +14 -0
- package/core/git-hooks/contract.js +176 -0
- package/core/git-hooks/pre-push +15 -0
- package/core/git-hooks/run-check.js +160 -0
- package/core/instructions/legacy-project-md-hashes.json +12 -0
- package/core/instructions/navigation.md +14 -0
- package/core/instructions/rules-body.md +454 -0
- package/core/lanes/lib/board.js +161 -0
- package/core/lanes/lib/land.js +301 -0
- package/core/lanes/lib/signals.js +207 -0
- package/core/lanes/lib/state.js +325 -0
- package/core/lib/frontmatter.js +163 -0
- package/core/lib/okf-migrate.js +374 -0
- package/core/lib/okf-types.js +61 -0
- package/core/lifecycle-contract.md +59 -0
- package/core/orchestration/capability-routing.js +120 -0
- package/core/orchestration/continue.js +102 -0
- package/core/orchestration/dispatch.js +366 -0
- package/core/orchestration/events.js +197 -0
- package/core/orchestration/handoff.js +317 -0
- package/core/orchestration/index.js +50 -0
- package/core/orchestration/run-artifact.js +93 -0
- package/core/orchestration/scout.js +327 -0
- package/core/orchestration/session-log.js +123 -0
- package/core/orchestration/tracking.js +180 -0
- package/core/orchestration/types.js +131 -0
- package/core/scripts/brainstorm-gate.sh +157 -0
- package/core/scripts/check-history-reminder.sh +110 -0
- package/core/scripts/sessionstart-handoff.sh +180 -0
- package/core/specs-templates/CLAUDE.md +484 -0
- package/core/specs-templates/README.md +32 -0
- package/core/specs-templates/specs/README.md +30 -0
- package/core/specs-templates/specs/adhoc/README.md +31 -0
- package/core/specs-templates/specs/adhoc/_TEMPLATE.md +29 -0
- package/core/specs-templates/specs/architecture/ecosystem.md +214 -0
- package/core/specs-templates/specs/backlog/backlog.md +46 -0
- package/core/specs-templates/specs/backlog/details/.gitkeep +0 -0
- package/core/specs-templates/specs/changelog/.gitkeep +0 -0
- package/core/specs-templates/specs/decisions/0000-template.md +33 -0
- package/core/specs-templates/specs/decisions/README.md +29 -0
- package/core/specs-templates/specs/decisions/impact-map.md +14 -0
- package/core/specs-templates/specs/decisions/index.md +4 -0
- package/core/specs-templates/specs/index.md +16 -0
- package/core/specs-templates/specs/phases/README.md +41 -0
- package/core/specs-templates/specs/phases/index.md +6 -0
- package/core/specs-templates/specs/planning/roadmap.md +23 -0
- package/core/specs-templates/specs/status.md +70 -0
- package/core/specs-templates/specs/vision/principles.md +16 -0
- package/core/specs-templates/specs/vision/project-charter.md +35 -0
- package/core/specs-templates/specs/vision/success-criteria.md +19 -0
- package/core/swarm/absorb.js +294 -0
- package/core/swarm/conductor.js +499 -0
- package/core/swarm/focus.js +151 -0
- package/core/swarm/inbox.js +222 -0
- package/core/swarm/join.js +138 -0
- package/core/swarm/lib/board.js +178 -0
- package/core/swarm/lib/brief.js +157 -0
- package/core/swarm/lib/git-sha-cache.js +71 -0
- package/core/swarm/lib/incremental-log.js +117 -0
- package/core/swarm/lib/manifest.js +578 -0
- package/core/swarm/lib/pre-merge.js +112 -0
- package/core/swarm/lib/saga.js +146 -0
- package/core/swarm/lib/sessions.js +83 -0
- package/core/swarm/lib/tokens.js +255 -0
- package/core/swarm/lib/wave-ordering.js +55 -0
- package/core/swarm/schema/board.schema.json +54 -0
- package/core/swarm/schema/contract.schema.json +56 -0
- package/core/swarm/schema/dispatch-run.schema.json +70 -0
- package/core/swarm/schema/manifest.schema.json +234 -0
- package/core/swarm/schema/signal.schema.json +61 -0
- package/core/swarm/signals.js +296 -0
- package/core/swarm/supervise.md +81 -0
- package/core/waves/lib/plan-graph.js +175 -0
- package/core/waves/lib/waves.js +83 -0
- 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
|