heddle-cli 0.4.0

An AI-native version control system
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
// SPDX-License-Identifier: Apache-2.0
//! Initialize command.

use std::{
    io::{self, Write},
    path::{Path, PathBuf},
};

use anyhow::{Result, bail};
use ingest::ImportOptions;
use objects::object::{Principal, ThreadName, Tree};
use refs::Head;
use repo::{Repository, RepositoryCapability, ThreadId};
use serde::Serialize;
use sley::{FullName, GitObjectType, ObjectId, ReferenceTarget, Repository as SleyRepository};
use tracing::{debug, info};

use super::{
    RecoveryAdvice,
    action_line::print_next,
    checkpoint::create_git_checkpoint,
    git_overlay_health::{RepositoryVerificationState, build_repository_verification_state},
    snapshot::{
        SnapshotAgentOverrides, create_snapshot, is_placeholder_principal,
        placeholder_principal_warning,
    },
};
use crate::{
    bridge::{
        GitBridge, WriteThroughOutcome, git_core::git_config_identity_with_global_fallback,
        git_ingest::import_git_history,
    },
    cli::{Cli, InitArgs, is_tty, should_output_json, style, worktree_status_options},
    config::UserConfig,
};

/// Short pointer file Heddle writes (and captures) when `--quickstart`
/// runs in a directory with no capturable user files yet, so the first
/// `heddle log` has a user-visible state to show.
const QUICKSTART_PLACEHOLDER: &str = "\
# Quickstart

This repository was bootstrapped with `heddle init --quickstart`.

Heddle captured this file as your first state so `heddle log` has
something to show. Replace it with your own work and run
`heddle capture -m \"...\"` to record your next step.

Next:
  heddle log       # see the history Heddle is tracking
  heddle status    # check what changed
";

#[derive(Serialize)]
struct InitOutput {
    output_kind: &'static str,
    status: String,
    action: String,
    path: PathBuf,
    repository_mode: String,
    git_detected: bool,
    heddle_initialized: bool,
    installed_heddleignore: bool,
    principal_configured: bool,
    principal_status: String,
    principal_source: Option<String>,
    principal: Option<InitPrincipalOutput>,
    principal_recommended_action: Option<String>,
    #[serde(skip)]
    placeholder_principal_warning: Option<String>,
    side_effects: Vec<String>,
    message: String,
    next_action: Option<String>,
    recommended_action: Option<String>,
    /// Quickstart actions (thread/capture/checkpoint). Render-only;
    /// excluded from the JSON contract so the `init` output schema is
    /// unchanged whether or not `--quickstart` was passed.
    #[serde(skip)]
    quickstart: Option<QuickstartSummary>,
    #[allow(dead_code)]
    #[serde(skip_serializing)]
    #[serde(rename = "verification")]
    trust: RepositoryVerificationState,
}

#[derive(Serialize)]
struct InitPrincipalOutput {
    name: String,
    email: String,
}

/// What `--quickstart` did after the normal init steps.
struct QuickstartSummary {
    thread: String,
    change_id: String,
    git_commit: Option<String>,
    wrote_placeholder: bool,
}

/// Outcome of the pre-write quickstart phase (confirmation + identity).
/// Resolved before any filesystem write so a Ctrl-C at a prompt leaves
/// the directory exactly as it was found.
struct QuickstartPreflight {
    proceed: bool,
    persist_principal: Option<(String, String)>,
    attachment: QuickstartAttachmentPlan,
    /// Harnesses explicitly selected for connection before any write.
    /// Installed only after the repo is created so scope errors leave the
    /// directory untouched.
    harness_install: Vec<String>,
}

impl Default for QuickstartPreflight {
    fn default() -> Self {
        Self {
            proceed: true,
            persist_principal: None,
            attachment: QuickstartAttachmentPlan::SkipUnborn,
            harness_install: Vec::new(),
        }
    }
}

/// Pre-capture Git checkout attachment plan, computed read-only in preflight.
///
/// The write path must not re-discover these preconditions after bootstrap or
/// import has already written `.heddle/`: every edge that decides whether it is
/// safe and meaningful to call `write_through_thread_checkout` belongs here.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum QuickstartAttachmentPlan {
    /// Current Git HEAD has an exportable commit state and the requested branch
    /// is absent or already points at that commit.
    Attach,
    /// Current Git HEAD has no exportable state yet (fresh/unborn/orphan), so
    /// the first capture should establish the requested thread state.
    SkipUnborn,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum QuickstartAttachmentDecision {
    Attach,
    SkipUnborn,
    RefuseCollision,
}

/// The single directory a `--quickstart` operates on, resolved ONCE by
/// read-only discovery and shared by BOTH the preflight (a read-only viability
/// probe) and the write path.
///
/// THE QUICKSTART INVARIANT (do not let this class regress): the preflight is a
/// read-only viability probe on a SINGLE resolved root shared with the write
/// path; every write — `Repository::open`'s HEAD-sync, the repo config, the
/// capture/checkpoint, and the harness install — happens only AFTER the
/// confirmation gate, capture-before-install. So no new repo-state / cwd /
/// config / identity case can pass the preflight and then (a) diverge on WHICH
/// root it writes to, or (b) mutate before a refusal. Resolving the root here,
/// once, is what makes (a) impossible; never re-derive it from the raw cwd in
/// one half and by discovery in the other (the bug that created nested native
/// repos and misclassified Git overlays on subdirectory invocations).
enum QuickstartTarget {
    /// An existing Heddle repo found by ancestor discovery at `root` (the
    /// read-only half of `Repository::open`'s walk). The write path opens it.
    /// `git_overlay` mirrors `repository_capability_for_root(root)` — Git
    /// metadata AT the repo root, so a native repo nested inside an ancestor
    /// Git checkout stays native, exactly as the opened repo would.
    Existing { root: PathBuf, git_overlay: bool },
    /// No Heddle yet, but `root` is a Git checkout root (discovered from a
    /// possibly-deeper cwd): a fresh Git-overlay bootstrap targets the Git
    /// ROOT — the same root `Repository::open`'s final fallback bootstraps —
    /// never the cwd subdirectory.
    FreshGitOverlay { root: PathBuf },
    /// Neither Heddle nor Git anywhere up the tree: a fresh native init at the
    /// cwd.
    FreshNative { root: PathBuf },
}

impl QuickstartTarget {
    fn root(&self) -> &Path {
        match self {
            QuickstartTarget::Existing { root, .. }
            | QuickstartTarget::FreshGitOverlay { root }
            | QuickstartTarget::FreshNative { root } => root,
        }
    }

    /// Whether the resolved repo runs as a Git overlay (and thus imports
    /// history + writes a Git checkpoint through a branch). Mirrors
    /// `repository_capability_for_root`.
    fn is_git_overlay(&self) -> bool {
        match self {
            QuickstartTarget::Existing { git_overlay, .. } => *git_overlay,
            QuickstartTarget::FreshGitOverlay { .. } => true,
            QuickstartTarget::FreshNative { .. } => false,
        }
    }
}

