@limina-labs/momentum 0.32.0 → 0.33.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 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`.**
@@ -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 |
@@ -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/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
- skipRelPaths: new Set(['CLAUDE.md']),
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, {
@@ -1471,6 +1650,7 @@ Waves — wave plan from dependency annotations (Phase 21c, one engine every sca
1471
1650
  OKF — specs/ as an Open Knowledge Format bundle (Phase 24, ADR-0005):
1472
1651
  momentum okf check [dir] OKF v0.1 conformance report for <dir>/specs
1473
1652
  momentum okf index [dir] Regenerate bundle indexes (root/phases/decisions)
1653
+ momentum config sync [target-dir] Re-infer project shape, show config drift, apply on approval
1474
1654
 
1475
1655
  Options:
1476
1656
  --agent <name> Agent to install for. \`init\` asks when this
@@ -1919,6 +2099,22 @@ async function main() {
1919
2099
  console.error(`\nError: ${err.message}`);
1920
2100
  exitCode = 1;
1921
2101
  }
2102
+ } else if (args[0] === 'config') {
2103
+ try {
2104
+ const sub = args[1];
2105
+ const target = args[2] ? path.resolve(args[2]) : process.cwd();
2106
+ if (sub === 'sync') {
2107
+ const dryRun = args.includes('--dry-run') || _dryRun;
2108
+ await syncConfig(target, { dryRun });
2109
+ } else {
2110
+ console.error(`Unknown config subcommand: ${sub || '(none)'}`);
2111
+ console.error('Usage: momentum config sync [target-dir] [--dry-run]');
2112
+ exitCode = 1;
2113
+ }
2114
+ } catch (err) {
2115
+ console.error(`\nError: ${err.message}`);
2116
+ exitCode = 1;
2117
+ }
1922
2118
  } else {
1923
2119
  console.error(`Unknown command: ${args[0]}`);
1924
2120
  console.error('Run "momentum --help" for usage.');
@@ -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?
@@ -18,6 +18,11 @@ Verify, finalize, and release a completed phase.
18
18
  - If any `[ ]` unchecked items remain → report to user, do NOT proceed
19
19
 
20
20
  3. **Run project-specific validation and capture fresh evidence** (per Rule 12 — Verify Before Claim):
21
+ - Read `specs/config.md` (`core/config.js` → `readConfig('specs')`)
22
+ for `test_command` and `build_command`. When the file is absent or a
23
+ value is missing, fall back to `npm test` / no build (the historical
24
+ defaults). Run `test_command` always; run `build_command` when it is not
25
+ `none`.
21
26
  - Run all defined validation commands: tests, linting, type checks, build, smoke tests
22
27
  - Capture each command's stdout/stderr (e.g., redirect to a temp file with `tee`)
23
28
  - Note exit codes
@@ -67,49 +72,62 @@ Verify, finalize, and release a completed phase.
67
72
  git push origin phase-N-shortname
68
73
  ```
69
74
 
70
- 9. Ask the user ONCE for approval to release — present the full plan:
75
+ 9. Ask the user ONCE for approval to release — present the full plan, walking
76
+ the project's `branch_flow` (from `specs/config.md`; default
77
+ `[staging, main]`) in order. The plan stops at the universal git primitives
78
+ (`git merge` + `git tag -a` + `git push origin <tag>`); forge-specific
79
+ release creation and registry publishes are NOT listed here — they run
80
+ from `## Project Extensions` + `specs/config.md` (`release_command`,
81
+ `publish_target`, `release_flow`) after the tag is pushed.
71
82
  ```
72
83
  Ready to release vX.Y.Z. This will:
73
- 1. Merge phase-N-shortname → staging
74
- 2. Merge staging main
75
- 3. Tag vX.Y.Z and create GitHub Release
84
+ 1. Merge phase-N-shortname → <branch_flow[0]> (e.g. staging)
85
+ 2. …promote through <branch_flow[1..]> (e.g. main)
86
+ 3. Tag vX.Y.Z and push the tag
87
+
88
+ Project-specific release/publish/deploy (forge release, npm publish, …)
89
+ run from ## Project Extensions + specs/config.md after the tag lands.
76
90
 
77
91
  Proceed?
78
92
  ```
79
93
  Wait for a single "yes" before running any of the following steps.
80
94
 
81
- 10. On approval, execute all three steps in sequence.
95
+ 10. On approval, execute the merge sequence + tag in order.
82
96
 
