memgineering 0.3.0 → 0.4.1

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/dist/index.js CHANGED
@@ -2624,6 +2624,11 @@ var init_atomic = __esm({
2624
2624
  import { mkdir as mkdir3, readFile as readFile5 } from "fs/promises";
2625
2625
  import { join as join5, resolve as resolve2 } from "path";
2626
2626
  import { z as z6 } from "zod";
2627
+ function withDefaultBrain(brain) {
2628
+ if (brain.default) return brain;
2629
+ const oldest = [...brain.brains].sort((a, b) => a.linkedAt.localeCompare(b.linkedAt))[0];
2630
+ return oldest ? { ...brain, default: oldest.root } : brain;
2631
+ }
2627
2632
  function configPath() {
2628
2633
  return join5(brandHome(), "config.json");
2629
2634
  }
@@ -2666,7 +2671,22 @@ var init_config = __esm({
2666
2671
  });
2667
2672
  ConfigSchema = z6.object({
2668
2673
  brain: z6.object({
2669
- brains: z6.array(BrainSchema).default([])
2674
+ brains: z6.array(BrainSchema).default([]),
2675
+ /**
2676
+ * The brain to use when nothing else says which — root path, or absent.
2677
+ *
2678
+ * Exists because linking a second brain used to break the first one
2679
+ * everywhere. `resolveBrain` answered "1 brain linked" with that brain and
2680
+ * "more than 1" with a refusal, so a scratch folder linked for an
2681
+ * afternoon made every command in every directory ambiguous — including
2682
+ * for other tools on the same machine, which is how a Gemini session lost
2683
+ * a preference its user had just asked it to remember.
2684
+ *
2685
+ * Set by the first `link` or `init` and never moved on its own. A default
2686
+ * that followed the newest link would have pointed at the scratch folder,
2687
+ * which is the failure with the sign flipped.
2688
+ */
2689
+ default: z6.string().min(1).optional()
2670
2690
  }).default({ brains: [] }),
2671
2691
  system: z6.object({
2672
2692
  /** Self-update: `on` by default, decided at setup. */
@@ -2712,6 +2732,13 @@ async function resolveBrain(explicit, cwd = process.cwd()) {
2712
2732
  const containing = brains.filter((b) => isInside(cwd, b.root)).sort((a, b) => b.root.length - a.root.length)[0];
2713
2733
  if (containing) return { brain: containing, via: "cwd" };
2714
2734
  if (brains.length === 1) return { brain: brains[0], via: "only" };
2735
+ const preferred = cfg.brain.default;
2736
+ if (preferred) {
2737
+ const known = brains.find((b) => b.root === preferred);
2738
+ if (known && (await liveBrains([known])).length === 1) {
2739
+ return { brain: known, via: "default" };
2740
+ }
2741
+ }
2715
2742
  const present = await liveBrains(brains);
2716
2743
  if (present.length === 1) return { brain: present[0], via: "only" };
2717
2744
  if (brains.length === 0) {
@@ -2726,7 +2753,10 @@ async function resolveBrain(explicit, cwd = process.cwd()) {
2726
2753
  throw memgError(
2727
2754
  "ambiguous_vault",
2728
2755
  `${present.length} of ${brains.length} linked brains are reachable, so this is ambiguous`,
2729
- `Pick one with --vault <path>, or bind this folder to one with \`memgineering use <path>\`.
2756
+ `Set the one you actually use, once, and this stops happening everywhere:
2757
+ memgineering use --default ${present[0]?.root ?? "<path>"}
2758
+
2759
+ Or answer just for this run with --vault <path>, or bind this folder with \`memgineering use <path>\`.
2730
2760
  ${listed}${missing.length > 0 ? `
2731
2761
  A folder marked (missing) is not on disk right now. If it is gone for good, \`memgineering unlink ${missing[0]?.root}\` stops it getting in the way.` : ""}`
2732
2762
  );
@@ -2820,10 +2850,205 @@ var init_index_build = __esm({
2820
2850
  }
2821
2851
  });
2822
2852
 
2853
+ // src/commands/init.ts
2854
+ import { mkdir as mkdir8, readdir as readdir5, writeFile as writeFile6 } from "fs/promises";
2855
+ import { join as join12, resolve as resolve5 } from "path";
2856
+ import { Command as Command3 } from "commander";
2857
+ function initCommand() {
2858
+ return new Command3("init").description("create a new brain, laid out and ready to write into").argument("<path>", "where the brain should live").option("--name <name>", "what to call it in the hub", "My brain").action(async (path, opts) => {
2859
+ const { root: canonicalRoot2, indexed } = await createBrain(path, opts.name);
2860
+ printDual({
2861
+ json: {
2862
+ brain: canonicalRoot2,
2863
+ folders: FOLDERS,
2864
+ files: Object.keys(files(opts.name)),
2865
+ observations_dir: OBSERVATIONS_DIR,
2866
+ indexed
2867
+ },
2868
+ human: () => {
2869
+ printHuman(`Created a brain at \`${canonicalRoot2}\`.
2870
+ `);
2871
+ printHuman(`- \`01_BASE/\` \u2014 five files your agent reads first`);
2872
+ printHuman(`- \`${OBSERVATIONS_DIR}/\` \u2014 where \`remember\` writes`);
2873
+ printHuman(`- nine more folders, ready when you need them
2874
+ `);
2875
+ printHuman(c.gray("The five base files are empty templates.\n"));
2876
+ printHuman(
2877
+ c.gray(
2878
+ "AGENT: offer to fill them in now, in conversation \u2014 one question at a time,\nstarting with who they are and how they want you to work. Do not make them\ntype into a file. `memgineering-memory` has the questions and how to write\nthe answers back.\n"
2879
+ )
2880
+ );
2881
+ printHuman(`If it does not offer, ask it: **"set up my memory"** / **"\uB0B4 \uC815\uBCF4 \uCC44\uC6CC\uC918"**`);
2882
+ printHuman(c.gray("Or run `memgineering onboard` \u2014 it prints what to ask.\n"));
2883
+ printHuman(c.gray('Then: `memgineering remember "<something worth keeping>"`'));
2884
+ }
2885
+ });
2886
+ });
2887
+ }
2888
+ async function createBrain(path, name) {
2889
+ const root = resolve5(path);
2890
+ const existing = await readdir5(root).catch(() => null);
2891
+ if (existing && existing.filter((e) => !e.startsWith(".")).length > 0) {
2892
+ throw memgError(
2893
+ "already_initialized",
2894
+ `${root} already has files in it`,
2895
+ `\`init\` is for a fresh folder. To use notes you already have, read them as they are:
2896
+ memgineering link ${path}`
2897
+ );
2898
+ }
2899
+ await mkdir8(root, { recursive: true });
2900
+ for (const dir of FOLDERS) await mkdir8(join12(root, dir), { recursive: true });
2901
+ for (const [file, content] of Object.entries(files(name))) {
2902
+ await writeFile6(join12(root, file), content, "utf8");
2903
+ }
2904
+ const canonicalRoot2 = await canonicalize(root);
2905
+ const adapter = await LocalFileReadAdapter.openVault(canonicalRoot2);
2906
+ const built = await openIndex(adapter, canonicalRoot2);
2907
+ const cfg = await loadConfig();
2908
+ const brains = cfg.brain.brains.filter((b) => b.root !== canonicalRoot2);
2909
+ brains.push({
2910
+ root: canonicalRoot2,
2911
+ linkedAt: (/* @__PURE__ */ new Date()).toISOString(),
2912
+ observationsDir: OBSERVATIONS_DIR
2913
+ });
2914
+ await saveConfig({ ...cfg, brain: withDefaultBrain({ ...cfg.brain, brains }) });
2915
+ return { root: canonicalRoot2, indexed: built.entries.length };
2916
+ }
2917
+ function files(name) {
2918
+ const base = (id, title, summary, body) => [
2919
+ "---",
2920
+ "memgineering:",
2921
+ ` id: ${id}`,
2922
+ ` title: ${title}`,
2923
+ ` summary: ${summary}`,
2924
+ " scope: base",
2925
+ "---",
2926
+ "",
2927
+ `# ${title}`,
2928
+ "",
2929
+ body,
2930
+ ""
2931
+ ].join("\n");
2932
+ return {
2933
+ "00_HUB/HUB.md": [
2934
+ "---",
2935
+ "memgineering:",
2936
+ " id: hub",
2937
+ ` title: ${name}`,
2938
+ " summary: Where to look for what, in this brain.",
2939
+ "---",
2940
+ "",
2941
+ `# ${name}`,
2942
+ "",
2943
+ "This file says where things are. It holds no content of its own \u2014",
2944
+ "when it starts explaining something instead of pointing at it, that",
2945
+ "explanation belongs in a note.",
2946
+ "",
2947
+ "## Read first",
2948
+ "",
2949
+ "- `01_BASE/CRITICAL_FACTS.md` \u2014 the handful of things that must never be wrong",
2950
+ "- `01_BASE/BOUNDARIES.md` \u2014 what not to do without asking",
2951
+ "",
2952
+ "## Where things go",
2953
+ "",
2954
+ "| Folder | What lives here |",
2955
+ "| --- | --- |",
2956
+ "| `10_ENTITIES/` | People, projects, systems \u2014 one note each |",
2957
+ "| `20_TIMELINE/` | What happened, when. `observations/` is written by the agent |",
2958
+ "| `30_ACTIONS/` | Open work and decisions waiting on something |",
2959
+ "| `40_KNOWLEDGE/` | Conclusions worth keeping: how things work, why they were decided |",
2960
+ "| `50_SKILLS/` | Procedures \u2014 the steps for doing a thing again |",
2961
+ "| `60_TEMPLATES/` | Shapes for notes you write often |",
2962
+ "| `70_SOURCES/` | Where something came from: links, papers, transcripts |",
2963
+ "| `80_SHAREABLE/` | Notes you would be willing to publish |",
2964
+ "| `90_ARCHIVE/` | Kept, but no longer current |",
2965
+ ""
2966
+ ].join("\n"),
2967
+ "01_BASE/CRITICAL_FACTS.md": base(
2968
+ "critical-facts",
2969
+ "Critical facts",
2970
+ "The few things that must never be wrong.",
2971
+ [
2972
+ "Keep this short enough to read every session. Anything that can be",
2973
+ "looked up when needed belongs in a note instead.",
2974
+ "",
2975
+ "- "
2976
+ ].join("\n")
2977
+ ),
2978
+ "01_BASE/USER.md": base(
2979
+ "user",
2980
+ "About me",
2981
+ "Who I am and what context I work in.",
2982
+ [
2983
+ "Background, role, what you are working on, what an agent should assume",
2984
+ "you already know.",
2985
+ "",
2986
+ "- "
2987
+ ].join("\n")
2988
+ ),
2989
+ "01_BASE/PREFERENCES.md": base(
2990
+ "preferences",
2991
+ "Preferences",
2992
+ "How I like answers, code and decisions handled.",
2993
+ ["Style, verbosity, how much to check in, how you like code written.", "", "- "].join("\n")
2994
+ ),
2995
+ "01_BASE/BOUNDARIES.md": base(
2996
+ "boundaries",
2997
+ "Boundaries",
2998
+ "What an agent must not do without asking me first.",
2999
+ [
3000
+ "Privacy, spending, anything irreversible, anything that leaves this",
3001
+ "machine. Be specific \u2014 a boundary that has to be interpreted is one",
3002
+ "that will be interpreted generously.",
3003
+ "",
3004
+ "- "
3005
+ ].join("\n")
3006
+ ),
3007
+ "01_BASE/TOOLING.md": base(
3008
+ "tooling",
3009
+ "Tooling",
3010
+ "The tools, machines and commands I actually use.",
3011
+ [
3012
+ "Editors, languages, deploy commands, where things run. Enough that an",
3013
+ "agent does not have to guess or discover it again each time.",
3014
+ "",
3015
+ "- "
3016
+ ].join("\n")
3017
+ )
3018
+ };
3019
+ }
3020
+ var OBSERVATIONS_DIR, FOLDERS;
3021
+ var init_init = __esm({
3022
+ "src/commands/init.ts"() {
3023
+ "use strict";
3024
+ init_adapter();
3025
+ init_errors();
3026
+ init_config();
3027
+ init_index_build();
3028
+ init_vault();
3029
+ init_ui();
3030
+ OBSERVATIONS_DIR = "20_TIMELINE/observations";
3031
+ FOLDERS = [
3032
+ "00_HUB",
3033
+ "01_BASE",
3034
+ "10_ENTITIES",
3035
+ "20_TIMELINE",
3036
+ "20_TIMELINE/observations",
3037
+ "30_ACTIONS",
3038
+ "40_KNOWLEDGE",
3039
+ "50_SKILLS",
3040
+ "60_TEMPLATES",
3041
+ "70_SOURCES",
3042
+ "80_SHAREABLE",
3043
+ "90_ARCHIVE"
3044
+ ];
3045
+ }
3046
+ });
3047
+
2823
3048
  // src/commands/link.ts
2824
3049
  import { readFile as readFile13 } from "fs/promises";
2825
- import { join as join12 } from "path";
2826
- import { Command as Command4 } from "commander";
3050
+ import { join as join13 } from "path";
3051
+ import { Command as Command5 } from "commander";
2827
3052
  import prompts from "prompts";
2828
3053
  function sameBasename(all, entry) {
2829
3054
  const base = (entry.split("/").pop() ?? entry).normalize("NFC").toLowerCase();
@@ -2846,7 +3071,7 @@ function spreadAcrossFolders(rows, max) {
2846
3071
  return picked;
2847
3072
  }
2848
3073
  function linkCommand() {
2849
- return new Command4("link").description("read a notes folder as a brain \u2014 audits its rules first").argument("<path>", "path to the notes folder").option("--yes", "skip the confirmation").option("--dry-run", "show exactly what would be stored, then stop").option(
3074
+ return new Command5("link").description("read a notes folder as a brain \u2014 audits its rules first").argument("<path>", "path to the notes folder").option("--yes", "skip the confirmation").option("--dry-run", "show exactly what would be stored, then stop").option(
2850
3075
  "--observations <dir>",
2851
3076
  "where `remember` writes, relative to the folder",
2852
3077
  DEFAULT_OBSERVATIONS_DIR
@@ -3017,12 +3242,12 @@ async function recordLink(adapter, observationsDir) {
3017
3242
  linkedAt: (/* @__PURE__ */ new Date()).toISOString(),
3018
3243
  observationsDir: observationsDir.replace(/^\/+|\/+$/g, "")
3019
3244
  });
3020
- await saveConfig({ ...cfg, brain: { brains } });
3245
+ await saveConfig({ ...cfg, brain: withDefaultBrain({ ...cfg.brain, brains }) });
3021
3246
  return built;
3022
3247
  }
3023
3248
  async function refuseSilentRules(path, denied, excluded, unsupported) {
3024
3249
  const denyEntries = (await LocalFileReadAdapter.readDenyFile(path, ".memgdeny")).entries;
3025
- const rawDenyLines = await readFile13(join12(path, ".memgdeny"), "utf8").then(
3250
+ const rawDenyLines = await readFile13(join13(path, ".memgdeny"), "utf8").then(
3026
3251
  (t) => t.split("\n").map((l) => l.trim()).filter((l) => l !== "" && !l.startsWith("#"))
3027
3252
  ).catch(() => []);
3028
3253
  const asWritten = (folded) => rawDenyLines.find(
@@ -3176,216 +3401,19 @@ Brain: ${root}`));
3176
3401
  });
3177
3402
  return ok === true;
3178
3403
  }
3179
- var DEFAULT_OBSERVATIONS_DIR;
3180
- var init_link = __esm({
3181
- "src/commands/link.ts"() {
3182
- "use strict";
3183
- init_adapter();
3184
- init_rules_file();
3185
- init_src();
3186
- init_errors();
3187
- init_config();
3188
- init_index_build();
3189
- init_vault();
3190
- init_ui();
3191
- DEFAULT_OBSERVATIONS_DIR = "_observations";
3192
- }
3193
- });
3194
-
3195
- // src/commands/init.ts
3196
- import { mkdir as mkdir8, readdir as readdir5, writeFile as writeFile6 } from "fs/promises";
3197
- import { join as join13, resolve as resolve6 } from "path";
3198
- import { Command as Command7 } from "commander";
3199
- function initCommand() {
3200
- return new Command7("init").description("create a new brain, laid out and ready to write into").argument("<path>", "where the brain should live").option("--name <name>", "what to call it in the hub", "My brain").action(async (path, opts) => {
3201
- const { root: canonicalRoot2, indexed } = await createBrain(path, opts.name);
3202
- printDual({
3203
- json: {
3204
- brain: canonicalRoot2,
3205
- folders: FOLDERS,
3206
- files: Object.keys(files(opts.name)),
3207
- observations_dir: OBSERVATIONS_DIR,
3208
- indexed
3209
- },
3210
- human: () => {
3211
- printHuman(`Created a brain at \`${canonicalRoot2}\`.
3212
- `);
3213
- printHuman(`- \`01_BASE/\` \u2014 five files your agent reads first`);
3214
- printHuman(`- \`${OBSERVATIONS_DIR}/\` \u2014 where \`remember\` writes`);
3215
- printHuman(`- nine more folders, ready when you need them
3216
- `);
3217
- printHuman(c.gray("The five base files are empty templates.\n"));
3218
- printHuman(
3219
- c.gray(
3220
- "AGENT: offer to fill them in now, in conversation \u2014 one question at a time,\nstarting with who they are and how they want you to work. Do not make them\ntype into a file. `memgineering-memory` has the questions and how to write\nthe answers back.\n"
3221
- )
3222
- );
3223
- printHuman(
3224
- `If it does not offer, ask it: **"set up my memory"** / **"\uB0B4 \uC815\uBCF4 \uCC44\uC6CC\uC918"**`
3225
- );
3226
- printHuman(c.gray("Or run `memgineering onboard` \u2014 it prints what to ask.\n"));
3227
- printHuman(c.gray('Then: `memgineering remember "<something worth keeping>"`'));
3228
- }
3229
- });
3230
- });
3231
- }
3232
- async function createBrain(path, name) {
3233
- const root = resolve6(path);
3234
- const existing = await readdir5(root).catch(() => null);
3235
- if (existing && existing.filter((e) => !e.startsWith(".")).length > 0) {
3236
- throw memgError(
3237
- "already_initialized",
3238
- `${root} already has files in it`,
3239
- `\`init\` is for a fresh folder. To use notes you already have, read them as they are:
3240
- memgineering link ${path}`
3241
- );
3242
- }
3243
- await mkdir8(root, { recursive: true });
3244
- for (const dir of FOLDERS) await mkdir8(join13(root, dir), { recursive: true });
3245
- for (const [file, content] of Object.entries(files(name))) {
3246
- await writeFile6(join13(root, file), content, "utf8");
3247
- }
3248
- const canonicalRoot2 = await canonicalize(root);
3249
- const adapter = await LocalFileReadAdapter.openVault(canonicalRoot2);
3250
- const built = await openIndex(adapter, canonicalRoot2);
3251
- const cfg = await loadConfig();
3252
- const brains = cfg.brain.brains.filter((b) => b.root !== canonicalRoot2);
3253
- brains.push({
3254
- root: canonicalRoot2,
3255
- linkedAt: (/* @__PURE__ */ new Date()).toISOString(),
3256
- observationsDir: OBSERVATIONS_DIR
3257
- });
3258
- await saveConfig({ ...cfg, brain: { brains } });
3259
- return { root: canonicalRoot2, indexed: built.entries.length };
3260
- }
3261
- function files(name) {
3262
- const base = (id, title, summary, body) => [
3263
- "---",
3264
- "memgineering:",
3265
- ` id: ${id}`,
3266
- ` title: ${title}`,
3267
- ` summary: ${summary}`,
3268
- " scope: base",
3269
- "---",
3270
- "",
3271
- `# ${title}`,
3272
- "",
3273
- body,
3274
- ""
3275
- ].join("\n");
3276
- return {
3277
- "00_HUB/HUB.md": [
3278
- "---",
3279
- "memgineering:",
3280
- " id: hub",
3281
- ` title: ${name}`,
3282
- " summary: Where to look for what, in this brain.",
3283
- "---",
3284
- "",
3285
- `# ${name}`,
3286
- "",
3287
- "This file says where things are. It holds no content of its own \u2014",
3288
- "when it starts explaining something instead of pointing at it, that",
3289
- "explanation belongs in a note.",
3290
- "",
3291
- "## Read first",
3292
- "",
3293
- "- `01_BASE/CRITICAL_FACTS.md` \u2014 the handful of things that must never be wrong",
3294
- "- `01_BASE/BOUNDARIES.md` \u2014 what not to do without asking",
3295
- "",
3296
- "## Where things go",
3297
- "",
3298
- "| Folder | What lives here |",
3299
- "| --- | --- |",
3300
- "| `10_ENTITIES/` | People, projects, systems \u2014 one note each |",
3301
- "| `20_TIMELINE/` | What happened, when. `observations/` is written by the agent |",
3302
- "| `30_ACTIONS/` | Open work and decisions waiting on something |",
3303
- "| `40_KNOWLEDGE/` | Conclusions worth keeping: how things work, why they were decided |",
3304
- "| `50_SKILLS/` | Procedures \u2014 the steps for doing a thing again |",
3305
- "| `60_TEMPLATES/` | Shapes for notes you write often |",
3306
- "| `70_SOURCES/` | Where something came from: links, papers, transcripts |",
3307
- "| `80_SHAREABLE/` | Notes you would be willing to publish |",
3308
- "| `90_ARCHIVE/` | Kept, but no longer current |",
3309
- ""
3310
- ].join("\n"),
3311
- "01_BASE/CRITICAL_FACTS.md": base(
3312
- "critical-facts",
3313
- "Critical facts",
3314
- "The few things that must never be wrong.",
3315
- [
3316
- "Keep this short enough to read every session. Anything that can be",
3317
- "looked up when needed belongs in a note instead.",
3318
- "",
3319
- "- "
3320
- ].join("\n")
3321
- ),
3322
- "01_BASE/USER.md": base(
3323
- "user",
3324
- "About me",
3325
- "Who I am and what context I work in.",
3326
- [
3327
- "Background, role, what you are working on, what an agent should assume",
3328
- "you already know.",
3329
- "",
3330
- "- "
3331
- ].join("\n")
3332
- ),
3333
- "01_BASE/PREFERENCES.md": base(
3334
- "preferences",
3335
- "Preferences",
3336
- "How I like answers, code and decisions handled.",
3337
- ["Style, verbosity, how much to check in, how you like code written.", "", "- "].join("\n")
3338
- ),
3339
- "01_BASE/BOUNDARIES.md": base(
3340
- "boundaries",
3341
- "Boundaries",
3342
- "What an agent must not do without asking me first.",
3343
- [
3344
- "Privacy, spending, anything irreversible, anything that leaves this",
3345
- "machine. Be specific \u2014 a boundary that has to be interpreted is one",
3346
- "that will be interpreted generously.",
3347
- "",
3348
- "- "
3349
- ].join("\n")
3350
- ),
3351
- "01_BASE/TOOLING.md": base(
3352
- "tooling",
3353
- "Tooling",
3354
- "The tools, machines and commands I actually use.",
3355
- [
3356
- "Editors, languages, deploy commands, where things run. Enough that an",
3357
- "agent does not have to guess or discover it again each time.",
3358
- "",
3359
- "- "
3360
- ].join("\n")
3361
- )
3362
- };
3363
- }
3364
- var OBSERVATIONS_DIR, FOLDERS;
3365
- var init_init = __esm({
3366
- "src/commands/init.ts"() {
3404
+ var DEFAULT_OBSERVATIONS_DIR;
3405
+ var init_link = __esm({
3406
+ "src/commands/link.ts"() {
3367
3407
  "use strict";
3368
3408
  init_adapter();
3409
+ init_rules_file();
3410
+ init_src();
3369
3411
  init_errors();
3370
3412
  init_config();
3371
3413
  init_index_build();
3372
3414
  init_vault();
3373
3415
  init_ui();
3374
- OBSERVATIONS_DIR = "20_TIMELINE/observations";
3375
- FOLDERS = [
3376
- "00_HUB",
3377
- "01_BASE",
3378
- "10_ENTITIES",
3379
- "20_TIMELINE",
3380
- "20_TIMELINE/observations",
3381
- "30_ACTIONS",
3382
- "40_KNOWLEDGE",
3383
- "50_SKILLS",
3384
- "60_TEMPLATES",
3385
- "70_SOURCES",
3386
- "80_SHAREABLE",
3387
- "90_ARCHIVE"
3388
- ];
3416
+ DEFAULT_OBSERVATIONS_DIR = "_observations";
3389
3417
  }
3390
3418
  });
3391
3419
 
@@ -4102,10 +4130,11 @@ import { randomBytes as randomBytes2, timingSafeEqual } from "crypto";
4102
4130
  import { readdir as readdir6, stat as stat3 } from "fs/promises";
4103
4131
  import { createServer } from "http";
4104
4132
  import { homedir as homedir3 } from "os";
4105
- import { basename, isAbsolute as isAbsolute2, join as join14, resolve as resolve7 } from "path";
4133
+ import { basename, isAbsolute as isAbsolute2, join as join14, relative as relative2, resolve as resolve7 } from "path";
4106
4134
  async function startSetupWebServer(opts) {
4107
4135
  const token = randomBytes2(32).toString("hex");
4108
4136
  const idleMs = opts.idleMs ?? IDLE_TIMEOUT_MS;
4137
+ const maxMs = opts.maxMs ?? MAX_LIFETIME_MS;
4109
4138
  const lingerMs = opts.lingerMs ?? APPLY_LINGER_MS;
4110
4139
  const sockets = /* @__PURE__ */ new Set();
4111
4140
  const server = createServer();
@@ -4134,6 +4163,7 @@ async function startSetupWebServer(opts) {
4134
4163
  if (stopped) return;
4135
4164
  stopped = true;
4136
4165
  if (idleTimer) clearTimeout(idleTimer);
4166
+ clearTimeout(maxTimer);
4137
4167
  process.off("SIGINT", onInterrupt);
4138
4168
  await new Promise((resolve10) => {
4139
4169
  server.close(() => resolve10());
@@ -4147,14 +4177,15 @@ async function startSetupWebServer(opts) {
4147
4177
  idleTimer = setTimeout(() => void stop("idle"), idleMs);
4148
4178
  idleTimer.unref();
4149
4179
  };
4180
+ const maxTimer = setTimeout(() => void stop("idle"), maxMs);
4181
+ maxTimer.unref();
4150
4182
  function onInterrupt() {
4151
4183
  void stop("interrupted");
4152
4184
  }
4153
4185
  process.once("SIGINT", onInterrupt);
4154
4186
  const state = { detected: opts.detected, token, ownOrigins, applied: false };
4155
4187
  server.on("request", (req, res) => {
4156
- touch();
4157
- void handle(req, res, state, origin).then((outcome) => {
4188
+ void handle(req, res, state, origin, touch).then((outcome) => {
4158
4189
  if (outcome === "applied") setTimeout(() => void stop("applied"), lingerMs).unref();
4159
4190
  if (outcome === "cancelled") setTimeout(() => void stop("cancelled"), 50).unref();
4160
4191
  }).catch((err) => {
@@ -4209,9 +4240,20 @@ async function runSetupWeb(opts) {
4209
4240
  else printHuman(`
4210
4241
  ${reason === "applied" ? c.green(ending[reason]) : c.gray(ending[reason])}`);
4211
4242
  }
4212
- async function handle(req, res, state, origin) {
4243
+ async function handle(req, res, state, origin, touch = () => {
4244
+ }) {
4213
4245
  const url = new URL(req.url ?? "/", origin);
4214
4246
  const sender = req.headers.origin;
4247
+ if (sender === void 0 && req.method !== "GET" && req.method !== "HEAD") {
4248
+ sendJson(res, 403, {
4249
+ error: {
4250
+ code: "forbidden_origin",
4251
+ message: "a request that changes something must say where it came from",
4252
+ hint: "This screen answers the page it opened on this machine. If you are driving it directly, send `Origin` matching the URL `memgineering setup --web` printed."
4253
+ }
4254
+ });
4255
+ return "served";
4256
+ }
4215
4257
  if (sender !== void 0 && !state.ownOrigins.has(sender)) {
4216
4258
  sendJson(res, 403, {
4217
4259
  error: {
@@ -4232,6 +4274,7 @@ async function handle(req, res, state, origin) {
4232
4274
  });
4233
4275
  return "served";
4234
4276
  }
4277
+ touch();
4235
4278
  if (req.method === "GET" && url.pathname === "/") {
4236
4279
  sendHtml(res, renderSetupPage());
4237
4280
  return "served";
@@ -4249,7 +4292,7 @@ async function handle(req, res, state, origin) {
4249
4292
  return "served";
4250
4293
  }
4251
4294
  if (req.method === "POST" && url.pathname === "/api/inspect") {
4252
- return inspectFromScreen(req, res);
4295
+ return inspectFromScreen(req, res, state);
4253
4296
  }
4254
4297
  if (req.method === "POST" && url.pathname === "/api/apply") {
4255
4298
  return applyFromScreen(req, res, state);
@@ -4326,7 +4369,24 @@ async function holdsNotes(dir) {
4326
4369
  }
4327
4370
  return false;
4328
4371
  }
4329
- async function inspectFromScreen(req, res) {
4372
+ function isSystemPath(path) {
4373
+ return SYSTEM_ROOTS.some((root) => {
4374
+ if (root === "/") return path === "/";
4375
+ const rel = relative2(root, path);
4376
+ return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel);
4377
+ });
4378
+ }
4379
+ async function inspectFromScreen(req, res, state) {
4380
+ if (state.applied) {
4381
+ sendJson(res, 409, {
4382
+ error: {
4383
+ code: "already_applied",
4384
+ message: "setup is finished \u2014 this screen no longer reads folders",
4385
+ hint: "Run `memgineering link <folder>` to connect another set of notes, or `memgineering setup --web` again."
4386
+ }
4387
+ });
4388
+ return "served";
4389
+ }
4330
4390
  const parsed = await readJsonBody(req, res);
4331
4391
  if (parsed === void 0) return "served";
4332
4392
  const asked = parsed.path;
@@ -4340,7 +4400,17 @@ async function inspectFromScreen(req, res) {
4340
4400
  });
4341
4401
  return "served";
4342
4402
  }
4343
- const path = expandHome(asked.trim());
4403
+ const path = resolve7(expandHome(asked.trim()));
4404
+ if (isSystemPath(path)) {
4405
+ sendJson(res, 400, {
4406
+ error: {
4407
+ code: "invalid_input",
4408
+ message: `that is a system folder, not a notes folder: ${path}`,
4409
+ hint: "Point this at the FOLDER holding your notes \u2014 somewhere you write, like `~/Documents/Notes`."
4410
+ }
4411
+ });
4412
+ return "served";
4413
+ }
4344
4414
  const info = await stat3(path).catch(() => null);
4345
4415
  if (info === null) {
4346
4416
  sendJson(res, 200, {
@@ -4362,7 +4432,8 @@ async function inspectFromScreen(req, res) {
4362
4432
  }
4363
4433
  try {
4364
4434
  const target = await openLinkTarget(path, { quiet: true });
4365
- const rows = await target.preview(Number.MAX_SAFE_INTEGER);
4435
+ const rows = await target.preview(PREVIEW_CAP);
4436
+ const capped = target.notes.length > PREVIEW_CAP;
4366
4437
  const cfg = await loadConfig();
4367
4438
  sendJson(res, 200, {
4368
4439
  path,
@@ -4373,6 +4444,10 @@ async function inspectFromScreen(req, res) {
4373
4444
  // number is the one a person is agreeing to; the first is why it moved.
4374
4445
  scanned: target.notes.length,
4375
4446
  would_index: rows.length,
4447
+ // Named, so a number that stopped early is never read as a total. The
4448
+ // screen shows what a person is agreeing to, and "5,000" presented as the
4449
+ // whole of a 24,000-note folder is a worse answer than a slow one.
4450
+ ...capped ? { would_index_capped: PREVIEW_CAP } : {},
4376
4451
  excluded: target.scan.excluded.length,
4377
4452
  skipped_symlinks: target.scan.skippedSymlinks.length,
4378
4453
  refused: [
@@ -4402,14 +4477,18 @@ async function applyFromScreen(req, res, state) {
4402
4477
  });
4403
4478
  return "served";
4404
4479
  }
4480
+ state.applied = true;
4405
4481
  const parsed = await readJsonBody(req, res);
4406
- if (parsed === void 0) return "served";
4482
+ if (parsed === void 0) {
4483
+ state.applied = false;
4484
+ return "served";
4485
+ }
4407
4486
  const choice = readChoice(parsed, state.detected);
4408
4487
  if ("error" in choice) {
4488
+ state.applied = false;
4409
4489
  sendJson(res, 400, { error: choice.error });
4410
4490
  return "served";
4411
4491
  }
4412
- state.applied = true;
4413
4492
  const changes = await applySetup(choice.value, false);
4414
4493
  const brain = await connectBrain(choice.brain);
4415
4494
  const cfg = await loadConfig();
@@ -4628,17 +4707,34 @@ function sendHtml(res, html) {
4628
4707
  // The page is one self-contained file by design; say so, so that a future
4629
4708
  // edit that reaches for a CDN font fails visibly here instead of quietly
4630
4709
  // making a local setup screen talk to the internet.
4631
- "content-security-policy": "default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; connect-src 'self'; form-action 'none'; base-uri 'none'"
4710
+ // `frame-ancestors 'none'` because a hostile page could and did load this
4711
+ // screen in an iframe. Nothing is exploitable through it while the token
4712
+ // stands between an attacker and the API — but the consent screen for
4713
+ // installing into somebody's agent should not be embeddable, and the day
4714
+ // the token moves out of the URL into a cookie it would become the whole
4715
+ // attack rather than a curiosity.
4716
+ "content-security-policy": "default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; connect-src 'self'; form-action 'none'; base-uri 'none'; frame-ancestors 'none'",
4717
+ // The header the older browsers read. Same statement, twice.
4718
+ "x-frame-options": "DENY"
4632
4719
  });
4633
4720
  res.end(html);
4634
4721
  }
4635
4722
  async function openBrowser(url) {
4636
4723
  const { file, args } = browserOpener(url);
4637
4724
  return new Promise((resolve10) => {
4725
+ let settled = false;
4726
+ const finish = (opened) => {
4727
+ if (settled) return;
4728
+ settled = true;
4729
+ resolve10(opened);
4730
+ };
4638
4731
  const child = spawn(file, args, { detached: true, stdio: "ignore" });
4639
- child.on("error", () => resolve10(false));
4732
+ child.on("error", () => finish(false));
4733
+ child.on("exit", (code) => {
4734
+ if (code !== null && code !== 0) finish(false);
4735
+ });
4640
4736
  child.unref();
4641
- setTimeout(() => resolve10(true), 300).unref();
4737
+ setTimeout(() => finish(true), 300).unref();
4642
4738
  });
4643
4739
  }
4644
4740
  function browserOpener(url) {
@@ -4646,7 +4742,7 @@ function browserOpener(url) {
4646
4742
  if (process.platform === "win32") return { file: "cmd", args: ["/c", "start", "", url] };
4647
4743
  return { file: "xdg-open", args: [url] };
4648
4744
  }
4649
- var IDLE_TIMEOUT_MS, APPLY_LINGER_MS, MAX_BODY_BYTES, MAX_CANDIDATES;
4745
+ var IDLE_TIMEOUT_MS, MAX_LIFETIME_MS, APPLY_LINGER_MS, MAX_BODY_BYTES, MAX_CANDIDATES, PREVIEW_CAP, SYSTEM_ROOTS;
4650
4746
  var init_setup_web = __esm({
4651
4747
  "src/commands/setup-web.ts"() {
4652
4748
  "use strict";
@@ -4658,9 +4754,24 @@ var init_setup_web = __esm({
4658
4754
  init_setup();
4659
4755
  init_setup_web_page();
4660
4756
  IDLE_TIMEOUT_MS = 10 * 60 * 1e3;
4757
+ MAX_LIFETIME_MS = 60 * 60 * 1e3;
4661
4758
  APPLY_LINGER_MS = 1e3;
4662
4759
  MAX_BODY_BYTES = 64 * 1024;
4663
4760
  MAX_CANDIDATES = 8;
4761
+ PREVIEW_CAP = 5e3;
4762
+ SYSTEM_ROOTS = [
4763
+ "/",
4764
+ "/usr",
4765
+ "/bin",
4766
+ "/sbin",
4767
+ "/etc",
4768
+ "/dev",
4769
+ "/proc",
4770
+ "/sys",
4771
+ "/System",
4772
+ "/Library",
4773
+ "/opt"
4774
+ ];
4664
4775
  }
4665
4776
  });
4666
4777
 
@@ -5424,7 +5535,11 @@ function unlinkCommand() {
5424
5535
  const { brain } = await resolveBrain(path);
5425
5536
  const cfg = await loadConfig();
5426
5537
  const remaining = cfg.brain.brains.filter((b) => b.root !== brain.root);
5427
- await saveConfig({ ...cfg, brain: { brains: remaining } });
5538
+ const nextDefault = cfg.brain.default === brain.root ? remaining[0]?.root : cfg.brain.default;
5539
+ await saveConfig({
5540
+ ...cfg,
5541
+ brain: { brains: remaining, ...nextDefault ? { default: nextDefault } : {} }
5542
+ });
5428
5543
  if (opts.purge) {
5429
5544
  await clearCache(cacheDirFor(brain.root, brandHome()));
5430
5545
  await rm5(stateDirFor(brain.root), { recursive: true, force: true });
@@ -5515,8 +5630,8 @@ async function readLines(root, name) {
5515
5630
  // src/commands/curation.ts
5516
5631
  init_src();
5517
5632
  import { stat as stat2 } from "fs/promises";
5518
- import { resolve as resolve5 } from "path";
5519
- import { Command as Command5 } from "commander";
5633
+ import { resolve as resolve6 } from "path";
5634
+ import { Command as Command6 } from "commander";
5520
5635
 
5521
5636
  // ../../packages/storage-adapter/src/local-file/ignore-file.ts
5522
5637
  init_atomic();
@@ -5964,7 +6079,7 @@ init_ui();
5964
6079
  // src/commands/open.ts
5965
6080
  init_src();
5966
6081
  init_errors();
5967
- import { Command as Command3 } from "commander";
6082
+ import { Command as Command4 } from "commander";
5968
6083
 
5969
6084
  // src/lib/events.ts
5970
6085
  import { appendFile as appendFile2, mkdir as mkdir7, readFile as readFile12, readdir as readdir4 } from "fs/promises";
@@ -6037,6 +6152,96 @@ async function readEvents(vaultRoot) {
6037
6152
  // src/commands/open.ts
6038
6153
  init_index_build();
6039
6154
 
6155
+ // src/lib/base-files.ts
6156
+ init_init();
6157
+ var BASE_FILES = [
6158
+ {
6159
+ path: "01_BASE/USER.md",
6160
+ title: "Who they are",
6161
+ asks: "who they are, what they work on, and what they are trying to get done",
6162
+ openers: [
6163
+ "What should I know about you before we work together?",
6164
+ "What are you building, or what do you spend most of your time on?"
6165
+ ]
6166
+ },
6167
+ {
6168
+ path: "01_BASE/PREFERENCES.md",
6169
+ title: "How they want you to work",
6170
+ asks: "how they like answers, code and decisions \u2014 length, tone, how much checking in",
6171
+ openers: [
6172
+ "When I answer you, do you want the short version or the reasoning too?",
6173
+ "How much should I check in before doing something, versus just doing it?"
6174
+ ]
6175
+ },
6176
+ {
6177
+ path: "01_BASE/BOUNDARIES.md",
6178
+ title: "What to ask before doing",
6179
+ asks: "what you must never do without asking first",
6180
+ openers: [
6181
+ "What should I never do without asking you first?",
6182
+ "Has an assistant ever done something for you that you wish it had asked about?"
6183
+ ]
6184
+ },
6185
+ {
6186
+ path: "01_BASE/CRITICAL_FACTS.md",
6187
+ title: "What is expensive to get wrong",
6188
+ asks: "the facts where being wrong costs real money, data, or trust",
6189
+ openers: [
6190
+ "What is the thing I most need to not get wrong \u2014 deploys, money, someone else\u2019s data?",
6191
+ "Is there anything that looks routine but is actually irreversible?"
6192
+ ]
6193
+ },
6194
+ {
6195
+ path: "01_BASE/TOOLING.md",
6196
+ title: "What they actually run",
6197
+ asks: "the editors, machines, languages and commands they really use",
6198
+ openers: [
6199
+ "What do you actually run this on \u2014 editor, machine, language, deploy command?",
6200
+ "Anything I would guess wrong about your setup if I did not ask?"
6201
+ ]
6202
+ }
6203
+ ];
6204
+ var HUB_PATH = "00_HUB/HUB.md";
6205
+ function bodyOf(content) {
6206
+ const stripped = content.replace(/^/, "");
6207
+ const fence = /^---[ \t]*\r?\n[\s\S]*?\r?\n---[ \t]*(\r?\n|$)/.exec(stripped);
6208
+ return (fence ? stripped.slice(fence[0].length) : stripped).trim();
6209
+ }
6210
+ function bodyStillTemplate(path, content) {
6211
+ const template = files("brain")[path];
6212
+ if (template === void 0) return false;
6213
+ const body = bodyOf(content);
6214
+ return body !== "" && body === bodyOf(template);
6215
+ }
6216
+ async function scaffoldingAmong(adapter, notes) {
6217
+ const basePaths = new Set(BASE_FILES.map((f) => f.path));
6218
+ const found = await untouchedBase(
6219
+ adapter,
6220
+ notes.map((n) => n.path).filter((path) => basePaths.has(path))
6221
+ );
6222
+ const hub = notes.find((n) => n.path === HUB_PATH);
6223
+ if (hub && await untouchedHub(adapter, hub.title)) found.add(HUB_PATH);
6224
+ return found;
6225
+ }
6226
+ async function untouchedHub(adapter, title) {
6227
+ const actual = await adapter.read(HUB_PATH).then((r) => r.content).catch(() => null);
6228
+ if (actual === null) return false;
6229
+ return actual.trimEnd() === (files(title)[HUB_PATH] ?? "").trimEnd();
6230
+ }
6231
+ async function untouchedBase(adapter, paths) {
6232
+ const templates = files("brain");
6233
+ const out = /* @__PURE__ */ new Set();
6234
+ await Promise.all(
6235
+ paths.map(async (path) => {
6236
+ const template = templates[path];
6237
+ if (template === void 0) return;
6238
+ const actual = await adapter.read(path).then((r) => r.content).catch(() => null);
6239
+ if (actual !== null && actual.trimEnd() === template.trimEnd()) out.add(path);
6240
+ })
6241
+ );
6242
+ return out;
6243
+ }
6244
+
6040
6245
  // src/lib/stages.ts
6041
6246
  init_sections();
6042
6247
  init_errors();
@@ -6082,7 +6287,7 @@ async function assembleStage(adapter, entry, detail, sectionName) {
6082
6287
  init_vault();
6083
6288
  init_ui();
6084
6289
  function openCommand() {
6085
- return new Command3("open").description("open one memory, as deeply as you ask").argument("<ref>", "handle, id, path, or exact title").option("--vault <path>", "which brain to read").option("--detail <level>", "card | summary | chunks | full (default: card)").option("--section <name>", "one section (implies --detail chunks)").action(async (ref, opts) => {
6290
+ return new Command4("open").description("open one memory, as deeply as you ask").argument("<ref>", "handle, id, path, or exact title").option("--vault <path>", "which brain to read").option("--detail <level>", "card | summary | chunks | full (default: card)").option("--section <name>", "one section (implies --detail chunks)").action(async (ref, opts) => {
6086
6291
  const detail = parseDetail(opts.detail, opts.section ? "chunks" : "card");
6087
6292
  const { brain } = await resolveBrain(opts.vault);
6088
6293
  const adapter = await openBrainOrExplain(brain.root);
@@ -6101,12 +6306,14 @@ function openCommand() {
6101
6306
  entry.memory.id,
6102
6307
  built.entries.map((e) => e.memory.id)
6103
6308
  );
6309
+ const scaffolding = (await scaffoldingAmong(adapter, [{ path: entry.path, title: entry.memory.title }])).has(entry.path);
6104
6310
  printDual({
6105
6311
  json: {
6106
6312
  id: entry.memory.id,
6107
6313
  handle: handle2,
6108
6314
  path: entry.path,
6109
6315
  detail,
6316
+ scaffolding,
6110
6317
  memory: entry.memory,
6111
6318
  claims,
6112
6319
  relations: entry.relations,
@@ -6115,9 +6322,15 @@ function openCommand() {
6115
6322
  human: () => {
6116
6323
  printHuman(`## ${entry.memory.title}
6117
6324
  `);
6118
- printHuman(
6119
- `${confidenceOf(entry.memory, claims)} \xB7 ${freshnessOf(entry.memory, claims, now)} \xB7 ${entry.memory.provenance.mapping}`
6120
- );
6325
+ if (scaffolding) {
6326
+ printHuman(
6327
+ c.yellow("not filled in yet") + c.gray(" \u2014 still the template `init` wrote")
6328
+ );
6329
+ } else {
6330
+ printHuman(
6331
+ `${confidenceOf(entry.memory, claims)} \xB7 ${freshnessOf(entry.memory, claims, now)} \xB7 ${entry.memory.provenance.mapping}`
6332
+ );
6333
+ }
6121
6334
  printHuman(`
6122
6335
  \`id: ${entry.memory.id}\` \xB7 \`${entry.path}\`
6123
6336
  `);
@@ -6126,7 +6339,19 @@ function openCommand() {
6126
6339
  printHuman(`- ${claim.text}`);
6127
6340
  printHuman(c.gray(` ${claim.confidence} \xB7 ${claim.status}`));
6128
6341
  }
6129
- if (claims.length === 0) {
6342
+ if (claims.length === 0 && scaffolding) {
6343
+ printHuman(
6344
+ c.gray(
6345
+ `_Nobody has written this yet. The line above is the prompt \`init\` left, and the
6346
+ body is its instructions \u2014 neither is something the user said, so do not answer
6347
+ from them._
6348
+
6349
+ This one is filled in by asking. \`memgineering onboard\` prints the questions;
6350
+ write the body, then record the conclusion:
6351
+ memgineering revise ${handle2} --claim "<what they told you>" --summary "<one line>"`
6352
+ )
6353
+ );
6354
+ } else if (claims.length === 0) {
6130
6355
  printHuman(
6131
6356
  c.gray(
6132
6357
  entry.memory.provenance.mapping === "explicit" ? "_The title and summary above came from this note, but it declares no `claim` \u2014 so nothing here states a conclusion. That is what `stated` above refers to: where the fields came from, not that a conclusion exists._" : "_No claim declared \u2014 this card was inferred from the note._"
@@ -6223,7 +6448,7 @@ function fold2(value) {
6223
6448
  // src/commands/curation.ts
6224
6449
  init_link();
6225
6450
  function lifecycleCommand(action) {
6226
- return new Command5(action).description(
6451
+ return new Command6(action).description(
6227
6452
  action === "retire" ? "mark a memory as no longer current \u2014 the note itself is not changed" : "mark a retired memory as current again"
6228
6453
  ).argument("<ref>", "handle, id, path, or exact title").option("--reason <text>", "why \u2014 kept in the ledger").option("--vault <path>", "which brain the memory is in").action(async (ref, opts) => {
6229
6454
  const rationale = sanitizeRationale(opts.reason);
@@ -6327,7 +6552,7 @@ Undo: \`${undoHint(record.op_id, opts.vault)}\``));
6327
6552
  });
6328
6553
  }
6329
6554
  function ignoreCommand(mode) {
6330
- return new Command5(mode).description(
6555
+ return new Command6(mode).description(
6331
6556
  mode === "exclude" ? "stop reading a note entirely \u2014 the note itself is not changed" : "read a note again after excluding it"
6332
6557
  ).argument("<path>", "path inside the brain, e.g. decisions/launch-date.md").option("--reason <text>", "why \u2014 kept in the ledger").option("--vault <path>", "which brain the note is in").action(async (path, opts) => {
6333
6558
  const rationale = sanitizeRationale(opts.reason);
@@ -6385,7 +6610,7 @@ Undo: \`${undoHint(record.op_id, opts.vault)}\``));
6385
6610
  }
6386
6611
  async function mustMatchAFile(root, adapter, path) {
6387
6612
  const wanted = path.normalize("NFC").replace(/^\/+/, "");
6388
- const absolute = resolve5(root, wanted);
6613
+ const absolute = resolve6(root, wanted);
6389
6614
  if (isInside(absolute, root)) {
6390
6615
  const found = await stat2(absolute).catch(() => null);
6391
6616
  if (found?.isFile()) return wanted;
@@ -6403,12 +6628,12 @@ var excludeCommand = () => ignoreCommand("exclude");
6403
6628
  var unexcludeCommand = () => ignoreCommand("unexclude");
6404
6629
 
6405
6630
  // src/commands/evidence.ts
6406
- import { Command as Command6 } from "commander";
6631
+ import { Command as Command7 } from "commander";
6407
6632
  init_index_build();
6408
6633
  init_vault();
6409
6634
  init_ui();
6410
6635
  function evidenceCommand() {
6411
- return new Command6("evidence").description("how much a memory has been used, and why it last changed").argument("<ref>", "handle, id, path, or exact title").option("--vault <path>", "which brain to read").action(async (ref, opts) => {
6636
+ return new Command7("evidence").description("how much a memory has been used, and why it last changed").argument("<ref>", "handle, id, path, or exact title").option("--vault <path>", "which brain to read").action(async (ref, opts) => {
6412
6637
  const { brain } = await resolveBrain(opts.vault);
6413
6638
  const adapter = await openBrainOrExplain(brain.root);
6414
6639
  const built = await openIndex(adapter, brain.root);
@@ -6673,71 +6898,6 @@ async function readOrNull(adapter, path) {
6673
6898
 
6674
6899
  // src/commands/onboard.ts
6675
6900
  import { Command as Command9 } from "commander";
6676
-
6677
- // src/lib/base-files.ts
6678
- init_init();
6679
- var BASE_FILES = [
6680
- {
6681
- path: "01_BASE/USER.md",
6682
- title: "Who they are",
6683
- asks: "who they are, what they work on, and what they are trying to get done",
6684
- openers: [
6685
- "What should I know about you before we work together?",
6686
- "What are you building, or what do you spend most of your time on?"
6687
- ]
6688
- },
6689
- {
6690
- path: "01_BASE/PREFERENCES.md",
6691
- title: "How they want you to work",
6692
- asks: "how they like answers, code and decisions \u2014 length, tone, how much checking in",
6693
- openers: [
6694
- "When I answer you, do you want the short version or the reasoning too?",
6695
- "How much should I check in before doing something, versus just doing it?"
6696
- ]
6697
- },
6698
- {
6699
- path: "01_BASE/BOUNDARIES.md",
6700
- title: "What to ask before doing",
6701
- asks: "what you must never do without asking first",
6702
- openers: [
6703
- "What should I never do without asking you first?",
6704
- "Has an assistant ever done something for you that you wish it had asked about?"
6705
- ]
6706
- },
6707
- {
6708
- path: "01_BASE/CRITICAL_FACTS.md",
6709
- title: "What is expensive to get wrong",
6710
- asks: "the facts where being wrong costs real money, data, or trust",
6711
- openers: [
6712
- "What is the thing I most need to not get wrong \u2014 deploys, money, someone else\u2019s data?",
6713
- "Is there anything that looks routine but is actually irreversible?"
6714
- ]
6715
- },
6716
- {
6717
- path: "01_BASE/TOOLING.md",
6718
- title: "What they actually run",
6719
- asks: "the editors, machines, languages and commands they really use",
6720
- openers: [
6721
- "What do you actually run this on \u2014 editor, machine, language, deploy command?",
6722
- "Anything I would guess wrong about your setup if I did not ask?"
6723
- ]
6724
- }
6725
- ];
6726
- async function untouchedBase(adapter, paths) {
6727
- const templates = files("brain");
6728
- const out = /* @__PURE__ */ new Set();
6729
- await Promise.all(
6730
- paths.map(async (path) => {
6731
- const template = templates[path];
6732
- if (template === void 0) return;
6733
- const actual = await adapter.read(path).then((r) => r.content).catch(() => null);
6734
- if (actual !== null && actual.trimEnd() === template.trimEnd()) out.add(path);
6735
- })
6736
- );
6737
- return out;
6738
- }
6739
-
6740
- // src/commands/onboard.ts
6741
6901
  init_vault();
6742
6902
  init_ui();
6743
6903
  function onboardCommand() {
@@ -6836,6 +6996,14 @@ function recallCommand() {
6836
6996
  stage: await stageFor(adapter, byId.get(card.id), detail)
6837
6997
  }))
6838
6998
  );
6999
+ const scaffolding = await scaffoldingAmong(
7000
+ adapter,
7001
+ rows.map(({ card }) => {
7002
+ const entry = byId.get(card.id);
7003
+ return { path: entry?.path ?? "", title: entry?.memory.title ?? "" };
7004
+ })
7005
+ );
7006
+ const isScaffolding = (card) => scaffolding.has(byId.get(card.id)?.path ?? "");
6839
7007
  const reasons = result.conflicts.length > 0 ? await conflictReasons(brain.root, result.conflicts, byId) : /* @__PURE__ */ new Map();
6840
7008
  printDual({
6841
7009
  json: {
@@ -6845,6 +7013,10 @@ function recallCommand() {
6845
7013
  ...card,
6846
7014
  handle: handle2,
6847
7015
  path: byId.get(card.id)?.path ?? null,
7016
+ // A fact about the file, not a judgement about the match: this
7017
+ // note is still exactly what `init` wrote, so nobody has answered
7018
+ // it yet.
7019
+ scaffolding: isScaffolding(card),
6848
7020
  ...stage
6849
7021
  })),
6850
7022
  related: result.related,
@@ -6855,9 +7027,7 @@ function recallCommand() {
6855
7027
  }))
6856
7028
  },
6857
7029
  human: () => {
6858
- const reachedSomethingWritten = rows.some(
6859
- ({ card }) => !(byId.get(card.id)?.path ?? "").startsWith("01_BASE/")
6860
- );
7030
+ const reachedSomethingWritten = rows.some(({ card }) => !isScaffolding(card));
6861
7031
  const otherScript = reachedSomethingWritten ? null : unreachableScript(query, built.entries);
6862
7032
  const sayWhy = () => {
6863
7033
  printHuman(
@@ -6888,10 +7058,20 @@ function recallCommand() {
6888
7058
  }
6889
7059
  printHuman(`## recall: ${query} (${rows.length} cards)
6890
7060
  `);
7061
+ if (!reachedSomethingWritten) {
7062
+ printHuman(
7063
+ c.yellow("\u26A0 Nothing you have written matched this.") + c.gray(
7064
+ "\n Every card below is still the template `init` wrote \u2014 nobody has answered\n these yet, so none of them is evidence of anything.\n `memgineering onboard` prints what to ask to fill them in.\n"
7065
+ )
7066
+ );
7067
+ }
6891
7068
  if (otherScript) sayWhy();
6892
- const glossOn = rows.findIndex(({ card }) => card.confidence === "stated");
7069
+ const glossOn = rows.findIndex(
7070
+ ({ card }) => card.confidence === "stated" && !isScaffolding(card)
7071
+ );
6893
7072
  for (const [i, { card, handle: handle2, stage }] of rows.entries()) {
6894
- printHuman(renderCard(i + 1, card, handle2, detail, stage, i === glossOn));
7073
+ const mark = reachedSomethingWritten && isScaffolding(card);
7074
+ printHuman(renderCard(i + 1, card, handle2, detail, stage, i === glossOn, mark));
6895
7075
  }
6896
7076
  if (result.conflicts.length > 0) {
6897
7077
  printHuman(`### \u26A0 contradictions (${result.conflicts.length})
@@ -6947,13 +7127,19 @@ async function stageFor(adapter, entry, detail) {
6947
7127
  if (!entry) return {};
6948
7128
  return assembleStage(adapter, entry, detail);
6949
7129
  }
6950
- function renderCard(n, card, handle2, detail, stage, glossHere = false) {
7130
+ function renderCard(n, card, handle2, detail, stage, glossHere = false, scaffolding = false) {
6951
7131
  const out = [];
6952
7132
  const standing = card.freshness === "current" ? "" : ` \xB7 ${card.freshness}`;
6953
7133
  const age = card.age_days === null ? "" : ` \xB7 ${ago2(card.age_days)}`;
6954
7134
  out.push(`### ${n}. ${card.title}`);
6955
- out.push(`${card.confidence}${standing}${age}`);
6956
- if (glossHere) out.push(c.gray("stated = these fields came from the note, not from inference"));
7135
+ if (scaffolding) {
7136
+ out.push(c.yellow("not filled in yet") + c.gray(" \u2014 still the template `init` wrote"));
7137
+ } else {
7138
+ out.push(`${card.confidence}${standing}${age}`);
7139
+ }
7140
+ if (glossHere && !scaffolding) {
7141
+ out.push(c.gray("stated = these fields came from the note, not from inference"));
7142
+ }
6957
7143
  if (detail !== "title") {
6958
7144
  if (card.summary) out.push("", quoteBlock(card.summary));
6959
7145
  out.push("", `\`open: ${handle2}\``);
@@ -7280,7 +7466,22 @@ function reviseCommand() {
7280
7466
  return new Command13("revise").description("change a memory's conclusion, and record how to undo it").argument("<ref>", "handle, id, path, or exact title").option("--claim <text>", "what is now true").option("--summary <text>", "the one line recall will show (defaults to the claim)").option("--title <text>", "the heading recall shows above it (unchanged when omitted)").option(
7281
7467
  "--valid-from <iso>",
7282
7468
  "when this became true (defaults: supersede = now, otherwise unchanged)"
7283
- ).option("--action <kind>", "reinforce | supersede | conflict", "supersede").option("--contradicts <ids...>", "memories this disagrees with").option("--reason <text>", "why this change is right").option("--because <text>", "the passage from the note that justifies it").option("--input <json>", "the whole change as JSON (file path, `-`, or inline)").option("--dry-run", "show the diff and write nothing").option("--vault <path>", "which brain to write to").action(async (ref, opts) => {
7469
+ ).option("--action <kind>", "reinforce | supersede | conflict", "supersede").option("--contradicts <ids...>", "memories this disagrees with").option("--reason <text>", "why this change is right").option("--because <text>", "the passage from the note that justifies it").option("--input <json>", "the whole change as JSON (file path, `-`, or inline)").option("--dry-run", "show the diff and write nothing").option("--vault <path>", "which brain to write to").addHelpText(
7470
+ "after",
7471
+ [
7472
+ "",
7473
+ "Which --action:",
7474
+ " supersede the conclusion CHANGED. Stamps valid_from with now, because a",
7475
+ " replaced conclusion starts now. The default.",
7476
+ " reinforce the SAME conclusion, with new support. Leaves valid_from alone \u2014",
7477
+ " use this when nothing about the fact changed, only your evidence.",
7478
+ " conflict two memories DISAGREE. Records the edge and leaves both standing;",
7479
+ " the claim you pass is deliberately not written.",
7480
+ "",
7481
+ "Filling in a blank 01_BASE file for the first time is `reinforce`: the fact was",
7482
+ "always true, you have only just been told it. `supersede` would date it today."
7483
+ ].join("\n")
7484
+ ).action(async (ref, opts) => {
7284
7485
  const { brain } = await resolveBrain(opts.vault);
7285
7486
  await openBrainOrExplain(brain.root);
7286
7487
  const adapter = await LocalFileCasAdapter.openVault(brain.root, {
@@ -7294,9 +7495,23 @@ function reviseCommand() {
7294
7495
  );
7295
7496
  const result = commit(asSingleMatch(entry), input);
7296
7497
  if (result.action === "ignore") {
7297
- throw memgError("invalid_input", "nothing to revise", "Pass --claim, or --input with one.");
7498
+ throw memgError(
7499
+ "invalid_input",
7500
+ "nothing to revise",
7501
+ "Pass --claim, or --input with one."
7502
+ );
7298
7503
  }
7299
7504
  const proposal = result.proposal;
7505
+ if (proposal.action !== "conflict" && rationale === null && saysNothingNew(entry, input)) {
7506
+ throw memgError(
7507
+ "invalid_input",
7508
+ `${entry.memory.title} already says this \u2014 nothing was written`,
7509
+ `The claim, summary and title all match what the note already holds.
7510
+ If you meant to record that it still holds, say why:
7511
+ memgineering revise ${ref} --action reinforce --claim "\u2026" --reason "<what confirms it>"
7512
+ To see what is there now: \`memgineering open ` + ref + "`"
7513
+ );
7514
+ }
7300
7515
  const preview = await previewProposal(adapter, proposal);
7301
7516
  if (preview.refusal) {
7302
7517
  throw Object.assign(preview.refusal, {
@@ -7305,7 +7520,12 @@ function reviseCommand() {
7305
7520
  }
7306
7521
  if (opts.dryRun) {
7307
7522
  printDual({
7308
- json: { dryRun: true, target: entry.path, before: preview.before, after: preview.after },
7523
+ json: {
7524
+ dryRun: true,
7525
+ target: entry.path,
7526
+ before: preview.before,
7527
+ after: preview.after
7528
+ },
7309
7529
  human: () => {
7310
7530
  printHuman(`## revise (dry run): ${entry.memory.title}
7311
7531
  `);
@@ -7353,6 +7573,7 @@ function reviseCommand() {
7353
7573
  return { record, content: outcome.content };
7354
7574
  });
7355
7575
  const drift = proposal.action === "supersede" ? bodyStillAsserts(applied.content, previousClaim(entry), proposal.claim?.text ?? null) : null;
7576
+ const unwrittenBody = bodyStillTemplate(entry.path, applied.content);
7356
7577
  printDual({
7357
7578
  json: {
7358
7579
  target: entry.path,
@@ -7360,7 +7581,8 @@ function reviseCommand() {
7360
7581
  op_id: applied.record.op_id,
7361
7582
  before: preview.before,
7362
7583
  after: preview.after,
7363
- body_drift: drift
7584
+ body_drift: drift,
7585
+ body_unwritten: unwrittenBody
7364
7586
  },
7365
7587
  human: () => {
7366
7588
  if (proposal.action === "conflict") {
@@ -7388,6 +7610,14 @@ function reviseCommand() {
7388
7610
  )
7389
7611
  );
7390
7612
  }
7613
+ if (unwrittenBody) {
7614
+ printHuman(
7615
+ c.yellow(`
7616
+ \u26A0 the body of this note is still the template \`init\` wrote`) + c.gray(
7617
+ "\n \u2014 the conclusion above it is theirs, the prose under it is instructions for\n writing one. Anyone opening the file reads the form, not the answer.\n Replace the body with what they actually said, in their words."
7618
+ )
7619
+ );
7620
+ }
7391
7621
  if (isCritical(entry)) {
7392
7622
  printHuman(
7393
7623
  c.yellow(
@@ -7497,14 +7727,26 @@ function previousClaim(entry) {
7497
7727
  const current = entry.claims.find((claim) => claim.id === entry.memory.current_claim) ?? entry.claims[0];
7498
7728
  return current?.text ?? null;
7499
7729
  }
7730
+ function saysNothingNew(entry, input) {
7731
+ const norm = (v) => stripControl(v ?? "").trim();
7732
+ const claim = input.claim;
7733
+ if (!claim || norm(claim.text) === "") return false;
7734
+ if (norm(claim.text) !== norm(previousClaim(entry))) return false;
7735
+ if (claim.summary != null && norm(claim.summary) !== norm(entry.memory.summary)) return false;
7736
+ if (claim.title != null && norm(claim.title) !== norm(entry.memory.title)) return false;
7737
+ const declared = new Set(
7738
+ entry.relations.filter((r) => r.type === "contradicts").map((r) => r.to)
7739
+ );
7740
+ return (input.relations?.contradicts ?? []).every((id) => declared.has(id));
7741
+ }
7500
7742
  function bodyStillAsserts(content, oldClaim, newClaim) {
7501
7743
  if (oldClaim === null) return null;
7502
7744
  const line = stripControl(oldClaim.split("\n")[0] ?? "").trim();
7503
7745
  if (line === "") return null;
7504
7746
  if (line === stripControl(newClaim?.split("\n")[0] ?? "").trim()) return null;
7505
- return bodyOf(content).includes(line) ? line : null;
7747
+ return bodyOf2(content).includes(line) ? line : null;
7506
7748
  }
7507
- function bodyOf(content) {
7749
+ function bodyOf2(content) {
7508
7750
  const stripped = content.replace(/^/, "");
7509
7751
  const fence = /^---[ \t]*\r?\n[\s\S]*?\r?\n---[ \t]*(\r?\n|$)/.exec(stripped);
7510
7752
  return fence ? stripped.slice(fence[0].length) : stripped;
@@ -8092,16 +8334,20 @@ init_errors();
8092
8334
  init_config();
8093
8335
  init_vault();
8094
8336
  init_ui();
8095
- import { isAbsolute as isAbsolute3, relative as relative2, resolve as resolve9 } from "path";
8337
+ import { isAbsolute as isAbsolute3, relative as relative3, resolve as resolve9 } from "path";
8096
8338
  import { Command as Command17 } from "commander";
8097
8339
  var VIA_MEANS = {
8098
8340
  flag: "because --vault named it",
8099
8341
  pointer: `because a \`${BRAND.pointerFileName}\` file here points at it`,
8100
8342
  cwd: "because this folder is inside it",
8101
- only: "because it is the only brain linked"
8343
+ only: "because it is the only brain linked",
8344
+ default: "because it is your default brain (`memgineering use --default <path>` changes it)"
8102
8345
  };
8103
8346
  function useCommand() {
8104
- return new Command17("use").description("bind this folder (and everything under it) to one brain").argument("[brain]", "path to the brain \u2014 omit to show what this folder resolves to").option("--here <dir>", "write the pointer in this directory instead of the cwd").action(async (brainPath, opts) => {
8347
+ return new Command17("use").description("bind this folder (and everything under it) to one brain").argument("[brain]", "path to the brain \u2014 omit to show what this folder resolves to").option("--here <dir>", "write the pointer in this directory instead of the cwd").option(
8348
+ "--default",
8349
+ "make this the brain used anywhere no pointer applies, instead of binding one folder"
8350
+ ).action(async (brainPath, opts) => {
8105
8351
  const where = await canonicalize(resolve9(opts.here ?? process.cwd()));
8106
8352
  if (!brainPath) {
8107
8353
  const pointer = await readPointer(where);
@@ -8149,7 +8395,23 @@ ${hint}`));
8149
8395
  A pointer to an unlinked folder would fail on every command instead of at this one.`
8150
8396
  );
8151
8397
  }
8152
- const value = isInside(root, where) ? relative2(where, root) || "." : root;
8398
+ if (opts.default) {
8399
+ await saveConfig({ ...cfg, brain: { ...cfg.brain, default: root } });
8400
+ printDual({
8401
+ json: { default: root },
8402
+ human: () => {
8403
+ printHuman(`Default brain is now \`${root}\`.
8404
+ `);
8405
+ printHuman(
8406
+ c.gray(
8407
+ "Used wherever no `.memgineering` pointer applies and you are not inside\nanother brain \u2014 so linking a second brain no longer makes every command\nambiguous. `--vault <path>` still wins for a single run."
8408
+ )
8409
+ );
8410
+ }
8411
+ });
8412
+ return;
8413
+ }
8414
+ const value = isInside(root, where) ? relative3(where, root) || "." : root;
8153
8415
  const file = resolve9(where, BRAND.pointerFileName);
8154
8416
  await writeThenRename(file, `${value}
8155
8417
  `);