/// Resolve, by READ-ONLY discovery, the single root a `--quickstart` operates
/// on. This is the discovery half of [`Repository::open`] WITHOUT its writes
/// (bootstrap, HEAD-sync) so the preflight can classify the target without
/// mutating anything — every write is deferred to the post-gate write path,
/// which consumes this SAME root. See [`QuickstartTarget`] for the invariant.
fn resolve_quickstart_target(path: &Path) -> Result<QuickstartTarget> {
    // Mirror `Repository::open`'s ancestor walk EXACTLY (same loop, same
    // predicates, minus the writes) so the read-only probe and the eventual
    // `open` never disagree on which root they target. Track the nearest
    // enclosing Git checkout going up — `has_git_metadata`'s mirror,
    // `dir_is_git_root` — so the nested-Git special case below can fire.
    let mut discovered_git_root: Option<PathBuf> = None;
    let mut current: Option<&Path> = Some(path);
    while let Some(dir) = current {
        if discovered_git_root.is_none() && dir_is_git_root(dir) {
            discovered_git_root = Some(dir.to_path_buf());
        }
        let heddle = dir.join(".heddle");
        if heddle.is_dir()
            && (heddle.join("objects").is_dir() || heddle.join("objectstore").is_file())
        {
            // `Repository::open`'s nested-Git special case: a Git checkout
            // BELOW this ancestor `.heddle` (and without its own `.heddle`)
            // bootstraps the NESTED Git root, not the ancestor — so quickstart
            // imports the nested Git history rather than writing the thread
            // into the parent and ignoring the nested repo (cid 3329409822).
            if let Some(git_root) = discovered_git_root.as_ref()
                && git_root != dir
                && git_root.starts_with(dir)
                && !git_root.join(".heddle").exists()
            {
                return Ok(QuickstartTarget::FreshGitOverlay {
                    root: git_root.clone(),
                });
            }
            return Ok(QuickstartTarget::Existing {
                root: dir.to_path_buf(),
                git_overlay: dir_is_git_root(dir),
            });
        }
        current = dir.parent();
    }

    // No Heddle anywhere above: inside a Git checkout, the fresh bootstrap
    // targets the Git ROOT (the nearest enclosing checkout discovered by the
    // walk above), not the cwd subdirectory — the same root `Repository::open`'s
    // final fallback bootstraps.
    match discovered_git_root {
        Some(root) => Ok(QuickstartTarget::FreshGitOverlay { root }),
        None => Ok(QuickstartTarget::FreshNative {
            root: path.to_path_buf(),
        }),
    }
}

/// Whether `dir` is itself a Git checkout root — Git metadata AT `dir`, mirroring
/// the repo crate's `has_git_metadata`/`repository_capability_for_root`. (A
/// a Git discovery probe would instead walk to an ANCESTOR Git checkout, which
/// is exactly the misclassification this avoids.)
fn dir_is_git_root(dir: &Path) -> bool {
    let dot_git = dir.join(".git");
    (dot_git.is_dir() || dot_git.is_file()) && SleyRepository::discover(dir).is_ok()
}