83
97
  **Landing Order (Rule 6/15):** if another lane landed on `main` since
84
98
  this branch last rebased, rebase this branch onto updated `main` and
85
99
  re-run the suite BEFORE merging. Never land two lanes back-to-back
86
100
  without the suite passing in between.
87
101
 
102
+ Walk `branch_flow` (default `[staging, main]`) hop by hop — one merge per
103
+ protected branch in order, approval covered by the single "yes" above:
88
104
  ```bash
89
- # step 1 — phase branch → staging
90
- git checkout staging && git pull origin staging
91
- git merge --no-ff phase-N-shortname -m "merge: phase-N-shortname → staging (vX.Y.Z)"
92
- git push origin staging
93
-
94
- # step 2 — staging main
95
- git checkout main && git pull origin main
96
- git merge --no-ff staging -m "merge: staging → main (vX.Y.Z — Phase N: {phase name})"
97
- git push origin main
98
-
99
- # step 3 — tag + GitHub Release
105
+ # step 1 — phase branch → branch_flow[0]
106
+ git checkout <branch_flow[0]> && git pull origin <branch_flow[0]>
107
+ git merge --no-ff phase-N-shortname -m "merge: phase-N-shortname → <branch_flow[0]> (vX.Y.Z)"
108
+ git push origin <branch_flow[0]>
109
+
110
+ # step 2..promote through each remaining branch_flow entry
111
+ # (e.g. staging main)
112
+ git checkout <branch_flow[1]> && git pull origin <branch_flow[1]>
113
+ git merge --no-ff <branch_flow[0]> -m "merge: <branch_flow[0]> → <branch_flow[1]> (vX.Y.Z — Phase N: {phase name})"
114
+ git push origin <branch_flow[1]>
115
+
116
+ # final step — tag + push the tag (universal git; forge release + publish
117
+ # are project-specific — run them from ## Project Extensions now)
100
118
  git tag -a vX.Y.Z -m "Phase N: {phase name}"
101
119
  git push origin vX.Y.Z
102
- gh release create vX.Y.Z \
103
- --title "vX.Y.Z — Phase N: {phase name}" \
104
- --notes "## Phase N: {phase name}
105
- {bullet summary of what was delivered}
106
- ### Next: Phase N+1 — {next phase name}" \
107
- --target main
108
120
  ```
121
+ For `end_state: feature-branch-only` (config), SKIP the merge
122
+ sequence — the branch is already pushed; only tag (if the project tags
123
+ releases on the feature branch) per `## Project Extensions`. For
124
+ `end_state: staging-promotion`, merge to `branch_flow[0]` only and stop;
125
+ tag + release happen after the user promotes to `main`.
109
126
 
110
127
  12. Report summary to user:
111
128
  - What was delivered
112
- - GitHub Release URL
129
+ - Tag URL (`git push origin vX.Y.Z` output) — forge release/publish
130
+ status from `## Project Extensions` if any ran
113
131
  - What's next
114
132
 
115
133
  13. Delete the merged phase branch (ENH-042 — now that merge → main + release
@@ -119,9 +119,33 @@ Once a phase plan is approved, this command executes the plan end-to-end without
119
119
 
120
120
  ### Hard stop — always
121
121
 
122
- **Merge to staging/main + release.** After the final group's verification passes, STOP. Ask the user:
123
-
124
- > "All groups complete and verified. Ready to merge `<phase-branch>` → staging (then main), tag `v<version>`, and run `npm publish --access public`. Approve to proceed?"
122
+ **Merge to a protected branch + release.** After the final group's
123
+ verification passes, STOP. Read the project's config
124
+ (`specs/config.md` via `core/config.js`
125
+ `readConfig('<repo>/specs')`; returns `null` when absent → use the
126
+ defaults below) and ask the user the gate question matching the project's
127
+ `end_state`:
128
+
129
+ - **`merge-after-yes`** (default): "All groups complete and verified. Ready
130
+ to merge `<phase-branch>` → `<branch_flow in order>` (e.g. `staging`, then
131
+ `main`), tag `v<version>`. Approve to proceed?"
132
+ - **`staging-promotion`**: "All groups complete and verified. Ready to merge
133
+ `<phase-branch>` → `staging`. Tag `v<version>` + release after you promote
134
+ to `main`. Approve to proceed?"
135
+ - **`feature-branch-only`**: "All groups complete and verified. The feature
136
+ branch `<phase-branch>` is pushed. Merge, tag, and release are yours to
137
+ do. Review the branch?"
138
+
139
+ Then append: "Project-specific release/publish/deploy steps (e.g.
140
+ `npm publish`, forge release creation) run from `## Project Extensions` in
141
+ CLAUDE.md/AGENTS.md — consult them now if any apply."
142
+
143
+ The gate stops at the truly universal git primitives — `git merge` +
144
+ `git tag -a` + `git push origin <tag>`. Forge-specific release creation
145
+ (`gh release create`, `glab release create`, …) and registry publishes
146
+ (`npm publish`, `twine upload`, …) are NOT in this template; they live in
147
+ `## Project Extensions` + `specs/config.md` (`release_command`,
148
+ `publish_target`, `release_flow`).
125
149
 
