@limina-labs/momentum 0.33.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/adapter.js +7 -0
- package/adapters/codex/instructions/AGENTS.md +7 -4
- package/adapters/codex/instructions/surfaces.md +1 -1
- package/adapters/opencode/adapter.js +2 -0
- package/adapters/opencode/instructions/AGENTS.md +7 -4
- package/adapters/opencode/instructions/surfaces.md +1 -1
- package/bin/antigravity.js +1 -0
- package/bin/ecosystem.js +9 -8
- package/bin/lanes.js +29 -6
- package/bin/momentum.js +44 -7
- package/bin/state-commands.js +112 -5
- package/core/commands/complete-phase.md +62 -25
- package/core/commands/lanes.md +18 -1
- package/core/commands/start-phase.md +19 -3
- package/core/commands/start-project.md +6 -5
- package/core/commands/validate.md +3 -0
- package/core/config.js +1 -1
- package/core/ecosystem/lib/pointer.js +42 -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/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.
|
|
@@ -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;
|
|
@@ -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 |
|
|
@@ -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');
|
|
@@ -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/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/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/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
|
@@ -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
|
|
@@ -1620,6 +1641,7 @@ Ecosystem use — entry/exit commands (run from any repo):
|
|
|
1620
1641
|
existing ecosystem
|
|
1621
1642
|
momentum leave Detach THIS repo from its ecosystem
|
|
1622
1643
|
momentum doctor Diagnose state + list available transitions
|
|
1644
|
+
momentum doctor --clean [--execute] Sweep stray artifacts (AppleDouble/.bak/orphan worktrees)
|
|
1623
1645
|
momentum antigravity plugin-pack Pack momentum skills as a native Antigravity plugin ([--global])
|
|
1624
1646
|
|
|
1625
1647
|
Ecosystem use — operator toolkit (advanced):
|
|
@@ -1982,12 +2004,27 @@ async function main() {
|
|
|
1982
2004
|
// upgrade can only ever install files as new as the CLI itself.
|
|
1983
2005
|
const staleWarning = formatStaleCliWarning(pkg.version, await updateCheckPromise);
|
|
1984
2006
|
if (staleWarning) console.log(staleWarning);
|
|
1985
|
-
//
|
|
1986
|
-
//
|
|
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 }); };
|
|
1987
2024
|
if (autostash && !dryRun) {
|
|
1988
|
-
withAutostash(path.resolve(targetDir),
|
|
2025
|
+
withAutostash(path.resolve(targetDir), runAll);
|
|
1989
2026
|
} else {
|
|
1990
|
-
|
|
2027
|
+
runAll();
|
|
1991
2028
|
}
|
|
1992
2029
|
} catch (err) {
|
|
1993
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.');
|
|
@@ -129,8 +128,103 @@ function cmdLeave(args) {
|
|
|
129
128
|
console.log(`✓ Left "${reg.ecosystemName}". Now standalone.`);
|
|
130
129
|
}
|
|
131
130
|
|
|
131
|
+
// ── Phase 27 G4 — stray-artifact sweep (`momentum doctor --clean`) ──────────
|
|
132
|
+
|
|
133
|
+
const BAK_ROOTS = ['.claude', '.agents', '.opencode', '.codex', '.githooks', 'scripts'];
|
|
134
|
+
|
|
135
|
+
/** Bounded walk collecting AppleDouble sidecars anywhere (always junk). */
|
|
136
|
+
function walkAppleDouble(dir, acc, depth) {
|
|
137
|
+
if (depth > 8) return;
|
|
138
|
+
let entries;
|
|
139
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
140
|
+
for (const e of entries) {
|
|
141
|
+
if (e.name === '.git' || e.name === 'node_modules') continue;
|
|
142
|
+
const abs = path.join(dir, e.name);
|
|
143
|
+
if (e.name.startsWith('._') || e.name === '.DS_Store') { acc.appleDouble.push(abs); continue; }
|
|
144
|
+
if (e.isDirectory()) walkAppleDouble(abs, acc, depth + 1);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** `.bak` files under a momentum-managed root only (never user backups). */
|
|
149
|
+
function collectBaks(dir, acc, depth) {
|
|
150
|
+
if (depth > 8) return;
|
|
151
|
+
let entries;
|
|
152
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
153
|
+
for (const e of entries) {
|
|
154
|
+
const abs = path.join(dir, e.name);
|
|
155
|
+
if (e.isDirectory()) collectBaks(abs, acc, depth + 1);
|
|
156
|
+
else if (e.name.endsWith('.bak')) acc.baks.push(abs);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Find stray artifacts: AppleDouble, momentum `.bak` litter, orphan worktrees. */
|
|
161
|
+
function findStrayArtifacts(root) {
|
|
162
|
+
const { spawnSync } = require('child_process');
|
|
163
|
+
const acc = { appleDouble: [], baks: [], prunableWorktrees: [], orphanWorktreeDirs: [] };
|
|
164
|
+
walkAppleDouble(root, acc, 0);
|
|
165
|
+
for (const sub of BAK_ROOTS) collectBaks(path.join(root, sub), acc, 0);
|
|
166
|
+
for (const f of ['CLAUDE.md.bak', 'AGENTS.md.bak']) {
|
|
167
|
+
const p = path.join(root, f);
|
|
168
|
+
if (fs.existsSync(p)) acc.baks.push(p);
|
|
169
|
+
}
|
|
170
|
+
// registered worktrees whose directory is gone → prunable
|
|
171
|
+
const registered = new Set();
|
|
172
|
+
const wl = spawnSync('git', ['worktree', 'list', '--porcelain'], { cwd: root, encoding: 'utf8' });
|
|
173
|
+
if (wl.status === 0) {
|
|
174
|
+
for (const line of wl.stdout.split('\n')) {
|
|
175
|
+
if (line.startsWith('worktree ')) {
|
|
176
|
+
const p = line.slice('worktree '.length);
|
|
177
|
+
registered.add(realpathOf(p));
|
|
178
|
+
if (p !== root && !fs.existsSync(p)) acc.prunableWorktrees.push(p);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
// .claude/worktrees/* leftover dirs not registered as git worktrees
|
|
183
|
+
const cw = path.join(root, '.claude', 'worktrees');
|
|
184
|
+
try {
|
|
185
|
+
for (const name of fs.readdirSync(cw)) {
|
|
186
|
+
const abs = path.join(cw, name);
|
|
187
|
+
if (fs.statSync(abs).isDirectory() && !registered.has(realpathOf(abs))) acc.orphanWorktreeDirs.push(abs);
|
|
188
|
+
}
|
|
189
|
+
} catch { /* no .claude/worktrees */ }
|
|
190
|
+
return acc;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function sweepStrayArtifacts(root, execute) {
|
|
194
|
+
const { spawnSync } = require('child_process');
|
|
195
|
+
const stray = findStrayArtifacts(root);
|
|
196
|
+
const total = stray.appleDouble.length + stray.baks.length
|
|
197
|
+
+ stray.prunableWorktrees.length + stray.orphanWorktreeDirs.length;
|
|
198
|
+
const rel = (p) => path.relative(root, p) || p;
|
|
199
|
+
|
|
200
|
+
console.log(execute ? 'Cleaning stray artifacts:' : 'Stray artifacts (dry-run — pass --execute to remove):');
|
|
201
|
+
if (total === 0) { console.log(' ✓ none — tree is clean'); return 0; }
|
|
202
|
+
|
|
203
|
+
for (const p of stray.appleDouble) {
|
|
204
|
+
if (execute) { try { fs.rmSync(p, { recursive: true, force: true }); } catch { /* ignore */ } }
|
|
205
|
+
console.log(` ${execute ? '✓ removed' : '·'} AppleDouble: ${rel(p)}`);
|
|
206
|
+
}
|
|
207
|
+
for (const p of stray.baks) {
|
|
208
|
+
if (execute) { try { fs.rmSync(p, { force: true }); } catch { /* ignore */ } }
|
|
209
|
+
console.log(` ${execute ? '✓ removed' : '·'} .bak: ${rel(p)}`);
|
|
210
|
+
}
|
|
211
|
+
for (const p of stray.orphanWorktreeDirs) {
|
|
212
|
+
if (execute) { try { fs.rmSync(p, { recursive: true, force: true }); } catch { /* ignore */ } }
|
|
213
|
+
console.log(` ${execute ? '✓ removed' : '·'} orphan worktree dir: ${rel(p)}`);
|
|
214
|
+
}
|
|
215
|
+
if (stray.prunableWorktrees.length) {
|
|
216
|
+
if (execute) spawnSync('git', ['worktree', 'prune'], { cwd: root });
|
|
217
|
+
for (const p of stray.prunableWorktrees) console.log(` ${execute ? '✓ pruned' : '·'} stale worktree ref: ${rel(p)}`);
|
|
218
|
+
}
|
|
219
|
+
console.log(execute ? `✓ cleaned ${total} stray artifact(s)` : `${total} stray artifact(s) — rerun with --execute to remove`);
|
|
220
|
+
return 0;
|
|
221
|
+
}
|
|
222
|
+
|
|
132
223
|
function cmdDoctor(args) {
|
|
133
|
-
|
|
224
|
+
// Phase 27 G4 — `momentum doctor --clean [--execute]` sweeps stray artifacts.
|
|
225
|
+
if (args && args.includes('--clean')) {
|
|
226
|
+
return sweepStrayArtifacts(realpathOf(process.cwd()), args.includes('--execute'));
|
|
227
|
+
}
|
|
134
228
|
const cwd = realpathOf(process.cwd());
|
|
135
229
|
lib._clearRootCache();
|
|
136
230
|
const state = stateLib.detectState(cwd);
|
|
@@ -176,6 +270,17 @@ function cmdDoctor(args) {
|
|
|
176
270
|
}
|
|
177
271
|
} catch (_err) { /* advisory is best-effort — never block doctor */ }
|
|
178
272
|
|
|
273
|
+
// Phase 27 G4 — stray-artifact advisory (best-effort, non-blocking).
|
|
274
|
+
try {
|
|
275
|
+
const stray = findStrayArtifacts(cwd);
|
|
276
|
+
const n = stray.appleDouble.length + stray.baks.length
|
|
277
|
+
+ stray.prunableWorktrees.length + stray.orphanWorktreeDirs.length;
|
|
278
|
+
if (n > 0) {
|
|
279
|
+
console.log(`Advisory: ${n} stray artifact(s) (AppleDouble / .bak / orphan worktrees) — run \`momentum doctor --clean\` to review, \`--clean --execute\` to remove.`);
|
|
280
|
+
console.log('');
|
|
281
|
+
}
|
|
282
|
+
} catch { /* best-effort */ }
|
|
283
|
+
|
|
179
284
|
const transitions = stateLib.availableTransitions(state, reg || {});
|
|
180
285
|
if (transitions.length === 0) {
|
|
181
286
|
console.log('No suggested next steps for this state.');
|
|
@@ -215,4 +320,6 @@ module.exports = {
|
|
|
215
320
|
cmdLeave,
|
|
216
321
|
cmdDoctor,
|
|
217
322
|
formatStateLabel,
|
|
323
|
+
findStrayArtifacts,
|
|
324
|
+
sweepStrayArtifacts,
|
|
218
325
|
};
|
|
@@ -53,22 +53,34 @@ Verify, finalize, and release a completed phase.
|
|
|
53
53
|
- Format as fenced code blocks with the command as the heading
|
|
54
54
|
- This is the durable record that Rule 12 was honored — it must exist before the release section runs
|
|
55
55
|
|
|
56
|
-
7. Update all tracking
|
|
56
|
+
7. Update all tracking (this happens BEFORE the release — see the Release
|
|
57
|
+
gate below; the release must never precede tracking):
|
|
57
58
|
- `specs/phases/README.md` → mark `Complete`
|
|
58
|
-
- `specs/status.md` →
|
|
59
|
-
(
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
- `specs/status.md` → (a) header **`Latest Release`** = this version + name;
|
|
60
|
+
(b) add the phase's row to the **Completed Phases** table; (c) remove this
|
|
61
|
+
lane's row from the Active Phase table (leave other lanes' rows untouched —
|
|
62
|
+
Rule 15); (d) update Current Phase
|
|
63
|
+
- `specs/planning/roadmap.md` → update phase status → Complete (+ version)
|
|
62
64
|
- `specs/changelog/YYYY-MM.md` → add release entry
|
|
63
65
|
|
|
64
66
|
### Release
|
|
65
67
|
|
|
66
|
-
> **Gate (Rule 12):** Do NOT enter Release if `retrospective.md` lacks a `## Verification Evidence` section. If evidence is missing, return to Step 3 and capture it.
|
|
67
|
-
|
|
68
|
-
|
|
68
|
+
> **Gate A (Rule 12):** Do NOT enter Release if `retrospective.md` lacks a `## Verification Evidence` section. If evidence is missing, return to Step 3 and capture it.
|
|
69
|
+
>
|
|
70
|
+
> **Gate B (tracking-before-release):** Do NOT merge, tag, or release until ALL
|
|
71
|
+
> tracking that reflects this release (Step 7 — `status.md` `Latest Release` +
|
|
72
|
+
> Completed Phases row, `specs/phases/README.md`, `roadmap.md`, the changelog)
|
|
73
|
+
> is written AND committed on the phase branch (Step 8). The release must never
|
|
74
|
+
> precede tracking — otherwise `status.md` says "in progress" while the tag is
|
|
75
|
+
> already public. Confirm the working tree is clean of tracking edits (Step 8
|
|
76
|
+
> committed them) before Step 9's approval and Step 10's merge/tag.
|
|
77
|
+
|
|
78
|
+
8. Commit all remaining changes (retrospective + ALL Step-7 tracking) and push.
|
|
79
|
+
This commit MUST land before the merge/tag — it is Gate B:
|
|
69
80
|
```bash
|
|
70
81
|
git add -A
|
|
71
82
|
git commit -m "docs: complete Phase N - {phase name}"
|
|
83
|
+
git status --porcelain # expect empty — tracking is committed (Gate B satisfied)
|
|
72
84
|
git push origin phase-N-shortname
|
|
73
85
|
```
|
|
74
86
|
|
|
@@ -77,7 +89,7 @@ Verify, finalize, and release a completed phase.
|
|
|
77
89
|
`[staging, main]`) in order. The plan stops at the universal git primitives
|
|
78
90
|
(`git merge` + `git tag -a` + `git push origin <tag>`); forge-specific
|
|
79
91
|
release creation and registry publishes are NOT listed here — they run
|
|
80
|
-
from
|
|
92
|
+
from `specs/project-rules.md` + `specs/config.md` (`release_command`,
|
|
81
93
|
`publish_target`, `release_flow`) after the tag is pushed.
|
|
82
94
|
```
|
|
83
95
|
Ready to release vX.Y.Z. This will:
|
|
@@ -86,7 +98,7 @@ Verify, finalize, and release a completed phase.
|
|
|
86
98
|
3. Tag vX.Y.Z and push the tag
|
|
87
99
|
|
|
88
100
|
Project-specific release/publish/deploy (forge release, npm publish, …)
|
|
89
|
-
run from
|
|
101
|
+
run from specs/project-rules.md + specs/config.md after the tag lands.
|
|
90
102
|
|
|
91
103
|
Proceed?
|
|
92
104
|
```
|
|
@@ -114,35 +126,60 @@ Verify, finalize, and release a completed phase.
|
|
|
114
126
|
git push origin <branch_flow[1]>
|
|
115
127
|
|
|
116
128
|
# final step — tag + push the tag (universal git; forge release + publish
|
|
117
|
-
# are project-specific — run them from
|
|
129
|
+
# are project-specific — run them from specs/project-rules.md now)
|
|
118
130
|
git tag -a vX.Y.Z -m "Phase N: {phase name}"
|
|
119
131
|
git push origin vX.Y.Z
|
|
120
132
|
```
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
133
|
+
**`end_state` variants** (config; the sequence above is the
|
|
134
|
+
`merge-after-yes` default):
|
|
135
|
+
- `feature-branch-only` — SKIP the merge/tag; the branch is pushed, the
|
|
136
|
+
human owns merge + release.
|
|
137
|
+
- `staging-promotion` — merge to `branch_flow[0]` only, then STOP; the human
|
|
138
|
+
promotes to `main` and tags.
|
|
139
|
+
- `open-pr` — do NOT merge; push the branch and open a PR for review
|
|
140
|
+
(config-gated, agent-run — `git_forge=github`: `gh pr create`; GitLab:
|
|
141
|
+
`glab mr create`), then STOP. Merge happens on the forge on human approval.
|
|
142
|
+
|
|
143
|
+
**Human handshake → verify → clean** (every non-`merge-after-yes`
|
|
144
|
+
end_state). Step-13 auto-cleanup only fires when the AGENT did the terminal
|
|
145
|
+
merge. When a human/forge does it, NEVER clean on trust: ask the user to
|
|
146
|
+
confirm once they have merged/promoted, then verify + clean in one step —
|
|
147
|
+
```bash
|
|
148
|
+
momentum lanes reconcile # is the branch a verified ancestor of the terminal branch yet?
|
|
149
|
+
momentum lanes reconcile --execute # only once it shows 'landed' — cleans worktree + branch + state
|
|
150
|
+
```
|
|
151
|
+
`reconcile` refuses to clean a lane whose branch is not yet contained in the
|
|
152
|
+
terminal branch — a "yes, I merged it" is never taken on trust (Rule 12).
|
|
126
153
|
|
|
127
154
|
12. Report summary to user:
|
|
128
155
|
- What was delivered
|
|
129
156
|
- Tag URL (`git push origin vX.Y.Z` output) — forge release/publish
|
|
130
|
-
status from
|
|
157
|
+
status from `specs/project-rules.md` if any ran
|
|
131
158
|
- What's next
|
|
132
159
|
|
|
133
|
-
13.
|
|
134
|
-
are confirmed; do NOT skip
|
|
160
|
+
13. Clean up the spent phase branch (ENH-042 / BUG-026 — now that merge → main
|
|
161
|
+
+ release are confirmed; do NOT skip, or stale branches/worktrees/state
|
|
162
|
+
accumulate). One command removes ALL of it, default-branch-safe:
|
|
135
163
|
```bash
|
|
136
|
-
|
|
137
|
-
|
|
164
|
+
# worktree (if the phase ran in a lane) + local branch + remote branch
|
|
165
|
+
# (skipped when it is the forge default — BUG-025 guard) + lane state.
|
|
166
|
+
momentum lanes cleanup phase-N-shortname
|
|
167
|
+
# --lane <id> if this phase ran as a registered `momentum lanes` lane
|
|
168
|
+
# --force only to override an unmerged-branch refusal AFTER you have
|
|
169
|
+
# verified the branch actually landed
|
|
138
170
|
```
|
|
171
|
+
If cleanup reports `blocked` on the remote branch because it is the forge
|
|
172
|
+
DEFAULT, STOP — that is the BUG-025 hijack. Reassign the default to the
|
|
173
|
+
terminal branch first (e.g. `git_forge=github`: `gh repo edit
|
|
174
|
+
--default-branch main`), then rerun. `-d` (not `-D`) refuses an unmerged
|
|
175
|
+
branch by design — investigate rather than force.
|
|
139
176
|
|
|
140
177
|
14. Branch-hygiene self-audit (ENH-042) — confirm no released phase left a
|
|
141
|
-
dangling branch:
|
|
178
|
+
dangling branch or worktree:
|
|
142
179
|
```bash
|
|
143
|
-
# Any origin branch for an already-released phase should be gone.
|
|
144
180
|
git branch -r | grep -E 'origin/(phase-|chore/|audit/)' || echo "✓ clean — no stale branches"
|
|
181
|
+
git worktree list # no leftover lane worktrees for landed work
|
|
145
182
|
```
|
|
146
|
-
For each match, verify it is fully merged into `main`
|
|
147
|
-
(`git branch -r --merged main`) and
|
|
183
|
+
For each stale match, verify it is fully merged into `main`
|
|
184
|
+
(`git branch -r --merged main`) and clean it per step 13. Leave any
|
|
148
185
|
*unmerged* branch alone and surface it to the user.
|
package/core/commands/lanes.md
CHANGED
|
@@ -75,7 +75,7 @@ landing) and ack what you've acted on.
|
|
|
75
75
|
momentum lanes done <id> # enter the landing queue
|
|
76
76
|
momentum lanes land <id> # validate: turn, freshness, graded gate
|
|
77
77
|
momentum lanes land <id> --execute# merge --no-ff into the CURRENT branch
|
|
78
|
-
momentum lanes close <id> [--rm-worktree]
|
|
78
|
+
momentum lanes close <id> [--rm-worktree] # retire: full cleanup (branch + state)
|
|
79
79
|
```
|
|
80
80
|
|
|
81
81
|
Landing follows the Rule 6 **Landing Order**: one lane at a time, run the
|
|
@@ -87,6 +87,23 @@ with a non-empty `## Verification Evidence` section (Rule 12).
|
|
|
87
87
|
`land` never pushes — pushes to protected branches keep their own
|
|
88
88
|
approval gate.
|
|
89
89
|
|
|
90
|
+
### Cleanup — a landed lane is spent (BUG-026)
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
momentum lanes reconcile # report lanes now merged out-of-band (human/forge)
|
|
94
|
+
momentum lanes reconcile --execute # clean them: worktree + branch + state
|
|
95
|
+
momentum lanes cleanup <branch> [--worktree P] [--dry-run] # one branch, manually
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
When the AGENT lands on the terminal branch (`branch_flow` last entry),
|
|
99
|
+
`land --execute` **auto-cleans** the worktree + branch + lane state
|
|
100
|
+
(default-branch-safe; `--keep` opts out). When a HUMAN or the FORGE does
|
|
101
|
+
the terminal merge (`end_state` `staging-promotion` / `feature-branch-only`
|
|
102
|
+
/ `open-pr`), cleanup is deferred: run `momentum lanes reconcile` — it
|
|
103
|
+
cleans only lanes whose branch is a **verified ancestor** of the terminal
|
|
104
|
+
branch (a "yes, I merged it" is never trusted, Rule 12). Cleanup never
|
|
105
|
+
deletes a branch that is the forge default (the BUG-025 guard).
|
|
106
|
+
|
|
90
107
|
## Tracking
|
|
91
108
|
|
|
92
109
|
Opening/closing lanes is not itself phase history; the WORK inside a lane
|