pub fn cmd_init(cli: &Cli, args: InitArgs) -> Result<()> {
    let path = match (args.path.clone(), cli.repo.clone()) {
        (Some(positional), Some(repo_path)) => {
            if absolute_path(&positional)? != absolute_path(&repo_path)? {
                bail!(RecoveryAdvice::init_path_conflict(
                    &positional.display().to_string(),
                    &repo_path.display().to_string(),
                ));
            }
            positional
        }
        (Some(positional), None) => positional,
        (None, Some(repo_path)) => repo_path,
        (None, None) => std::env::current_dir()
            .map_err(|e| anyhow::anyhow!("Failed to determine current directory: {}", e))?,
    };
    let path = path.canonicalize().unwrap_or(path.clone());

    info!(path = %path.display(), "Initializing repository");

    // If the directory already has a `.git` (or is inside one), leave the
    // `main` thread unseeded: the user almost certainly wants to import from
    // Git next, and pre-seeding would make `main` point at a throwaway
    // empty-tree snapshot. Otherwise, seed `main` so the repo is immediately
    // usable for snapshot/history/etc.
    let has_git = SleyRepository::discover(&path).is_ok();

    // Resolve the single quickstart root ONCE, by read-only discovery, so the
    // preflight (a read-only viability probe) and the write path below operate
    // on the SAME directory — see [`QuickstartTarget`] for the invariant.
    let target = if args.quickstart {
        Some(resolve_quickstart_target(&path)?)
    } else {
        None
    };

    // Quickstart confirms and resolves identity BEFORE any write so a
    // Ctrl-C (or declined prompt) leaves the directory untouched — no
    // half-written `.heddle/`. The preflight reads only: it never opens the
    // repo (whose HEAD-sync would write), so a refused/declined quickstart
    // performs zero writes.
    let preflight = match target.as_ref() {
        Some(target) => quickstart_preflight(cli, &args, target)?,
        None => QuickstartPreflight::default(),
    };
    if !preflight.proceed {
        return Ok(());
    }

    // Writes begin here — only after the preflight returned `proceed`, and on
    // the SAME root it validated. For quickstart, branch on the resolved
    // target so a subdirectory invocation opens the discovered repo / boots the
    // discovered Git root rather than creating a nested repo at the cwd.
    let repo = match target.as_ref() {
        Some(QuickstartTarget::Existing { root, .. }) => Repository::open(root)?,
        Some(QuickstartTarget::FreshGitOverlay { root }) => {
            Repository::bootstrap_git_overlay(root)?
        }
        Some(QuickstartTarget::FreshNative { root }) => Repository::init_default(root)?,
        None if has_git => Repository::bootstrap_git_overlay(&path)?,
        None => Repository::init_default(&path)?,
    };

    debug!(heddle_dir = %repo.heddle_dir().display(), "Repository initialized");

    let installed_heddleignore = false;

    let mut user_config = UserConfig::load_default()?;
    let mut principal_configured = false;
    // Quickstart writes the resolved identity to the *repo* config
    // (`.heddle/config.toml`) rather than the global user config: the
    // flag/prompt identity must win over an ambient Git `user.*`, and
    // `resolve_principal`'s precedence ranks repo config above Git
    // config but ranks user config below it. Re-open the repo afterwards
    // so the in-memory `repo.config()` the capture reads reflects it.
    let mut repo = repo;
    let repo_root = repo.root().to_path_buf();
    if args.quickstart {
        if let Some((name, email)) = &preflight.persist_principal {
            let config_path = repo.heddle_dir().join("config.toml");
            let mut repo_config = repo::RepoConfig::load(&config_path).unwrap_or_default();
            repo_config.set_principal(name.clone(), email.clone());
            repo_config.save(&config_path)?;
            info!(principal_name = %name, principal_email = %email, "Principal configured");
            debug!(config_path = %config_path.display(), "Repo config updated");
            repo = Repository::open(&repo_root)?;
            principal_configured = true;
        }
    } else if args.principal_name.is_some() || args.principal_email.is_some() {
        let name = args.principal_name.clone().ok_or_else(|| {
            anyhow::anyhow!(RecoveryAdvice::init_principal_field_required(
                "--principal-name"
            ))
        })?;
        let email = args.principal_email.clone().ok_or_else(|| {
            anyhow::anyhow!(RecoveryAdvice::init_principal_field_required(
                "--principal-email"
            ))
        })?;
        user_config.set_principal(name.clone(), email.clone());
        let config_path = user_config.save_default()?;
        info!(principal_name = %name, principal_email = %email, "Principal configured");
        debug!(config_path = %config_path.display(), "User config updated");
        principal_configured = true;
    }

    let quickstart = if args.quickstart {
        // Capture FIRST, then install harnesses. The initial capture must
        // record the user's own first state; installing harness scaffolding
        // (`.claude/settings.json`, …) before the capture would make
        // `ensure_capturable_content` treat that scaffolding as the user's
        // content — skipping the `QUICKSTART.md` placeholder and recording
        // integration files as the first state. The install decision was made
        // up front in the preflight; only the write runs here, post-capture.
        let summary = run_quickstart_actions(&repo, &args, preflight.attachment)?;
        super::perform_init_install(cli, &repo, &args, &preflight.harness_install)?;
        Some(summary)
    } else {
        super::maybe_prompt_init_install(cli, &repo, &args)?;
        None
    };

    // Output reflects the repo that was actually created/opened. For quickstart
    // that is the resolved target's capability (a subdirectory invocation may
    // have opened a native repo even though Git discovery finds an ancestor
    // Git checkout); the non-quickstart path keeps its prior `has_git` framing.
    let repo_is_git_overlay = if args.quickstart {
        repo.capability() == RepositoryCapability::GitOverlay
    } else {
        has_git
    };
    let message = if repo_is_git_overlay {
        format!(
            "Initialized Heddle data in {} for Git-overlay workflows",
            repo.heddle_dir().display()
        )
    } else {
        format!(
            "Initialized Heddle repository in {}",
            repo.heddle_dir().display()
        )
    };

    let trust = build_repository_verification_state(&repo);
    // After a quickstart the user has a captured state to inspect, so
    // point them at `heddle log` regardless of the trust-derived action.
    //
    // A non-quickstart init must never end without a next step
    // (heddle#644). When the repo has existing Git history the trust
    // state already recommends the exact adopt/import command; when it
    // doesn't (fresh native repo, or a Git checkout with no commits),
    // trust has nothing to flag, so point at the first save — `heddle
    // commit` records the first state (and, in Git-overlay repos, the
    // matching Git checkpoint).
    let next_action = if quickstart.is_some() {
        Some("heddle log".to_string())
    } else if !trust.recommended_action.is_empty() {
        Some(trust.recommended_action.clone())
    } else {
        Some("heddle commit -m \"...\"".to_string())
    };
    let principal_status = init_principal_status(&repo, &user_config)?;
    let placeholder_principal_warning = principal_status
        .principal
        .as_ref()
        .map(|principal| Principal::new(&principal.name, &principal.email))
        .filter(is_placeholder_principal)
        .map(|principal| placeholder_principal_warning(&principal));

    let output = InitOutput {
        output_kind: "init",
        status: "initialized".to_string(),
        action: "init".to_string(),
        path: repo.heddle_dir().to_path_buf(),
        repository_mode: repo.capability_label().to_string(),
        git_detected: repo_is_git_overlay,
        heddle_initialized: true,
        installed_heddleignore,
        principal_configured,
        principal_status: principal_status.status,
        principal_source: principal_status.source,
        principal: principal_status.principal,
        principal_recommended_action: principal_status.recommended_action,
        placeholder_principal_warning,
        side_effects: init_side_effects(repo_is_git_overlay, principal_configured),
        message,
        next_action: next_action.clone(),
        recommended_action: next_action,
        quickstart,
        trust,
    };

    render_init(&output, should_output_json(cli, Some(repo.config())))
}

fn absolute_path(path: &std::path::Path) -> Result<PathBuf> {
    if path.is_absolute() {
        Ok(path.to_path_buf())
    } else {
        Ok(std::env::current_dir()
            .map_err(|e| anyhow::anyhow!("Failed to determine current directory: {}", e))?
            .join(path))
    }
}

fn render_init(output: &InitOutput, json: bool) -> Result<()> {
    if json {
        println!("{}", serde_json::to_string(output)?);
    } else {
        println!("{}", output.message);
        match output.principal.as_ref() {
            Some(principal) => {
                let source = output
                    .principal_source
                    .as_deref()
                    .map(|source| format!(" from {source}"))
                    .unwrap_or_default();
                println!(
                    "Principal: {} <{}>{source}",
                    principal.name, principal.email
                );
            }
            None => {
                println!("Principal: not configured");
                if let Some(action) = output.principal_recommended_action.as_deref() {
                    println!("  set with: {action}");
                }
            }
        }
        if let Some(warning) = output.placeholder_principal_warning.as_deref() {
            eprintln!("{}", style::warn(warning));
        }
        if !output.side_effects.is_empty() {
            println!("Side effects:");
            for effect in &output.side_effects {
                println!("  - {effect}");
            }
        }
        if let Some(quickstart) = output.quickstart.as_ref() {
            if quickstart.wrote_placeholder {
                println!(
                    "Wrote {} and captured it as your first state.",
                    style::accent("QUICKSTART.md")
                );
            }
            println!("Thread: {}", style::bold(&quickstart.thread));
            println!("Captured: {}", style::change_id(&quickstart.change_id));
            if let Some(commit) = quickstart.git_commit.as_deref() {
                println!(
                    "Checkpoint: {}",
                    style::dim(&commit[..commit.len().min(12)])
                );
            }
        }
        if let Some(next) = output.recommended_action.as_deref() {
            print_next(next);
        }
    }
    Ok(())
}

struct InitPrincipalStatus {
    status: String,
    source: Option<String>,
    principal: Option<InitPrincipalOutput>,
    recommended_action: Option<String>,
}