126
150
  This is the only place the engine asks. Do NOT skip it.
127
151
 
@@ -51,14 +51,20 @@ If you're still exploring the idea, run `/brainstorm-idea` first.
51
51
  - `status.md` Summary paragraph (what this project is)
52
52
  - For monorepo: first-pass architecture sketch (core abstractions, interfaces)
53
53
  - Phase 0 contents using the [Group Execution Pattern](#group-execution-pattern)
54
+ - **Config** (`specs/config.md`, ADR-0009): the project-shape
55
+ settings recipe templates read at execution time (forge, publish target,
56
+ branch flow, verification commands — see the [Config Format](#config-format)).
57
+ Carry the values settled during `/brainstorm-idea`; when `momentum init`
58
+ already inferred them, confirm the inferred file. `protected_branches`
59
+ is derived from `branch_flow`.
54
60
  - Use the [Foundation Doc Formats](#foundation-doc-formats) below; author
55
61
  real content from the brainstorm — never emit `_(TBD)_` placeholders.
56
62
  Where understanding is genuinely thin, write the best-effort version,
57
63
  mark the specific line `<!-- refine: ... -->`, and ask the user.
58
64
 
59
65
  4. Present the founding draft for approval:
60
- - Show the charter, roadmap, and key Phase 0 sections in chat
61
- - List every file that will be created
66
+ - Show the charter, roadmap, config table, and key Phase 0 sections in chat
67
+ - List every file that will be created (including `specs/config.md`)
62
68
  - Ask: "Ready to found the project? This will write N files. Approve to proceed."
63
69
 
64
70
  5. **On approval — exit the gate**:
@@ -71,12 +77,17 @@ If you're still exploring the idea, run `/brainstorm-idea` first.
71
77
  - `specs/vision/principles.md`
72
78
  - `specs/vision/success-criteria.md`
73
79
  - `specs/planning/roadmap.md`
80
+ - `specs/config.md` (from the config table above; overwrite the
81
+ `momentum init`-inferred file if present — founding owns the content)
74
82
  - Update `specs/status.md`: Summary, Current Phase (Phase 0 `not started`),
75
83
  Upcoming Phases, Next Actions
76
84
  - For monorepo: `specs/architecture/` first-pass doc(s)
77
85
  - `specs/phases/phase-0-shortname/{overview,plan,tasks,history}.md`
78
86
  (log the founding decisions from this conversation in `history.md`)
79
- - Refresh bundle listings: `momentum okf index`
87
+ - Refresh the derived cache: `core/config.js`
88
+ `writeConfigCache('.', readConfig('specs'))` so the pre-push
89
+ hook's `protected_branches` matches the authored `branch_flow`;
90
+ then `momentum okf index` to refresh bundle listings
80
91
 
81
92
  7. Commit the founding:
82
93
  ```bash
@@ -202,6 +213,41 @@ type: Roadmap
202
213
 
203
214
  ---
204
215
 
216
+ ## Config Format
217
+
218
+ Author `specs/config.md` in this shape (`type: Config` → OKF bundle).
219
+ Lists (`branch_flow`, `protected_branches`) are comma-separated;
220
+ `protected_branches` is derived from `branch_flow`. The trust layer (human
221
+ authorization for protected-branch pushes) is invariant, NOT a preference
222
+ (ADR-0009); bypass only via `MOMENTUM_SKIP_HOOKS=1`.
223
+
224
+ ```markdown
225
+ ---
226
+ type: Config
227
+ ---
228
+
229
+ # Project Config
230
+
231
+ > Recipes read these at execution time; missing values fall back to npm/GitHub
232
+ > defaults. Edit freely.
233
+
234
+ | Key | Value |
235
+ |-----|-------|
236
+ | language | node |
237
+ | framework | nextjs |
238
+ | test_command | npm test |
239
+ | build_command | npm run build |
240
+ | publish_target | npm |
241
+ | git_forge | github |
242
+ | release_command | gh release create |
243
+ | release_flow | tag-and-publish |
244
+ | end_state | merge-after-yes |
245
+ | branch_flow | staging, main |
246
+ | protected_branches | staging, main |
247
+ ```
248
+
249
+ ---
250
+
205
251
  ## Brainstorm Gate Contract
206
252
 
207
253
  This command runs in two phases: **brainstorm** (conversational, no disk writes) and **commit** (writes files to disk on explicit approval).
@@ -0,0 +1,43 @@
1
+ Sync the project's `specs/config.md` with the project's current shape — review drift and apply on approval.
2
+
3
+ Momentum infers project shape (language, framework, publish target, git forge)
4
+ from manifests (`package.json` / `pyproject.toml` / `Cargo.toml` / `go.mod` / …) and
5
+ the git remote. When that shape changes — you switched CI from GitHub Actions to
6
+ GitLab, adopted a framework, renamed the package — `specs/config.md` can drift
7
+ from reality. This command re-infers and shows the difference, then asks before
8
+ writing anything (ENH-062 — approval-gated, never a silent mutation).
9
+
10
+ ## Steps
11
+
12
+ 1. Run:
13
+ ```
14
+ momentum config sync
15
+ ```
16
+ (pass a `target-dir` to sync a different project; `--dry-run` to preview.)
17
+
18
+ 2. Read the output:
19
+ - `✓ matches the inferred project shape — nothing to sync` → no drift, done.
20
+ - A per-field `old → new` list → drift detected.
21
+
22
+ 3. At the prompt, decide:
23
+ - `a` / `all` → apply every drifted field.
24
+ - `language,git_forge` (comma-separated keys) → apply only those.
25
+ - `s` / `skip` / empty → leave `specs/config.md` unchanged.
26
+
27
+ 4. On approval, momentum rewrites `specs/config.md` and refreshes the derived
28
+ `.momentum/config-cache.json`. The **trust layer is untouched** — protected
29
+ branches can only ever be *added*, never removed (ADR-0009). Only the
30
+ inferable fields (`language`, `framework`, `publish_target`, `git_forge`) are
31
+ ever offered; user-authored fields (`branch_flow`, `end_state`, commands) are
32
+ never flagged or overwritten.
33
+
34
+ 5. Report to the user what changed (or that nothing changed). If drift was
35
+ found and skipped, note they can re-run `momentum config sync` any time.
36
+
37
+ ## When to run this proactively
38
+
39
+ - After a visible project-shape change (new framework, forge move, language
40
+ bump) — offer to run `/sync-config` rather than silently editing config.
41
+ - `momentum upgrade` prints a drift warning when it detects one; route the user
42
+ to this command to resolve it.
43
+ - Never auto-edit `specs/config.md` outside this approval flow.
@@ -31,6 +31,20 @@ Run with no arguments for a fast index-first check, or pass `--deep` for a full
31
31
  report informationally ("not founded yet — `/start-project` when
32
32
  ready"), never as a failure
33
33
 
34
+ 2c. Config (ADR-0009) — WARNING, never a failure (recipes fall back to
35
+ npm/GitHub defaults when absent):
36
+ - founded AND no `specs/config.md` → WARNING: "config not set
37
+ — recipes will use npm/GitHub defaults; run `momentum upgrade` to
38
+ infer them, or author at `/start-project`"
39
+ - `specs/config.md` exists → drift check: compare the
40
+ machine-inferable fields (`language`, `framework`, `publish_target`,
41
+ `git_forge`) against the manifests + `git remote get-url origin`
42
+ (`core/config.js` → `inferConfig('.')` vs
43
+ `readConfig('specs')`); on mismatch → WARNING: "config
44
+ drifted from manifests on: <fields> — edit by hand or delete the file
45
+ + re-run `momentum upgrade`"
46
+ - Not founded → skip (config author at founding)
47
+
34
48
  3. For each phase directory under `specs/phases/` (OKF bundle, ADR-0005):
35
49
  - Verify all 4 files present: `overview.md`, `plan.md`, `tasks.md`, `history.md`
36
50
  - Verify `overview.md` frontmatter carries `type: Phase` and a `status:`