@mmnto/cli 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/adapters/gh-utils.test.js +2 -0
  2. package/dist/adapters/gh-utils.test.js.map +1 -1
  3. package/dist/adapters/github-cli-pr.test.js +2 -0
  4. package/dist/adapters/github-cli-pr.test.js.map +1 -1
  5. package/dist/adapters/github-cli.test.js +2 -0
  6. package/dist/adapters/github-cli.test.js.map +1 -1
  7. package/dist/assets/compiled-baseline.d.ts.map +1 -1
  8. package/dist/assets/compiled-baseline.js +47 -8
  9. package/dist/assets/compiled-baseline.js.map +1 -1
  10. package/dist/commands/compile.d.ts.map +1 -1
  11. package/dist/commands/compile.js +32 -0
  12. package/dist/commands/compile.js.map +1 -1
  13. package/dist/commands/explain.d.ts +2 -0
  14. package/dist/commands/explain.d.ts.map +1 -0
  15. package/dist/commands/explain.js +98 -0
  16. package/dist/commands/explain.js.map +1 -0
  17. package/dist/commands/init.d.ts +3 -1
  18. package/dist/commands/init.d.ts.map +1 -1
  19. package/dist/commands/init.js +235 -217
  20. package/dist/commands/init.js.map +1 -1
  21. package/dist/commands/run-compiled-rules.d.ts.map +1 -1
  22. package/dist/commands/run-compiled-rules.js +12 -3
  23. package/dist/commands/run-compiled-rules.js.map +1 -1
  24. package/dist/commands/spec.d.ts +1 -1
  25. package/dist/commands/spec.d.ts.map +1 -1
  26. package/dist/commands/spec.js +36 -7
  27. package/dist/commands/spec.js.map +1 -1
  28. package/dist/git.test.js +2 -0
  29. package/dist/git.test.js.map +1 -1
  30. package/dist/index.js +15 -2
  31. package/dist/index.js.map +1 -1
  32. package/dist/orchestrators/conformance.test.js +1 -0
  33. package/dist/orchestrators/conformance.test.js.map +1 -1
  34. package/dist/orchestrators/shell-orchestrator.test.js +1 -0
  35. package/dist/orchestrators/shell-orchestrator.test.js.map +1 -1
  36. package/package.json +2 -2
@@ -688,7 +688,7 @@ function applyReflexUpgrade(filePath) {
688
688
  fs.writeFileSync(filePath, updated, 'utf-8');
689
689
  return clean;
690
690
  }