fn init_principal_status(
    repo: &Repository,
    user_config: &UserConfig,
) -> Result<InitPrincipalStatus> {
    if let Some(principal) = Principal::from_env()
        && !principal_is_unconfigured(&principal)
    {
        return Ok(configured_principal_status("environment", principal));
    }

    if let Some(config) = &repo.config().principal {
        let principal = Principal::new(&config.name, &config.email);
        if !principal_is_unconfigured(&principal) {
            return Ok(configured_principal_status("repository", principal));
        }
    }

    if repo.capability() == RepositoryCapability::GitOverlay {
        let principal = repo.get_principal()?;
        if !principal_is_unconfigured(&principal) {
            return Ok(configured_principal_status("git_config", principal));
        }
    }

    if let Some(config) = &user_config.principal {
        let principal = Principal::new(&config.name, &config.email);
        if !principal_is_unconfigured(&principal) {
            return Ok(configured_principal_status("user_config", principal));
        }
    }

    Ok(InitPrincipalStatus {
        status: "not_configured".to_string(),
        source: None,
        principal: None,
        recommended_action: Some(set_principal_command().to_string()),
    })
}

fn configured_principal_status(source: &str, principal: Principal) -> InitPrincipalStatus {
    InitPrincipalStatus {
        status: "configured".to_string(),
        source: Some(source.to_string()),
        principal: Some(InitPrincipalOutput {
            name: principal.name,
            email: principal.email,
        }),
        recommended_action: None,
    }
}

fn principal_is_unconfigured(principal: &Principal) -> bool {
    principal.name.trim().is_empty()
        || principal.email.trim().is_empty()
        || (principal.name.trim() == "Unknown" && principal.email.trim() == "unknown@example.com")
}

fn set_principal_command() -> &'static str {
    "heddle init --principal-name <name> --principal-email <email>"
}

fn init_side_effects(has_git: bool, principal_configured: bool) -> Vec<String> {
    let mut side_effects = Vec::new();
    if has_git {
        side_effects.push("created Heddle sidecar for the existing Git repository".to_string());
        side_effects.push("updated .git/info/exclude for Heddle metadata".to_string());
        side_effects.push("left Git-tracked files untouched".to_string());
    } else {
        side_effects.push("created Heddle repository metadata".to_string());
    }
    if principal_configured {
        side_effects.push("updated default principal attribution".to_string());
    }
    side_effects
}

/// Pre-write phase of `--quickstart`: run the confirmation gate and
/// resolve the principal identity. Everything here is READ-ONLY — it never
/// opens the repository (whose HEAD-sync would write) nor touches the
/// filesystem — so a Ctrl-C, a declined prompt, or any refusal leaves the
/// directory exactly as it was found.
fn quickstart_preflight(
    cli: &Cli,
    args: &InitArgs,
    target: &QuickstartTarget,
) -> Result<QuickstartPreflight> {
    // The quickstart preflight is a READ-ONLY DRY-RUN of the real init/identity
    // path: every viability decision below shares the SAME predicate the write
    // path uses — capability from the resolved target (mirroring
    // `repository_capability_for_root`), identity via the read-only mirror of
    // `resolve_principal` (`resolve_quickstart_principal`, which follows the
    // objectstore pointer and the shared-checkout parent Git config exactly as
    // `Repository::get_principal` does), the thread name via the ref/branch
    // validators, and the harness scope via the install path's
    // `IntegrationScope::parse`. It must NOT call `Repository::open`: for a
    // Git-overlay repo `open` synchronizes `.heddle/HEAD` to Git's HEAD — a
    // write that would fire before a refusal. The single resolved `target` (see
    // [`QuickstartTarget`]) means the write path opens that SAME root, so the
    // read-only probe and the eventual open never disagree on which dir or
    // whether it is a Git overlay.
    let root = target.root();
    let is_git_overlay = target.is_git_overlay();

    // Honor the repo's on-disk `[output].format` so a `json`-configured repo
    // never gets a text confirmation prompt before a JSON envelope. Read it
    // off disk (following an objectstore pointer to the shared dir) without
    // opening — `None` for a fresh directory.
    let repo_config = resolve_existing_repo_config(root);
    let json = should_output_json(cli, repo_config.as_ref());

    // A detached Git HEAD has no branch for the checkpoint to advance, and
    // `create_git_checkpoint` refuses it only AFTER the import/capture have
    // written `.heddle/` state. Refuse here, before any write — but ONLY when
    // the repo will actually run as a Git overlay. A native repo nested inside
    // an ancestor Git checkout creates no checkpoint, so it must not be refused
    // for the ancestor's detached HEAD.
    if is_git_overlay && git_head_is_detached(root) {
        bail!(quickstart_detached_head_advice());
    }

    // A shallow Git checkout (`.git/shallow`) can't be imported until full
    // ancestry is available — `import_all` refuses it, but only AFTER
    // `bootstrap_git_overlay` has created `.heddle/` and edited the Git
    // excludes, leaving a half-initialized sidecar. Detect it here, read-only,
    // and refuse before any write — but only when the repo will run as a Git
    // overlay AND has history to import (the exact condition under which
    // `run_quickstart_actions` calls `import_all`). Mirrors `import_all`'s own
    // `git_dir()/shallow` probe (cid 3329409826).
    if is_git_overlay && git_has_commits(root) && git_is_shallow(root) {
        bail!(quickstart_shallow_clone_advice());
    }

    // Validate the requested thread name BEFORE any write, using the SAME
    // centralized rule every thread-creation boundary enforces ([`ThreadId::new`]
    // / `validate_thread_id`) — one rule, not an ad-hoc copy. A bad name
    // (`a..b`, `my feature`, a shell metacharacter, …) must fail here rather
    // than after init/bootstrap/import have already written `.heddle/` data,
    // leaving a half-initialized repo for a pure argument error.
    let thread = args.quickstart_thread.as_deref().unwrap_or("quickstart");
    if let Err(err) = ThreadId::new(thread) {
        bail!(RecoveryAdvice::invalid_usage(
            "quickstart_thread_name_invalid",
            err.to_string(),
            "Choose a thread name using only letters, digits, and _ - . / @ : + = \
             (no spaces or shell metacharacters).",
            "heddle init --quickstart --quickstart-thread <name>",
        ));
    }

    // A Git-overlay quickstart creates a real `refs/heads/<thread>`, so the
    // name must additionally satisfy Git's BRANCH-shorthand rules. These are
    // stricter than validating the assembled `refs/heads/<thread>` full ref:
    // a full ref accepts names Git refuses as a branch (e.g. `HEAD`, a leading
    // `-`). Validate the shorthand here so such a name fails before any write
    // rather than after `create_snapshot` has written Heddle state. Native
    // (non-Git) quickstarts keep Heddle's rules only.
    if is_git_overlay && !git_branch_name_is_valid(thread) {
        bail!(RecoveryAdvice::invalid_usage(
            "quickstart_thread_name_invalid",
            format!("'{thread}' is not a valid Git branch name"),
            "Choose a thread name Git accepts as a branch: no spaces, '~', '^', ':', '?', '*', '[', backslashes, control characters, a leading '-', or the reserved name 'HEAD'.",
            "heddle init --quickstart --quickstart-thread <name>",
        ));
    }

    // Decide the whole pre-capture Git checkout attachment path up front,
    // read-only. This is the single gate for the attachment class: attach only
    // when current HEAD has an exportable commit and the target branch is
    // absent or already at that commit; skip for unborn/no-state HEADs so the
    // first capture establishes the thread; refuse divergent target branches
    // before any `.heddle/` writes.
    let attachment = match quickstart_attachment_decision(root, is_git_overlay, thread) {
        QuickstartAttachmentDecision::Attach => QuickstartAttachmentPlan::Attach,
        QuickstartAttachmentDecision::SkipUnborn => QuickstartAttachmentPlan::SkipUnborn,
        QuickstartAttachmentDecision::RefuseCollision => {
            bail!(quickstart_thread_branch_collision_advice(thread));
        }
    };

    // Confirmation gate before touching a directory that already holds
    // work. Truly fresh directories skip straight through.
    let heddle_exists = root.join(".heddle").exists();
    let git_nonempty = is_git_overlay && git_has_commits(root);
    if (heddle_exists || git_nonempty) && !args.yes {
        if !json {
            println!(
                "{}",
                style::warn(
                    "heddle init --quickstart would act on a directory that already has work:"
                )
            );
            if heddle_exists {
                println!("  - existing .heddle/ data is present");
            }
            if git_nonempty {
                println!("  - this Git repository already has commits");
            }
            println!(
                "It would resolve your identity, start the '{thread}' thread, capture once, and (on Git-overlay) checkpoint once."
            );
            println!("Existing files are not modified.");
        }
        // No interactive terminal to confirm at: require an explicit
        // `--yes` rather than proceeding silently.
        if json || cli.quiet || !is_tty() {
            bail!(quickstart_needs_confirmation_advice());
        }
        print!("Proceed? [y/N] ");
        io::stdout().flush().ok();
        let mut input = String::new();
        io::stdin().read_line(&mut input)?;
        if !matches!(input.trim().to_ascii_lowercase().as_str(), "y" | "yes") {
            println!("Aborted; no changes made.");
            return Ok(QuickstartPreflight {
                proceed: false,
                ..QuickstartPreflight::default()
            });
        }
    }

    let persist_principal = resolve_quickstart_identity(cli, args, root, is_git_overlay, json)?;
    // Resolve explicit harness installs before any write, at the SAME
    // resolved root the install writes to (`repo.root()` in `cmd_init`),
    // not the raw cwd. The install itself runs post-write in `cmd_init`.
    let harness_install = super::prompt_init_install_decision(cli, root, args, json)?;
    Ok(QuickstartPreflight {
        proceed: true,
        persist_principal,
        attachment,
        harness_install,
    })
}

