@limina-labs/momentum 0.34.0 → 0.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/adapters/antigravity/instructions/AGENTS.md +12 -5
  2. package/adapters/antigravity/instructions/manifest.json +8 -0
  3. package/adapters/claude-code/instructions/manifest.json +8 -0
  4. package/adapters/codex/instructions/AGENTS.md +13 -6
  5. package/adapters/codex/instructions/manifest.json +8 -0
  6. package/adapters/codex/instructions/surfaces.md +1 -1
  7. package/adapters/opencode/instructions/AGENTS.md +13 -6
  8. package/adapters/opencode/instructions/manifest.json +8 -0
  9. package/adapters/opencode/instructions/surfaces.md +1 -1
  10. package/bin/ecosystem.js +20 -9
  11. package/bin/momentum.js +99 -17
  12. package/bin/state-commands.js +3 -4
  13. package/core/commands/complete-phase.md +4 -4
  14. package/core/commands/start-phase.md +2 -2
  15. package/core/commands/start-project.md +3 -4
  16. package/core/commands/validate.md +3 -0
  17. package/core/ecosystem/lib/pointer.js +42 -0
  18. package/core/instructions/README.md +69 -0
  19. package/core/instructions/rules-body.md +2 -2
  20. package/core/lib/instruction-compose.js +175 -0
  21. package/core/lib/okf-types.js +1 -0
  22. package/core/lib/project-rules.js +167 -0
  23. package/core/specs-templates/CLAUDE.md +13 -6
  24. package/package.json +1 -1
  25. package/adapters/antigravity/instructions/header.md +0 -3
  26. package/adapters/antigravity/instructions/vars.json +0 -1
  27. package/adapters/claude-code/instructions/header.md +0 -3
  28. package/adapters/claude-code/instructions/vars.json +0 -1
  29. package/adapters/codex/instructions/header.md +0 -3
  30. package/adapters/codex/instructions/vars.json +0 -1
  31. package/adapters/opencode/instructions/header.md +0 -3
  32. package/adapters/opencode/instructions/vars.json +0 -1
@@ -176,6 +176,10 @@ lives in this AGENTS.md primary-instruction text as a belt-and-braces
176
176
  fallback: if a `.momentum/inbox/handoff-NNN.md` file exists at session
177
177
  start, read it and acknowledge before continuing.
178
178
 
179
+ ## In-Session Task Tool
180
+
181
+ When a rule mentions tracking in-session progress (Rule 2), Antigravity's task tool is the native **task list** artifact (task.md) — in-session only; the durable record is `specs/phases/<phase>/tasks.md`.
182
+
179
183
  ## Autonomous Behaviors (Always-On Rules)
180
184
 
181
185
  ### Rule 1: Always Orient First
