@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.
- package/adapters/antigravity/instructions/AGENTS.md +6 -3
- package/adapters/codex/instructions/AGENTS.md +7 -4
- package/adapters/codex/instructions/surfaces.md +1 -1
- package/adapters/opencode/instructions/AGENTS.md +7 -4
- package/adapters/opencode/instructions/surfaces.md +1 -1
- package/bin/ecosystem.js +9 -8
- package/bin/momentum.js +43 -7
- package/bin/state-commands.js +3 -4
- package/core/commands/complete-phase.md +4 -4
- package/core/commands/start-phase.md +2 -2
- package/core/commands/start-project.md +3 -4
- package/core/commands/validate.md +3 -0
- package/core/ecosystem/lib/pointer.js +42 -0
- package/core/lib/okf-types.js +1 -0
- package/core/lib/project-rules.js +167 -0
- package/core/specs-templates/CLAUDE.md +6 -3
- package/package.json +1 -1
|
@@ -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
|
-
|
|
642
|
-
>
|
|
643
|
-
>
|
|
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
|
-
|
|
696
|
-
>
|
|
697
|
-
>
|
|
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
|
-
|
|
609
|
-
>
|
|
610
|
-
>
|
|
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
|
-
|
|
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
|
|
645
|
-
|
|
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,
|
|
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
|
-
|
|
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 =
|
|
1354
|
-
|
|
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
|
-
//
|
|
1987
|
-
//
|
|
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),
|
|
2025
|
+
withAutostash(path.resolve(targetDir), runAll);
|
|
1990
2026
|
} else {
|
|
1991
|
-
|
|
2027
|
+
runAll();
|
|
1992
2028
|
}
|
|
1993
2029
|
} catch (err) {
|
|
1994
2030
|
console.error(`\nError: ${err.message}`);
|
package/bin/state-commands.js
CHANGED
|
@@ -103,10 +103,9 @@ function cmdLeave(args) {
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
if (state === 'broken-pointer') {
|
|
106
|
-
// Pointer present but no registration.
|
|
107
|
-
const
|
|
108
|
-
if (
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
};
|
package/core/lib/okf-types.js
CHANGED
|
@@ -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
|
-
|
|
488
|
-
>
|
|
489
|
-
>
|
|
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