/// Resolve the principal for `--quickstart`. Priority: explicit
/// `--principal-*` flags → an already-resolvable identity (env, repo
/// config, Git config, or user config) → an interactive prompt. Returns
/// the `(name, email)` to persist when it came from flags or the prompt,
/// or `None` when an identity is already available without writing. Fails
/// fast (no placeholder) when nothing usable is resolvable and there is no
/// TTY to prompt.
///
/// Every path that yields an identity is checked against the SAME sentinel
/// predicate (`principal_is_unconfigured`) the real capture uses, over the
/// SAME precedence `resolve_principal` walks — see
/// [`resolve_quickstart_principal`]. This is the single source of truth:
/// flags, prompt, repo config, user config, and Git config (and a
/// higher-precedence sentinel shadowing a lower valid source) are all
/// caught HERE, before any write, instead of by `build_attribution` after
/// `.heddle/` already exists. It resolves identity READ-ONLY — without opening
/// the repository — so a refusal (sentinel env/flag, or no resolvable identity
/// with no TTY) never triggers `Repository::open`'s HEAD-sync write.
fn resolve_quickstart_identity(
    cli: &Cli,
    args: &InitArgs,
    root: &Path,
    is_git_overlay: bool,
    json: bool,
) -> Result<Option<(String, String)>> {
    // `resolve_principal` lets env win OUTRIGHT — it returns the env identity
    // even when it is the sentinel, before considering repo config (where
    // flags land), Git, or user config. So a sentinel env identity shadows a
    // valid `--principal-*` flag: the capture would still be attributed to the
    // env sentinel and rejected by `build_attribution`, but only AFTER init has
    // written `.heddle/config.toml` (and quickstart may have written
    // QUICKSTART.md). Reject the env sentinel here, before any write, instead
    // of persisting lower-precedence flags that env will shadow.
    if let Some(env_principal) = Principal::from_env()
        && principal_is_unconfigured(&env_principal)
    {
        bail!(quickstart_identity_required_advice());
    }

    // Explicit flags become the repo-level `[principal]` — the highest
    // precedence source after env in `resolve_principal`. Validate them
    // against the sentinel here so `--principal-name Unknown
    // --principal-email unknown@example.com` fails before any write rather
    // than being persisted and then rejected by `build_attribution`.
    let flag_principal = match (args.principal_name.clone(), args.principal_email.clone()) {
        (Some(name), Some(email)) => Some((name, email)),
        (Some(_), None) => {
            bail!(RecoveryAdvice::init_principal_field_required(
                "--principal-email"
            ))
        }
        (None, Some(_)) => {
            bail!(RecoveryAdvice::init_principal_field_required(
                "--principal-name"
            ))
        }
        (None, None) => None,
    };
    if let Some((name, email)) = flag_principal {
        if principal_is_unconfigured(&Principal::new(&name, &email)) {
            bail!(quickstart_identity_required_advice());
        }
        return Ok(Some((name, email)));
    }

    // No flags: ask what the capture would be attributed to, READ-ONLY.
    // `resolve_quickstart_principal` mirrors `resolve_principal`'s full
    // precedence off disk — including a shared-dir `[principal]` and a
    // shared-checkout parent's Git identity (the sources `resolve_principal`
    // reaches only through `Repository::get_principal`) — so it is faithful to
    // the capture without opening the repo (whose HEAD-sync would write before
    // this can still refuse).
    let resolved = resolve_quickstart_principal(root, is_git_overlay);
    if !principal_is_unconfigured(&resolved) {
        return Ok(None);
    }

    if is_tty() && !cli.quiet && !json {
        let name = prompt_line("Your name: ")?;
        let email = prompt_line("Your email: ")?;
        // Validate the collected identity with the same sentinel predicate
        // so a prompted `Unknown / unknown@example.com` is rejected before
        // any write, exactly like the flag path.
        if principal_is_unconfigured(&Principal::new(&name, &email)) {
            bail!(quickstart_identity_required_advice());
        }
        return Ok(Some((name, email)));
    }

    bail!(quickstart_identity_required_advice())
}

