@limina-labs/momentum 0.34.0 → 0.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -638,6 +638,9 @@ Use `infra:` for CI, build, deploy, tooling, and release-pipeline changes that d
638
638
 
639
639
  ## Project Extensions
640
640
 
641
- > Everything below this heading is preserved across `momentum upgrade`.
642
- > Add project-specific navigation, rules, cross-repo references, etc. here.
643
- > Anything above this heading is managed by momentum and may be replaced on upgrade.
641
+ <!-- momentum:project-rules-pointer -->
642
+ > **Project-specific rules live in `specs/project-rules.md`** — read it now.
643
+ > Session-start self-audits, project constraints, and any project-specific
644
+ > guidance are there, shared identically by every agent (ADR-0010). This
645
+ > section is a momentum-managed pointer; edit `specs/project-rules.md`, not
646
+ > this file.
@@ -48,7 +48,7 @@ The shipped recipe set (one skill per recipe, all under
48
48
  | log | Append a manual narrative entry to the history of the phase bound to your branch (fallback: status.md) |
49
49
  | migrate | Onboard an existing project into momentum |
50
50
  | validate | Check spec structure health |
51
- | sync-config | Re-infer project shape, show config drift, apply on approval (ENH-062)
51
+ | sync-config | Re-infer project shape, show config drift, apply on approval (ENH-062) |
52
52
  | ecosystem | Cross-repo ecosystem coordination |
53
53
  | initiative | Manage cross-repo initiatives |
54
54
  | session | Append a manual narrative entry to today's ecosystem session log |
@@ -692,6 +692,9 @@ Use `infra:` for CI, build, deploy, tooling, and release-pipeline changes that d
692
692
 
693
693
  ## Project Extensions
694
694
 
695
- > Everything below this heading is preserved across `momentum upgrade`.
696
- > Add project-specific navigation, rules, cross-repo references, etc. here.
697
- > Anything above this heading is managed by momentum and may be replaced on upgrade.
695
+ <!-- momentum:project-rules-pointer -->
696
+ > **Project-specific rules live in `specs/project-rules.md`** — read it now.
697
+ > Session-start self-audits, project constraints, and any project-specific
698
+ > guidance are there, shared identically by every agent (ADR-0010). This
699
+ > section is a momentum-managed pointer; edit `specs/project-rules.md`, not
700
+ > this file.
@@ -24,7 +24,7 @@ The shipped recipe set (one skill per recipe, all under
24
24
  | log | Append a manual narrative entry to the history of the phase bound to your branch (fallback: status.md) |
25
25
  | migrate | Onboard an existing project into momentum |
26
26
  | validate | Check spec structure health |
27
- | sync-config | Re-infer project shape, show config drift, apply on approval (ENH-062)
27
+ | sync-config | Re-infer project shape, show config drift, apply on approval (ENH-062) |
28
28
  | ecosystem | Cross-repo ecosystem coordination |
29
29
  | initiative | Manage cross-repo initiatives |
30
30
  | session | Append a manual narrative entry to today's ecosystem session log |
@@ -48,7 +48,7 @@ The shipped recipe set (one command per recipe):
48
48
  | lanes | Work with lanes — concurrent workstreams in one repo (Rule 15) |
49
49
  | migrate | Onboard an existing project into momentum |
50
50
  | validate | Check spec structure health |
51
- | sync-config | Re-infer project shape, show config drift, apply on approval (ENH-062)
51
+ | sync-config | Re-infer project shape, show config drift, apply on approval (ENH-062) |
52
52
  | ecosystem | Cross-repo ecosystem coordination |
53
53
  | initiative | Manage cross-repo initiatives |
54
54
  | session | Append a manual narrative entry to today's ecosystem session log |
@@ -605,6 +605,9 @@ Use `infra:` for CI, build, deploy, tooling, and release-pipeline changes that d
605
605
 
606
606
  ## Project Extensions
607
607
 
608
- > Everything below this heading is preserved across `momentum upgrade`.
609
- > Add project-specific navigation, rules, cross-repo references, etc. here.
610
- > Anything above this heading is managed by momentum and may be replaced on upgrade.
608
+ <!-- momentum:project-rules-pointer -->
609
+ > **Project-specific rules live in `specs/project-rules.md`** — read it now.
610
+ > Session-start self-audits, project constraints, and any project-specific
611
+ > guidance are there, shared identically by every agent (ADR-0010). This
612
+ > section is a momentum-managed pointer; edit `specs/project-rules.md`, not
613
+ > this file.
@@ -24,7 +24,7 @@ The shipped recipe set (one command per recipe):
24
24
  | lanes | Work with lanes — concurrent workstreams in one repo (Rule 15) |
25
25
  | migrate | Onboard an existing project into momentum |
26
26
  | validate | Check spec structure health |
27
- | sync-config | Re-infer project shape, show config drift, apply on approval (ENH-062)
27
+ | sync-config | Re-infer project shape, show config drift, apply on approval (ENH-062) |
28
28
  | ecosystem | Cross-repo ecosystem coordination |
29
29
  | initiative | Manage cross-repo initiatives |
30
30
  | session | Append a manual narrative entry to today's ecosystem session log |
package/bin/ecosystem.js CHANGED
@@ -24,8 +24,11 @@ const {
24
24
  POINTER_END,
25
25
  PRIMARY_INSTRUCTION_CANDIDATES,
26
26
  findPrimaryInstructionFile,
27
+ findAllInstructionFiles,
27
28
  ensurePointerInjected,
29
+ ensurePointerInjectedAll,
28
30
  stripPointer,
31
+ stripPointerAll,
29
32
  } = require('../core/ecosystem/lib/pointer');
30
33
 
31
34
  // ─────────────────────────────────────────────────────────────────────────────
@@ -616,7 +619,7 @@ function cmdAdd(args) {
616
619
  // Idempotent: if the path matches, do nothing. If it differs, fail loud.
617
620
  if (existing.path === relPath) {
618
621
  console.log(`add: "${id}" is already registered (path ${relPath}). No changes.`);
619
- ensurePointerInjected(absRepo, primary, root, manifest.name);
622
+ ensurePointerInjectedAll(absRepo, root, manifest.name);
620
623
  return;
621
624
  }
622
625
  throw new Error(
@@ -641,11 +644,12 @@ function cmdAdd(args) {
641
644
  'utf8',
642
645
  );
643
646
 
644
- // Inject the fenced pointer into the target's primary instruction.
645
- ensurePointerInjected(absRepo, primary, root, manifest.name);
647
+ // Inject the fenced pointer into EVERY instruction file (Phase 28 — every
648
+ // agent, not just the preferred one).
649
+ const injected = ensurePointerInjectedAll(absRepo, root, manifest.name);
646
650
 
647
651
  console.log(`Added member "${id}" (${role}) at ${relPath}.`);
648
- console.log(`Pointer injected into ${path.relative(root, path.join(absRepo, primary))}.`);
652
+ console.log(`Pointer injected into ${injected.map((f) => path.relative(root, path.join(absRepo, f))).join(', ')}.`);
649
653
  }
650
654
 
651
655
  // ─────────────────────────────────────────────────────────────────────────────
@@ -686,10 +690,7 @@ function cmdRemove(args) {
686
690
  // remove must succeed even when the member repo is gone.
687
691
  const absRepo = path.resolve(root, member.path);
688
692
  if (fs.existsSync(absRepo)) {
689
- const primary = findPrimaryInstructionFile(absRepo);
690
- if (primary) {
691
- stripPointer(path.join(absRepo, primary));
692
- }
693
+ stripPointerAll(absRepo); // Phase 28 — strip from every instruction file
693
694
  }
694
695
 
695
696
  console.log(`Removed member "${id}".`);
package/bin/momentum.js CHANGED
@@ -1348,11 +1348,32 @@ function upgrade(targetDir, agent, opts = {}) {
1348
1348
  // would dirty files mid-upgrade and break autostash restores).
1349
1349
  const pointerLib = require('../core/ecosystem/lib/pointer');
1350
1350
  const primaryRel = adapter.primaryInstruction.destination.join('/');
1351
+
1352
+ // Phase 28 (ADR-0010): migrate any authored `## Project Extensions` prose into
1353
+ // specs/project-rules.md and rewrite that section as the managed pointer,
1354
+ // BEFORE the marker-aware rewrite preserves it. Migrate-never-drop, idempotent.
1355
+ if (!_dryRun) {
1356
+ const projectRules = require('../core/lib/project-rules');
1357
+ const migRes = projectRules.migrateProjectExtensions(
1358
+ path.join(target, primaryRel),
1359
+ path.join(target, 'specs'),
1360
+ { projectName: getProjectName(target) }
1361
+ );
1362
+ if (migRes.appended) {
1363
+ console.log(` ↳ migrated ${primaryRel} Project Extensions → specs/project-rules.md (ADR-0010)`);
1364
+ }
1365
+ }
1366
+
1367
+ // Snapshot whether THIS agent's instruction file carries the pointer BEFORE
1368
+ // the managed rewrite — we only ever PRESERVE it (BUG-022), never add one the
1369
+ // file didn't have (adding would dirty files mid-upgrade and break autostash
1370
+ // restores). Phase 28: since `upgrade` now runs per installed agent (G1), this
1371
+ // preserves every agent's pointer; ADDING a missing pointer (cause #2) is
1372
+ // `momentum ecosystem add`'s job, which now injects into all instruction files.
1351
1373
  let hadPointerBlock = false;
1352
1374
  try {
1353
- hadPointerBlock = pointerLib.POINTER_BEGIN_RE.test(
1354
- fs.readFileSync(path.join(target, primaryRel), 'utf8')
1355
- );
1375
+ hadPointerBlock = fs.readFileSync(path.join(target, primaryRel), 'utf8')
1376
+ .includes(pointerLib.POINTER_BEGIN_PREFIX);
1356
1377
  } catch (_e) { /* absent file → nothing to preserve */ }
1357
1378
 
1358
1379
  // Upgrade adapter-owned root instruction file
@@ -1983,12 +2004,27 @@ async function main() {
1983
2004
  // upgrade can only ever install files as new as the CLI itself.
1984
2005
  const staleWarning = formatStaleCliWarning(pkg.version, await updateCheckPromise);
1985
2006
  if (staleWarning) console.log(staleWarning);
1986
- // --autostash: stash a dirty tree, upgrade, restore. Dry-run writes
1987
- // nothing, so it never needs to stash.
2007
+ // Phase 28 (ADR-0010): with no explicit --agent, refresh EVERY installed
2008
+ // agent so no agent's instruction file silently drifts (cause #1 of the
2009
+ // CLAUDE.md/AGENTS.md divergence). Explicit --agent targets that one.
2010
+ let agentsToUpgrade;
2011
+ if (agentExplicit) {
2012
+ agentsToUpgrade = [agent];
2013
+ } else {
2014
+ const installed = loadInstalledState(path.resolve(targetDir));
2015
+ const names = Object.keys(installed.agents || {});
2016
+ agentsToUpgrade = names.length ? names : [agent];
2017
+ }
2018
+ if (agentsToUpgrade.length > 1) {
2019
+ console.log(`Upgrading all installed agents: ${agentsToUpgrade.join(', ')}\n`);
2020
+ }
2021
+ // --autostash: stash a dirty tree, upgrade, restore — once around all
2022
+ // agents. Dry-run writes nothing, so it never needs to stash.
2023
+ const runAll = () => { for (const a of agentsToUpgrade) upgrade(targetDir, a, { dryRun }); };
1988
2024
  if (autostash && !dryRun) {
1989
- withAutostash(path.resolve(targetDir), () => upgrade(targetDir, agent, { dryRun }));
2025
+ withAutostash(path.resolve(targetDir), runAll);
1990
2026
  } else {
1991
- upgrade(targetDir, agent, { dryRun });
2027
+ runAll();
1992
2028
  }
1993
2029
  } catch (err) {
1994
2030
  console.error(`\nError: ${err.message}`);
@@ -103,10 +103,9 @@ function cmdLeave(args) {
103
103
  }
104
104
 
105
105
  if (state === 'broken-pointer') {
106
- // Pointer present but no registration. Just strip pointer to recover.
107
- const primary = pointerLib.findPrimaryInstructionFile(cwd);
108
- if (primary) {
109
- pointerLib.stripPointer(path.join(cwd, primary));
106
+ // Pointer present but no registration. Strip from every instruction file.
107
+ const stripped = pointerLib.stripPointerAll(cwd);
108
+ if (stripped.length) {
110
109
  console.log('Stripped dangling pointer. Now standalone.');
111
110
  } else {
112
111
  console.log('No pointer block found. No changes.');
@@ -89,7 +89,7 @@ Verify, finalize, and release a completed phase.
89
89
  `[staging, main]`) in order. The plan stops at the universal git primitives
90
90
  (`git merge` + `git tag -a` + `git push origin <tag>`); forge-specific
91
91
  release creation and registry publishes are NOT listed here — they run
92
- from `## Project Extensions` + `specs/config.md` (`release_command`,
92
+ from `specs/project-rules.md` + `specs/config.md` (`release_command`,
93
93
  `publish_target`, `release_flow`) after the tag is pushed.
94
94
  ```
95
95
  Ready to release vX.Y.Z. This will:
@@ -98,7 +98,7 @@ Verify, finalize, and release a completed phase.
98
98
  3. Tag vX.Y.Z and push the tag
99
99
 
100
100
  Project-specific release/publish/deploy (forge release, npm publish, …)
101
- run from ## Project Extensions + specs/config.md after the tag lands.
101
+ run from specs/project-rules.md + specs/config.md after the tag lands.
102
102
 
103
103
  Proceed?
104
104
  ```
@@ -126,7 +126,7 @@ Verify, finalize, and release a completed phase.
126
126
  git push origin <branch_flow[1]>
127
127
 
128
128
  # final step — tag + push the tag (universal git; forge release + publish
129
- # are project-specific — run them from ## Project Extensions now)
129
+ # are project-specific — run them from specs/project-rules.md now)
130
130
  git tag -a vX.Y.Z -m "Phase N: {phase name}"
131
131
  git push origin vX.Y.Z
132
132
  ```
@@ -154,7 +154,7 @@ Verify, finalize, and release a completed phase.
154
154
  12. Report summary to user:
155
155
  - What was delivered
156
156
  - Tag URL (`git push origin vX.Y.Z` output) — forge release/publish
157
- status from `## Project Extensions` if any ran
157
+ status from `specs/project-rules.md` if any ran
158
158
  - What's next
159
159
 
160
160
  13. Clean up the spent phase branch (ENH-042 / BUG-026 — now that merge → main
@@ -153,14 +153,14 @@ defaults below) and ask the user the gate question matching the project's
153
153
  reconcile --execute` cleans up the branch + worktree + state."
154
154
 
155
155
  Then append: "Project-specific release/publish/deploy steps (e.g.
156
- `npm publish`, forge release creation) run from `## Project Extensions` in
156
+ `npm publish`, forge release creation) run from `specs/project-rules.md` in
157
157
  CLAUDE.md/AGENTS.md — consult them now if any apply."
158
158
 
159
159
  The gate stops at the truly universal git primitives — `git merge` +
160
160
  `git tag -a` + `git push origin <tag>`. Forge-specific release creation
161
161
  (`gh release create`, `glab release create`, …) and registry publishes
162
162
  (`npm publish`, `twine upload`, …) are NOT in this template; they live in
163
- `## Project Extensions` + `specs/config.md` (`release_command`,
163
+ `specs/project-rules.md` + `specs/config.md` (`release_command`,
164
164
  `publish_target`, `release_flow`).
165
165
 
166
166
  This is the only place the engine asks. Do NOT skip it.
@@ -79,15 +79,14 @@ If you're still exploring the idea, run `/brainstorm-idea` first.
79
79
  - `specs/planning/roadmap.md`
80
80
  - `specs/config.md` (from the config table above; overwrite the
81
81
  `momentum init`-inferred file if present — founding owns the content)
82
+ - `specs/project-rules.md` (project-specific prose rules; each instruction
83
+ file's `## Project Extensions` points here — ADR-0010)
82
84
  - Update `specs/status.md`: Summary, Current Phase (Phase 0 `not started`),
83
85
  Upcoming Phases, Next Actions
84
86
  - For monorepo: `specs/architecture/` first-pass doc(s)
85
87
  - `specs/phases/phase-0-shortname/{overview,plan,tasks,history}.md`
86
88
  (log the founding decisions from this conversation in `history.md`)
87
- - Refresh the derived cache: `core/config.js`
88
- `writeConfigCache('.', readConfig('specs'))` so the pre-push
89
- hook's `protected_branches` matches the authored `branch_flow`;
90
- then `momentum okf index` to refresh bundle listings
89
+ - Refresh derived cache (`writeConfigCache`) + `momentum okf index`
91
90
 
92
91
  7. Commit the founding:
93
92
  ```bash
@@ -30,6 +30,9 @@ Run with no arguments for a fast index-first check, or pass `--deep` for a full
30
30
  - Not founded with no phase directories is the VALID Installed state —
31
31
  report informationally ("not founded yet — `/start-project` when
32
32
  ready"), never as a failure
33
+ - founded AND no `specs/project-rules.md` (ADR-0010) → WARNING: "project
34
+ rules surface missing — run `momentum upgrade` to create it; every agent
35
+ instruction file's `## Project Extensions` pointer expects it"
33
36
 
34
37
  2c. Config (ADR-0009) — WARNING, never a failure (recipes fall back to
35
38
  npm/GitHub defaults when absent):
@@ -159,6 +159,45 @@ function ensurePointerInjected(absRepo, primaryFile, root, ecosystemName) {
159
159
  fs.writeFileSync(filePath, next, 'utf8');
160
160
  }
161
161
 
162
+ /**
163
+ * All present primary instruction files in a member repo (not just the first).
164
+ * Phase 28 (ADR-0010): the ecosystem pointer must reach EVERY agent's file, so
165
+ * a Codex/opencode session reading AGENTS.md is not blind to the ecosystem.
166
+ */
167
+ function findAllInstructionFiles(repoPath) {
168
+ return PRIMARY_INSTRUCTION_CANDIDATES.filter((name) =>
169
+ fs.existsSync(path.join(repoPath, name)));
170
+ }
171
+
172
+ /**
173
+ * Inject/refresh the pointer block in EVERY present instruction file
174
+ * (Phase 28). Returns the list of files touched. When none exist, returns [].
175
+ */
176
+ function ensurePointerInjectedAll(absRepo, root, ecosystemName) {
177
+ const files = findAllInstructionFiles(absRepo);
178
+ for (const primaryFile of files) {
179
+ ensurePointerInjected(absRepo, primaryFile, root, ecosystemName);
180
+ }
181
+ return files;
182
+ }
183
+
184
+ /**
185
+ * Strip the pointer block from EVERY present instruction file (Phase 28).
186
+ * Returns only the files that actually HAD a pointer (so callers can report
187
+ * "nothing to strip" accurately).
188
+ */
189
+ function stripPointerAll(absRepo) {
190
+ const stripped = [];
191
+ for (const primaryFile of findAllInstructionFiles(absRepo)) {
192
+ const fp = path.join(absRepo, primaryFile);
193
+ let had = false;
194
+ try { had = fs.readFileSync(fp, 'utf8').includes(POINTER_BEGIN_PREFIX); } catch { /* absent */ }
195
+ stripPointer(fp);
196
+ if (had) stripped.push(primaryFile);
197
+ }
198
+ return stripped;
199
+ }
200
+
162
201
  /**
163
202
  * Strip the pointer block (any version) and one surrounding blank
164
203
  * line from the given file. Idempotent: silently no-ops if the file
@@ -191,8 +230,11 @@ module.exports = {
191
230
  POINTER_BLOCK_RE,
192
231
  PRIMARY_INSTRUCTION_CANDIDATES,
193
232
  findPrimaryInstructionFile,
233
+ findAllInstructionFiles,
194
234
  hasPointerBlock,
195
235
  ensurePointerInjected,
236
+ ensurePointerInjectedAll,
196
237
  stripPointer,
238
+ stripPointerAll,
197
239
  renderPointerBody,
198
240
  };
@@ -23,6 +23,7 @@ function isReserved(relPath) {
23
23
  const RULES = [
24
24
  [/^status\.md$/, 'Status'],
25
25
  [/^config\.md$/, 'Config'],
26
+ [/^project-rules\.md$/, 'Project Rules'],
26
27
  [/^backlog\/backlog\.md$/, 'Backlog'],
27
28
  [/^backlog\/details\/.+\.md$/, 'Backlog Detail'],
28
29
  [/^planning\/roadmap\.md$/, 'Roadmap'],
@@ -0,0 +1,167 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * `specs/project-rules.md` — the single shared home for project-specific PROSE
5
+ * (Phase 28, ADR-0010). Every agent instruction file's `## Project Extensions`
6
+ * section becomes a managed POINTER to this file, so project rules live once and
7
+ * cannot diverge across adapters.
8
+ *
9
+ * This module owns:
10
+ * - the pointer block that replaces `## Project Extensions`,
11
+ * - the `project-rules.md` scaffold,
12
+ * - `migrateProjectExtensions()` — the migrate-never-drop upgrade path that
13
+ * moves an instruction file's authored extensions into project-rules.md and
14
+ * rewrites its `## Project Extensions` as the pointer.
15
+ *
16
+ * Pure logic is exported separately from the fs-touching migration so it is
17
+ * unit-testable. Zero dependencies — node builtins only.
18
+ */
19
+
20
+ const fs = require('fs');
21
+ const path = require('path');
22
+
23
+ const MARKER = '## Project Extensions';
24
+ const POINTER_SENTINEL = '<!-- momentum:project-rules-pointer -->';
25
+ const PROJECT_RULES_REL = 'project-rules.md';
26
+
27
+ /** The managed pointer block that IS the `## Project Extensions` section. */
28
+ function renderPointerBlock() {
29
+ return [
30
+ MARKER,
31
+ '',
32
+ POINTER_SENTINEL,
33
+ '> **Project-specific rules live in `specs/project-rules.md`** — read it now.',
34
+ '> Session-start self-audits, project constraints, and any project-specific',
35
+ '> guidance are there, shared identically by every agent (ADR-0010). This',
36
+ '> section is a momentum-managed pointer; edit `specs/project-rules.md`, not',
37
+ '> this file.',
38
+ '',
39
+ ].join('\n');
40
+ }
41
+
42
+ /** Fresh `project-rules.md` scaffold (optionally carrying migrated body). */
43
+ function renderProjectRules(projectName, migratedBody) {
44
+ const head = [
45
+ '---',
46
+ 'type: Project Rules',
47
+ '---',
48
+ '',
49
+ `# Project Rules — ${projectName || 'this project'}`,
50
+ '',
51
+ '> Project-specific rules every agent reads — referenced by each instruction',
52
+ "> file's `## Project Extensions` pointer (ADR-0010). One home, shared by all",
53
+ '> adapters. Edit freely. For a rule that applies to only one agent, annotate',
54
+ '> it inline (e.g. *(Claude Code only)*).',
55
+ '',
56
+ ].join('\n');
57
+ const body = (migratedBody && migratedBody.trim())
58
+ ? migratedBody.trim() + '\n'
59
+ : '_(no project-specific rules yet)_\n';
60
+ return head + body;
61
+ }
62
+
63
+ /**
64
+ * Extract authored prose from a `## Project Extensions` region — everything
65
+ * after the heading and its leading `>` boilerplate note. Returns '' when the
66
+ * region is empty, boilerplate-only, or already the pointer.
67
+ */
68
+ function extractAuthoredProse(extensionsRegion) {
69
+ if (!extensionsRegion) return '';
70
+ if (extensionsRegion.includes(POINTER_SENTINEL)) return ''; // already a pointer
71
+ const lines = extensionsRegion.replace(/^\n+/, '').split('\n');
72
+ // drop the `## Project Extensions` heading line
73
+ if (lines[0] && lines[0].trim() === MARKER) lines.shift();
74
+ // drop leading blank + blockquote boilerplate lines
75
+ while (lines.length && (lines[0].trim() === '' || lines[0].trimStart().startsWith('>'))) {
76
+ lines.shift();
77
+ }
78
+ return lines.join('\n').trim();
79
+ }
80
+
81
+ /** Replace a file's `## Project Extensions` region with the pointer block. */
82
+ function pointerizeContent(content) {
83
+ const idx = content.indexOf('\n' + MARKER);
84
+ const managed = idx === -1 ? content.replace(/\s+$/, '') : content.slice(0, idx).replace(/\s+$/, '');
85
+ return managed + '\n\n' + renderPointerBlock();
86
+ }
87
+
88
+ /** True when a file's extensions region is already the managed pointer. */
89
+ function isPointerized(content) {
90
+ return content.includes(POINTER_SENTINEL);
91
+ }
92
+
93
+ /**
94
+ * Migrate one instruction file's Project Extensions into project-rules.md and
95
+ * rewrite the section as the pointer. Migrate-never-drop, idempotent.
96
+ *
97
+ * @param {string} instructionFile absolute path (CLAUDE.md / AGENTS.md)
98
+ * @param {string} specsDir absolute path to the project's specs/ dir
99
+ * @param {object} [opts] { dryRun, projectName }
100
+ * @returns {{ changed, appended, alreadyPointer, proseChars }}
101
+ */
102
+ function migrateProjectExtensions(instructionFile, specsDir, opts = {}) {
103
+ const dryRun = Boolean(opts.dryRun);
104
+ const result = { changed: false, appended: false, alreadyPointer: false, proseChars: 0 };
105
+ if (!fs.existsSync(instructionFile)) return result;
106
+
107
+ const content = fs.readFileSync(instructionFile, 'utf8');
108
+ if (isPointerized(content)) { result.alreadyPointer = true; return result; }
109
+
110
+ const idx = content.indexOf('\n' + MARKER);
111
+ // No `## Project Extensions` marker → a pre-marker / custom file. Leave it to
112
+ // the standard upgrade's pre-marker path (backup .bak + append under marker),
113
+ // which preserves the user's content; never pointerize it here.
114
+ if (idx === -1) { result.noMarker = true; return result; }
115
+ const extensionsRegion = content.slice(idx);
116
+ const prose = extractAuthoredProse(extensionsRegion);
117
+ result.proseChars = prose.length;
118
+
119
+ const rulesPath = path.join(specsDir, PROJECT_RULES_REL);
120
+
121
+ if (prose) {
122
+ const label = path.basename(instructionFile);
123
+ const heading = `## Migrated from ${label}`;
124
+ let existing = fs.existsSync(rulesPath) ? fs.readFileSync(rulesPath, 'utf8') : null;
125
+ if (existing === null) {
126
+ existing = renderProjectRules(opts.projectName, '');
127
+ }
128
+ // Idempotent: only append if this file's migration block isn't already there.
129
+ if (!existing.includes(heading)) {
130
+ const block = `\n${heading}\n\n${prose}\n`;
131
+ const next = existing.replace(/\s+$/, '') + '\n' + block;
132
+ if (!dryRun) {
133
+ fs.mkdirSync(specsDir, { recursive: true });
134
+ fs.writeFileSync(rulesPath, next);
135
+ }
136
+ result.appended = true;
137
+ }
138
+ }
139
+
140
+ // Rewrite the instruction file's extensions region as the pointer.
141
+ const pointerized = pointerizeContent(content);
142
+ if (pointerized !== content) {
143
+ if (!dryRun) fs.writeFileSync(instructionFile, pointerized);
144
+ result.changed = true;
145
+ }
146
+
147
+ // The pointer must resolve: ensure project-rules.md exists (scaffold if the
148
+ // section had no prose to migrate), so we never leave a dangling pointer.
149
+ if (result.changed && !fs.existsSync(rulesPath) && !dryRun) {
150
+ fs.mkdirSync(specsDir, { recursive: true });
151
+ fs.writeFileSync(rulesPath, renderProjectRules(opts.projectName, ''));
152
+ result.scaffolded = true;
153
+ }
154
+ return result;
155
+ }
156
+
157
+ module.exports = {
158
+ MARKER,
159
+ POINTER_SENTINEL,
160
+ PROJECT_RULES_REL,
161
+ renderPointerBlock,
162
+ renderProjectRules,
163
+ extractAuthoredProse,
164
+ pointerizeContent,
165
+ isPointerized,
166
+ migrateProjectExtensions,
167
+ };
@@ -484,6 +484,9 @@ Use `infra:` for CI, build, deploy, tooling, and release-pipeline changes that d
484
484
 
485
485
  ## Project Extensions
486
486
 
487
- > Everything below this heading is preserved across `momentum upgrade`.
488
- > Add project-specific navigation, rules, cross-repo references, etc. here.
489
- > Anything above this heading is managed by momentum and may be replaced on upgrade.
487
+ <!-- momentum:project-rules-pointer -->
488
+ > **Project-specific rules live in `specs/project-rules.md`** — read it now.
489
+ > Session-start self-audits, project constraints, and any project-specific
490
+ > guidance are there, shared identically by every agent (ADR-0010). This
491
+ > section is a momentum-managed pointer; edit `specs/project-rules.md`, not
492
+ > this file.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@limina-labs/momentum",
3
- "version": "0.34.0",
3
+ "version": "0.35.0",
4
4
  "description": "Agent-agnostic, specs-driven development framework — single project or multi-project ecosystem",
5
5
  "homepage": "https://trymomentum.github.io",
6
6
  "bin": {