@@ -197,7 +201,7 @@ After completing ANY meaningful work, automatically update:
197
201
  2. **`specs/status.md`** — if phase progress, blockers, or P0 items changed (touch only your own lane's row — Rule 15)
198
202
  3. **`specs/changelog/YYYY-MM.md`** — log what changed (one line per change, append-only)
199
203
 
200
- Use the native **task list** artifact (task.md) to track in-session task progress. Do NOT wait for the user to ask you to update tracking.
204
+ Use your in-session task-tracking tool (named in the "In-Session Task Tool" note above) to track in-session task progress. Do NOT wait for the user to ask you to update tracking.
201
205
 
202
206
  #### Why
203
207
  Tracking debt compounds invisibly. A task list one day stale is recoverable; one week stale is fiction. Status drift is how phases silently lose direction.
@@ -215,7 +219,7 @@ Tracking debt compounds invisibly. A task list one day stale is recoverable; one
215
219
  #### Anti-Rationalization Counters
216
220
 
217
221
  - "It's faster to do the work first and track at the end" — wrong: reconstruction takes 2-3× longer than real-time logging.
218
- - "The task-list artifact is enough" — The task-list artifact is in-session only; `tasks.md` is the durable record.
222
+ - "Your in-session task-tracking tool is enough" — it is in-session only; `tasks.md` is the durable record.
219
223
  - "Mid-task tracking interrupts flow" — a one-line update costs <30s; reconstructing a day later costs 30 minutes.
220
224
 
221
225
  ### Rule 3: Auto-Track Discoveries
@@ -638,6 +642,9 @@ Use `infra:` for CI, build, deploy, tooling, and release-pipeline changes that d
638
642
 
639
643
  ## Project Extensions
640
644
 
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.
645
+ <!-- momentum:project-rules-pointer -->
646
+ > **Project-specific rules live in `specs/project-rules.md`** — read it now.
647
+ > Session-start self-audits, project constraints, and any project-specific
648
+ > guidance are there, shared identically by every agent (ADR-0010). This
649
+ > section is a momentum-managed pointer; edit `specs/project-rules.md`, not
650
+ > this file.
@@ -0,0 +1,8 @@
1
+ {
2
+ "id": "antigravity",
3
+ "displayName": "Antigravity",
4
+ "surface": "agents-md",
5
+ "taskTool": "the native **task list** artifact (task.md)",
6
+ "taskToolName": "The task-list artifact",
7
+ "hasSurfaceDelta": true
8
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "id": "claude-code",
3
+ "displayName": "Claude Code",
4
+ "surface": "claude-md",
5
+ "taskTool": "the built-in **TodoWrite** tool",
6
+ "taskToolName": "TodoWrite",
7
+ "hasSurfaceDelta": false
8
+ }
@@ -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 |
@@ -230,6 +230,10 @@ This is the documented escalation point referenced in Phase 18 G1
230
230
  plan.md; if the MCP shim is widely broken, momentum will ship a
231
231
  minimal `core/swarm/mcp-cwd-server.js` in a follow-up.
232
232
 
233
+ ## In-Session Task Tool
234
+
235
+ When a rule mentions tracking in-session progress (Rule 2), Codex's task tool is the built-in **plan** tool — in-session only; the durable record is `specs/phases/<phase>/tasks.md`.
236
+
233
237
  ## Autonomous Behaviors (Always-On Rules)
234
238
 
235
239
  ### Rule 1: Always Orient First
@@ -251,7 +255,7 @@ After completing ANY meaningful work, automatically update:
251
255
  2. **`specs/status.md`** — if phase progress, blockers, or P0 items changed (touch only your own lane's row — Rule 15)
252
256
  3. **`specs/changelog/YYYY-MM.md`** — log what changed (one line per change, append-only)
253
257
 
254
- Use the built-in **plan** tool to track in-session task progress. Do NOT wait for the user to ask you to update tracking.
258
+ Use your in-session task-tracking tool (named in the "In-Session Task Tool" note above) to track in-session task progress. Do NOT wait for the user to ask you to update tracking.
255
259
 
256
260
  #### Why
257
261
  Tracking debt compounds invisibly. A task list one day stale is recoverable; one week stale is fiction. Status drift is how phases silently lose direction.
@@ -269,7 +273,7 @@ Tracking debt compounds invisibly. A task list one day stale is recoverable; one
269
273
  #### Anti-Rationalization Counters
270
274
 
271
275
  - "It's faster to do the work first and track at the end" — wrong: reconstruction takes 2-3× longer than real-time logging.
272
- - "The plan tool is enough" — The plan tool is in-session only; `tasks.md` is the durable record.
276
+ - "Your in-session task-tracking tool is enough" — it is in-session only; `tasks.md` is the durable record.
273
277
  - "Mid-task tracking interrupts flow" — a one-line update costs <30s; reconstructing a day later costs 30 minutes.
274
278
 
275
279
  ### Rule 3: Auto-Track Discoveries
@@ -692,6 +696,9 @@ Use `infra:` for CI, build, deploy, tooling, and release-pipeline changes that d
692
696
 
693
697
  ## Project Extensions
694
698
 
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.
699
+ <!-- momentum:project-rules-pointer -->
700
+ > **Project-specific rules live in `specs/project-rules.md`** — read it now.
701
+ > Session-start self-audits, project constraints, and any project-specific
702
+ > guidance are there, shared identically by every agent (ADR-0010). This
703
+ > section is a momentum-managed pointer; edit `specs/project-rules.md`, not
704
+ > this file.
@@ -0,0 +1,8 @@
1
+ {
2
+ "id": "codex",
3
+ "displayName": "Codex",
4
+ "surface": "agents-md",
5
+ "taskTool": "the built-in **plan** tool",
6
+ "taskToolName": "The plan tool",
7
+ "hasSurfaceDelta": true
8
+ }
@@ -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 |
@@ -143,6 +143,10 @@ skill paths (`.claude/skills/`, `.agents/skills/`); momentum's opencode
143
143
  surfaces live under `.opencode/` so multi-adapter installs stay
144
144
  side-by-side without collisions.
145
145
 
146
+ ## In-Session Task Tool
147
+
148
+ When a rule mentions tracking in-session progress (Rule 2), opencode's task tool is the built-in **task** tool (subagent fan-out) — in-session only; the durable record is `specs/phases/<phase>/tasks.md`.
149
+
146
150
  ## Autonomous Behaviors (Always-On Rules)
147
151
 
148
152
  ### Rule 1: Always Orient First
@@ -164,7 +168,7 @@ After completing ANY meaningful work, automatically update:
164
168
  2. **`specs/status.md`** — if phase progress, blockers, or P0 items changed (touch only your own lane's row — Rule 15)
165
169
  3. **`specs/changelog/YYYY-MM.md`** — log what changed (one line per change, append-only)
166
170
 
167
- Use the built-in **task** tool (subagent fan-out) to track in-session task progress. Do NOT wait for the user to ask you to update tracking.
171
+ Use your in-session task-tracking tool (named in the "In-Session Task Tool" note above) to track in-session task progress. Do NOT wait for the user to ask you to update tracking.
168
172
 
169
173
  #### Why
170
174
  Tracking debt compounds invisibly. A task list one day stale is recoverable; one week stale is fiction. Status drift is how phases silently lose direction.
@@ -182,7 +186,7 @@ Tracking debt compounds invisibly. A task list one day stale is recoverable; one
182
186
  #### Anti-Rationalization Counters
183
187
 
184
188
  - "It's faster to do the work first and track at the end" — wrong: reconstruction takes 2-3× longer than real-time logging.
185
- - "The task tool is enough" — The task tool is in-session only; `tasks.md` is the durable record.
189
+ - "Your in-session task-tracking tool is enough" — it is in-session only; `tasks.md` is the durable record.
186
190
  - "Mid-task tracking interrupts flow" — a one-line update costs <30s; reconstructing a day later costs 30 minutes.
187
191
 
188
192
  ### Rule 3: Auto-Track Discoveries
@@ -605,6 +609,9 @@ Use `infra:` for CI, build, deploy, tooling, and release-pipeline changes that d
605
609
 
606
610
  ## Project Extensions
607
611
 
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.
612
+ <!-- momentum:project-rules-pointer -->
613
+ > **Project-specific rules live in `specs/project-rules.md`** — read it now.
614
+ > Session-start self-audits, project constraints, and any project-specific
615
+ > guidance are there, shared identically by every agent (ADR-0010). This
616
+ > section is a momentum-managed pointer; edit `specs/project-rules.md`, not
617
+ > this file.
@@ -0,0 +1,8 @@
1
+ {
2
+ "id": "opencode",
3
+ "displayName": "opencode",
4
+ "surface": "agents-md",
5
+ "taskTool": "the built-in **task** tool (subagent fan-out)",
6
+ "taskToolName": "The task tool",
7
+ "hasSurfaceDelta": true
8
+ }
@@ -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}".`);
@@ -776,9 +777,18 @@ function detectMemberAgent(repoPath) {
776
777
  const m = JSON.parse(
777
778
  fs.readFileSync(path.join(repoPath, '.momentum', 'installed.json'), 'utf8'),
778
779
  );
779
- if (m && m.agent) return m.agent;
780
+ // ADR-0007: the lock is `{ agents: { <id>: … } }`; the pre-0007 lock was
781
+ // `{ agent }`. Read the map first (else every AGENTS.md agent — opencode,
782
+ // Antigravity, Codex — falls through to the `AGENTS.md ⇒ codex` heuristic
783
+ // below and is misidentified). ADR-0011.
784
+ if (m && m.agents && typeof m.agents === 'object') {
785
+ const ids = Object.keys(m.agents);
786
+ if (ids.length) return ids.includes('claude-code') ? 'claude-code' : ids.sort()[0];
787
+ }
788
+ if (m && m.agent) return m.agent; // legacy single-agent lock
780
789
  } catch (_e) { /* fall through to heuristic */ }
781
790
  if (fs.existsSync(path.join(repoPath, '.codex'))) return 'codex';
791
+ if (fs.existsSync(path.join(repoPath, '.opencode'))) return 'opencode';
782
792
  if (fs.existsSync(path.join(repoPath, '.claude'))) return 'claude-code';
783
793
  if (
784
794
  fs.existsSync(path.join(repoPath, '.agents', 'hooks.json')) ||
@@ -1296,6 +1306,7 @@ module.exports = {
1296
1306
  ensurePointerInjected,
1297
1307
  stripPointer,
1298
1308
  findPrimaryInstructionFile,
1309
+ detectMemberAgent,
1299
1310
  sanitizeId,
1300
1311
  POINTER_BEGIN,
1301
1312
  POINTER_END,
package/bin/momentum.js CHANGED
@@ -719,7 +719,46 @@ function resolveAdapterSource(srcRoot, adapterDir, fileSpec) {
719
719
  return path.join(base, ...fileSpec.source);
720
720
  }
721
721
 
722
- function installPrimaryInstruction(srcRoot, targetDir, adapterDir, primaryInstruction) {
722
+ /**
723
+ * ADR-0011 — the raw primary-instruction template content for `agent`.
724
+ * A claude-md agent (and a lone installed agents-md agent) gets the static
725
+ * committed template. When MORE THAN ONE agents-md agent is installed, AGENTS.md
726
+ * is COMPOSED — the neutral spine once + one integration section per installed
727
+ * AGENTS.md agent — so switching or running several agents never drops an
728
+ * integration (fixes the same-path collision). Project name is not yet rendered.
729
+ */
730
+ function resolvePrimaryInstructionContent(srcRoot, targetDir, agent, srcPath) {
731
+ const readStatic = () => fs.readFileSync(srcPath, 'utf8');
732
+ let compose;
733
+ try {
734
+ compose = require('../core/lib/instruction-compose');
735
+ } catch (_e) {
736
+ return readStatic();
737
+ }
738
+ let manifest;
739
+ try {
740
+ manifest = compose.readManifest(srcRoot, agent);
741
+ } catch (_e) {
742
+ return readStatic(); // unknown / manifest-less agent → static template
743
+ }
744
+ if (manifest.surface !== 'agents-md') return readStatic(); // claude-md → static
745
+
746
+ // The set of installed AGENTS.md agents ∪ the one being (re)installed.
747
+ const valid = new Set(compose.discoverAgents(srcRoot));
748
+ const ids = new Set(valid.has(agent) ? [agent] : []);
749
+ const state = loadInstalledState(targetDir);
750
+ for (const id of Object.keys(state.agents || {})) {
751
+ if (!valid.has(id)) continue;
752
+ try {
753
+ if (compose.readManifest(srcRoot, id).surface === 'agents-md') ids.add(id);
754
+ } catch (_e) { /* skip an agent we can't read */ }
755
+ }
756
+ const sorted = [...ids].sort();
757
+ if (sorted.length <= 1) return readStatic(); // N=1 → static committed template
758
+ return compose.composeInstruction(srcRoot, sorted); // N>1 → composed
759
+ }
760
+
761
+ function installPrimaryInstruction(srcRoot, targetDir, adapterDir, primaryInstruction, agent) {
723
762
  if (!primaryInstruction) return null;
724
763
  const srcPath = resolveAdapterSource(srcRoot, adapterDir, primaryInstruction);
725
764
  const destPath = path.join(targetDir, ...primaryInstruction.destination);
@@ -733,7 +772,7 @@ function installPrimaryInstruction(srcRoot, targetDir, adapterDir, primaryInstru
733
772
  return 'added';
734
773
  }
735
774
  const rendered = renderProjectName(
736
- fs.readFileSync(srcPath, 'utf8'),
775
+ resolvePrimaryInstructionContent(srcRoot, targetDir, agent, srcPath),
737
776
  getProjectName(targetDir)
738
777
  );
739
778
  fs.mkdirSync(path.dirname(destPath), { recursive: true });
@@ -745,7 +784,7 @@ function installPrimaryInstruction(srcRoot, targetDir, adapterDir, primaryInstru
745
784
  return 'skipped';
746
785
  }
747
786
 
748
- function upgradePrimaryInstruction(srcRoot, targetDir, adapterDir, primaryInstruction) {
787
+ function upgradePrimaryInstruction(srcRoot, targetDir, adapterDir, primaryInstruction, agent) {
749
788
  if (!primaryInstruction) return null;
750
789
  const srcPath = resolveAdapterSource(srcRoot, adapterDir, primaryInstruction);
751
790
  const destPath = path.join(targetDir, ...primaryInstruction.destination);
@@ -753,12 +792,15 @@ function upgradePrimaryInstruction(srcRoot, targetDir, adapterDir, primaryInstru
753
792
  const projectName = getProjectName(targetDir);
754
793
  recordManaged(destPath); // managed (marker-owned)
755
794
 
795
+ // ADR-0011: compose AGENTS.md across all installed AGENTS.md agents.
796
+ const rawContent = resolvePrimaryInstructionContent(srcRoot, targetDir, agent, srcPath);
797
+
756
798
  console.log(`→ Upgrading ${label}...`);
757
799
  if (primaryInstruction.markerAware) {
758
- return upgradeMarkedFile(srcPath, destPath, label, targetDir, projectName);
800
+ return upgradeMarkedFile(srcPath, destPath, label, targetDir, projectName, rawContent);
759
801
  }
760
802
 
761
- const srcContent = renderProjectName(fs.readFileSync(srcPath, 'utf8'), projectName);
803
+ const srcContent = renderProjectName(rawContent, projectName);
762
804
  const relDest = path.relative(targetDir, destPath);
763
805
  if (!fileExists(destPath)) {
764
806
  if (_dryRun) {
@@ -895,11 +937,14 @@ function partitionByMarker(content) {
895
937
  *
896
938
  * Returns one of: 'added', 'updated', 'unchanged', 'migrated'.
897
939
  */
898
- function upgradeMarkedFile(srcPath, destPath, label, root, projectName) {
940
+ function upgradeMarkedFile(srcPath, destPath, label, root, projectName, srcContentRaw) {
899
941
  const rel = path.relative(root || process.cwd(), destPath);
900
942
  recordManaged(destPath); // managed (marker-owned) in all branches
901
943
  const render = (content) =>
902
944
  projectName ? renderProjectName(content, projectName) : content;
945
+ // ADR-0011: callers may pass composed content (multi-agent AGENTS.md); else
946
+ // read the static committed template.
947
+ const rawSrc = srcContentRaw != null ? srcContentRaw : fs.readFileSync(srcPath, 'utf8');
903
948
 
904
949
  if (!fileExists(destPath)) {
905
950
  if (_dryRun) {
@@ -907,12 +952,12 @@ function upgradeMarkedFile(srcPath, destPath, label, root, projectName) {
907
952
  return 'added';
908
953
  }
909
954
  fs.mkdirSync(path.dirname(destPath), { recursive: true });
910
- fs.writeFileSync(destPath, render(fs.readFileSync(srcPath, 'utf8')));
955
+ fs.writeFileSync(destPath, render(rawSrc));
911
956
  console.log(` + added: ${rel}`);
912
957
  return 'added';
913
958
  }
914
959
 
915
- const srcContent = render(fs.readFileSync(srcPath, 'utf8'));
960
+ const srcContent = render(rawSrc);
916
961
  const destContent = fs.readFileSync(destPath, 'utf8');
917
962
  const destParts = partitionByMarker(destContent);
918
963
 
@@ -1215,7 +1260,7 @@ function init(targetDir, agent, opts = {}) {
1215
1260
  console.log('→ Inferring project config...');
1216
1261
  installConfig(target, { dryRun: _dryRun });
1217
1262
 
1218
- installPrimaryInstruction(src, target, adapterDir, adapter.primaryInstruction);
1263
+ installPrimaryInstruction(src, target, adapterDir, adapter.primaryInstruction, agent);
1219
1264
 
1220
1265
  // Adapter overlay — per-agent commands/agent-rules/scripts (additive)
1221
1266
  applyOverlay(adapterDir, target, dests, { backup: true, root: target });
@@ -1348,11 +1393,32 @@ function upgrade(targetDir, agent, opts = {}) {
1348
1393
  // would dirty files mid-upgrade and break autostash restores).
1349
1394
  const pointerLib = require('../core/ecosystem/lib/pointer');
1350
1395
  const primaryRel = adapter.primaryInstruction.destination.join('/');
1396
+
1397
+ // Phase 28 (ADR-0010): migrate any authored `## Project Extensions` prose into
1398
+ // specs/project-rules.md and rewrite that section as the managed pointer,
1399
+ // BEFORE the marker-aware rewrite preserves it. Migrate-never-drop, idempotent.
1400
+ if (!_dryRun) {
1401
+ const projectRules = require('../core/lib/project-rules');
1402
+ const migRes = projectRules.migrateProjectExtensions(
1403
+ path.join(target, primaryRel),
1404
+ path.join(target, 'specs'),
1405
+ { projectName: getProjectName(target) }
1406
+ );
1407
+ if (migRes.appended) {
1408
+ console.log(` ↳ migrated ${primaryRel} Project Extensions → specs/project-rules.md (ADR-0010)`);
1409
+ }
1410
+ }
1411
+
1412
+ // Snapshot whether THIS agent's instruction file carries the pointer BEFORE
1413
+ // the managed rewrite — we only ever PRESERVE it (BUG-022), never add one the
1414
+ // file didn't have (adding would dirty files mid-upgrade and break autostash
1415
+ // restores). Phase 28: since `upgrade` now runs per installed agent (G1), this
1416
+ // preserves every agent's pointer; ADDING a missing pointer (cause #2) is
1417
+ // `momentum ecosystem add`'s job, which now injects into all instruction files.
1351
1418
  let hadPointerBlock = false;
1352
1419
  try {
1353
- hadPointerBlock = pointerLib.POINTER_BEGIN_RE.test(
1354
- fs.readFileSync(path.join(target, primaryRel), 'utf8')
1355
- );
1420
+ hadPointerBlock = fs.readFileSync(path.join(target, primaryRel), 'utf8')
1421
+ .includes(pointerLib.POINTER_BEGIN_PREFIX);
1356
1422
  } catch (_e) { /* absent file → nothing to preserve */ }
1357
1423
 
1358
1424
  // Upgrade adapter-owned root instruction file
@@ -1360,7 +1426,8 @@ function upgrade(targetDir, agent, opts = {}) {
1360
1426
  src,
1361
1427
  target,
1362
1428
  adapterDir,
1363
- adapter.primaryInstruction
1429
+ adapter.primaryInstruction,
1430
+ agent
1364
1431
  );
1365
1432
 
1366
1433
  if (hadPointerBlock && !_dryRun) {
@@ -1983,12 +2050,27 @@ async function main() {
1983
2050
  // upgrade can only ever install files as new as the CLI itself.
1984
2051
  const staleWarning = formatStaleCliWarning(pkg.version, await updateCheckPromise);
1985
2052
  if (staleWarning) console.log(staleWarning);
1986
- // --autostash: stash a dirty tree, upgrade, restore. Dry-run writes
1987
- // nothing, so it never needs to stash.
2053
+ // Phase 28 (ADR-0010): with no explicit --agent, refresh EVERY installed
2054
+ // agent so no agent's instruction file silently drifts (cause #1 of the
2055
+ // CLAUDE.md/AGENTS.md divergence). Explicit --agent targets that one.
2056
+ let agentsToUpgrade;
2057
+ if (agentExplicit) {
2058
+ agentsToUpgrade = [agent];
2059
+ } else {
2060
+ const installed = loadInstalledState(path.resolve(targetDir));
2061
+ const names = Object.keys(installed.agents || {});
2062
+ agentsToUpgrade = names.length ? names : [agent];
2063
+ }
2064
+ if (agentsToUpgrade.length > 1) {
2065
+ console.log(`Upgrading all installed agents: ${agentsToUpgrade.join(', ')}\n`);
2066
+ }
2067
+ // --autostash: stash a dirty tree, upgrade, restore — once around all
2068
+ // agents. Dry-run writes nothing, so it never needs to stash.
2069
+ const runAll = () => { for (const a of agentsToUpgrade) upgrade(targetDir, a, { dryRun }); };
1988
2070
  if (autostash && !dryRun) {
1989
- withAutostash(path.resolve(targetDir), () => upgrade(targetDir, agent, { dryRun }));
2071
+ withAutostash(path.resolve(targetDir), runAll);
1990
2072
  } else {
1991
- upgrade(targetDir, agent, { dryRun });
2073
+ runAll();
1992
2074
  }
1993
2075
  } catch (err) {
1994
2076
  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
  };
@@ -0,0 +1,69 @@
1
+ # Instruction sources — the three-tier variation model (ADR-0011)
2
+
3
+ The primary instruction file each agent auto-loads (`CLAUDE.md` for Claude Code,
4
+ `AGENTS.md` for every AGENTS.md-standard agent) is **generated**, never
5
+ hand-edited. It is a projection of **(principal constants × per-agent variation
6
+ manifest)**. Regenerate with `npm run generate-instructions`; the suite's drift
7
+ guard fails if a committed template diverges from its sources.
8
+
9
+ ## The three tiers
10
+
11
+ | Tier | What | Where | Rule |
12
+ |------|------|-------|------|
13
+ | **1 — Principal constants** (the neutral spine) | Navigation table, Rules 1–15, Naming, Constraints, Project Extensions pointer | `core/instructions/navigation.md`, `core/instructions/rules-body.md` | **Byte-identical** across every agent + surface (`CLAUDE.md` ≡ `AGENTS.md` body) |
14
+ | **2 — Variation manifest** | Everything that varies per agent | `adapters/<agent>/instructions/manifest.json` | One declarative record; generator auto-discovers it (OCP) |
15
+ | **3 — Surface delta** | Irreducibly agent-specific integration prose + the task-tool mapping line | `adapters/<agent>/instructions/surfaces.md` (optional) | A scoped delta, never the file's whole identity (SRP) |
16
+
17
+ The rules body carries **no agent-specific token** — the task tool is named only
18
+ in the Tier-3 delta, which every agent still loads. That is what makes the spine
19
+ byte-identical (and therefore a testable invariant, not a claim).
20
+
21
+ ## `manifest.json` schema
22
+
23
+ ```json
24
+ {
25
+ "id": "opencode",
26
+ "displayName": "opencode",
27
+ "surface": "agents-md",
28
+ "taskTool": "the built-in **task** tool (subagent fan-out)",
29
+ "taskToolName": "The task tool",
30
+ "hasSurfaceDelta": true
31
+ }
32
+ ```
33
+
34
+ | Field | Type | Meaning |
35
+ |-------|------|---------|
36
+ | `id` | string | Adapter id — must match the `adapters/<id>/` directory name |
37
+ | `displayName` | string | Human name used in the header scaffold + integration heading |
38
+ | `surface` | `"claude-md"` \| `"agents-md"` | Which instruction file this agent reads; drives the install destination |
39
+ | `taskTool` | string | Rendered into the Tier-3 task-tool mapping line (may contain markdown) |
40
+ | `taskToolName` | string | Capitalized short form for prose references |
41
+ | `hasSurfaceDelta` | boolean | Whether a `surfaces.md` integration delta exists for this agent |
42
+
43
+ ## Assembly order
44
+
45
+ ```
46
+ header (scaffold, display name from manifest)
47
+ → navigation (Tier 1)
48
+ → surfaces delta (Tier 3, if hasSurfaceDelta)
49
+ → rules body (Tier 1, agent-neutral)
50
+ → Project Extensions pointer tail (ADR-0010)
51
+ ```
52
+
53
+ ## Adding a new agent (the OCP path)
54
+
55
+ 1. Create `adapters/<id>/instructions/manifest.json`.
56
+ 2. Optionally add `adapters/<id>/instructions/surfaces.md` (integration delta).
57
+ 3. Run `npm run generate-instructions`.
58
+
59
+ No edit to `scripts/generate-instructions.js` is required — it discovers
60
+ adapters by scanning `adapters/*/instructions/`. A suite test proves this by
61
+ generating for a synthetic adapter with zero generator edits.
62
+
63
+ ## AGENTS.md when multiple AGENTS.md agents are installed
64
+
65
+ `AGENTS.md` is a shared standard, so one repo can host several AGENTS.md agents.
66
+ The committed per-agent template is the single-agent (N=1) projection. At
67
+ install/upgrade, when `installed.json.agents` holds more than one `agents-md`
68
+ agent, `AGENTS.md` is **composed**: the neutral spine once, plus one integration
69
+ section per installed AGENTS.md agent. `CLAUDE.md` is unaffected (Claude-only).
@@ -19,7 +19,7 @@ After completing ANY meaningful work, automatically update:
19
19
  2. **`specs/status.md`** — if phase progress, blockers, or P0 items changed (touch only your own lane's row — Rule 15)
20
20
  3. **`specs/changelog/YYYY-MM.md`** — log what changed (one line per change, append-only)
21
21
 
22
- Use {{TASK_TOOL}} to track in-session task progress. Do NOT wait for the user to ask you to update tracking.
22
+ Use your in-session task-tracking tool (named in the "In-Session Task Tool" note above) to track in-session task progress. Do NOT wait for the user to ask you to update tracking.
23
23
 
24
24
  #### Why
25
25
  Tracking debt compounds invisibly. A task list one day stale is recoverable; one week stale is fiction. Status drift is how phases silently lose direction.
@@ -37,7 +37,7 @@ Tracking debt compounds invisibly. A task list one day stale is recoverable; one
37
37
  #### Anti-Rationalization Counters
38
38
 
39
39
  - "It's faster to do the work first and track at the end" — wrong: reconstruction takes 2-3× longer than real-time logging.
40
- - "{{TASK_TOOL_NAME}} is enough" — {{TASK_TOOL_NAME}} is in-session only; `tasks.md` is the durable record.
40
+ - "Your in-session task-tracking tool is enough" — it is in-session only; `tasks.md` is the durable record.
41
41
  - "Mid-task tracking interrupts flow" — a one-line update costs <30s; reconstructing a day later costs 30 minutes.
42
42
 
43
43
  ### Rule 3: Auto-Track Discoveries
@@ -0,0 +1,175 @@
1
+ 'use strict';
2
+
3
+ // ADR-0011 — the instruction file is a projection of
4
+ // (principal constants × per-agent variation manifest).
5
+ //
6
+ // Pure assembly logic, shipped in core/ so BOTH build-time generation
7
+ // (scripts/generate-instructions.js) and install-time multi-agent AGENTS.md
8
+ // composition (bin/momentum.js) use ONE code path. Every function takes
9
+ // `root` = the momentum package/repo root that holds core/ + adapters/.
10
+ //
11
+ // Three tiers:
12
+ // 1. Principal constants (neutral spine): core/instructions/navigation.md +
13
+ // rules-body.md — byte-identical across every agent + surface.
14
+ // 2. Variation manifest: adapters/<id>/instructions/manifest.json.
15
+ // 3. Surface delta: adapters/<id>/instructions/surfaces.md (optional).
16
+ //
17
+ // Assembly:
18
+ // marker → header → navigation → (per agent: surfaces? + task-tool note)
19
+ // → neutral rules body → Project Extensions pointer tail (ADR-0010)
20
+
21
+ const fs = require('fs');
22
+ const path = require('path');
23
+
24
+ const MARKER_COMMENT = [
25
+ '<!-- momentum-managed (generated) — regenerate with `npm run generate-instructions`',
26
+ ' in the momentum repo; sources: core/instructions/ + adapters/<agent>/instructions/.',
27
+ " Everything above '## Project Extensions' may be replaced by `momentum upgrade`. -->",
28
+ ].join('\n');
29
+
30
+ // ADR-0010: `## Project Extensions` is a managed POINTER to specs/project-rules.md,
31
+ // not an authoring surface. Kept byte-identical to core/lib/project-rules.js.
32
+ const EXTENSIONS_TAIL = [
33
+ '---',
34
+ '',
35
+ '## Project Extensions',
36
+ '',
37
+ '<!-- momentum:project-rules-pointer -->',
38
+ '> **Project-specific rules live in `specs/project-rules.md`** — read it now.',
39
+ '> Session-start self-audits, project constraints, and any project-specific',
40
+ '> guidance are there, shared identically by every agent (ADR-0010). This',
41
+ '> section is a momentum-managed pointer; edit `specs/project-rules.md`, not',
42
+ '> this file.',
43
+ ].join('\n');
44
+
45
+ function readFragment(root, rel) {
46
+ return fs.readFileSync(path.join(root, ...rel), 'utf8');
47
+ }
48
+
49
+ /** Discover every adapter that ships an instruction manifest (OCP: add a dir, no edits). */
50
+ function discoverAgents(root) {
51
+ const dir = path.join(root, 'adapters');
52
+ return fs
53
+ .readdirSync(dir)
54
+ .filter((id) => fs.existsSync(path.join(dir, id, 'instructions', 'manifest.json')))
55
+ .sort();
56
+ }
57
+
58
+ function readManifest(root, id) {
59
+ const m = JSON.parse(readFragment(root, ['adapters', id, 'instructions', 'manifest.json']));
60
+ for (const key of ['id', 'displayName', 'surface', 'taskTool', 'taskToolName']) {
61
+ if (typeof m[key] !== 'string' || !m[key]) {
62
+ throw new Error(`adapters/${id}/instructions/manifest.json: missing/invalid "${key}"`);
63
+ }
64
+ }
65
+ if (m.id !== id) throw new Error(`manifest id "${m.id}" != adapter dir "${id}"`);
66
+ if (m.surface !== 'claude-md' && m.surface !== 'agents-md') {
67
+ throw new Error(`adapters/${id}: surface "${m.surface}" — expected claude-md | agents-md`);
68
+ }
69
+ return m;
70
+ }
71
+
72
+ /** The install destination filename implied by the surface. */
73
+ function surfaceDestination(surface) {
74
+ if (surface === 'claude-md') return 'CLAUDE.md';
75
+ if (surface === 'agents-md') return 'AGENTS.md';
76
+ throw new Error(`unknown surface "${surface}"`);
77
+ }
78
+
79
+ /** Where this agent's committed (N=1) template lives, derived from its surface. */
80
+ function templatePath(id, surface) {
81
+ if (surface === 'claude-md') return ['core', 'specs-templates', 'CLAUDE.md'];
82
+ if (surface === 'agents-md') return ['adapters', id, 'instructions', 'AGENTS.md'];
83
+ throw new Error(`unknown surface "${surface}" for adapter "${id}"`);
84
+ }
85
+
86
+ /** Header: single scaffold constant, agent display name(s) the only variable. */
87
+ function renderHeader(manifests) {
88
+ const title = '# Project Rules: <Project Name>';
89
+ if (manifests.length === 1) {
90
+ return `${title}\n\n> ${manifests[0].displayName} configuration for this momentum-managed project.`;
91
+ }
92
+ const names = manifests.map((m) => m.displayName).join(', ');
93
+ return (
94
+ `${title}\n\n> Configuration for this momentum-managed project. This shared ` +
95
+ `AGENTS.md serves ${manifests.length} agents (${names}); each has its own ` +
96
+ `integration section below.`
97
+ );
98
+ }
99
+
100
+ /** The one agent-specific line lifted out of the neutral spine (SRP). */
101
+ function renderTaskToolNote(m) {
102
+ return [
103
+ '## In-Session Task Tool',
104
+ '',
105
+ `When a rule mentions tracking in-session progress (Rule 2), ${m.displayName}'s ` +
106
+ `task tool is ${m.taskTool} — in-session only; the durable record is ` +
107
+ '`specs/phases/<phase>/tasks.md`.',
108
+ ].join('\n');
109
+ }
110
+
111
+ function readSurfaces(root, m) {
112
+ if (!m.hasSurfaceDelta) return null;
113
+ const p = path.join(root, 'adapters', m.id, 'instructions', 'surfaces.md');
114
+ return fs.existsSync(p) ? fs.readFileSync(p, 'utf8') : null;
115
+ }
116
+
117
+ function finalize(parts) {
118
+ const content = parts.map((p) => p.replace(/\s+$/, '')).join('\n\n') + '\n';
119
+ const leftover = content.match(/\{\{[A-Z_]+\}\}/);
120
+ if (leftover) throw new Error(`unrendered placeholder ${leftover[0]}`);
121
+ return content;
122
+ }
123
+
124
+ /**
125
+ * Compose an instruction file for one or more agents.
126
+ * - Single id → the committed N=1 projection (a static template).
127
+ * - Multiple ids → neutral spine + one integration section per agent
128
+ * (install-time AGENTS.md when several AGENTS.md agents are installed).
129
+ */
130
+ function composeInstruction(root, ids) {
131
+ if (!ids.length) throw new Error('composeInstruction: no agents');
132
+ const manifests = ids.map((id) => readManifest(root, id));
133
+ const navigation = readFragment(root, ['core', 'instructions', 'navigation.md']);
134
+ const body = readFragment(root, ['core', 'instructions', 'rules-body.md']);
135
+
136
+ const parts = [MARKER_COMMENT, renderHeader(manifests), navigation];
137
+ for (const m of manifests) {
138
+ const surfaces = readSurfaces(root, m);
139
+ if (surfaces) parts.push(surfaces);
140
+ parts.push(renderTaskToolNote(m));
141
+ }
142
+ parts.push(body, EXTENSIONS_TAIL);
143
+ return finalize(parts);
144
+ }
145
+
146
+ /** One committed template descriptor for a single agent. */
147
+ function generateFor(root, id) {
148
+ const m = readManifest(root, id);
149
+ const target = templatePath(id, m.surface);
150
+ return {
151
+ agent: id,
152
+ targetRel: target.join('/'),
153
+ targetPath: path.join(root, ...target),
154
+ content: composeInstruction(root, [id]),
155
+ manifest: m,
156
+ };
157
+ }
158
+
159
+ function generateAll(root) {
160
+ return discoverAgents(root).map((id) => generateFor(root, id));
161
+ }
162
+
163
+ module.exports = {
164
+ MARKER_COMMENT,
165
+ EXTENSIONS_TAIL,
166
+ discoverAgents,
167
+ readManifest,
168
+ surfaceDestination,
169
+ templatePath,
170
+ renderHeader,
171
+ renderTaskToolNote,
172
+ composeInstruction,
173
+ generateFor,
174
+ generateAll,
175
+ };
@@ -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
+ };
@@ -4,7 +4,7 @@
4
4
 
5
5
  # Project Rules: <Project Name>
6
6
 
7
- > Claude Code configuration for this project.
7
+ > Claude Code configuration for this momentum-managed project.
8
8
 
9
9
  ## Navigation (Where to Find Things)
10
10
 
@@ -22,6 +22,10 @@
22
22
 
23
23
  ---
24
24
 
25
+ ## In-Session Task Tool
26
+
27
+ When a rule mentions tracking in-session progress (Rule 2), Claude Code's task tool is the built-in **TodoWrite** tool — in-session only; the durable record is `specs/phases/<phase>/tasks.md`.
28
+
25
29
  ## Autonomous Behaviors (Always-On Rules)
26
30
 
27
31
  ### Rule 1: Always Orient First
@@ -43,7 +47,7 @@ After completing ANY meaningful work, automatically update:
43
47
  2. **`specs/status.md`** — if phase progress, blockers, or P0 items changed (touch only your own lane's row — Rule 15)
44
48
  3. **`specs/changelog/YYYY-MM.md`** — log what changed (one line per change, append-only)
45
49
 
46
- Use the built-in **TodoWrite** tool to track in-session task progress. Do NOT wait for the user to ask you to update tracking.
50
+ Use your in-session task-tracking tool (named in the "In-Session Task Tool" note above) to track in-session task progress. Do NOT wait for the user to ask you to update tracking.
47
51
 
48
52
  #### Why
49
53
  Tracking debt compounds invisibly. A task list one day stale is recoverable; one week stale is fiction. Status drift is how phases silently lose direction.
@@ -61,7 +65,7 @@ Tracking debt compounds invisibly. A task list one day stale is recoverable; one
61
65
  #### Anti-Rationalization Counters
62
66
 
63
67
  - "It's faster to do the work first and track at the end" — wrong: reconstruction takes 2-3× longer than real-time logging.
64
- - "TodoWrite is enough" — TodoWrite is in-session only; `tasks.md` is the durable record.
68
+ - "Your in-session task-tracking tool is enough" — it is in-session only; `tasks.md` is the durable record.
65
69
  - "Mid-task tracking interrupts flow" — a one-line update costs <30s; reconstructing a day later costs 30 minutes.
66
70
 
67
71
  ### Rule 3: Auto-Track Discoveries
@@ -484,6 +488,9 @@ Use `infra:` for CI, build, deploy, tooling, and release-pipeline changes that d
484
488
 
485
489
  ## Project Extensions
486
490
 
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.
491
+ <!-- momentum:project-rules-pointer -->
492
+ > **Project-specific rules live in `specs/project-rules.md`** — read it now.
493
+ > Session-start self-audits, project constraints, and any project-specific
494
+ > guidance are there, shared identically by every agent (ADR-0010). This
495
+ > section is a momentum-managed pointer; edit `specs/project-rules.md`, not
496
+ > 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.36.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": {
@@ -1,3 +0,0 @@
1
- # Project Rules: <Project Name>
2
-
3
- > Antigravity configuration for this momentum-managed project.
@@ -1 +0,0 @@
1
- { "TASK_TOOL": "the native **task list** artifact (task.md)", "TASK_TOOL_NAME": "The task-list artifact" }
@@ -1,3 +0,0 @@
1
- # Project Rules: <Project Name>
2
-
3
- > Claude Code configuration for this project.
@@ -1 +0,0 @@
1
- { "TASK_TOOL": "the built-in **TodoWrite** tool", "TASK_TOOL_NAME": "TodoWrite" }
@@ -1,3 +0,0 @@
1
- # Project Rules: <Project Name>
2
-
3
- > Codex configuration for this momentum-managed project.
@@ -1 +0,0 @@
1
- { "TASK_TOOL": "the built-in **plan** tool", "TASK_TOOL_NAME": "The plan tool" }
@@ -1,3 +0,0 @@
1
- # Project Rules: <Project Name>
2
-
3
- > opencode configuration for this momentum-managed project.
@@ -1 +0,0 @@
1
- { "TASK_TOOL": "the built-in **task** tool (subagent fan-out)", "TASK_TOOL_NAME": "The task tool" }