/// Resolve, READ-ONLY (without opening the repository), the ambient principal a
/// quickstart with no `--principal-*` flags would be attributed to. This is the
/// faithful mirror of `resolve_principal` (snapshot.rs) → `get_principal`
/// (repo crate): same order, same STOP-at-first-present semantics, same sources
/// — resolved off disk so it never triggers `Repository::open`'s HEAD-sync
/// (a write that must not happen before a refusal). Handles BOTH a fresh
/// directory and an already-initialized repo (including a materialized
/// checkout, whose `.heddle/` is just an objectstore pointer). Flag/prompt
/// identities are validated separately at their source in
/// `resolve_quickstart_identity`, since they occupy the repo-config slot by
/// being written there before the capture.
///
/// Precedence (identical to `resolve_principal`): env → repo
/// `.heddle/config.toml` `[principal]` (following the objectstore pointer) →
/// the repo's own Git config (Git-overlay only, when non-sentinel) → a
/// materialized checkout's shared-dir parent Git config (when non-sentinel) →
/// user config → the `Unknown` sentinel. The two Git sources stop only on a
/// non-sentinel identity, matching `resolve_principal`'s fall-through.
fn resolve_quickstart_principal(root: &Path, is_git_overlay: bool) -> Principal {
    // env wins outright — `resolve_principal` returns it unconditionally
    // (even when it is the sentinel), so mirror that: stop here.
    if let Some(principal) = Principal::from_env() {
        return principal;
    }
    // Repo-level config slot: stop at the on-disk repo `[principal]` if
    // present, even the sentinel — `resolve_principal` does. Resolve config the
    // way `Repository::open` does: in a materialized checkout the local
    // `.heddle/` is just an objectstore pointer and the real `[principal]`
    // lives in the SHARED dir it points at, so a local-only probe would
    // wrongly report "no identity" there.
    if let Some(repo_config) = resolve_existing_repo_config(root)
        && let Some(config) = &repo_config.principal
    {
        return Principal::new(&config.name, &config.email);
    }
    // Git config: `resolve_principal` falls through when Git's identity is the
    // sentinel, so only a non-sentinel Git identity stops here.
    if is_git_overlay && let Ok(Some(identity)) = git_config_identity_with_global_fallback(root) {
        let principal = Principal::new(&identity.name, &identity.email);
        if !principal_is_unconfigured(&principal) {
            return principal;
        }
    }
    // Materialized-checkout source: `get_principal` reaches a shared-dir
    // parent's Git identity via `shared_checkout_parent_git_principal`. Mirror
    // it read-only so the existing-repo case no longer needs `Repository::open`.
    if let Some(principal) = quickstart_shared_checkout_parent_principal(root)
        && !principal_is_unconfigured(&principal)
    {
        return principal;
    }
    if let Ok(user_config) = UserConfig::load_default()
        && let Some(config) = &user_config.principal
    {
        return Principal::new(&config.name, &config.email);
    }
    Principal::new("Unknown", "unknown@example.com")
}

/// Read-only mirror of `Repository::shared_checkout_parent_git_principal`: in a
/// materialized checkout the local `.heddle/` is an objectstore pointer to a
/// SHARED dir; when that shared dir sits inside a Git checkout, a capture can be
/// attributed through the shared dir's PARENT Git config. Follow the pointer
/// here (no open, no HEAD-sync) so a quickstart that will refuse writes nothing.
fn quickstart_shared_checkout_parent_principal(root: &Path) -> Option<Principal> {
    let pointer = root.join(".heddle").join("objectstore");
    if !pointer.is_file() {
        return None;
    }
    let content = std::fs::read_to_string(&pointer).ok()?;
    let shared = parse_objectstore_pointer(&content)?.canonicalize().ok()?;
    let parent = shared.parent()?;
    if parent == root {
        return None;
    }
    let identity = git_config_identity_with_global_fallback(parent).ok()??;
    Some(Principal::new(&identity.name, &identity.email))
}

fn prompt_line(label: &str) -> Result<String> {
    print!("{label}");
    io::stdout().flush().ok();
    let mut input = String::new();
    io::stdin().read_line(&mut input)?;
    Ok(input.trim().to_string())
}

/// Whether the discovered Git repository at `path` has any commits on ANY
/// local ref — not just the current HEAD. A repo with commits on another
/// ref (e.g. after `git switch --orphan scratch`, where the current HEAD is
/// unborn but `main` still carries history) must be treated as having
/// existing history so the quickstart confirms AND imports rather than
/// acting as if the repo were empty (which would leave partial/wrong state).
fn git_has_commits(path: &Path) -> bool {
    let Ok(repo) = SleyRepository::discover(path) else {
        return false;
    };
    if repo.head().ok().and_then(|head| head.oid).is_some() {
        return true;
    }
    let Ok(refs) = repo.references().list_refs() else {
        return false;
    };
    for reference in refs {
        let ReferenceTarget::Direct(oid) = reference.target else {
            continue;
        };
        if object_peels_to_commit(&repo, oid) {
            return true;
        }
    }
    false
}

fn object_peels_to_commit(repo: &SleyRepository, mut oid: ObjectId) -> bool {
    loop {
        let Ok(object) = repo.read_object(&oid) else {
            return false;
        };
        match object.object_type {
            GitObjectType::Commit => return true,
            GitObjectType::Tag => {
                let Ok(tag) = repo.read_tag(&oid) else {
                    return false;
                };
                oid = tag.object;
            }
            _ => return false,
        }
    }
}

/// Read-only decision for the Git-overlay quickstart's pre-capture checkout
/// attachment.
///
/// After `import_all`, `ensure_quickstart_thread` may point the requested
/// Heddle thread at the CURRENT state and `write_through_thread_checkout` would
/// then write that state to `refs/heads/<thread>`. That is valid only when the
/// current Git HEAD resolves to a real commit and an existing target branch is
/// already at that same commit. An unborn/orphan current HEAD has no exportable
/// state yet, so attachment must be deferred to the first capture. A divergent
/// target branch must refuse before any write, because attachment would move
/// the user's branch onto unrelated history (cid 3335757978, cid 3336080241).
fn quickstart_attachment_decision(
    path: &Path,
    is_git_overlay: bool,
    thread: &str,
) -> QuickstartAttachmentDecision {
    if !is_git_overlay || !path.join(".git").exists() || !git_has_commits(path) {
        return QuickstartAttachmentDecision::SkipUnborn;
    }

    let Ok(repo) = SleyRepository::discover(path) else {
        return QuickstartAttachmentDecision::SkipUnborn;
    };
    let Some(head) = repo.head().ok().and_then(|head| head.oid) else {
        return QuickstartAttachmentDecision::SkipUnborn;
    };
    let Ok(Some(reference)) = repo.find_reference(&format!("refs/heads/{thread}")) else {
        return QuickstartAttachmentDecision::Attach;
    };
    let Ok(Some(branch_tip)) = reference.peeled_oid(&repo) else {
        return QuickstartAttachmentDecision::Attach;
    };
    if head == branch_tip {
        QuickstartAttachmentDecision::Attach
    } else {
        QuickstartAttachmentDecision::RefuseCollision
    }
}