691
- export async function initCommand() {
691
+ export async function initCommand(options) {
692
692
  const cwd = process.cwd();
693
693
  const configPath = path.join(cwd, 'totem.config.ts');
694
694
  const totemDir = path.join(cwd, '.totem');
@@ -700,68 +700,81 @@ export async function initCommand() {
700
700
  if (!configExists) {
701
701
  // --- Fresh install: generate config ---
702
702
  log.info('Totem', 'Scanning project...');
703
- const detected = detectProject(cwd);
704
- const detections = [];
705
- if (detected.hasTypeScript)
706
- detections.push('TypeScript');
707
- if (detected.hasSrc)
708
- detections.push('src/');
709
- if (detected.hasDocs)
710
- detections.push('docs/');
711
- if (detected.hasSpecs)
712
- detections.push('specs/');
713
- if (detected.hasContext)
714
- detections.push('context/');
715
- if (detected.hasSessions)
716
- detections.push('session logs');
717
- if (detections.length > 0) {
718
- log.info('Totem', `Detected: ${bold(detections.join(', '))}`);
719
- }
720
- else {
721
- log.dim('Totem', 'No specific project structure detected. Using markdown defaults.');
722
- }
723
- const targets = buildTargets(detected);
724
- // Auto-detect embedding tier from environment
703
+ let targets = [];
725
704
  let embeddingTier = detectEmbeddingTier(cwd);
726
- if (embeddingTier === 'openai') {
727
- log.info('Totem', `Detected ${bold('OPENAI_API_KEY')} in environment. Using OpenAI embeddings.`);
728
- }
729
- else if (embeddingTier === 'gemini') {
730
- log.info('Totem', `Detected ${bold('GEMINI_API_KEY')} in environment. Using Gemini embeddings (single-key DX).`);
705
+ if (options?.bare) {
706
+ log.info('Totem', `Initializing in ${bold('bare mode')} (non-code repository)`);
707
+ targets = [
708
+ { glob: '.totem/lessons/*.md', type: 'lesson', strategy: 'markdown-heading' },
709
+ { glob: '.totem/lessons.md', type: 'lesson', strategy: 'markdown-heading' },
710
+ { glob: '**/*.md', type: 'spec', strategy: 'markdown-heading' },
711
+ ];
712
+ embeddingTier = 'none'; // Force Lite tier for bare repos
731
713
  }
732
714
  else {
733
- // No key detected prompt the user
734
- const answer = await rl.question('Enter your OpenAI API key, type "ollama" for a local model, or press Enter for Lite tier: ');
735
- const input = answer.trim().replace(/[\r\n]/g, '');
736
- if (input.toLowerCase() === 'ollama') {
737
- embeddingTier = 'ollama';
738
- log.info('Totem', 'Configured for Ollama. Make sure it is running locally.');
715
+ const detected = detectProject(cwd);
716
+ const detections = [];
717
+ if (detected.hasTypeScript)
718
+ detections.push('TypeScript');
719
+ if (detected.hasSrc)
720
+ detections.push('src/');
721
+ if (detected.hasDocs)
722
+ detections.push('docs/');
723
+ if (detected.hasSpecs)
724
+ detections.push('specs/');
725
+ if (detected.hasContext)
726
+ detections.push('context/');
727
+ if (detected.hasSessions)
728
+ detections.push('session logs');
729
+ if (detections.length > 0) {
730
+ log.info('Totem', `Detected: ${bold(detections.join(', '))}`);
731
+ }
732
+ else {
733
+ log.dim('Totem', 'No specific project structure detected. Using markdown defaults.');
739
734
  }
740
- else if (input) {
741
- if (!/^sk-[a-zA-Z0-9_-]+$/.test(input)) {
742
- log.warn('Totem', 'API key does not look like a valid OpenAI key (expected sk-...). Starting in Lite tier.');
735
+ targets = buildTargets(detected);
736
+ if (embeddingTier === 'openai') {
737
+ log.info('Totem', `Detected ${bold('OPENAI_API_KEY')} in environment. Using OpenAI embeddings.`);
738
+ }
739
+ else if (embeddingTier === 'gemini') {
740
+ log.info('Totem', `Detected ${bold('GEMINI_API_KEY')} in environment. Using Gemini embeddings (single-key DX).`);
741
+ }
742
+ else {
743
+ // No key detected — prompt the user
744
+ const answer = await rl.question('Enter your OpenAI API key, type "ollama" for a local model, or press Enter for Lite tier: ');
745
+ const input = answer.trim().replace(/[\r\n]/g, '');
746
+ if (input.toLowerCase() === 'ollama') {
747
+ embeddingTier = 'ollama';
748
+ log.info('Totem', 'Configured for Ollama. Make sure it is running locally.');
743
749
  }
744
- else {
745
- const envPath = path.join(cwd, '.env');
746
- const envLine = `OPENAI_API_KEY="${input}"\n`;
747
- if (fs.existsSync(envPath)) {
748
- const existing = fs.readFileSync(envPath, 'utf-8');
749
- if (!/^\s*OPENAI_API_KEY\s*=/m.test(existing)) {
750
- const prefix = existing.length > 0 && !existing.endsWith('\n') ? '\n' : '';
751
- fs.appendFileSync(envPath, prefix + envLine);
752
- }
750
+ else if (input) {
751
+ if (!/^sk-[a-zA-Z0-9_-]+$/.test(input)) {
752
+ log.warn('Totem', 'API key does not look like a valid OpenAI key (expected sk-...). Starting in Lite tier.');
753
753
  }
754
754
  else {
755
- fs.writeFileSync(envPath, envLine);
755
+ const envPath = path.join(cwd, '.env');
756
+ const envLine = `OPENAI_API_KEY="${input}"\n`;
757
+ if (fs.existsSync(envPath)) {
758
+ const existing = fs.readFileSync(envPath, 'utf-8');
759
+ if (!/^\s*OPENAI_API_KEY\s*=/m.test(existing)) {
760
+ const prefix = existing.length > 0 && !existing.endsWith('\n') ? '\n' : '';
761
+ fs.appendFileSync(envPath, prefix + envLine);
762
+ }
763
+ }
764
+ else {
765
+ fs.writeFileSync(envPath, envLine);
766
+ }
767
+ embeddingTier = 'openai';
768
+ summary.push({ file: '.env', action: 'Saved OpenAI API key' });
756
769
  }
757
- embeddingTier = 'openai';
758
- summary.push({ file: '.env', action: 'Saved OpenAI API key' });
759
770
  }
760
771
  }
761
772
  }
762
773
  if (embeddingTier === 'none') {
763
774
  log.info('Totem', `Starting in ${bold('Lite')} tier (add-lesson, bridge, eject only).`);
764
- log.dim('Totem', 'Set OPENAI_API_KEY and re-run `totem init` to unlock sync/search/shield.');
775
+ if (!options?.bare) {
776
+ log.dim('Totem', 'Set OPENAI_API_KEY and re-run `totem init` to unlock sync/search/shield.');
777
+ }
765
778
  }
766
779
  const configContent = await generateConfig(targets, embeddingTier, cwd);
767
780
  fs.writeFileSync(configPath, configContent, 'utf-8');
@@ -817,194 +830,199 @@ export async function initCommand() {
817
830
  log.dim('Totem', `Could not install pre-compiled rules: ${err instanceof Error ? err.message : String(err)}`);
818
831
  }
819
832
  }
820
- // --- Unified AI tool selection ---
821
- const detectedTools = detectAiTools(cwd);
822
- if (detectedTools.length > 0) {
823
- const toolNames = detectedTools.map((t) => t.name).join(', ');
824
- log.info('Totem', `Detected AI tools: ${bold(toolNames)}`);
825
- const toolAnswer = await rl.question('Which tools should Totem configure? [all/none/select] (default: all): ');
826
- let selectedTools;
827
- const trimmed = toolAnswer.trim().toLowerCase();
828
- if (trimmed === 'none') {
829
- selectedTools = [];
830
- }
831
- else if (trimmed === 'select') {
832
- selectedTools = [];
833
- for (const tool of detectedTools) {
834
- const pick = await rl.question(` Configure ${tool.name}? (Y/n): `);
835
- if (pick.trim().toLowerCase() !== 'n' && pick.trim().toLowerCase() !== 'no') {
836
- selectedTools.push(tool);
837
- }
838
- }
839
- }
840
- else {
841
- // 'all' or Enter (default)
842
- selectedTools = detectedTools;
843
- }
844
- // --- MCP scaffolding for selected tools ---
845
- for (const tool of selectedTools) {
846
- if (!tool.mcpPath || !tool.serverEntry)
847
- continue;
848
- const filePath = path.join(cwd, tool.mcpPath);
849
- const result = scaffoldMcpConfig(filePath, tool.serverEntry);
850
- if (result.err) {
851
- log.error('Totem Error', result.err); // totem-ignore — result.err is internal scaffolding error, not LLM output
852
- console.error(`To fix this, add the following manually to your ${tool.mcpPath} under "mcpServers":\n`);
853
- console.error(` "totem": ${JSON.stringify(tool.serverEntry, null, 2)}\n`);
833
+ if (options?.bare) {
834
+ log.info('Totem', 'Skipping AI tool and hook installation for bare mode.');
835
+ }
836
+ else {
837
+ // --- Unified AI tool selection ---
838
+ const detectedTools = detectAiTools(cwd);
839
+ if (detectedTools.length > 0) {
840
+ const toolNames = detectedTools.map((t) => t.name).join(', ');
841
+ log.info('Totem', `Detected AI tools: ${bold(toolNames)}`);
842
+ const toolAnswer = await rl.question('Which tools should Totem configure? [all/none/select] (default: all): ');
843
+ let selectedTools;
844
+ const trimmed = toolAnswer.trim().toLowerCase();
845
+ if (trimmed === 'none') {
846
+ selectedTools = [];
854
847
  }
855
- else if (result.action === 'created') {
856
- summary.push({ file: tool.mcpPath, action: `Created with Totem MCP server` });
848
+ else if (trimmed === 'select') {
849
+ selectedTools = [];
850
+ for (const tool of detectedTools) {
851
+ const pick = await rl.question(` Configure ${tool.name}? (Y/n): `);
852
+ if (pick.trim().toLowerCase() !== 'n' && pick.trim().toLowerCase() !== 'no') {
853
+ selectedTools.push(tool);
854
+ }
855
+ }
857
856
  }
858
- else if (result.action === 'merged') {
859
- summary.push({ file: tool.mcpPath, action: `Added totem to mcpServers` });
857
+ else {
858
+ // 'all' or Enter (default)
859
+ selectedTools = detectedTools;
860
860
  }
861
- }
862
- // --- Reflex injection & upgrade for selected tools ---
863
- const outdatedFiles = [];
864
- for (const tool of selectedTools) {
865
- if (!tool.reflexFile)
866
- continue;
867
- const filePath = path.join(cwd, tool.reflexFile);
868
- try {
869
- const result = injectReflexes(filePath);
870
- if (result === 'injected') {
871
- summary.push({ file: tool.reflexFile, action: 'Injected memory reflexes (v2)' });
861
+ // --- MCP scaffolding for selected tools ---
862
+ for (const tool of selectedTools) {
863
+ if (!tool.mcpPath || !tool.serverEntry)
864
+ continue;
865
+ const filePath = path.join(cwd, tool.mcpPath);
866
+ const result = scaffoldMcpConfig(filePath, tool.serverEntry);
867
+ if (result.err) {
868
+ log.error('Totem Error', result.err); // totem-ignore — result.err is internal scaffolding error, not LLM output
869
+ console.error(`To fix this, add the following manually to your ${tool.mcpPath} under "mcpServers":\n`);
870
+ console.error(` "totem": ${JSON.stringify(tool.serverEntry, null, 2)}\n`);
872
871
  }
873
- else if (result === 'outdated') {
874
- outdatedFiles.push({ tool, filePath });
872
+ else if (result.action === 'created') {
873
+ summary.push({ file: tool.mcpPath, action: `Created with Totem MCP server` });
874
+ }
875
+ else if (result.action === 'merged') {
876
+ summary.push({ file: tool.mcpPath, action: `Added totem to mcpServers` });
875
877
  }
876
878
  }
877
- catch (err) {
878
- const message = err instanceof Error ? err.message : String(err);
879
- log.error('Totem Error', `Failed to inject reflexes into ${tool.reflexFile}: ${message}`);
880
- }
881
- }
882
- // Prompt once for all outdated reflex files
883
- if (outdatedFiles.length > 0) {
884
- const fileList = outdatedFiles.map((f) => f.tool.reflexFile).join(', ');
885
- log.warn('Totem', `Outdated reflexes found in: ${bold(fileList)}`);
886
- let shouldUpgrade = false;
887
- if (process.stdin.isTTY) {
888
- const answer = await rl.question(`Upgrade reflexes to v${REFLEX_VERSION}? (Y/n): `);
889
- shouldUpgrade =
890
- answer.trim().toLowerCase() !== 'n' && answer.trim().toLowerCase() !== 'no';
891
- }
892
- else {
893
- // Non-TTY (CI/scripted): auto-upgrade to match baseline lessons behavior
894
- shouldUpgrade = true;
895
- log.info('Totem', 'Non-interactive mode — auto-upgrading reflexes.');
879
+ // --- Reflex injection & upgrade for selected tools ---
880
+ const outdatedFiles = [];
881
+ for (const tool of selectedTools) {
882
+ if (!tool.reflexFile)
883
+ continue;
884
+ const filePath = path.join(cwd, tool.reflexFile);
885
+ try {
886
+ const result = injectReflexes(filePath);
887
+ if (result === 'injected') {
888
+ summary.push({ file: tool.reflexFile, action: 'Injected memory reflexes (v2)' });
889
+ }
890
+ else if (result === 'outdated') {
891
+ outdatedFiles.push({ tool, filePath });
892
+ }
893
+ }
894
+ catch (err) {
895
+ const message = err instanceof Error ? err.message : String(err);
896
+ log.error('Totem Error', `Failed to inject reflexes into ${tool.reflexFile}: ${message}`);
897
+ }
896
898
  }
897
- if (shouldUpgrade) {
898
- for (const { tool, filePath } of outdatedFiles) {
899
- try {
900
- const clean = applyReflexUpgrade(filePath);
901
- if (clean) {
902
- summary.push({
903
- file: tool.reflexFile,
904
- action: `Upgraded reflexes to v${REFLEX_VERSION}`,
905
- });
899
+ // Prompt once for all outdated reflex files
900
+ if (outdatedFiles.length > 0) {
901
+ const fileList = outdatedFiles.map((f) => f.tool.reflexFile).join(', ');
902
+ log.warn('Totem', `Outdated reflexes found in: ${bold(fileList)}`);
903
+ let shouldUpgrade = false;
904
+ if (process.stdin.isTTY) {
905
+ const answer = await rl.question(`Upgrade reflexes to v${REFLEX_VERSION}? (Y/n): `);
906
+ shouldUpgrade =
907
+ answer.trim().toLowerCase() !== 'n' && answer.trim().toLowerCase() !== 'no';
908
+ }
909
+ else {
910
+ // Non-TTY (CI/scripted): auto-upgrade to match baseline lessons behavior
911
+ shouldUpgrade = true;
912
+ log.info('Totem', 'Non-interactive mode — auto-upgrading reflexes.');
913
+ }
914
+ if (shouldUpgrade) {
915
+ for (const { tool, filePath } of outdatedFiles) {
916
+ try {
917
+ const clean = applyReflexUpgrade(filePath);
918
+ if (clean) {
919
+ summary.push({
920
+ file: tool.reflexFile,
921
+ action: `Upgraded reflexes to v${REFLEX_VERSION}`,
922
+ });
923
+ }
924
+ else {
925
+ summary.push({
926
+ file: tool.reflexFile,
927
+ action: `Appended v${REFLEX_VERSION} reflexes (manual cleanup needed — remove old block)`,
928
+ });
929
+ log.warn('Totem', `Could not cleanly replace old reflexes in ${tool.reflexFile}. New block appended — please remove the old one manually.`);
930
+ }
906
931
  }
907
- else {
908
- summary.push({
909
- file: tool.reflexFile,
910
- action: `Appended v${REFLEX_VERSION} reflexes (manual cleanup needed — remove old block)`,
911
- });
912
- log.warn('Totem', `Could not cleanly replace old reflexes in ${tool.reflexFile}. New block appended — please remove the old one manually.`);
932
+ catch (err) {
933
+ const message = err instanceof Error ? err.message : String(err);
934
+ log.error('Totem Error', `Failed to upgrade reflexes in ${tool.reflexFile}: ${message}`);
913
935
  }
914
936
  }
915
- catch (err) {
916
- const message = err instanceof Error ? err.message : String(err);
917
- log.error('Totem Error', `Failed to upgrade reflexes in ${tool.reflexFile}: ${message}`);
937
+ }
938
+ else {
939
+ for (const { tool } of outdatedFiles) {
940
+ summary.push({
941
+ file: tool.reflexFile,
942
+ action: 'Outdated reflexes — upgrade declined',
943
+ });
918
944
  }
919
945
  }
920
946
  }
921
- else {
922
- for (const { tool } of outdatedFiles) {
923
- summary.push({
924
- file: tool.reflexFile,
925
- action: 'Outdated reflexes upgrade declined',
926
- });
947
+ // --- Hook installation for selected tools ---
948
+ for (const tool of selectedTools) {
949
+ if (!tool.hookInstaller)
950
+ continue;
951
+ const results = await tool.hookInstaller(cwd);
952
+ for (const result of results) {
953
+ if (result.err) {
954
+ log.error('Totem Error', `Hook scaffolding failed for ${result.file}: ${result.err}`); // totem-ignore — internal hook installer error
955
+ }
956
+ else if (result.action === 'created') {
957
+ summary.push({ file: result.file, action: `Scaffolded ${tool.name} hook` });
958
+ }
959
+ else if (result.action === 'merged') {
960
+ summary.push({
961
+ file: result.file,
962
+ action: `Merged ${tool.name} hook into existing config`,
963
+ });
964
+ }
927
965
  }
928
966
  }
929
967
  }
930
- // --- Hook installation for selected tools ---
931
- for (const tool of selectedTools) {
932
- if (!tool.hookInstaller)
933
- continue;
934
- const results = await tool.hookInstaller(cwd);
935
- for (const result of results) {
936
- if (result.err) {
937
- log.error('Totem Error', `Hook scaffolding failed for ${result.file}: ${result.err}`); // totem-ignore — internal hook installer error
938
- }
939
- else if (result.action === 'created') {
940
- summary.push({ file: result.file, action: `Scaffolded ${tool.name} hook` });
941
- }
942
- else if (result.action === 'merged') {
943
- summary.push({
944
- file: result.file,
945
- action: `Merged ${tool.name} hook into existing config`,
946
- });
947
- }
948
- }
968
+ // --- Always run: enforcement hooks (pre-commit + pre-push) ---
969
+ const enforcement = await installEnforcementHooks(cwd, rl);
970
+ if (enforcement.preCommit === 'installed' || enforcement.preCommit === 'appended') {
971
+ summary.push({
972
+ file: '.git/hooks/pre-commit',
973
+ action: `${enforcement.preCommit === 'installed' ? 'Installed' : 'Appended'} main-branch protection`,
974
+ });
949
975
  }
950
- }
951
- // --- Always run: enforcement hooks (pre-commit + pre-push) ---
952
- const enforcement = await installEnforcementHooks(cwd, rl);
953
- if (enforcement.preCommit === 'installed' || enforcement.preCommit === 'appended') {
954
- summary.push({
955
- file: '.git/hooks/pre-commit',
956
- action: `${enforcement.preCommit === 'installed' ? 'Installed' : 'Appended'} main-branch protection`,
957
- });
958
- }
959
- else if (enforcement.preCommit === 'skipped-non-shell') {
960
- summary.push({
961
- file: '.git/hooks/pre-commit',
962
- action: 'Skipped — non-shell hook detected (manual integration needed)',
963
- });
964
- }
965
- if (enforcement.prePush === 'installed' || enforcement.prePush === 'appended') {
966
- summary.push({
967
- file: '.git/hooks/pre-push',
968
- action: `${enforcement.prePush === 'installed' ? 'Installed' : 'Appended'} deterministic shield gate`,
969
- });
970
- }
971
- else if (enforcement.prePush === 'skipped-non-shell') {
972
- summary.push({
973
- file: '.git/hooks/pre-push',
974
- action: 'Skipped — non-shell hook detected (manual integration needed)',
975
- });
976
- }
977
- // --- Always run: post-merge git hook ---
978
- await installPostMergeHook(cwd, rl);
979
- // --- Always run: .gitignore ---
980
- const gitignorePath = path.join(cwd, '.gitignore');
981
- if (fs.existsSync(gitignorePath)) {
982
- const gitignore = fs.readFileSync(gitignorePath, 'utf-8');
983
- if (!gitignore.includes('.lancedb')) {
984
- fs.appendFileSync(gitignorePath, '\n# Totem\n.lancedb/\n');
985
- summary.push({ file: '.gitignore', action: 'Added .lancedb/ exclusion' });
976
+ else if (enforcement.preCommit === 'skipped-non-shell') {
977
+ summary.push({
978
+ file: '.git/hooks/pre-commit',
979
+ action: 'Skipped non-shell hook detected (manual integration needed)',
980
+ });
986
981
  }
987
- }
988
- // --- Auto-ingest cursor rules (ADR-048) ---
989
- const { scanCursorInstructions } = await import('@mmnto/totem');
990
- const cursorInstructions = scanCursorInstructions(cwd);
991
- if (cursorInstructions.length > 0) {
992
- const answer = await rl.question(`\nFound ${cursorInstructions.length} existing AI rule(s) (.cursorrules / .mdc). Compile into deterministic invariants? (Y/n): `);
993
- if (answer.trim().toLowerCase() !== 'n' && answer.trim().toLowerCase() !== 'no') {
994
- try {
995
- const { compileCommand } = await import('./compile.js');
996
- await compileCommand({ fromCursor: true });
997
- summary.push({
998
- file: '.totem/compiled-rules.json',
999
- action: `Compiled ${cursorInstructions.length} cursor rule(s) into invariants`,
1000
- });
982
+ if (enforcement.prePush === 'installed' || enforcement.prePush === 'appended') {
983
+ summary.push({
984
+ file: '.git/hooks/pre-push',
985
+ action: `${enforcement.prePush === 'installed' ? 'Installed' : 'Appended'} deterministic shield gate`,
986
+ });
987
+ }
988
+ else if (enforcement.prePush === 'skipped-non-shell') {
989
+ summary.push({
990
+ file: '.git/hooks/pre-push',
991
+ action: 'Skipped — non-shell hook detected (manual integration needed)',
992
+ });
993
+ }
994
+ // --- Always run: post-merge git hook ---
995
+ await installPostMergeHook(cwd, rl);
996
+ // --- Always run: .gitignore ---
997
+ const gitignorePath = path.join(cwd, '.gitignore');
998
+ if (fs.existsSync(gitignorePath)) {
999
+ const gitignore = fs.readFileSync(gitignorePath, 'utf-8');
1000
+ if (!gitignore.includes('.lancedb')) {
1001
+ fs.appendFileSync(gitignorePath, '\n# Totem\n.lancedb/\n');
1002
+ summary.push({ file: '.gitignore', action: 'Added .lancedb/ exclusion' });
1001
1003
  }
1002
- catch (err) {
1003
- const detail = err instanceof Error ? err.message : String(err);
1004
- log.warn('Totem', `Could not compile cursor rules: ${detail}`);
1004
+ }
1005
+ // --- Auto-ingest cursor rules (ADR-048) ---
1006
+ const { scanCursorInstructions } = await import('@mmnto/totem');
1007
+ const cursorInstructions = scanCursorInstructions(cwd);
1008
+ if (cursorInstructions.length > 0) {
1009
+ const answer = await rl.question(`\nFound ${cursorInstructions.length} existing AI rule(s) (.cursorrules / .mdc). Compile into deterministic invariants? (Y/n): `);
1010
+ if (answer.trim().toLowerCase() !== 'n' && answer.trim().toLowerCase() !== 'no') {
1011
+ try {
1012
+ const { compileCommand } = await import('./compile.js');
1013
+ await compileCommand({ fromCursor: true });
1014
+ summary.push({
1015
+ file: '.totem/compiled-rules.json',
1016
+ action: `Compiled ${cursorInstructions.length} cursor rule(s) into invariants`,
1017
+ });
1018
+ }
1019
+ catch (err) {
1020
+ const detail = err instanceof Error ? err.message : String(err);
1021
+ log.warn('Totem', `Could not compile cursor rules: ${detail}`);
1022
+ }
1005
1023
  }
1006
1024
  }
1007
- }
1025
+ } // end of bare mode else block
1008
1026
  // --- Print summary ---
1009
1027
  if (summary.length > 0) {
1010
1028
  console.error(`\n${brand('--- Totem Init Summary ---')}`);