@limina-labs/momentum 0.32.0 โ 0.34.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/README.md +1 -0
- package/adapters/antigravity/instructions/AGENTS.md +1 -0
- package/adapters/codex/adapter.js +7 -0
- package/adapters/codex/instructions/AGENTS.md +2 -0
- package/adapters/codex/instructions/surfaces.md +1 -0
- package/adapters/opencode/adapter.js +2 -0
- package/adapters/opencode/instructions/AGENTS.md +2 -0
- package/adapters/opencode/instructions/surfaces.md +1 -0
- package/bin/antigravity.js +1 -0
- package/bin/lanes.js +29 -6
- package/bin/momentum.js +198 -1
- package/bin/state-commands.js +109 -1
- package/core/commands/brainstorm-idea.md +19 -0
- package/core/commands/brainstorm-phase.md +7 -0
- package/core/commands/complete-phase.md +94 -39
- package/core/commands/lanes.md +18 -1
- package/core/commands/start-phase.md +44 -4
- package/core/commands/start-project.md +52 -4
- package/core/commands/sync-config.md +43 -0
- package/core/commands/validate.md +14 -0
- package/core/config-templates.js +77 -0
- package/core/config.js +520 -0
- package/core/git-hooks/contract.js +98 -0
- package/core/git-hooks/run-check.js +22 -1
- package/core/instructions/navigation.md +1 -0
- package/core/lanes/lib/cleanup.js +349 -0
- package/core/lanes/lib/land.js +34 -1
- package/core/lanes/lib/reconcile.js +129 -0
- package/core/lib/okf-types.js +1 -0
- package/core/specs-templates/CLAUDE.md +1 -0
- package/core/specs-templates/specs/config.md +31 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -182,6 +182,7 @@ and is idempotent. (ADR-0005.)
|
|
|
182
182
|
| ๐งญ **Phases** | Plan โ execute โ verify โ release. Every phase has a brainstorm, plan, tasks, history. |
|
|
183
183
|
| ๐ **Backlog** | Bugs / features / tech debt / enhancements with priorities and per-item context. |
|
|
184
184
|
| ๐ **History** | Append-only log of decisions, discoveries, scope changes. The *why* outlives any session. |
|
|
185
|
+
| โ๏ธ **Config** | `specs/config.md` โ forge, publish target, branch flow, test/build commands. Recipes adapt to your project, not npm+GitHub defaults. |
|
|
185
186
|
| โ๏ธ **Rules** | 13 autonomous agent rules โ orient first, verify before claim, log every decision. |
|
|
186
187
|
| ๐ ๏ธ **Skills** | ~15 slash commands your agent runs โ start, complete, sync, review, validate. |
|
|
187
188
|
| ๐ **Multi-project** | Ecosystem mode (state layer) + Orchestration primitives (action layer) for cross-project work. |
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
| Phase tasks/progress? | `specs/phases/phase-N-*/tasks.md` |
|
|
16
16
|
| Why was X chosen? | `specs/decisions/NNNN-*.md` |
|
|
17
17
|
| Roadmap / timeline? | `specs/planning/roadmap.md` (authored at founding โ `/start-project`) |
|
|
18
|
+
| Project config (forge, publish, branch flow)? | `specs/config.md` (inferred by `momentum init`, authored at `/start-project`) |
|
|
18
19
|
| How to contribute? | `docs/developer-guide.md` |
|
|
19
20
|
|
|
20
21
|
> **First file to read: ALWAYS `specs/status.md`.**
|
|
@@ -167,6 +167,10 @@ function transformCommandsIntoSkills(targetDir, recordManaged) {
|
|
|
167
167
|
|
|
168
168
|
let count = 0;
|
|
169
169
|
for (const file of fs.readdirSync(commandsDir)) {
|
|
170
|
+
// Skip macOS AppleDouble sidecars FIRST: `._complete-phase.md` ends with
|
|
171
|
+
// `.md`, so an endsWith check alone would mint a `._complete-phase/SKILL.md`
|
|
172
|
+
// junk skill dir (Phase 27 G4 โ the source of the ._* skill-dir litter).
|
|
173
|
+
if (file.startsWith('._') || file === '.DS_Store') continue;
|
|
170
174
|
if (!file.endsWith('.md')) continue;
|
|
171
175
|
const name = file.replace(/\.md$/, '');
|
|
172
176
|
const body = fs.readFileSync(path.join(commandsDir, file), 'utf8');
|
|
@@ -223,3 +227,6 @@ function renderSkillMarkdown(name, description, recipeBody) {
|
|
|
223
227
|
recipeBody.endsWith('\n') ? recipeBody : recipeBody + '\n',
|
|
224
228
|
].join('\n');
|
|
225
229
|
}
|
|
230
|
+
|
|
231
|
+
// Phase 27 G4 โ exported for the AppleDouble-filter regression test.
|
|
232
|
+
module.exports.transformCommandsIntoSkills = transformCommandsIntoSkills;
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
| Phase tasks/progress? | `specs/phases/phase-N-*/tasks.md` |
|
|
16
16
|
| Why was X chosen? | `specs/decisions/NNNN-*.md` |
|
|
17
17
|
| Roadmap / timeline? | `specs/planning/roadmap.md` (authored at founding โ `/start-project`) |
|
|
18
|
+
| Project config (forge, publish, branch flow)? | `specs/config.md` (inferred by `momentum init`, authored at `/start-project`) |
|
|
18
19
|
| How to contribute? | `docs/developer-guide.md` |
|
|
19
20
|
|
|
20
21
|
> **First file to read: ALWAYS `specs/status.md`.**
|
|
@@ -47,6 +48,7 @@ The shipped recipe set (one skill per recipe, all under
|
|
|
47
48
|
| log | Append a manual narrative entry to the history of the phase bound to your branch (fallback: status.md) |
|
|
48
49
|
| migrate | Onboard an existing project into momentum |
|
|
49
50
|
| validate | Check spec structure health |
|
|
51
|
+
| sync-config | Re-infer project shape, show config drift, apply on approval (ENH-062)
|
|
50
52
|
| ecosystem | Cross-repo ecosystem coordination |
|
|
51
53
|
| initiative | Manage cross-repo initiatives |
|
|
52
54
|
| session | Append a manual narrative entry to today's ecosystem session log |
|
|
@@ -24,6 +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
28
|
| ecosystem | Cross-repo ecosystem coordination |
|
|
28
29
|
| initiative | Manage cross-repo initiatives |
|
|
29
30
|
| session | Append a manual narrative entry to today's ecosystem session log |
|
|
@@ -173,6 +173,8 @@ function ensureCommandFrontmatter(targetDir) {
|
|
|
173
173
|
}
|
|
174
174
|
let count = 0;
|
|
175
175
|
for (const file of fs.readdirSync(commandsDir)) {
|
|
176
|
+
// Skip AppleDouble sidecars first โ `._x.md` ends with `.md` (Phase 27 G4).
|
|
177
|
+
if (file.startsWith('._') || file === '.DS_Store') continue;
|
|
176
178
|
if (!file.endsWith('.md')) continue;
|
|
177
179
|
const filePath = path.join(commandsDir, file);
|
|
178
180
|
const body = fs.readFileSync(filePath, 'utf8');
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
| Phase tasks/progress? | `specs/phases/phase-N-*/tasks.md` |
|
|
16
16
|
| Why was X chosen? | `specs/decisions/NNNN-*.md` |
|
|
17
17
|
| Roadmap / timeline? | `specs/planning/roadmap.md` (authored at founding โ `/start-project`) |
|
|
18
|
+
| Project config (forge, publish, branch flow)? | `specs/config.md` (inferred by `momentum init`, authored at `/start-project`) |
|
|
18
19
|
| How to contribute? | `docs/developer-guide.md` |
|
|
19
20
|
|
|
20
21
|
> **First file to read: ALWAYS `specs/status.md`.**
|
|
@@ -47,6 +48,7 @@ The shipped recipe set (one command per recipe):
|
|
|
47
48
|
| lanes | Work with lanes โ concurrent workstreams in one repo (Rule 15) |
|
|
48
49
|
| migrate | Onboard an existing project into momentum |
|
|
49
50
|
| validate | Check spec structure health |
|
|
51
|
+
| sync-config | Re-infer project shape, show config drift, apply on approval (ENH-062)
|
|
50
52
|
| ecosystem | Cross-repo ecosystem coordination |
|
|
51
53
|
| initiative | Manage cross-repo initiatives |
|
|
52
54
|
| session | Append a manual narrative entry to today's ecosystem session log |
|
|
@@ -24,6 +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
28
|
| ecosystem | Cross-repo ecosystem coordination |
|
|
28
29
|
| initiative | Manage cross-repo initiatives |
|
|
29
30
|
| session | Append a manual narrative entry to today's ecosystem session log |
|
package/bin/antigravity.js
CHANGED
|
@@ -29,6 +29,7 @@ const PLUGIN_NAME = 'momentum';
|
|
|
29
29
|
function copyDirRecursive(src, dest, dryRun, log) {
|
|
30
30
|
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
31
31
|
for (const entry of entries) {
|
|
32
|
+
if (entry.name.startsWith('._') || entry.name === '.DS_Store') continue; // Phase 27 G4 โ no AppleDouble litter
|
|
32
33
|
const s = path.join(src, entry.name);
|
|
33
34
|
const d = path.join(dest, entry.name);
|
|
34
35
|
if (entry.isDirectory()) {
|
package/bin/lanes.js
CHANGED
|
@@ -18,6 +18,7 @@ const { spawnSync } = require('child_process');
|
|
|
18
18
|
|
|
19
19
|
const MOMENTUM_ROOT = path.resolve(__dirname, '..');
|
|
20
20
|
const state = require(path.join(MOMENTUM_ROOT, 'core', 'lanes', 'lib', 'state'));
|
|
21
|
+
const cleanup = require(path.join(MOMENTUM_ROOT, 'core', 'lanes', 'lib', 'cleanup'));
|
|
21
22
|
|
|
22
23
|
// โโโ small helpers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
23
24
|
|
|
@@ -39,7 +40,7 @@ function parseFlags(argv) {
|
|
|
39
40
|
for (let i = 0; i < argv.length; i++) {
|
|
40
41
|
const a = argv[i];
|
|
41
42
|
if (a === '--no-worktree' || a === '--rm-worktree' || a === '--execute' ||
|
|
42
|
-
a === '--force' || a === '--json' || a === '--ack-all') {
|
|
43
|
+
a === '--force' || a === '--json' || a === '--ack-all' || a === '--no-remote') {
|
|
43
44
|
flags[a.slice(2)] = true;
|
|
44
45
|
} else if (a.startsWith('--')) {
|
|
45
46
|
flags[a.slice(2)] = argv[++i];
|
|
@@ -197,7 +198,7 @@ function cmdDone(cwd, argv) {
|
|
|
197
198
|
function cmdClose(cwd, argv) {
|
|
198
199
|
const { flags, positional } = parseFlags(argv);
|
|
199
200
|
const id = positional[0];
|
|
200
|
-
if (!id) return fail('usage: momentum lanes close <lane-id> [--rm-worktree]');
|
|
201
|
+
if (!id) return fail('usage: momentum lanes close <lane-id> [--rm-worktree] [--force] [--no-remote]');
|
|
201
202
|
const anchor = requireAnchor(cwd);
|
|
202
203
|
if (!anchor) return;
|
|
203
204
|
let lane;
|
|
@@ -206,10 +207,21 @@ function cmdClose(cwd, argv) {
|
|
|
206
207
|
} catch (err) {
|
|
207
208
|
return fail(err.message);
|
|
208
209
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
210
|
+
// ENH-063: close does FULL cleanup โ local + remote branch (default-branch-
|
|
211
|
+
// safe) + lane state, plus the worktree with --rm-worktree. Best-effort: an
|
|
212
|
+
// unmerged branch is refused (never force-deleted) but that does not fail close.
|
|
213
|
+
const res = cleanup.cleanupTarget({
|
|
214
|
+
cwd,
|
|
215
|
+
branch: lane.branch,
|
|
216
|
+
worktree: flags['rm-worktree'] ? lane.worktree : null,
|
|
217
|
+
laneId: id,
|
|
218
|
+
deleteRemote: !flags['no-remote'],
|
|
219
|
+
force: Boolean(flags.force),
|
|
220
|
+
});
|
|
221
|
+
const glyph = { removed: 'โ', deleted: 'โ', cleared: 'โ', skipped: 'โน', blocked: 'โ ' };
|
|
222
|
+
for (const a of res.actions) console.log(` ${glyph[a.status] || 'ยท'} ${a.step}: ${a.detail}`);
|
|
223
|
+
if (!res.ok) {
|
|
224
|
+
console.log(`โ some cleanup refused (${res.blocked.join(', ')}) โ e.g. an unmerged branch; rerun with --force only if intended`);
|
|
213
225
|
}
|
|
214
226
|
console.log(`โ lane '${id}' closed`);
|
|
215
227
|
}
|
|
@@ -243,6 +255,13 @@ Usage:
|
|
|
243
255
|
momentum lanes land <id> [--into <ref>] [--execute] [--force] [--mark-landed]
|
|
244
256
|
--mark-landed bookkeeping only: record an out-of-band merge that
|
|
245
257
|
already happened (done + merged lane โ landed)
|
|
258
|
+
momentum lanes cleanup <branch> [--worktree P] [--lane ID] [flags]
|
|
259
|
+
Remove a spent branch's worktree + branch + lane state, default-branch-safe
|
|
260
|
+
--no-remote skip origin delete --force allow -D / dirty worktree
|
|
261
|
+
--dry-run report only --keep-state leave lane dir untouched
|
|
262
|
+
momentum lanes reconcile [--execute] [--into <ref>] [--json]
|
|
263
|
+
Sweep landed/merged lanes whose terminal merge happened out-of-band
|
|
264
|
+
(human/forge) and clean them once verified contained in <ref>
|
|
246
265
|
|
|
247
266
|
State lives at <git-common-dir>/momentum/lanes โ shared by all worktrees,
|
|
248
267
|
untracked, no daemon. Substrate: plain git worktrees by default; treehouse
|
|
@@ -287,6 +306,10 @@ function dispatch(argv, cwd) {
|
|
|
287
306
|
return delegate('signals', 'cmdInbox', rest);
|
|
288
307
|
case 'land':
|
|
289
308
|
return delegate('land', 'cmdLand', rest);
|
|
309
|
+
case 'cleanup':
|
|
310
|
+
return delegate('cleanup', 'cmdCleanup', rest);
|
|
311
|
+
case 'reconcile':
|
|
312
|
+
return delegate('reconcile', 'cmdReconcile', rest);
|
|
290
313
|
case '--help':
|
|
291
314
|
case '-h':
|
|
292
315
|
case 'help':
|
package/bin/momentum.js
CHANGED
|
@@ -451,6 +451,172 @@ function migrateFoundationDocs(srcRoot, target) {
|
|
|
451
451
|
return result;
|
|
452
452
|
}
|
|
453
453
|
|
|
454
|
+
/**
|
|
455
|
+
* Phase 26 โ Project Config (ADR-0009).
|
|
456
|
+
*
|
|
457
|
+
* init: after the specs skeleton, infer + write `specs/config.md` and the
|
|
458
|
+
* derived `.momentum/config-cache.json` (only when the file is absent โ
|
|
459
|
+
* re-init leaves an authored file alone).
|
|
460
|
+
*
|
|
461
|
+
* upgrade: write inferred config for FOUNDED projects only (charter +
|
|
462
|
+
* roadmap exist โ ADR-0008); unfounded projects author them at /start-project.
|
|
463
|
+
* When the file exists, refresh the derived cache from the content source of
|
|
464
|
+
* truth and report any drift on the machine-inferable fields (never clobber
|
|
465
|
+
* user edits).
|
|
466
|
+
*
|
|
467
|
+
* specs/ is user content โ the config file is NOT recorded in the managed
|
|
468
|
+
* manifest (orphan cleanup must never touch it). The cache is gitignored by the
|
|
469
|
+
* existing `.momentum/*` rule.
|
|
470
|
+
*/
|
|
471
|
+
function installConfig(target, { dryRun, upgradeMode } = {}) {
|
|
472
|
+
const prefsLib = require('../core/config');
|
|
473
|
+
const specsDir = path.join(target, 'specs');
|
|
474
|
+
if (!fileExists(specsDir)) return; // no specs skeleton โ nothing to do
|
|
475
|
+
const prefsPath = path.join(specsDir, 'config.md');
|
|
476
|
+
const exists = fileExists(prefsPath);
|
|
477
|
+
|
|
478
|
+
if (!exists) {
|
|
479
|
+
// upgrade only writes config for founded projects (migration).
|
|
480
|
+
if (upgradeMode && !prefsLib.isFounded(target)) return;
|
|
481
|
+
const prefs = prefsLib.inferConfig(target);
|
|
482
|
+
if (dryRun) {
|
|
483
|
+
console.log(` โ would add: specs/config.md (inferred: language=${prefs.language}, forge=${prefs.git_forge})`);
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
prefsLib.writeConfig(specsDir, prefs, { inferred: true });
|
|
487
|
+
prefsLib.writeConfigCache(target, prefs);
|
|
488
|
+
console.log(` + added: specs/config.md (inferred: language=${prefs.language}, forge=${prefs.git_forge})`);
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// File exists: refresh the derived cache from the content source of truth.
|
|
493
|
+
// A present-but-garbage file parses to an all-defaults object; detect that
|
|
494
|
+
// (zero known keys present in the raw file) and LEAVE it untouched rather
|
|
495
|
+
// than silently overwriting the user's (broken) content with defaults.
|
|
496
|
+
const raw = (() => {
|
|
497
|
+
try {
|
|
498
|
+
return prefsLib.parseConfigMarkdown(fs.readFileSync(prefsPath, 'utf8'));
|
|
499
|
+
} catch {
|
|
500
|
+
return {};
|
|
501
|
+
}
|
|
502
|
+
})();
|
|
503
|
+
const hasAnyKey = prefsLib.KNOWN_KEYS.some((k) => k in raw);
|
|
504
|
+
if (!hasAnyKey) {
|
|
505
|
+
console.log(` โ ๏ธ specs/config.md exists but is empty/unparseable โ left untouched (fix or delete it)`);
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
const stored = prefsLib.readConfig(specsDir);
|
|
509
|
+
if (!dryRun) prefsLib.writeConfigCache(target, stored);
|
|
510
|
+
|
|
511
|
+
// Drift detection (upgrade only, founded only): report changed inferable
|
|
512
|
+
// fields and point at the approval-gated fix. Never clobber user edits โ
|
|
513
|
+
// the user applies the change explicitly via `momentum config sync`.
|
|
514
|
+
if (upgradeMode && prefsLib.isFounded(target)) {
|
|
515
|
+
const inferred = prefsLib.inferConfig(target);
|
|
516
|
+
const drifted = prefsLib.INFERABLE_KEYS.filter((k) => !prefsLib.valuesEqual(inferred[k], stored[k]));
|
|
517
|
+
if (drifted.length) {
|
|
518
|
+
console.log(` โ ๏ธ specs/config.md drifted from manifests on: ${drifted.join(', ')} โ run 'momentum config sync' to review + apply`);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Ask the user a single question and resolve with their trimmed answer.
|
|
525
|
+
* Works whether stdin is a TTY or piped (tests pipe input). On EOF without an
|
|
526
|
+
* answer, resolves to the empty string so callers can treat it as "skip".
|
|
527
|
+
* @param {string} query
|
|
528
|
+
* @returns {Promise<string>}
|
|
529
|
+
*/
|
|
530
|
+
function askQuestion(query) {
|
|
531
|
+
const readline = require('node:readline');
|
|
532
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
533
|
+
return new Promise((resolve) => {
|
|
534
|
+
let done = false;
|
|
535
|
+
const finish = (v) => {
|
|
536
|
+
if (done) return;
|
|
537
|
+
done = true;
|
|
538
|
+
rl.close();
|
|
539
|
+
resolve(v);
|
|
540
|
+
};
|
|
541
|
+
// EOF (piped stdin, no answer) resolves to '' so callers treat it as skip.
|
|
542
|
+
rl.on('close', () => finish(''));
|
|
543
|
+
rl.question(query, (answer) => finish((answer || '').trim()));
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* ENH-062 โ proactive, approval-gated config drift sync. Re-infers the project
|
|
549
|
+
* shape and diffs it against specs/config.md. When drift is found, shows the
|
|
550
|
+
* per-field oldโnew diff and asks the user which fields to apply (or "all" /
|
|
551
|
+
* "skip"). Nothing is written without explicit approval.
|
|
552
|
+
* @param {string} targetDir
|
|
553
|
+
* @param {object} [opts] { dryRun }
|
|
554
|
+
*/
|
|
555
|
+
async function syncConfig(targetDir, opts = {}) {
|
|
556
|
+
const prefsLib = require('../core/config');
|
|
557
|
+
const specsDir = path.join(targetDir, 'specs');
|
|
558
|
+
if (!fileExists(specsDir)) {
|
|
559
|
+
console.log(' โ ๏ธ no specs/ skeleton โ run momentum init first');
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
const { exists, drift } = prefsLib.diffConfig(specsDir, targetDir);
|
|
563
|
+
|
|
564
|
+
if (!exists) {
|
|
565
|
+
// Migration: a founded project with no config.md yet โ create it.
|
|
566
|
+
if (prefsLib.isFounded(targetDir)) {
|
|
567
|
+
if (opts.dryRun) {
|
|
568
|
+
const p = prefsLib.inferConfig(targetDir);
|
|
569
|
+
console.log(` โ would add: specs/config.md (inferred: language=${p.language}, forge=${p.git_forge})`);
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
const prefs = prefsLib.inferConfig(targetDir);
|
|
573
|
+
prefsLib.writeConfig(specsDir, prefs, { inferred: true });
|
|
574
|
+
prefsLib.writeConfigCache(targetDir, prefs);
|
|
575
|
+
console.log(` + added: specs/config.md (inferred: language=${prefs.language}, forge=${prefs.git_forge})`);
|
|
576
|
+
} else {
|
|
577
|
+
console.log(' โ ๏ธ project not founded โ run /start-project to author specs/config.md');
|
|
578
|
+
}
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
if (drift.length === 0) {
|
|
583
|
+
console.log(' โ specs/config.md matches the inferred project shape โ nothing to sync');
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
console.log('\n Project shape changed since specs/config.md was written:\n');
|
|
588
|
+
for (const d of drift) {
|
|
589
|
+
console.log(` โข ${d.key}: ${JSON.stringify(d.old)} โ ${JSON.stringify(d.new)}`);
|
|
590
|
+
}
|
|
591
|
+
console.log('');
|
|
592
|
+
|
|
593
|
+
if (opts.dryRun) {
|
|
594
|
+
console.log(` โ would sync ${drift.length} field(s) โ re-run without --dry-run to apply`);
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const answer = await askQuestion(' Apply? [a]ll / comma-separated keys / [s]kip: ');
|
|
599
|
+
const a = answer.toLowerCase();
|
|
600
|
+
if (a === 's' || a === '' || a === 'skip') {
|
|
601
|
+
console.log(' โฉ๏ธ skipped โ specs/config.md left unchanged');
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
let keys;
|
|
605
|
+
if (a === 'a' || a === 'all' || a === 'y' || a === 'yes') {
|
|
606
|
+
keys = drift.map((d) => d.key);
|
|
607
|
+
} else {
|
|
608
|
+
const wanted = new Set(a.split(',').map((s) => s.trim()));
|
|
609
|
+
keys = drift.filter((d) => wanted.has(d.key)).map((d) => d.key);
|
|
610
|
+
if (keys.length === 0) {
|
|
611
|
+
console.log(' โฉ๏ธ no matching keys โ specs/config.md left unchanged');
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
const updated = prefsLib.mergeConfigDrift(specsDir, targetDir, keys);
|
|
616
|
+
console.log(` โ synced ${keys.join(', ')} โ specs/config.md (cache refreshed)`);
|
|
617
|
+
return updated;
|
|
618
|
+
}
|
|
619
|
+
|
|
454
620
|
function removeOrphans(targetDir, prevManifest, currentSet, opts = {}) {
|
|
455
621
|
if (!prevManifest || !Array.isArray(prevManifest.managedFiles)) return [];
|
|
456
622
|
const currentRel = new Set(
|
|
@@ -1038,10 +1204,17 @@ function init(targetDir, agent, opts = {}) {
|
|
|
1038
1204
|
const specsSrc = path.join(src, 'core', 'specs-templates');
|
|
1039
1205
|
copyDir(specsSrc, target, {
|
|
1040
1206
|
skipIfExists: true,
|
|
1041
|
-
|
|
1207
|
+
// CLAUDE.md is adapter-owned (written separately); config.md is
|
|
1208
|
+
// inferred per-project by installConfig (Phase 26) โ neither is
|
|
1209
|
+
// copied as a static template.
|
|
1210
|
+
skipRelPaths: new Set(['CLAUDE.md', 'config.md']),
|
|
1042
1211
|
record: false, // specs are install-once / user-owned โ never orphan them
|
|
1043
1212
|
});
|
|
1044
1213
|
|
|
1214
|
+
// Phase 26 โ infer + write specs/config.md + the derived hook cache.
|
|
1215
|
+
console.log('โ Inferring project config...');
|
|
1216
|
+
installConfig(target, { dryRun: _dryRun });
|
|
1217
|
+
|
|
1045
1218
|
installPrimaryInstruction(src, target, adapterDir, adapter.primaryInstruction);
|
|
1046
1219
|
|
|
1047
1220
|
// Adapter overlay โ per-agent commands/agent-rules/scripts (additive)
|
|
@@ -1243,6 +1416,12 @@ function upgrade(targetDir, agent, opts = {}) {
|
|
|
1243
1416
|
console.log(' = no untouched placeholders (foundation docs are authored or absent)');
|
|
1244
1417
|
}
|
|
1245
1418
|
|
|
1419
|
+
// Phase 26 โ project config (ADR-0009). Founded projects get an
|
|
1420
|
+
// inferred specs/config.md on first upgrade; existing files get their
|
|
1421
|
+
// derived cache refreshed + drift reported (never clobbered).
|
|
1422
|
+
console.log('โ Inferring project config...');
|
|
1423
|
+
installConfig(target, { dryRun: _dryRun, upgradeMode: true });
|
|
1424
|
+
|
|
1246
1425
|
// Orphan cleanup โ only remove files this agent's prior version installed
|
|
1247
1426
|
// that this version no longer ships. Other agents' files are untouched.
|
|
1248
1427
|
orphans = removeOrphans(target, { managedFiles: prevAgentFiles }, _managedCollector, {
|
|
@@ -1441,6 +1620,7 @@ Ecosystem use โ entry/exit commands (run from any repo):
|
|
|
1441
1620
|
existing ecosystem
|
|
1442
1621
|
momentum leave Detach THIS repo from its ecosystem
|
|
1443
1622
|
momentum doctor Diagnose state + list available transitions
|
|
1623
|
+
momentum doctor --clean [--execute] Sweep stray artifacts (AppleDouble/.bak/orphan worktrees)
|
|
1444
1624
|
momentum antigravity plugin-pack Pack momentum skills as a native Antigravity plugin ([--global])
|
|
1445
1625
|
|
|
1446
1626
|
Ecosystem use โ operator toolkit (advanced):
|
|
@@ -1471,6 +1651,7 @@ Waves โ wave plan from dependency annotations (Phase 21c, one engine every sca
|
|
|
1471
1651
|
OKF โ specs/ as an Open Knowledge Format bundle (Phase 24, ADR-0005):
|
|
1472
1652
|
momentum okf check [dir] OKF v0.1 conformance report for <dir>/specs
|
|
1473
1653
|
momentum okf index [dir] Regenerate bundle indexes (root/phases/decisions)
|
|
1654
|
+
momentum config sync [target-dir] Re-infer project shape, show config drift, apply on approval
|
|
1474
1655
|
|
|
1475
1656
|
Options:
|
|
1476
1657
|
--agent <name> Agent to install for. \`init\` asks when this
|
|
@@ -1919,6 +2100,22 @@ async function main() {
|
|
|
1919
2100
|
console.error(`\nError: ${err.message}`);
|
|
1920
2101
|
exitCode = 1;
|
|
1921
2102
|
}
|
|
2103
|
+
} else if (args[0] === 'config') {
|
|
2104
|
+
try {
|
|
2105
|
+
const sub = args[1];
|
|
2106
|
+
const target = args[2] ? path.resolve(args[2]) : process.cwd();
|
|
2107
|
+
if (sub === 'sync') {
|
|
2108
|
+
const dryRun = args.includes('--dry-run') || _dryRun;
|
|
2109
|
+
await syncConfig(target, { dryRun });
|
|
2110
|
+
} else {
|
|
2111
|
+
console.error(`Unknown config subcommand: ${sub || '(none)'}`);
|
|
2112
|
+
console.error('Usage: momentum config sync [target-dir] [--dry-run]');
|
|
2113
|
+
exitCode = 1;
|
|
2114
|
+
}
|
|
2115
|
+
} catch (err) {
|
|
2116
|
+
console.error(`\nError: ${err.message}`);
|
|
2117
|
+
exitCode = 1;
|
|
2118
|
+
}
|
|
1922
2119
|
} else {
|
|
1923
2120
|
console.error(`Unknown command: ${args[0]}`);
|
|
1924
2121
|
console.error('Run "momentum --help" for usage.');
|
package/bin/state-commands.js
CHANGED
|
@@ -129,8 +129,103 @@ function cmdLeave(args) {
|
|
|
129
129
|
console.log(`โ Left "${reg.ecosystemName}". Now standalone.`);
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
// โโ Phase 27 G4 โ stray-artifact sweep (`momentum doctor --clean`) โโโโโโโโโโ
|
|
133
|
+
|
|
134
|
+
const BAK_ROOTS = ['.claude', '.agents', '.opencode', '.codex', '.githooks', 'scripts'];
|
|
135
|
+
|
|
136
|
+
/** Bounded walk collecting AppleDouble sidecars anywhere (always junk). */
|
|
137
|
+
function walkAppleDouble(dir, acc, depth) {
|
|
138
|
+
if (depth > 8) return;
|
|
139
|
+
let entries;
|
|
140
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
141
|
+
for (const e of entries) {
|
|
142
|
+
if (e.name === '.git' || e.name === 'node_modules') continue;
|
|
143
|
+
const abs = path.join(dir, e.name);
|
|
144
|
+
if (e.name.startsWith('._') || e.name === '.DS_Store') { acc.appleDouble.push(abs); continue; }
|
|
145
|
+
if (e.isDirectory()) walkAppleDouble(abs, acc, depth + 1);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** `.bak` files under a momentum-managed root only (never user backups). */
|
|
150
|
+
function collectBaks(dir, acc, depth) {
|
|
151
|
+
if (depth > 8) return;
|
|
152
|
+
let entries;
|
|
153
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
154
|
+
for (const e of entries) {
|
|
155
|
+
const abs = path.join(dir, e.name);
|
|
156
|
+
if (e.isDirectory()) collectBaks(abs, acc, depth + 1);
|
|
157
|
+
else if (e.name.endsWith('.bak')) acc.baks.push(abs);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Find stray artifacts: AppleDouble, momentum `.bak` litter, orphan worktrees. */
|
|
162
|
+
function findStrayArtifacts(root) {
|
|
163
|
+
const { spawnSync } = require('child_process');
|
|
164
|
+
const acc = { appleDouble: [], baks: [], prunableWorktrees: [], orphanWorktreeDirs: [] };
|
|
165
|
+
walkAppleDouble(root, acc, 0);
|
|
166
|
+
for (const sub of BAK_ROOTS) collectBaks(path.join(root, sub), acc, 0);
|
|
167
|
+
for (const f of ['CLAUDE.md.bak', 'AGENTS.md.bak']) {
|
|
168
|
+
const p = path.join(root, f);
|
|
169
|
+
if (fs.existsSync(p)) acc.baks.push(p);
|
|
170
|
+
}
|
|
171
|
+
// registered worktrees whose directory is gone โ prunable
|
|
172
|
+
const registered = new Set();
|
|
173
|
+
const wl = spawnSync('git', ['worktree', 'list', '--porcelain'], { cwd: root, encoding: 'utf8' });
|
|
174
|
+
if (wl.status === 0) {
|
|
175
|
+
for (const line of wl.stdout.split('\n')) {
|
|
176
|
+
if (line.startsWith('worktree ')) {
|
|
177
|
+
const p = line.slice('worktree '.length);
|
|
178
|
+
registered.add(realpathOf(p));
|
|
179
|
+
if (p !== root && !fs.existsSync(p)) acc.prunableWorktrees.push(p);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// .claude/worktrees/* leftover dirs not registered as git worktrees
|
|
184
|
+
const cw = path.join(root, '.claude', 'worktrees');
|
|
185
|
+
try {
|
|
186
|
+
for (const name of fs.readdirSync(cw)) {
|
|
187
|
+
const abs = path.join(cw, name);
|
|
188
|
+
if (fs.statSync(abs).isDirectory() && !registered.has(realpathOf(abs))) acc.orphanWorktreeDirs.push(abs);
|
|
189
|
+
}
|
|
190
|
+
} catch { /* no .claude/worktrees */ }
|
|
191
|
+
return acc;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function sweepStrayArtifacts(root, execute) {
|
|
195
|
+
const { spawnSync } = require('child_process');
|
|
196
|
+
const stray = findStrayArtifacts(root);
|
|
197
|
+
const total = stray.appleDouble.length + stray.baks.length
|
|
198
|
+
+ stray.prunableWorktrees.length + stray.orphanWorktreeDirs.length;
|
|
199
|
+
const rel = (p) => path.relative(root, p) || p;
|
|
200
|
+
|
|
201
|
+
console.log(execute ? 'Cleaning stray artifacts:' : 'Stray artifacts (dry-run โ pass --execute to remove):');
|
|
202
|
+
if (total === 0) { console.log(' โ none โ tree is clean'); return 0; }
|
|
203
|
+
|
|
204
|
+
for (const p of stray.appleDouble) {
|
|
205
|
+
if (execute) { try { fs.rmSync(p, { recursive: true, force: true }); } catch { /* ignore */ } }
|
|
206
|
+
console.log(` ${execute ? 'โ removed' : 'ยท'} AppleDouble: ${rel(p)}`);
|
|
207
|
+
}
|
|
208
|
+
for (const p of stray.baks) {
|
|
209
|
+
if (execute) { try { fs.rmSync(p, { force: true }); } catch { /* ignore */ } }
|
|
210
|
+
console.log(` ${execute ? 'โ removed' : 'ยท'} .bak: ${rel(p)}`);
|
|
211
|
+
}
|
|
212
|
+
for (const p of stray.orphanWorktreeDirs) {
|
|
213
|
+
if (execute) { try { fs.rmSync(p, { recursive: true, force: true }); } catch { /* ignore */ } }
|
|
214
|
+
console.log(` ${execute ? 'โ removed' : 'ยท'} orphan worktree dir: ${rel(p)}`);
|
|
215
|
+
}
|
|
216
|
+
if (stray.prunableWorktrees.length) {
|
|
217
|
+
if (execute) spawnSync('git', ['worktree', 'prune'], { cwd: root });
|
|
218
|
+
for (const p of stray.prunableWorktrees) console.log(` ${execute ? 'โ pruned' : 'ยท'} stale worktree ref: ${rel(p)}`);
|
|
219
|
+
}
|
|
220
|
+
console.log(execute ? `โ cleaned ${total} stray artifact(s)` : `${total} stray artifact(s) โ rerun with --execute to remove`);
|
|
221
|
+
return 0;
|
|
222
|
+
}
|
|
223
|
+
|
|
132
224
|
function cmdDoctor(args) {
|
|
133
|
-
|
|
225
|
+
// Phase 27 G4 โ `momentum doctor --clean [--execute]` sweeps stray artifacts.
|
|
226
|
+
if (args && args.includes('--clean')) {
|
|
227
|
+
return sweepStrayArtifacts(realpathOf(process.cwd()), args.includes('--execute'));
|
|
228
|
+
}
|
|
134
229
|
const cwd = realpathOf(process.cwd());
|
|
135
230
|
lib._clearRootCache();
|
|
136
231
|
const state = stateLib.detectState(cwd);
|
|
@@ -176,6 +271,17 @@ function cmdDoctor(args) {
|
|
|
176
271
|
}
|
|
177
272
|
} catch (_err) { /* advisory is best-effort โ never block doctor */ }
|
|
178
273
|
|
|
274
|
+
// Phase 27 G4 โ stray-artifact advisory (best-effort, non-blocking).
|
|
275
|
+
try {
|
|
276
|
+
const stray = findStrayArtifacts(cwd);
|
|
277
|
+
const n = stray.appleDouble.length + stray.baks.length
|
|
278
|
+
+ stray.prunableWorktrees.length + stray.orphanWorktreeDirs.length;
|
|
279
|
+
if (n > 0) {
|
|
280
|
+
console.log(`Advisory: ${n} stray artifact(s) (AppleDouble / .bak / orphan worktrees) โ run \`momentum doctor --clean\` to review, \`--clean --execute\` to remove.`);
|
|
281
|
+
console.log('');
|
|
282
|
+
}
|
|
283
|
+
} catch { /* best-effort */ }
|
|
284
|
+
|
|
179
285
|
const transitions = stateLib.availableTransitions(state, reg || {});
|
|
180
286
|
if (transitions.length === 0) {
|
|
181
287
|
console.log('No suggested next steps for this state.');
|
|
@@ -215,4 +321,6 @@ module.exports = {
|
|
|
215
321
|
cmdLeave,
|
|
216
322
|
cmdDoctor,
|
|
217
323
|
formatStateLabel,
|
|
324
|
+
findStrayArtifacts,
|
|
325
|
+
sweepStrayArtifacts,
|
|
218
326
|
};
|
|
@@ -44,6 +44,25 @@ When you're ready to turn the result into a project, run `/start-project` โ it
|
|
|
44
44
|
|
|
45
45
|
4. Iterate until the idea is clear.
|
|
46
46
|
|
|
47
|
+
4b. **Config discovery** โ once the idea is settled, gather the
|
|
48
|
+
project-shape config that `/start-project` will author into
|
|
49
|
+
`specs/config.md` (ADR-0009). Ask one question at a time; if the
|
|
50
|
+
user is unsure, suggest a default from the idea's context:
|
|
51
|
+
- **Git forge**: "Which forge? GitHub / GitLab / Bitbucket / Gitea /
|
|
52
|
+
Forgejo / bare-ssh?" (default from `git remote get-url origin` if a
|
|
53
|
+
repo exists, else GitHub)
|
|
54
|
+
- **Language + framework**: "What language/framework? Node+Next.js /
|
|
55
|
+
Python+FastAPI / Rust+Actix / Go / โฆ?" (default from manifests if a
|
|
56
|
+
repo exists, else ask)
|
|
57
|
+
- **Publish/deploy target**: "How does this ship? npm publish / pypi /
|
|
58
|
+
crates.io / deploy-only (Vercel/Fly/โฆ) / none?" (sets `publish_target`
|
|
59
|
+
+ `release_flow`)
|
|
60
|
+
- **Branch flow**: "How do changes land? feature โ staging โ main /
|
|
61
|
+
feature โ main / feature-branch-only?" (sets `branch_flow` +
|
|
62
|
+
`end_state`; default `feature โ staging โ main`)
|
|
63
|
+
Carry the settled config as context into `/start-project` so it
|
|
64
|
+
authors `specs/config.md` alongside the charter/roadmap in one batch.
|
|
65
|
+
|
|
47
66
|
5. **Exit the brainstorm gate** before any follow-up command:
|
|
48
67
|
```bash
|
|
49
68
|
rm .momentum/brainstorm-active
|
|
@@ -34,6 +34,13 @@ The brainstorm output IS the phase files โ there is no intermediate design doc
|
|
|
34
34
|
- Read `specs/planning/roadmap.md` โ what's the next planned phase?
|
|
35
35
|
- Is the planned phase still the right next step given what was learned?
|
|
36
36
|
|
|
37
|
+
2b. Read verification defaults from config:
|
|
38
|
+
- Read `specs/config.md` (`core/config.js` โ `readConfig('specs')`)
|
|
39
|
+
for `test_command` and `build_command`. When absent, default to
|
|
40
|
+
`npm test` / no build. Use these as the default verification commands in
|
|
41
|
+
the phase's deliverables table (overview.md) unless the phase needs
|
|
42
|
+
something different โ call out any deviation explicitly.
|
|
43
|
+
|
|
37
44
|
3. Define scope with the user (one question at a time):
|
|
38
45
|
- What is the goal of this phase?
|
|
39
46
|
- What are the key deliverables?
|