/// Whether `name` is valid as a Git BRANCH — the shorthand written under
/// `refs/heads/` — matching `git check-ref-format --branch`. This is stricter
/// than validating the assembled `refs/heads/<name>` full ref: a syntactically
/// valid full ref can still name an unusable branch. `git check-ref-format
/// refs/heads/HEAD` accepts the full ref, but `git check-ref-format --branch
/// HEAD` rejects it; the same holds for a leading `-` or a bare `@`. The Git
/// checkpoint write-through points `.git/HEAD` at `refs/heads/<name>`, so
/// reject here exactly what Git's porcelain would refuse there.
fn git_branch_name_is_valid(name: &str) -> bool {
    if FullName::try_from(format!("refs/heads/{name}").as_str()).is_err() {
        return false;
    }
    // Branch-shorthand rules `--branch` adds on top of full-ref syntax: not
    // the reserved `HEAD`, not a bare `@`, and no leading `-`.
    !(name == "HEAD" || name == "@" || name.starts_with('-'))
}

/// Whether the discovered Git repository at `path` is a shallow checkout — its
/// `git_dir` holds a `shallow` file. Mirrors the exact probe `import_all` uses
/// (`repo.git_dir().join("shallow").is_file()`) so the preflight refuses a
/// shallow clone before any write rather than after `bootstrap_git_overlay`
/// already created `.heddle/`.
fn git_is_shallow(path: &Path) -> bool {
    SleyRepository::discover(path)
        .ok()
        .map(|repo| repo.git_dir().join("shallow").is_file())
        .unwrap_or(false)
}

/// Whether the discovered Git repository at `path` has a detached HEAD
/// (HEAD points directly at a commit instead of an attached branch). An
/// unborn HEAD reads as not-detached.
fn git_head_is_detached(path: &Path) -> bool {
    SleyRepository::discover(path)
        .ok()
        .and_then(|repo| repo.head().ok().map(|head| head.is_detached()))
        .unwrap_or(false)
}

/// Resolve the on-disk repo config the way `Repository::open` does: when the
/// local `.heddle/` is a worktree pointer (`.heddle/objectstore`), the real
/// config lives in the shared dir it points at; otherwise it is the local
/// `.heddle/config.toml`. Returns `None` when there is no readable `.heddle`
/// config yet (a fresh directory).
fn resolve_existing_repo_config(path: &Path) -> Option<repo::RepoConfig> {
    let heddle_dir = path.join(".heddle");
    if !heddle_dir.is_dir() {
        return None;
    }
    let pointer = heddle_dir.join("objectstore");
    let config_path = if pointer.is_file() {
        let content = std::fs::read_to_string(&pointer).ok()?;
        let shared = parse_objectstore_pointer(&content)?;
        shared.canonicalize().ok()?.join("config.toml")
    } else {
        heddle_dir.join("config.toml")
    };
    repo::RepoConfig::load(&config_path).ok()
}

/// Minimal mirror of the repo crate's objectstore pointer parse: the file
/// holds a line of the form `objectstore: <absolute path>`.
fn parse_objectstore_pointer(content: &str) -> Option<PathBuf> {
    content.lines().find_map(|line| {
        line.strip_prefix("objectstore:")
            .map(str::trim)
            .filter(|path| !path.is_empty())
            .map(PathBuf::from)
    })
}

/// The write batch of `--quickstart`: start the thread, make one
/// capture, and (Git-overlay only) one checkpoint. Runs only after the
/// preflight has confirmed and resolved identity, so every fallible
/// prompt is already behind us.
fn run_quickstart_actions(
    repo: &Repository,
    args: &InitArgs,
    attachment: QuickstartAttachmentPlan,
) -> Result<QuickstartSummary> {
    // A Git-overlay repo that already has commits must have that history
    // imported into Heddle before a capture/checkpoint has a base to
    // build on — the same import `heddle adopt` performs. Fresh/empty
    // Git repos (no commits) and native repos skip this.
    if repo.capability() == RepositoryCapability::GitOverlay && git_has_commits(repo.root()) {
        let mut bridge = GitBridge::new(repo);
        import_git_history(
            &mut bridge,
            Some(repo.root()),
            &[],
            ImportOptions::default(),
            None,
        )?;
    }

    let thread = args
        .quickstart_thread
        .clone()
        .unwrap_or_else(|| "quickstart".to_string());
    ensure_quickstart_thread(repo, &thread)?;

    // On a Git overlay with imported history, `head_ref()` deliberately
    // resolves back to the live Git branch (e.g. `main`) — so merely writing
    // `.heddle/HEAD = Attached{thread}` above is NOT enough: the capture and
    // checkpoint below both target `head_ref()`, and would advance the Git
    // branch while the quickstart thread stays at the imported tip, even though
    // the output says `Thread: <thread>` (cid 3329409824). The preflight has
    // already decided whether all attachment preconditions hold; execute that
    // plan here without re-discovering partial after-the-fact guards.
    match attachment {
        QuickstartAttachmentPlan::Attach => {
            let mut bridge = GitBridge::new(repo);
            if let WriteThroughOutcome::Skipped(reason) =
                bridge.write_through_thread_checkout(&thread)?
            {
                bail!(RecoveryAdvice::safety_refusal(
                    "quickstart_thread_checkout_skipped",
                    format!("Could not attach the Git checkout to thread '{thread}': {reason}"),
                    "Resolve the Git checkout issue and re-run `heddle init --quickstart`.",
                    reason.to_string(),
                    "quickstart would capture and checkpoint on the requested thread, but the Git checkout could not be attached to its branch",
                    "the current Heddle state was preserved",
                    "heddle init --quickstart",
                    vec!["heddle init --quickstart".to_string()],
                ));
            }
        }
        QuickstartAttachmentPlan::SkipUnborn => {}
    }

    let user_config = UserConfig::load_default().unwrap_or_default();
    let wrote_placeholder = ensure_capturable_content(repo)?;
    let snapshot = create_snapshot(
        repo,
        &user_config,
        Some("quickstart: initial capture".to_string()),
        None,
        SnapshotAgentOverrides {
            provider: None,
            model: None,
            session: None,
            segment: None,
            policy: None,
            no_policy: false,
            no_agent: false,
        },
    )?;

    // Checkpoint is Git-overlay only; on native repos the capture above
    // is the user-visible "first commit".
    let git_commit = if repo.capability() == RepositoryCapability::GitOverlay {
        let record = create_git_checkpoint(
            repo,
            Some("quickstart: first commit"),
            worktree_status_options(Some(repo.config())),
        )?;
        Some(record.git_commit)
    } else {
        None
    };

    Ok(QuickstartSummary {
        thread,
        change_id: snapshot.change_id,
        git_commit,
        wrote_placeholder,
    })
}

