@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,70 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/LiminaLabsAI/momentum/schemas/swarm-dispatch-run-v1.json",
4
+ "title": "Momentum Swarm Dispatch-Run (Per-Supervisor Saga Step)",
5
+ "description": "Extension of the existing Phase 11 dispatch-run-NNN artifact for swarm-aware supervisors. Lives at <repo>/.momentum/runs/dispatch-run-NNN.json (one per supervisor per wave). The conductor never reads this directly — it watches HEAD SHAs + reads materialized board.json. This schema is the durable per-step saga record that makes /swarm resume possible.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["run_id", "primitive", "swarm_id", "saga_id", "wave", "repo", "phase_slug", "started", "step_n", "done"],
9
+ "properties": {
10
+ "run_id": {
11
+ "type": "string",
12
+ "pattern": "^[0-9]{3,6}$",
13
+ "description": "Zero-padded counter per repo per primitive. Matches the filename suffix."
14
+ },
15
+ "primitive": {
16
+ "type": "string",
17
+ "const": "swarm-supervise",
18
+ "description": "Marks this as a swarm supervisor run (distinct from scout/dispatch/handoff dispatch-runs that share the same .momentum/runs/ directory)."
19
+ },
20
+ "swarm_id": {"type": "string", "pattern": "^[0-9]{4}-[a-z][a-z0-9-]*$"},
21
+ "saga_id": {"type": "string", "pattern": "^sg_[a-z0-9]{4,16}$"},
22
+ "wave": {"type": "integer", "minimum": 1},
23
+ "repo": {
24
+ "type": "string",
25
+ "pattern": "^[a-z][a-z0-9-]*$",
26
+ "description": "Ecosystem member id this supervisor is pinned to."
27
+ },
28
+ "phase_slug": {
29
+ "type": "string",
30
+ "pattern": "^phase-[0-9]+(?:[a-z])?-[a-z][a-z0-9-]*$"
31
+ },
32
+ "branch": {"type": "string", "minLength": 1},
33
+ "started": {"type": "string", "format": "date-time"},
34
+ "step_n": {
35
+ "type": "integer",
36
+ "minimum": 0,
37
+ "description": "Monotonic step counter inside this wave. Each supervisor /start-phase iteration bumps step_n."
38
+ },
39
+ "done": {
40
+ "type": "boolean",
41
+ "description": "True once /complete-phase succeeded in this supervisor's repo for this wave."
42
+ },
43
+ "ended": {"type": "string", "format": "date-time"},
44
+ "exit_status": {
45
+ "type": "string",
46
+ "enum": ["success", "failed", "cancelled", "in-progress"]
47
+ },
48
+ "tasks_done": {"type": "integer", "minimum": 0},
49
+ "tasks_total": {"type": "integer", "minimum": 0},
50
+ "tokens_used": {"type": "integer", "minimum": 0},
51
+ "head_sha": {"type": "string", "pattern": "^[0-9a-f]{7,40}$"},
52
+ "last_commit": {"type": "string", "description": "Last commit subject the supervisor wrote."},
53
+ "current_file": {"type": "string", "description": "Free-form: what file the supervisor is editing right now."},
54
+ "inbox_items": {
55
+ "type": "array",
56
+ "items": {"type": "string"},
57
+ "description": "Slugs of inbox items this supervisor has written. Empty when none pending."
58
+ },
59
+ "history_tail_offset": {
60
+ "type": "integer",
61
+ "minimum": 0,
62
+ "description": "Byte offset into <repo>/specs/phases/<slug>/history.md where the supervisor was last seen. Lets /swarm resume seek to new entries without re-reading the full file."
63
+ },
64
+ "claimed_by_session": {
65
+ "type": "string",
66
+ "minLength": 1,
67
+ "description": "Session UUID that wrote this step. Portability hook."
68
+ }
69
+ }
70
+ }
@@ -0,0 +1,234 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/LiminaLabsAI/momentum/schemas/swarm-manifest-v1.json",
4
+ "title": "Momentum Swarm Manifest",
5
+ "description": "Declares a cross-repo work unit driven from one user (conductor) session. Lives at <ecosystem-root>/swarms/<id>/manifest.json. The saga record — repos, waves, owner/lease, contract pins, token budgets, audit. All conductor + supervisor writes go through mkdir-locked CRUD in core/swarm/lib/manifest.js.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["swarm_id", "version", "saga_id", "mode", "initiative", "created", "ecosystem", "repos", "waves"],
9
+ "properties": {
10
+ "swarm_id": {
11
+ "type": "string",
12
+ "pattern": "^[0-9]{4}-[a-z][a-z0-9-]*$",
13
+ "description": "Zero-padded NNNN- prefix plus kebab slug, e.g. 0007-user-auth. Matches the on-disk directory name."
14
+ },
15
+ "version": {
16
+ "type": "integer",
17
+ "const": 1,
18
+ "description": "Manifest schema version. Locked to 1."
19
+ },
20
+ "saga_id": {
21
+ "type": "string",
22
+ "pattern": "^sg_[a-z0-9]{4,16}$",
23
+ "description": "Opaque short identifier (sg_<hex>) — used in dispatch-run records to associate per-repo saga steps with this swarm."
24
+ },
25
+ "mode": {
26
+ "type": "string",
27
+ "enum": ["autopilot", "checkpoint", "interactive"],
28
+ "description": "Conductor intervention mode. v0.20.0 ships autopilot + checkpoint; interactive reserved for v0.20.x."
29
+ },
30
+ "initiative": {
31
+ "type": "string",
32
+ "pattern": "^[a-z][a-z0-9-]*$",
33
+ "description": "Slug of the cross-repo initiative this swarm advances. Must reference a real <eco>/initiatives/NNNN-<slug>.md."
34
+ },
35
+ "created": {
36
+ "type": "string",
37
+ "format": "date-time",
38
+ "description": "ISO-8601 timestamp the swarm was started."
39
+ },
40
+ "ecosystem": {
41
+ "type": "string",
42
+ "pattern": "^[a-z][a-z0-9-]*$",
43
+ "description": "Ecosystem name (mirrors ecosystem.json name). Defensive copy — lets a swarm survive an ecosystem rename without losing context."
44
+ },
45
+ "repos": {
46
+ "type": "object",
47
+ "minProperties": 1,
48
+ "description": "Per-repo swarm state, keyed by ecosystem member id.",
49
+ "patternProperties": {
50
+ "^[a-z][a-z0-9-]*$": {
51
+ "type": "object",
52
+ "additionalProperties": false,
53
+ "required": ["wave", "status", "phase_slug", "branch", "owner"],
54
+ "properties": {
55
+ "wave": {
56
+ "type": "integer",
57
+ "minimum": 1,
58
+ "description": "Wave index this repo is assigned to. Computed from ecosystem.json dependency edges at start; immutable per swarm."
59
+ },
60
+ "status": {
61
+ "type": "string",
62
+ "enum": ["queued", "running", "blocked", "complete", "failed", "cancelled"],
63
+ "description": "Current execution status. Materialized into board.json."
64
+ },
65
+ "phase_slug": {
66
+ "type": "string",
67
+ "pattern": "^phase-[0-9]+(?:[a-z])?-[a-z][a-z0-9-]*$",
68
+ "description": "The phase directory slug inside this repo (e.g. phase-3-user-auth). The supervisor's /start-phase loop drives this phase."
69
+ },
70
+ "branch": {
71
+ "type": "string",
72
+ "minLength": 1,
73
+ "description": "Git branch the supervisor commits to in this repo. Defaults to phase-N-<slug>."
74
+ },
75
+ "owner": {
76
+ "type": "string",
77
+ "minLength": 1,
78
+ "description": "Session id (UUID-like) currently authoritative for this repo. Portability hook — Phase 17.5 lights up lease enforcement."
79
+ },
80
+ "lease_expires_at": {
81
+ "type": "string",
82
+ "format": "date-time",
83
+ "description": "When the current owner's lease expires. Conductor renews each turn. Phase 17.5 enforces; v0.20.0 audits only."
84
+ },
85
+ "lease_renewed_at": {
86
+ "type": "string",
87
+ "format": "date-time",
88
+ "description": "Last time the owner refreshed the lease. Conductor writes each conductor turn for repos it owns."
89
+ },
90
+ "claimed_by_session": {
91
+ "type": "string",
92
+ "minLength": 1,
93
+ "description": "Session UUID that holds the active claim. Mirrors brief frontmatter claimed_by_session."
94
+ },
95
+ "last_seen_sha": {
96
+ "type": "string",
97
+ "pattern": "^[0-9a-f]{7,40}$",
98
+ "description": "Last HEAD SHA the conductor observed for this repo. Cheap revalidation: if rev-parse HEAD matches, cache is fresh."
99
+ },
100
+ "tasks_done": {
101
+ "type": "integer",
102
+ "minimum": 0,
103
+ "description": "Number of supervisor checklist items completed."
104
+ },
105
+ "tasks_total": {
106
+ "type": "integer",
107
+ "minimum": 0,
108
+ "description": "Total checklist size at supervisor spawn time."
109
+ },
110
+ "tokens_used": {
111
+ "type": "integer",
112
+ "minimum": 0,
113
+ "description": "Output tokens spent by this supervisor (approximate, supervisor-reported)."
114
+ },
115
+ "tokens_budget": {
116
+ "type": "integer",
117
+ "minimum": 1,
118
+ "description": "Hard cap on supervisor output tokens. /swarm budget extends. Default 300000."
119
+ },
120
+ "inbox_count": {
121
+ "type": "integer",
122
+ "minimum": 0,
123
+ "description": "Count of unresolved inbox items written by this supervisor."
124
+ },
125
+ "commits": {
126
+ "type": "integer",
127
+ "minimum": 0,
128
+ "description": "Number of git commits the supervisor has pushed on its branch."
129
+ },
130
+ "current_task": {
131
+ "type": "string",
132
+ "description": "Free-form label of what the supervisor is working on right now."
133
+ },
134
+ "waiting_on": {
135
+ "type": "array",
136
+ "items": {"type": "string", "pattern": "^[a-z][a-z0-9-]*$"},
137
+ "description": "Member ids this repo's supervisor is blocked on. Used in board.json rendering for queued repos."
138
+ },
139
+ "contract_refs": {
140
+ "type": "array",
141
+ "items": {"type": "string"},
142
+ "description": "Contract surfaces this repo produces or consumes — slugs that resolve to contracts/<slug>.contract.json."
143
+ }
144
+ }
145
+ }
146
+ },
147
+ "additionalProperties": false
148
+ },
149
+ "waves": {
150
+ "type": "array",
151
+ "minItems": 1,
152
+ "description": "Ordered wave plan. Each entry lists the member ids in that wave. Waves execute sequentially; repos within a wave run in parallel.",
153
+ "items": {
154
+ "type": "object",
155
+ "additionalProperties": false,
156
+ "required": ["index", "repos"],
157
+ "properties": {
158
+ "index": {"type": "integer", "minimum": 1},
159
+ "repos": {
160
+ "type": "array",
161
+ "minItems": 1,
162
+ "items": {"type": "string", "pattern": "^[a-z][a-z0-9-]*$"}
163
+ },
164
+ "status": {
165
+ "type": "string",
166
+ "enum": ["queued", "running", "complete", "cancelled"]
167
+ },
168
+ "checkpoint_resolved_at": {
169
+ "type": "string",
170
+ "format": "date-time",
171
+ "description": "When the user approved the wave checkpoint (checkpoint mode). Null/missing in autopilot."
172
+ }
173
+ }
174
+ }
175
+ },
176
+ "sessions": {
177
+ "type": "array",
178
+ "description": "Registry of every conductor session that has touched this swarm. Portability hook — Phase 17.5 uses for join/absorb.",
179
+ "items": {
180
+ "type": "object",
181
+ "additionalProperties": false,
182
+ "required": ["session_id", "first_seen"],
183
+ "properties": {
184
+ "session_id": {"type": "string", "minLength": 1},
185
+ "first_seen": {"type": "string", "format": "date-time"},
186
+ "last_seen": {"type": "string", "format": "date-time"}
187
+ }
188
+ }
189
+ },
190
+ "contracts": {
191
+ "type": "array",
192
+ "description": "Cross-repo contract surfaces pinned at start. Each references contracts/<surface>.contract.json. Mismatches at wave boundaries block the next wave.",
193
+ "items": {
194
+ "type": "object",
195
+ "additionalProperties": false,
196
+ "required": ["surface", "owner", "consumers", "version"],
197
+ "properties": {
198
+ "surface": {"type": "string", "pattern": "^[a-z][a-z0-9-]*$"},
199
+ "owner": {"type": "string", "pattern": "^[a-z][a-z0-9-]*$"},
200
+ "consumers": {
201
+ "type": "array",
202
+ "items": {"type": "string", "pattern": "^[a-z][a-z0-9-]*$"}
203
+ },
204
+ "version": {"type": "integer", "minimum": 1},
205
+ "content_hash": {"type": "string", "pattern": "^[0-9a-f]{8,64}$"}
206
+ }
207
+ }
208
+ },
209
+ "audit": {
210
+ "type": "array",
211
+ "description": "Append-only audit log: /swarm tell, /swarm broadcast, /swarm budget, /swarm cancel, /swarm verify events. Materialized into board.json as recent_activity.",
212
+ "items": {
213
+ "type": "object",
214
+ "additionalProperties": false,
215
+ "required": ["ts", "actor", "event"],
216
+ "properties": {
217
+ "ts": {"type": "string", "format": "date-time"},
218
+ "actor": {"type": "string", "minLength": 1},
219
+ "event": {
220
+ "type": "string",
221
+ "enum": ["start", "tell", "broadcast", "budget", "cancel", "verify", "checkpoint", "complete", "resume", "wave-transition", "inbox-resolved", "contract-bump", "claim", "release", "focus", "join", "absorb", "lease-takeover"]
222
+ },
223
+ "repo": {"type": "string", "pattern": "^[a-z][a-z0-9-]*$"},
224
+ "detail": {"type": "string"}
225
+ }
226
+ }
227
+ },
228
+ "status": {
229
+ "type": "string",
230
+ "enum": ["running", "paused", "complete", "cancelled", "failed"],
231
+ "description": "Overall swarm state. Derived from per-repo + wave statuses; materialized into board.json."
232
+ }
233
+ }
234
+ }
@@ -0,0 +1,61 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/LiminaLabsAI/momentum/schemas/swarm-signal-v1.json",
4
+ "title": "Momentum Swarm Signal",
5
+ "description": "Typed cross-session message in a swarm's signals/ directory. One file per signal at <eco>/swarms/<id>/signals/NNNN-<type>-<slug>.json. Writes are mkdir-locked. Read once and archived to signals/processed/. Phase 17.5 portability protocol.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["signal_id", "version", "type", "ts", "from_session"],
9
+ "properties": {
10
+ "signal_id": {
11
+ "type": "string",
12
+ "pattern": "^[0-9]{4}-(focus-request|claim-request|absorb-proposed|lease-expired)-[a-z0-9][a-z0-9-]*$",
13
+ "description": "Zero-padded NNNN-<type>-<slug>. Matches the on-disk filename minus extension."
14
+ },
15
+ "version": {
16
+ "type": "integer",
17
+ "const": 1,
18
+ "description": "Schema version. Locked to 1."
19
+ },
20
+ "type": {
21
+ "type": "string",
22
+ "enum": ["focus-request", "claim-request", "absorb-proposed", "lease-expired"],
23
+ "description": "Signal kind. Drives conductor poller branching."
24
+ },
25
+ "ts": {
26
+ "type": "string",
27
+ "format": "date-time",
28
+ "description": "ISO-8601 timestamp the signal was written."
29
+ },
30
+ "from_session": {
31
+ "type": "string",
32
+ "minLength": 1,
33
+ "description": "Session id that wrote the signal."
34
+ },
35
+ "to_session": {
36
+ "type": "string",
37
+ "minLength": 1,
38
+ "description": "Optional target session id. Null/missing = broadcast to all conductors that poll."
39
+ },
40
+ "repo": {
41
+ "type": "string",
42
+ "pattern": "^[a-z][a-z0-9-]*$",
43
+ "description": "Ecosystem member id this signal concerns. Required for focus-request / claim-request / lease-expired."
44
+ },
45
+ "token": {
46
+ "type": "string",
47
+ "pattern": "^[0-9a-f]{16}$",
48
+ "description": "Transfer token id (16-hex). Set on focus-request when a token has been issued for the receiver to consume."
49
+ },
50
+ "source_swarm": {
51
+ "type": "string",
52
+ "pattern": "^[0-9]{4}-[a-z][a-z0-9-]*$",
53
+ "description": "Other swarm id being proposed for absorb. Required for absorb-proposed."
54
+ },
55
+ "detail": {
56
+ "type": "string",
57
+ "maxLength": 2000,
58
+ "description": "Optional human-readable note. Surfaced in conductor's poll output."
59
+ }
60
+ }
61
+ }
@@ -0,0 +1,296 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Swarm signal protocol — typed cross-session messages.
5
+ *
6
+ * Phase 17.5 lights up the `signals/` directory reserved in v0.20.0.
7
+ * Each signal is one file at `<eco>/swarms/<id>/signals/NNNN-<type>-<slug>.json`.
8
+ * The conductor polls the directory each turn, surfaces unprocessed
9
+ * signals, and on processing moves the file to `signals/processed/`.
10
+ *
11
+ * Types:
12
+ * - focus-request issued by `/swarm focus <repo>`; receiver consumes the token via `/swarm join --token`
13
+ * - claim-request issued by `/swarm claim <repo>` when the requester doesn't yet hold the lease
14
+ * - absorb-proposed issued by `/swarm absorb` before performing the merge — gives the other conductor a chance to see it
15
+ * - lease-expired issued by enforcement when a write was rejected against an expired lease
16
+ *
17
+ * Concurrency: writes use the same mkdir-lock pattern as manifest.js +
18
+ * inbox.js. Filenames embed the signal type so the poller can branch
19
+ * without reading bodies.
20
+ */
21
+
22
+ const fs = require('fs');
23
+ const path = require('path');
24
+
25
+ const manifestLib = require('./lib/manifest');
26
+
27
+ const SIGNALS_DIR = 'signals';
28
+ const PROCESSED_DIR = 'processed';
29
+ const INDEX_FILENAME = 'INDEX.md';
30
+
31
+ const VALID_TYPES = Object.freeze([
32
+ 'focus-request',
33
+ 'claim-request',
34
+ 'absorb-proposed',
35
+ 'lease-expired',
36
+ ]);
37
+
38
+ const SLUG = /^[a-z0-9][a-z0-9-]*$/;
39
+ const TYPE_ALT = VALID_TYPES.join('|');
40
+ const FILENAME = new RegExp(`^(\\d{4})-(${TYPE_ALT})-([a-z0-9][a-z0-9-]*)\\.json$`);
41
+ const SIGNAL_ID = new RegExp(`^[0-9]{4}-(?:${TYPE_ALT})-[a-z0-9][a-z0-9-]*$`);
42
+ const TOKEN_HEX = /^[0-9a-f]{16}$/;
43
+ const SWARM_ID = /^[0-9]{4}-[a-z][a-z0-9-]*$/;
44
+ const ISO_DATETIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})$/;
45
+
46
+ // ─────────────────────────────────────────────────────────────────────────────
47
+ // Path helpers
48
+ // ─────────────────────────────────────────────────────────────────────────────
49
+
50
+ function signalsDir(ecosystemRoot, swarmId) {
51
+ return path.join(manifestLib.swarmDir(ecosystemRoot, swarmId), SIGNALS_DIR);
52
+ }
53
+
54
+ function processedDir(ecosystemRoot, swarmId) {
55
+ return path.join(signalsDir(ecosystemRoot, swarmId), PROCESSED_DIR);
56
+ }
57
+
58
+ function indexPath(ecosystemRoot, swarmId) {
59
+ return path.join(signalsDir(ecosystemRoot, swarmId), INDEX_FILENAME);
60
+ }
61
+
62
+ function ensureLayout(ecosystemRoot, swarmId) {
63
+ manifestLib.ensureSwarmLayout(ecosystemRoot, swarmId);
64
+ fs.mkdirSync(processedDir(ecosystemRoot, swarmId), { recursive: true });
65
+ }
66
+
67
+ // ─────────────────────────────────────────────────────────────────────────────
68
+ // Validation
69
+ // ─────────────────────────────────────────────────────────────────────────────
70
+
71
+ function validateSignal(obj) {
72
+ const errors = [];
73
+ const push = (p, m) => errors.push({ path: p, message: m });
74
+
75
+ if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) {
76
+ return { ok: false, errors: [{ path: '$', message: 'signal must be a JSON object' }] };
77
+ }
78
+
79
+ if (typeof obj.signal_id !== 'string' || !SIGNAL_ID.test(obj.signal_id)) {
80
+ push('$.signal_id', 'must match /^[0-9]{4}-<type>-[a-z0-9][a-z0-9-]*$/');
81
+ }
82
+ if (obj.version !== 1) push('$.version', 'must be the integer 1');
83
+ if (!VALID_TYPES.includes(obj.type)) {
84
+ push('$.type', `must be one of: ${VALID_TYPES.join(', ')}`);
85
+ }
86
+ if (typeof obj.ts !== 'string' || !ISO_DATETIME.test(obj.ts)) {
87
+ push('$.ts', 'must be ISO-8601 date-time');
88
+ }
89
+ if (typeof obj.from_session !== 'string' || obj.from_session.length === 0) {
90
+ push('$.from_session', 'required non-empty string');
91
+ }
92
+ if (obj.to_session !== undefined && (typeof obj.to_session !== 'string' || obj.to_session.length === 0)) {
93
+ push('$.to_session', 'must be non-empty string when present');
94
+ }
95
+ if (obj.repo !== undefined && (typeof obj.repo !== 'string' || !SLUG.test(obj.repo))) {
96
+ push('$.repo', 'must match /^[a-z0-9][a-z0-9-]*$/ when present');
97
+ }
98
+ if (obj.token !== undefined && (typeof obj.token !== 'string' || !TOKEN_HEX.test(obj.token))) {
99
+ push('$.token', 'must be 16-hex when present');
100
+ }
101
+ if (obj.source_swarm !== undefined && (typeof obj.source_swarm !== 'string' || !SWARM_ID.test(obj.source_swarm))) {
102
+ push('$.source_swarm', 'must match swarm-id pattern when present');
103
+ }
104
+ if (obj.detail !== undefined && typeof obj.detail !== 'string') {
105
+ push('$.detail', 'must be string when present');
106
+ }
107
+
108
+ // Per-type required fields
109
+ if (obj.type === 'focus-request' || obj.type === 'claim-request' || obj.type === 'lease-expired') {
110
+ if (typeof obj.repo !== 'string') push('$.repo', `required for ${obj.type}`);
111
+ }
112
+ if (obj.type === 'absorb-proposed') {
113
+ if (typeof obj.source_swarm !== 'string') push('$.source_swarm', 'required for absorb-proposed');
114
+ }
115
+
116
+ return errors.length === 0 ? { ok: true } : { ok: false, errors };
117
+ }
118
+
119
+ // ─────────────────────────────────────────────────────────────────────────────
120
+ // ID generation
121
+ // ─────────────────────────────────────────────────────────────────────────────
122
+
123
+ function nextSignalId(ecosystemRoot, swarmId) {
124
+ ensureLayout(ecosystemRoot, swarmId);
125
+ let max = 0;
126
+ const scan = (dir) => {
127
+ if (!fs.existsSync(dir)) return;
128
+ for (const name of fs.readdirSync(dir)) {
129
+ const m = name.match(FILENAME);
130
+ if (m) {
131
+ const n = parseInt(m[1], 10);
132
+ if (n > max) max = n;
133
+ }
134
+ }
135
+ };
136
+ scan(signalsDir(ecosystemRoot, swarmId));
137
+ scan(processedDir(ecosystemRoot, swarmId));
138
+ return String(max + 1).padStart(4, '0');
139
+ }
140
+
141
+ // ─────────────────────────────────────────────────────────────────────────────
142
+ // Write
143
+ // ─────────────────────────────────────────────────────────────────────────────
144
+
145
+ /**
146
+ * Write a typed signal. Returns { signal_id, filePath }.
147
+ *
148
+ * @param {object} args
149
+ * @param {string} args.ecosystemRoot
150
+ * @param {string} args.swarmId
151
+ * @param {string} args.type one of VALID_TYPES
152
+ * @param {string} args.slug kebab slug (used in filename + signal_id)
153
+ * @param {string} args.fromSession issuing session id
154
+ * @param {string} args.nowIso ISO timestamp
155
+ * @param {string} [args.toSession]
156
+ * @param {string} [args.repo]
157
+ * @param {string} [args.token]
158
+ * @param {string} [args.sourceSwarm]
159
+ * @param {string} [args.detail]
160
+ */
161
+ function writeSignal(args) {
162
+ const {
163
+ ecosystemRoot, swarmId, type, slug, fromSession, nowIso,
164
+ toSession, repo, token, sourceSwarm, detail,
165
+ } = args;
166
+ if (!VALID_TYPES.includes(type)) {
167
+ throw new TypeError(`writeSignal: invalid type ${JSON.stringify(type)}`);
168
+ }
169
+ if (typeof slug !== 'string' || !SLUG.test(slug)) {
170
+ throw new TypeError(`writeSignal: invalid slug ${JSON.stringify(slug)}`);
171
+ }
172
+ ensureLayout(ecosystemRoot, swarmId);
173
+ const id = nextSignalId(ecosystemRoot, swarmId);
174
+ const signalId = `${id}-${type}-${slug}`;
175
+ const filePath = path.join(signalsDir(ecosystemRoot, swarmId), `${signalId}.json`);
176
+
177
+ const payload = {
178
+ signal_id: signalId,
179
+ version: 1,
180
+ type,
181
+ ts: nowIso,
182
+ from_session: fromSession,
183
+ };
184
+ if (toSession) payload.to_session = toSession;
185
+ if (repo) payload.repo = repo;
186
+ if (token) payload.token = token;
187
+ if (sourceSwarm) payload.source_swarm = sourceSwarm;
188
+ if (detail) payload.detail = detail;
189
+
190
+ const v = validateSignal(payload);
191
+ if (!v.ok) {
192
+ const summary = v.errors.map((e) => ` ${e.path}: ${e.message}`).join('\n');
193
+ throw new Error(`writeSignal: validation failed:\n${summary}`);
194
+ }
195
+
196
+ manifestLib.withLock(filePath, () => {
197
+ fs.writeFileSync(filePath, JSON.stringify(payload, null, 2) + '\n', 'utf8');
198
+ });
199
+ rebuildIndex(ecosystemRoot, swarmId);
200
+ return { signal_id: signalId, filePath };
201
+ }
202
+
203
+ // ─────────────────────────────────────────────────────────────────────────────
204
+ // Read / poll
205
+ // ─────────────────────────────────────────────────────────────────────────────
206
+
207
+ function readSignal(filePath) {
208
+ const raw = fs.readFileSync(filePath, 'utf8');
209
+ return JSON.parse(raw);
210
+ }
211
+
212
+ /**
213
+ * List unprocessed signals (excluding processed/). Optionally filter by
214
+ * `type` and/or `toSession` (broadcast signals — those without
215
+ * `to_session` — always match).
216
+ */
217
+ function listPendingSignals(ecosystemRoot, swarmId, opts = {}) {
218
+ const dir = signalsDir(ecosystemRoot, swarmId);
219
+ if (!fs.existsSync(dir)) return [];
220
+ const wantType = opts.type;
221
+ const wantSession = opts.toSession;
222
+ const out = [];
223
+ for (const name of fs.readdirSync(dir)) {
224
+ const m = name.match(FILENAME);
225
+ if (!m) continue;
226
+ const filePath = path.join(dir, name);
227
+ let body;
228
+ try { body = readSignal(filePath); } catch (_e) { continue; }
229
+ if (wantType && body.type !== wantType) continue;
230
+ if (wantSession && body.to_session && body.to_session !== wantSession) continue;
231
+ out.push({ ...body, filePath });
232
+ }
233
+ out.sort((a, b) => a.signal_id.localeCompare(b.signal_id));
234
+ return out;
235
+ }
236
+
237
+ /**
238
+ * Move a signal file to processed/. Idempotent — if already in
239
+ * processed/, returns silently.
240
+ */
241
+ function markProcessed(ecosystemRoot, swarmId, signalId) {
242
+ ensureLayout(ecosystemRoot, swarmId);
243
+ const dir = signalsDir(ecosystemRoot, swarmId);
244
+ const fromPath = path.join(dir, `${signalId}.json`);
245
+ const toPath = path.join(processedDir(ecosystemRoot, swarmId), `${signalId}.json`);
246
+ if (!fs.existsSync(fromPath)) {
247
+ if (fs.existsSync(toPath)) return { signalId, processedPath: toPath };
248
+ throw new Error(`markProcessed: signal ${signalId} not found`);
249
+ }
250
+ manifestLib.withLock(fromPath, () => {
251
+ fs.renameSync(fromPath, toPath);
252
+ });
253
+ rebuildIndex(ecosystemRoot, swarmId);
254
+ return { signalId, processedPath: toPath };
255
+ }
256
+
257
+ // ─────────────────────────────────────────────────────────────────────────────
258
+ // INDEX
259
+ // ─────────────────────────────────────────────────────────────────────────────
260
+
261
+ function rebuildIndex(ecosystemRoot, swarmId) {
262
+ ensureLayout(ecosystemRoot, swarmId);
263
+ const items = listPendingSignals(ecosystemRoot, swarmId);
264
+ const lines = ['# Signals — pending', ''];
265
+ if (items.length === 0) {
266
+ lines.push('_(no pending signals)_');
267
+ } else {
268
+ lines.push('| ID | Type | Repo | From | To | Token |');
269
+ lines.push('|----|------|------|------|----|-------|');
270
+ for (const it of items) {
271
+ lines.push(
272
+ `| ${it.signal_id} | ${it.type} | ${it.repo || '—'} | ${it.from_session} | ${it.to_session || '*'} | ${it.token || '—'} |`
273
+ );
274
+ }
275
+ }
276
+ lines.push('');
277
+ fs.writeFileSync(indexPath(ecosystemRoot, swarmId), lines.join('\n'), 'utf8');
278
+ }
279
+
280
+ module.exports = {
281
+ SIGNALS_DIR,
282
+ PROCESSED_DIR,
283
+ INDEX_FILENAME,
284
+ VALID_TYPES,
285
+ signalsDir,
286
+ processedDir,
287
+ indexPath,
288
+ ensureLayout,
289
+ validateSignal,
290
+ nextSignalId,
291
+ writeSignal,
292
+ readSignal,
293
+ listPendingSignals,
294
+ markProcessed,
295
+ rebuildIndex,
296
+ };