/// Create (or repoint) the named quickstart thread and attach HEAD to it.
/// Idempotent: a re-run that is already on the thread is a no-op.
///
/// When a current state exists (a freshly-seeded native repo, a Git overlay
/// whose history we just imported, or simply another thread the user is
/// currently on) the quickstart thread is pointed AT that current state, so
/// the subsequent capture's parent is the current worktree's state. This
/// covers two cases that must behave identically:
///   - the thread does not exist yet → create it at the current state;
///   - the thread already exists but is NOT the one we're attached to →
///     repoint it to the current state. Otherwise `write_head` would attach
///     to the thread's STALE tip without checking out its tree, and the
///     capture would record the current worktree as a child of that stale
///     tip — the wrong parent (corrupting history when `--quickstart --yes`
///     is rerun after switching away from an existing quickstart thread).
///
/// When already attached to the thread, its tip already IS the current
/// state, so it is left untouched (the idempotent no-op rerun).
///
/// An unborn Git overlay has NO current state yet: we must NOT fabricate a
/// bootstrap snapshot here, or the quickstart would land an extra empty
/// parent commit before `QUICKSTART.md` is even written — breaking the
/// promised single initial capture/checkpoint. In that case we just attach
/// HEAD to the thread; the subsequent quickstart capture creates the
/// thread's first (root) state and advances the ref.
fn ensure_quickstart_thread(repo: &Repository, name: &str) -> Result<()> {
    let target = ThreadName::new(name);
    let already_attached =
        matches!(repo.head_ref()?, Head::Attached { thread } if thread == target);
    if !already_attached && let Some(state) = repo.current_state()? {
        repo.refs().set_thread(&target, &state.change_id)?;
    }
    if !already_attached {
        repo.refs().write_head(&Head::Attached { thread: target })?;
    }
    Ok(())
}

/// Ensure there is something user-visible to capture. When the worktree
/// has no capturable files (a fresh empty directory), write the
/// root-level `QUICKSTART.md` pointer and report that we did. The
/// root-level path matters: the default ignore list excludes `.heddle/`
/// (`repo_config::default_ignore`), so a placeholder under `.heddle/`
/// would be silently dropped by the capture walk. Non-destructive: an
/// existing `QUICKSTART.md` is left untouched.
fn ensure_capturable_content(repo: &Repository) -> Result<bool> {
    let options = worktree_status_options(Some(repo.config()));
    let (status, _) = repo.compare_worktree_cached_profiled_with_options(&Tree::new(), &options)?;
    if !status.added.is_empty() {
        return Ok(false);
    }
    let placeholder = repo.root().join("QUICKSTART.md");
    if !placeholder.exists() {
        std::fs::write(&placeholder, QUICKSTART_PLACEHOLDER)?;
    }
    Ok(true)
}

fn quickstart_needs_confirmation_advice() -> RecoveryAdvice {
    RecoveryAdvice::safety_refusal(
        "quickstart_needs_confirmation",
        "Refusing to run --quickstart non-interactively against a directory that already has Heddle data or Git history",
        "Re-run with `--yes` to confirm, or run `heddle init --quickstart` in an interactive terminal to answer the prompt.",
        "the target directory already has .heddle/ data or non-empty Git history and no interactive terminal is available to confirm",
        "quickstart would start a thread and capture in a directory that already holds work",
        "no repository objects, refs, metadata, or worktree files were changed",
        "heddle init --quickstart --yes",
        vec!["heddle init --quickstart --yes".to_string()],
    )
}

fn quickstart_thread_branch_collision_advice(thread: &str) -> RecoveryAdvice {
    RecoveryAdvice::safety_refusal(
        "quickstart_thread_branch_collision",
        format!(
            "Refusing to run --quickstart: a Git branch named '{thread}' already exists at a different commit than the current checkout"
        ),
        format!(
            "Pass `--quickstart-thread <name>` to use a different thread name, or switch to '{thread}' (`git switch {thread}`) and run the normal capture flow."
        ),
        format!(
            "a Git branch '{thread}' already exists and points at history unrelated to the current branch"
        ),
        format!(
            "quickstart would attach the '{thread}' thread to the current branch's state and move refs/heads/{thread} onto it, silently discarding the existing branch's history"
        ),
        "no repository objects, refs, metadata, or worktree files were changed",
        "heddle init --quickstart --quickstart-thread <name>",
        vec![
            "heddle init --quickstart --quickstart-thread <name>".to_string(),
            format!("git switch {thread}"),
        ],
    )
}

fn quickstart_detached_head_advice() -> RecoveryAdvice {
    RecoveryAdvice::safety_refusal(
        "quickstart_detached_head",
        "Refusing to run --quickstart on a detached Git HEAD",
        "Attach a branch first with `git switch -c <branch>` (or `git switch <branch>`), then re-run `heddle init --quickstart`.",
        "Git HEAD points directly at a commit instead of an attached branch",
        "quickstart would import history and write a Git checkpoint through a branch, but a detached HEAD has no branch to advance and could reattach or move the wrong ref",
        "no repository objects, refs, metadata, or worktree files were changed",
        "git switch -c <branch>",
        vec![
            "git switch -c <branch>".to_string(),
            "heddle init --quickstart".to_string(),
        ],
    )
}

fn quickstart_shallow_clone_advice() -> RecoveryAdvice {
    RecoveryAdvice::safety_refusal(
        "quickstart_shallow_clone",
        "Refusing to run --quickstart on a shallow Git clone",
        "Fetch full history first with `git fetch --unshallow`, then re-run `heddle init --quickstart`.",
        "the Git checkout is shallow (.git/shallow is present)",
        "quickstart would import Git history, but Heddle cannot import a shallow clone until its full ancestry is available",
        "no repository objects, refs, metadata, or worktree files were changed",
        "git fetch --unshallow",
        vec![
            "git fetch --unshallow".to_string(),
            "heddle init --quickstart".to_string(),
        ],
    )
}

fn quickstart_identity_required_advice() -> RecoveryAdvice {
    RecoveryAdvice::safety_refusal(
        "quickstart_identity_required",
        "Refusing to run --quickstart without an accountable identity",
        "Pass `--principal-name <name> --principal-email <email>`, configure identity first, or run in an interactive terminal to be prompted.",
        "no principal was resolvable from flags, environment, user config, or Git config, and no interactive terminal is available to prompt",
        "quickstart would capture history attributed to Unknown <unknown@example.com>",
        "no repository objects, refs, metadata, or worktree files were changed",
        "heddle init --quickstart --principal-name <name> --principal-email <email>",
        vec![
            "heddle init --quickstart --principal-name <name> --principal-email <email>"
                .to_string(),
        ],
    )
}