trusty-memory 0.27.2

MCP server (stdio + Unix socket) for trusty-memory
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
//! Drawer creator-attribution tag helpers.
//!
//! Why: prior to this module, drawers carried content, room, importance,
//! and free-form tags but no first-class metadata describing the writer.
//! Operators who saw noise drawers in a palace had no way to trace which
//! client wrote them — was it the trusty-memory MCP, a curl from a shell
//! script, claude-mpm's Python hook, the dashboard form? This module
//! defines a reserved `creator:*` tag namespace that every write path
//! (HTTP, MCP, CLI, hook) attaches automatically. With `creator:client=…`
//! present on every drawer, "where did this come from?" becomes
//! grep-able. The namespace approach (vs. a `Drawer` schema change)
//! piggy-backs on the existing `msg:` tag pattern from #99 so no
//! migration is required.
//!
//! What:
//!   - `CREATOR_*_PREFIX` constants — the four reserved tag prefixes.
//!   - [`CreatorInfo`] — small value type carrying client name, version,
//!     source, and optional cwd. `into_tags()` renders the four tag
//!     strings (or three, when cwd is absent) in a stable order.
//!   - [`is_creator_tag`] — predicate used by UI render code that wants
//!     to hide the namespace from the main tag chips (mirroring how
//!     `msg:*` is filtered today).
//!
//! Test: see the `tests` module at the bottom — covers tag composition,
//! prefix detection, and round-trip via `is_creator_tag`.
//!
//! # Spec References
//!
//! - [`SPEC-WSCLAIM-03~draft`](docs/specs/DOC-53-workstream-claim-drawer-convention.md#SPEC-WSCLAIM-03~draft) (§4 workstream-attributed memory)

use crate::ActivitySource;

/// Tag prefix carrying the writing client's short name
/// (e.g. `creator:client=trusty-memory-mcp`).
///
/// Why: the dominant question "who wrote this drawer?" reduces to a
/// single substring search against this prefix. Stable string so curl
/// and grep workflows keep working over time.
/// Test: `creator_info_renders_all_fields`.
pub const CREATOR_CLIENT_PREFIX: &str = "creator:client=";

/// Tag prefix carrying the writing client's version string
/// (e.g. `creator:version=0.5.1`).
///
/// Why: lets operators distinguish "old buggy client wrote this" from
/// "current client wrote this" without rummaging through logs.
/// Test: `creator_info_renders_all_fields`.
pub const CREATOR_VERSION_PREFIX: &str = "creator:version=";

/// Tag prefix carrying the originating subsystem (`http`/`mcp`/`hook`/`cli`).
///
/// Why: same labels as [`ActivitySource`] for HTTP / MCP / hook; CLI is
/// a fourth value we accept here because drawers written from the
/// `trusty-memory send-message` CLI never travel through the activity
/// log emit path but still need attribution.
/// What: lowercase string after the prefix.
/// Test: `creator_info_renders_all_fields`.
pub const CREATOR_SOURCE_PREFIX: &str = "creator:source=";

/// Tag prefix carrying the writing process' cwd at write time
/// (e.g. `creator:cwd=/Users/alice/projects/foo`).
///
/// Why: cwd is the single most useful clue when chasing noise — if a
/// drawer carries `creator:cwd=/Users/alice/projects/claude-mpm`, the
/// operator knows the write came from that working directory and can
/// look at *what* was running there. Absent when the writer could not
/// resolve a cwd (e.g. a remote HTTP client that did not send the
/// optional header).
/// Test: `creator_info_omits_cwd_when_absent`.
pub const CREATOR_CWD_PREFIX: &str = "creator:cwd=";

/// Tag prefix carrying the short session id of the writer (issue #202).
///
/// Why: when a session UUID is already attached as a bare tag, the TUI
/// activity panel cannot easily pick it out of the tag list. Emitting a
/// dedicated `creator:session=<first-8>` tag puts the session shorthand
/// in the same reserved namespace as the rest of the attribution data so
/// the dashboard / TUI can render it without bespoke parsing.
/// What: prefix string; the suffix is the first 8 hex characters of the
/// originating UUID.
/// Test: `session_tag_from_tags_returns_first_uuid_short`.
pub const CREATOR_SESSION_PREFIX: &str = "creator:session=";

/// Tag prefix carrying the originating tm workstream's name (DOC-53).
///
/// Why: mirrors the rest of the `creator:*` namespace, but for the *workstream*
/// (tm PM session) that wrote the drawer rather than the writing *client*
/// binary. Lets operators (and the claim-drawer convention, DOC-53 §3) answer
/// "which workstream wrote this?" the same grep-able way `creator:client=`
/// answers "which binary wrote this?".
/// What: rendered only when [`resolve_workstream_name`] returns `Some` — never
/// a placeholder value.
/// Test: `creator_info_renders_workstream_tags_when_resolvable`.
pub const CREATOR_WORKSTREAM_PREFIX: &str = "creator:workstream=";

/// Bare, non-namespaced tag carrying the same workstream name as
/// [`CREATOR_WORKSTREAM_PREFIX`] (DOC-53 §4.2).
///
/// Why: `creator:*` tags are hidden from the primary tag chips
/// ([`is_creator_tag`]) and from `memory_list`'s exact-tag filter unless the
/// caller already knows the reserved-prefix form. A first-class `ws:<name>`
/// tag lets `memory_list(tag: "ws:<name>")` find every drawer a workstream
/// touched — both auto-stamped writes (this module) and hand-written claim
/// drawers (DOC-53 §3.1), which use the identical `ws:<name>` tag by
/// convention.
/// What: always rendered together with `creator:workstream=`, never alone.
/// Test: `creator_info_renders_workstream_tags_when_resolvable`.
pub const WORKSTREAM_TAG_PREFIX: &str = "ws:";

/// Environment variable carrying an explicit, human-readable workstream name
/// (DOC-53 §4.3).
///
/// Why: `tm` does not currently export this — the only session-identity
/// environment variable a managed session inherits today is
/// `TM_MANAGED_SESSION_ID` (a UUID). This constant exists so resolution is
/// forward-compatible: the day `tm` starts exporting a human-readable
/// workstream name under this key, [`resolve_workstream_name`] picks it up
/// with no code change here. Until then, resolution falls back to the
/// cwd-derived heuristic (see that function).
/// Test: `resolve_workstream_name_prefers_env_var`.
pub const WORKSTREAM_NAME_ENV: &str = "TM_WORKSTREAM_NAME";

/// HTTP request header carrying the writing client's short name.
///
/// Why: lets remote HTTP callers self-identify so the recipient daemon
/// can populate `creator:client=` without guessing. The dashboard /
/// claude-mpm / future trusty-* clients all set this when they make
/// writes; clients that don't get the conservative fallback below.
/// Test: `rpc_drawer_create_attributes_the_caller_it_was_given` — the
/// header channel the three `drawer_creator_attribution_http_*` tests drove
/// went with the listener (#6286); `CallerParams` carries the same three
/// values in `params` and that test is what proves they reach the drawer.
pub const X_TRUSTY_CLIENT_NAME: &str = "x-trusty-client-name";

/// HTTP request header carrying the writing client's cwd.
///
/// Why: trusts the caller's self-reported cwd because the daemon has
/// no other way to know it (the HTTP request originates from a remote
/// process whose cwd is opaque). Absent header → `creator:cwd=` is
/// omitted from the drawer tags rather than synthesised from the
/// daemon's own cwd, which would be wrong.
/// Test: `rpc_drawer_create_attributes_the_caller_it_was_given`.
pub const X_TRUSTY_CLIENT_CWD: &str = "x-trusty-client-cwd";

/// HTTP request header carrying the writing client's explicit workstream
/// name (DOC-53 §4.3), the HTTP-transport counterpart of the MCP
/// `args["workstream"]` field the stdio bridge injects
/// (`commands::serve_stdio_bridge::inject_caller_context`).
///
/// Why: symmetric with [`X_TRUSTY_CLIENT_CWD`] — an HTTP caller that already
/// knows its own workstream name (rather than relying on the
/// `.worktrees/<name>` cwd-path heuristic [`resolve_workstream_name`]
/// applies) can self-report it directly. Absent header → `creator:workstream=`
/// falls back to the cwd heuristic, same precedence
/// [`CreatorInfo::new_for_caller`] applies to the MCP path.
/// Test: `rpc_drawer_create_attributes_the_caller_it_was_given`.
pub const X_TRUSTY_CLIENT_WORKSTREAM: &str = "x-trusty-client-workstream";

/// Default client name used when an HTTP caller omits the
/// `X-Trusty-Client-Name` header.
///
/// Why: every drawer must carry a `creator:client=` tag so the
/// dashboard renders a consistent "client" column; a missing header
/// must not yield a missing tag. The fallback is verbose on purpose so
/// operators can tell "the caller forgot to identify itself" apart from
/// "the caller is a known trusty-* binary".
/// Test: `rpc_drawer_create_attributes_the_caller_it_was_given`.
pub const HTTP_DEFAULT_CLIENT: &str = "unknown-http-client";

/// Client name attached to drawers written by the MCP tool surface.
pub const MCP_CLIENT_NAME: &str = "trusty-memory-mcp";

/// Client name attached to drawers written by the `trusty-memory` CLI.
pub const CLI_CLIENT_NAME: &str = "trusty-memory-cli";

/// Client name attached to drawers written by hook-driven code paths.
///
/// Why: hooks currently only read; the constant is reserved here so a
/// future hook that *does* write a drawer (e.g. an inbox auto-archive)
/// would tag itself consistently with the rest of the namespace.
/// Test: `creator_info_renders_all_fields`.
pub const HOOK_CLIENT_NAME: &str = "trusty-memory-hook";

/// Originating-subsystem labels emitted into `creator:source=`.
///
/// Why: matches [`ActivitySource`] for HTTP/MCP/hook plus a fourth `cli`
/// label that has no analogue on the activity-feed source enum (CLI
/// writes go through the HTTP API, but the *origin* of the request was a
/// CLI process; the user wants to see that distinction).
/// What: stable lower-case strings.
/// Test: `creator_info_renders_all_fields`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CreatorSource {
    Http,
    Mcp,
    Hook,
    Cli,
}

impl CreatorSource {
    /// Stable lower-case string used in the `creator:source=` tag.
    pub fn as_str(&self) -> &'static str {
        match self {
            Self::Http => "http",
            Self::Mcp => "mcp",
            Self::Hook => "hook",
            Self::Cli => "cli",
        }
    }
}

impl From<ActivitySource> for CreatorSource {
    fn from(s: ActivitySource) -> Self {
        match s {
            ActivitySource::Http => Self::Http,
            ActivitySource::Mcp => Self::Mcp,
            ActivitySource::Hook => Self::Hook,
        }
    }
}

/// Value type describing the writer of a drawer.
///
/// Why: each write path builds one of these and merges the rendered tags
/// into the caller-supplied tag list before persisting. Keeping the
/// rendering centralised guarantees every write produces tags in the
/// same order with the same prefixes, so curl + grep workflows stay
/// stable.
/// What: holds an owned client name, an owned version string, the source
/// enum, and an optional cwd. `into_tags()` consumes the value and
/// returns the rendered tag list.
/// Test: `creator_info_renders_all_fields`,
/// `creator_info_omits_cwd_when_absent`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CreatorInfo {
    pub client: String,
    pub version: String,
    pub source: CreatorSource,
    pub cwd: Option<String>,
    pub workstream: Option<String>,
}

impl CreatorInfo {
    /// Build a `CreatorInfo` with the supplied client + source, defaulting
    /// the version to this crate's `CARGO_PKG_VERSION` and the cwd/workstream
    /// to whatever *this process* has at construction time.
    ///
    /// # ⚠️ Daemon-vs-caller hazard — read before calling this from a shared
    /// # server dispatch path
    ///
    /// `new_self` resolves identity from the CALLING PROCESS' own
    /// environment (`std::env::current_dir()`, `TM_WORKSTREAM_NAME`). That is
    /// correct only when the process constructing the `CreatorInfo` genuinely
    /// **is** the writer — a standalone CLI invocation, or a hook that runs
    /// once per invocation in the caller's own process tree. It is **WRONG**
    /// for any handler that runs inside `trusty-memory`'s shared HTTP/MCP
    /// daemon (every `crate::tools::*` handler, reached via `POST /rpc` from
    /// the stdio bridge or any other remote caller): the daemon is ONE
    /// long-lived process serving MANY concurrently-attached sessions, so
    /// `new_self` there would resolve the **daemon's own** cwd/env — the same
    /// value for every caller — producing cross-session mis-attribution (the
    /// exact bug DOC-53's caller-supplied design (`new_for_caller`) exists to
    /// prevent). Use [`CreatorInfo::new_for_caller`] for any write reached
    /// through the shared daemon's dispatch surface; reserve `new_self` for
    /// code that truly executes in the writer's own process.
    /// What: `client.into()` + `env!("CARGO_PKG_VERSION").into()` +
    /// `std::env::current_dir().ok().map(...)` + [`resolve_own_workstream_name`].
    /// Test: `creator_info_self_populates_version_and_cwd`.
    pub fn new_self(client: impl Into<String>, source: CreatorSource) -> Self {
        let cwd = std::env::current_dir()
            .ok()
            .map(|p| p.to_string_lossy().into_owned());
        let workstream = resolve_own_workstream_name(cwd.as_deref());
        Self {
            client: client.into(),
            version: env!("CARGO_PKG_VERSION").to_string(),
            source,
            cwd,
            workstream,
        }
    }

    /// Build a `CreatorInfo` for a write reached through the shared daemon's
    /// dispatch surface (MCP `tools/call`/direct-method, or HTTP), using ONLY
    /// caller-supplied context — never the daemon process' own env/cwd.
    ///
    /// Why (critical fix, DOC-53 §4.3): the daemon serves every
    /// concurrently-attached session from ONE process. `new_self`'s
    /// `std::env::current_dir()`/`TM_WORKSTREAM_NAME` reads would resolve the
    /// *daemon's* identity, identically for every caller — this is the
    /// constructor that instead trusts only what the specific request
    /// carried, mirroring the existing `args["cwd"]` precedent in
    /// `tools::palace_ops::handle_palace_create` (caller value wins; no
    /// silent daemon-identity fallback for either field).
    /// What: `cwd` is `caller_cwd` verbatim (empty-string treated as absent,
    /// never re-derived from the daemon's own cwd). `workstream` prefers
    /// `caller_workstream` when it passes [`is_valid_workstream_name`] — an
    /// explicit-but-invalid value resolves to `None`, NOT a silent fallback
    /// to the cwd heuristic (same "explicit-but-bad is `None`" rule
    /// [`resolve_own_workstream_name`] already applies to the env var) —
    /// else falls back to [`resolve_workstream_name`] against `caller_cwd`.
    /// Neither field ever touches this process' own env or cwd.
    /// Test: `new_for_caller_prefers_explicit_workstream_over_cwd`,
    /// `new_for_caller_falls_back_to_cwd_when_workstream_absent`,
    /// `new_for_caller_omits_workstream_when_neither_resolvable`,
    /// `new_for_caller_invalid_explicit_workstream_returns_none_not_cwd_fallback`.
    pub fn new_for_caller(
        client: impl Into<String>,
        source: CreatorSource,
        caller_cwd: Option<&str>,
        caller_workstream: Option<&str>,
    ) -> Self {
        let cwd = caller_cwd.map(str::to_string).filter(|c| !c.is_empty());
        let workstream = match caller_workstream.filter(|w| !w.is_empty()) {
            Some(w) => is_valid_workstream_name(w).then(|| w.to_string()),
            None => resolve_workstream_name(cwd.as_deref()),
        };
        Self {
            client: client.into(),
            version: env!("CARGO_PKG_VERSION").to_string(),
            source,
            cwd,
            workstream,
        }
    }

    /// Render the rendered tag strings in stable order.
    ///
    /// Why: stable order keeps tests deterministic and gives operators a
    /// predictable layout when they grep through palaces with `jq`.
    /// What: `[client, version, source, cwd?, creator:workstream?, ws?]`.
    /// `cwd` and the workstream pair are each omitted when absent rather
    /// than rendered with an empty/placeholder value, so downstream
    /// consumers can distinguish "writer didn't share this" from "writer's
    /// value was literally empty" (DOC-53 §4.1 — no placeholder ever).
    /// Test: `creator_info_renders_all_fields`,
    /// `creator_info_omits_cwd_when_absent`,
    /// `creator_info_renders_workstream_tags_when_resolvable`,
    /// `creator_info_omits_workstream_tags_when_absent_or_invalid`.
    pub fn into_tags(self) -> Vec<String> {
        let mut out = Vec::with_capacity(6);
        out.push(format!("{CREATOR_CLIENT_PREFIX}{}", self.client));
        out.push(format!("{CREATOR_VERSION_PREFIX}{}", self.version));
        out.push(format!("{CREATOR_SOURCE_PREFIX}{}", self.source.as_str()));
        if let Some(cwd) = self.cwd.filter(|c| !c.is_empty()) {
            out.push(format!("{CREATOR_CWD_PREFIX}{cwd}"));
        }
        if let Some(ws) = self.workstream.filter(|w| is_valid_workstream_name(w)) {
            out.push(format!("{CREATOR_WORKSTREAM_PREFIX}{ws}"));
            out.push(format!("{WORKSTREAM_TAG_PREFIX}{ws}"));
        }
        out
    }

    /// Render the tags and append them to an existing tag list.
    ///
    /// Why: write-path call sites already hold a `Vec<String>` of
    /// user-supplied tags; merging in place avoids an allocation and
    /// preserves the caller's ordering.
    /// What: pushes each rendered tag onto `dst`. Does not deduplicate —
    /// caller is expected to pass a freshly-built or de-duplicated list.
    /// Test: `merge_into_appends_creator_tags`.
    pub fn merge_into(self, dst: &mut Vec<String>) {
        for tag in self.into_tags() {
            dst.push(tag);
        }
    }

    /// Render the tags and append them to an existing tag list, skipping any
    /// tag already present verbatim in `dst`.
    ///
    /// Why (MEDIUM 1, DOC-53 §3.1): a hand-written claim drawer already
    /// carries `ws:<name>` in its caller-supplied tags by convention (the
    /// claim-drawer shape); `merge_into` would then append a *second*,
    /// identical `ws:<name>` (and, since the caller-supplied workstream and
    /// the auto-stamped one are the same value, an identical
    /// `creator:workstream=<name>` too if the caller happened to write that
    /// literal tag). Use this instead of `merge_into` for any write path
    /// where the caller's own tags may already overlap the auto-stamped
    /// namespace — currently [`crate::tools::helpers::attach_mcp_attribution`].
    /// What: exact-string dedup only (not case-insensitive, not prefix-aware)
    /// — a tag is skipped iff it already appears verbatim in `dst`.
    /// Test: `merge_into_deduped_skips_tags_already_present`,
    /// `merge_into_deduped_appends_when_no_overlap`.
    pub fn merge_into_deduped(self, dst: &mut Vec<String>) {
        for tag in self.into_tags() {
            if !dst.contains(&tag) {
                dst.push(tag);
            }
        }
    }
}

/// Return `true` when a tag belongs to the `creator:*` reserved namespace.
///
/// Why: render paths (TUI, dashboard) want to hide attribution tags from
/// the main tag chips so they don't clutter the UI alongside meaningful
/// user-supplied tags (same pattern as `msg:*` hiding from #99). A single
/// predicate keeps every renderer in lock-step.
/// What: returns `tag.starts_with("creator:")`.
/// Test: `is_creator_tag_detects_namespace`.
pub fn is_creator_tag(tag: &str) -> bool {
    tag.starts_with("creator:")
}

/// Resolve a workstream name from a cwd, if any (DOC-53 §4.3).
///
/// Why: the sole heuristic available for a cwd whose owner is not
/// necessarily this process — a `.worktrees/<name>` path segment is already
/// implicit in the cwd used for `creator:cwd=` (self-reported by an MCP/CLI
/// call, or client-reported over HTTP via `X-Trusty-Client-Cwd`), so no new
/// input is required to try it. Deliberately does **not** consult
/// [`WORKSTREAM_NAME_ENV`] — that variable describes *this process'*
/// environment, which is meaningless for a remote HTTP caller's cwd; only
/// [`resolve_own_workstream_name`] (this process' own identity) reads it.
/// What: the path segment immediately following a `.worktrees` component in
/// `cwd`, gated by [`is_valid_workstream_name`] — an invalid candidate
/// (empty, UUID-shaped, unsafe characters, too long) resolves to `None`,
/// never a sanitized substitute.
/// Test: `resolve_workstream_name_falls_back_to_worktrees_cwd_segment`,
/// `resolve_workstream_name_rejects_uuid_shaped_cwd_segment`,
/// `resolve_workstream_name_none_when_unresolvable`.
pub fn resolve_workstream_name(cwd: Option<&str>) -> Option<String> {
    let cwd = cwd?;
    // #5204: match the CONFIGURED worktree base as well as the built-in
    // `.worktrees`. Hardcoding the literal here silently dropped the
    // `creator:workstream=` tag off every memory written after a retarget —
    // and looked retroactive, since drawers written before it kept their tags.
    let names = trusty_common::workspace_layout::WorktreeDirNames::resolve();
    let mut components = cwd.split('/');
    while let Some(part) = components.next() {
        if names.matches(part) {
            let candidate = components.next()?;
            return is_valid_workstream_name(candidate).then(|| candidate.to_string());
        }
    }
    None
}

/// Resolve *this process'* workstream name (DOC-53 §4.3).
///
/// Why: `tm` does not currently export a human-readable workstream-name
/// environment variable to a managed session (only `TM_MANAGED_SESSION_ID`,
/// a UUID) — see DOC-53 §4.3 for the investigation. Rather than block the
/// feature on a session-launch change (a surface with several in-flight PRs
/// at spec-authoring time), resolution uses only context this process
/// already has: [`WORKSTREAM_NAME_ENV`] first (forward-compatible — the key
/// `tm` would use if it starts exporting one), else the
/// [`resolve_workstream_name`] cwd fallback. An env var that is *set but
/// invalid* is treated as an explicit-but-bad signal and resolves to `None`
/// rather than silently falling through to the cwd the caller never asked
/// for.
/// What: called from [`CreatorInfo::new_self`] (code that truly runs in its
/// own writer process — see that method's doc for the daemon-vs-caller
/// hazard) and from the MCP stdio bridge
/// (`commands::serve_stdio_bridge::run_stdio_bridge`), which — unlike the
/// shared daemon it proxies to — genuinely IS a fresh, per-session process,
/// so resolving "this process' own identity" there is correct and is the
/// mechanism that turns into the caller-supplied `args["workstream"]` the
/// daemon-side [`CreatorInfo::new_for_caller`] then trusts. The HTTP write
/// path (`web::rpc::creator_info_from_http`) and the daemon-side MCP
/// dispatch handlers instead call [`resolve_workstream_name`] /
/// [`CreatorInfo::new_for_caller`] directly against caller-supplied context,
/// since the shared daemon's own environment says nothing about a specific
/// caller's identity.
/// Test: `resolve_workstream_name_prefers_env_var`,
/// `resolve_workstream_name_invalid_env_var_returns_none`.
pub(crate) fn resolve_own_workstream_name(cwd: Option<&str>) -> Option<String> {
    if let Ok(name) = std::env::var(WORKSTREAM_NAME_ENV) {
        return is_valid_workstream_name(&name).then_some(name);
    }
    resolve_workstream_name(cwd)
}

/// Validate a workstream-name candidate before it is ever rendered into a
/// tag (DOC-53 §4.3).
///
/// Why: two independent hazards must both be rejected — an empty/overlong/
/// unsafe-character candidate would corrupt the tag grammar, and a
/// UUID-shaped candidate (the naming convention for ephemeral/anonymous
/// scratch worktrees, as opposed to named workstream worktrees) would stamp
/// noise indistinguishable from a real workstream name. Rejecting both means
/// the caller omits the tag cleanly rather than sanitizing-and-including.
/// What: non-empty, at most 64 bytes, matches
/// `^[A-Za-z0-9][A-Za-z0-9_.-]*$`, and does not parse as a [`uuid::Uuid`].
/// Test: `is_valid_workstream_name_accepts_slugs`,
/// `is_valid_workstream_name_rejects_uuid_and_unsafe_names`.
pub fn is_valid_workstream_name(name: &str) -> bool {
    if name.is_empty() || name.len() > 64 {
        return false;
    }
    if uuid::Uuid::parse_str(name).is_ok() {
        return false;
    }
    let mut chars = name.chars();
    let Some(first) = chars.next() else {
        return false;
    };
    if !first.is_ascii_alphanumeric() {
        return false;
    }
    chars.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '.' || c == '-')
}

/// Build a `creator:session=<first-8-chars>` tag from the first bare UUID
/// found in `tags`, if any (issue #202).
///
/// Why: MCP writers (claude-mpm hooks, in particular) already pass the
/// session UUID as a free-form tag in the `tags` array. Turning that into
/// an explicit `creator:session=...` tag puts the session id alongside
/// the rest of the attribution data so the dashboard / TUI can surface
/// it without inspecting every tag for UUID-shaped strings.
/// What: scans the slice in order, parses each entry with
/// `uuid::Uuid::parse_str`, and on the first success returns
/// `Some("creator:session=<first-8-hex>")`. Returns `None` when no entry
/// parses as a UUID, or when the matching tag is itself already a
/// `creator:*` tag (so dashboard-supplied creator tags don't get
/// re-projected).
/// Test: `session_tag_from_tags_returns_first_uuid_short`,
/// `session_tag_from_tags_skips_non_uuid_entries`.
pub fn session_tag_from_tags(tags: &[String]) -> Option<String> {
    for tag in tags {
        // Skip the reserved-namespace tags so a stray
        // `creator:cwd=<uuid-shaped-path>` can never be misinterpreted
        // as a session id. We only consider free-form bare tags.
        if is_creator_tag(tag) {
            continue;
        }
        if let Ok(uuid) = uuid::Uuid::parse_str(tag) {
            // `uuid.simple()` renders as 32 lowercase hex chars; the
            // first 8 are the same characters that appear before the
            // first dash in the hyphenated form. Both forms parse to the
            // same `Uuid`, so we render canonically here for stability.
            let simple = uuid.simple().to_string();
            let short: String = simple.chars().take(8).collect();
            return Some(format!("{CREATOR_SESSION_PREFIX}{short}"));
        }
    }
    None
}

#[cfg(test)]
mod tests {
    use super::*;

    /// Why: every render path must emit the four tags in stable order
    /// (`client`, `version`, `source`, `cwd`) so dashboards can rely on
    /// the layout. A regression that swapped two would silently change
    /// every downstream consumer's parsing.
    /// What: constructs a `CreatorInfo` with all fields populated and
    /// asserts the rendered list.
    /// Test: itself.
    #[test]
    fn creator_info_renders_all_fields() {
        let info = CreatorInfo {
            client: "qa-curl".into(),
            version: "0.1.2".into(),
            source: CreatorSource::Http,
            cwd: Some("/tmp/proj".into()),
            workstream: None,
        };
        let tags = info.into_tags();
        assert_eq!(
            tags,
            vec![
                "creator:client=qa-curl".to_string(),
                "creator:version=0.1.2".to_string(),
                "creator:source=http".to_string(),
                "creator:cwd=/tmp/proj".to_string(),
            ]
        );
    }

    /// Why: absent cwd must produce three tags, not four with an empty
    /// `cwd=` — that would force every parser to special-case the empty
    /// suffix. Same for an empty-string cwd.
    /// What: omits cwd and renders; then sets it to "" and renders.
    /// Test: itself.
    #[test]
    fn creator_info_omits_cwd_when_absent() {
        let info = CreatorInfo {
            client: "mcp".into(),
            version: "0.1.0".into(),
            source: CreatorSource::Mcp,
            cwd: None,
            workstream: None,
        };
        assert_eq!(info.into_tags().len(), 3);

        let info_empty = CreatorInfo {
            client: "mcp".into(),
            version: "0.1.0".into(),
            source: CreatorSource::Mcp,
            cwd: Some(String::new()),
            workstream: None,
        };
        assert_eq!(info_empty.into_tags().len(), 3);
    }

    /// Why: `new_self` is the one-line convenience entry point most call
    /// sites use; it must populate the version from the crate version and
    /// the cwd from the running process so tests don't have to wire it up
    /// by hand.
    /// What: constructs and asserts version + cwd are non-empty. Serialises
    /// on [`crate::commands::env_test_lock`] and clears
    /// [`WORKSTREAM_NAME_ENV`] first since `new_self` now also resolves a
    /// workstream name from the environment.
    /// Test: itself.
    #[tokio::test]
    async fn creator_info_self_populates_version_and_cwd() {
        let _guard = crate::commands::env_test_lock().lock().await;
        // SAFETY: serialised by env_test_lock; only this var is touched.
        unsafe {
            std::env::remove_var(WORKSTREAM_NAME_ENV);
        }
        let info = CreatorInfo::new_self("client", CreatorSource::Cli);
        assert!(!info.version.is_empty(), "version must be populated");
        assert!(info.cwd.is_some(), "cwd should resolve in tests");
    }

    /// Why: the merge helper exists so call sites with an existing tag
    /// vec don't have to allocate; the contract is "appends in stable
    /// order".
    /// What: starts with one caller-supplied tag, merges, asserts the
    /// trailing tags are the creator tags in order.
    /// Test: itself.
    #[test]
    fn merge_into_appends_creator_tags() {
        let mut tags = vec!["user-supplied".to_string()];
        CreatorInfo {
            client: "x".into(),
            version: "1".into(),
            source: CreatorSource::Cli,
            cwd: None,
            workstream: None,
        }
        .merge_into(&mut tags);
        assert_eq!(
            tags,
            vec![
                "user-supplied".to_string(),
                "creator:client=x".to_string(),
                "creator:version=1".to_string(),
                "creator:source=cli".to_string(),
            ]
        );
    }

    /// Why: dashboards / TUI renderers must hide `creator:*` tags from
    /// the main tag chips so the user-supplied tags remain prominent.
    /// What: tests true / false cases against the predicate.
    /// Test: itself.
    #[test]
    fn is_creator_tag_detects_namespace() {
        assert!(is_creator_tag("creator:client=foo"));
        assert!(is_creator_tag("creator:cwd=/tmp"));
        assert!(is_creator_tag(CREATOR_VERSION_PREFIX));
        assert!(!is_creator_tag("user-tag"));
        assert!(!is_creator_tag("msg:v1"));
        assert!(!is_creator_tag("creatorx"));
    }

    /// Why: issue #202 — MCP writers (claude-mpm hooks) commonly pass
    /// the session UUID as a bare tag in the `tags` array. The helper
    /// must pick out the first parseable UUID and emit the short form
    /// in the reserved `creator:session=` namespace so the TUI activity
    /// panel renders it without bespoke parsing.
    /// What: feeds a mixed tag list and asserts the first 8 hex chars
    /// of the UUID round-trip into the returned tag.
    /// Test: itself.
    #[test]
    fn session_tag_from_tags_returns_first_uuid_short() {
        let tags = vec![
            "user-tag".to_string(),
            "01919e90-8a2e-7c1d-9f8b-1234567890ab".to_string(),
            "ignored-second-uuid:11111111-2222-3333-4444-555555555555".to_string(),
        ];
        let session = session_tag_from_tags(&tags).expect("session tag");
        assert_eq!(session, "creator:session=01919e90");
    }

    /// Why: non-UUID entries (free-form tags, scoped tags like `idx:0`)
    /// must not be misinterpreted as session ids — the helper has to
    /// return `None` when no entry parses as a UUID.
    /// What: feeds a tag list with no UUIDs and asserts `None`.
    /// Test: itself.
    #[test]
    fn session_tag_from_tags_skips_non_uuid_entries() {
        let tags = vec![
            "user-tag".to_string(),
            "idx:0".to_string(),
            "session-prefix-not-a-uuid".to_string(),
        ];
        assert!(session_tag_from_tags(&tags).is_none());

        // Empty list returns `None`.
        assert!(session_tag_from_tags(&[]).is_none());
    }

    /// Why: a tag in the reserved `creator:*` namespace must never be
    /// re-projected as a session id, even if its value parses as a UUID.
    /// `creator:cwd=` carrying a UUID-shaped temporary path is the
    /// motivating example.
    /// What: feeds a `creator:` tag whose value parses as a UUID and a
    /// real bare UUID later in the list, then asserts the real one wins.
    /// Test: itself.
    #[test]
    fn session_tag_from_tags_skips_reserved_namespace() {
        let tags = vec![
            // Reserved namespace tag with a UUID-shaped value — must be skipped.
            "creator:cwd=11111111-1111-1111-1111-111111111111".to_string(),
            // The real session tag — must win.
            "22222222-2222-2222-2222-222222222222".to_string(),
        ];
        let session = session_tag_from_tags(&tags).expect("session tag");
        assert_eq!(session, "creator:session=22222222");
    }

    /// Why: the `From<ActivitySource>` impl lets the HTTP path build a
    /// `CreatorSource` from the existing `ActivitySource::Http` without
    /// a manual match; the mapping must be identity for the three shared
    /// variants.
    /// What: round-trips each variant.
    /// Test: itself.
    #[test]
    fn creator_source_from_activity_source() {
        assert_eq!(
            CreatorSource::from(ActivitySource::Http),
            CreatorSource::Http
        );
        assert_eq!(CreatorSource::from(ActivitySource::Mcp), CreatorSource::Mcp);
        assert_eq!(
            CreatorSource::from(ActivitySource::Hook),
            CreatorSource::Hook
        );
    }

    /// Why: `into_tags` must render both the reserved `creator:workstream=`
    /// tag and the ergonomic bare `ws:` tag, in that order, immediately
    /// after `cwd` — DOC-53 §4.1/§4.2.
    /// What: constructs a `CreatorInfo` with a resolvable workstream and
    /// asserts both trailing tags.
    /// Test: itself.
    #[test]
    fn creator_info_renders_workstream_tags_when_resolvable() {
        let info = CreatorInfo {
            client: "mcp".into(),
            version: "0.1.0".into(),
            source: CreatorSource::Mcp,
            cwd: Some("/tmp/proj".into()),
            workstream: Some("feat-ws-memory-claims".into()),
        };
        let tags = info.into_tags();
        assert_eq!(
            tags,
            vec![
                "creator:client=mcp".to_string(),
                "creator:version=0.1.0".to_string(),
                "creator:source=mcp".to_string(),
                "creator:cwd=/tmp/proj".to_string(),
                "creator:workstream=feat-ws-memory-claims".to_string(),
                "ws:feat-ws-memory-claims".to_string(),
            ]
        );
    }

    /// Why: DOC-53 §4.1's "no placeholder, ever" rule must hold both when
    /// `workstream` is `None` (the common case) AND when a caller manually
    /// sets an invalid value — `into_tags` must re-validate rather than
    /// trust its input, since [`CreatorInfo`] is a public struct any caller
    /// can construct directly.
    /// What: `None` renders no trailing tags; an unsafe/UUID-shaped value
    /// also renders none, rather than being sanitized and included.
    /// Test: itself.
    #[test]
    fn creator_info_omits_workstream_tags_when_absent_or_invalid() {
        let absent = CreatorInfo {
            client: "mcp".into(),
            version: "0.1.0".into(),
            source: CreatorSource::Mcp,
            cwd: None,
            workstream: None,
        };
        assert_eq!(absent.into_tags().len(), 3);

        let invalid = CreatorInfo {
            client: "mcp".into(),
            version: "0.1.0".into(),
            source: CreatorSource::Mcp,
            cwd: None,
            workstream: Some("11111111-1111-1111-1111-111111111111".into()),
        };
        assert_eq!(invalid.into_tags().len(), 3);
    }

    /// Why (CRITICAL fix, DOC-53 §4.3): `new_for_caller` is the ONLY
    /// constructor the shared daemon's dispatch handlers should use — it
    /// must never touch this process' own env/cwd, only what the caller
    /// supplied. This is the base case: an explicit, valid workstream wins
    /// even when a plausible cwd fallback is also present.
    /// What: supplies both a caller cwd and a distinct caller workstream;
    /// asserts the explicit workstream is used, not one derived from cwd.
    /// Test: itself.
    #[test]
    fn new_for_caller_prefers_explicit_workstream_over_cwd() {
        let info = CreatorInfo::new_for_caller(
            "trusty-memory-mcp",
            CreatorSource::Mcp,
            Some("/x/.worktrees/cwd-derived-name"),
            Some("explicit-ws"),
        );
        assert_eq!(info.workstream.as_deref(), Some("explicit-ws"));
        assert_eq!(info.cwd.as_deref(), Some("/x/.worktrees/cwd-derived-name"));
    }

    /// Why: when the caller omits `workstream` entirely (but supplies
    /// `cwd`), the cwd-derived heuristic is the correct fallback — same
    /// derivation [`resolve_workstream_name`] already provides for the HTTP
    /// path.
    /// What: caller workstream `None`, caller cwd a `.worktrees/<name>`
    /// path; asserts the derived name.
    /// Test: itself.
    #[test]
    fn new_for_caller_falls_back_to_cwd_when_workstream_absent() {
        let info = CreatorInfo::new_for_caller(
            "trusty-memory-mcp",
            CreatorSource::Mcp,
            Some("/x/.worktrees/cwd-derived-name"),
            None,
        );
        assert_eq!(info.workstream.as_deref(), Some("cwd-derived-name"));
    }

    /// Why: no caller cwd AND no caller workstream is the honest "we don't
    /// know" case — DOC-53 §4.1's omit-cleanly rule, at the caller-supplied
    /// constructor.
    /// What: both `None`; asserts `workstream` is `None` (never falls back
    /// to this process' own identity).
    /// Test: itself.
    #[test]
    fn new_for_caller_omits_workstream_when_neither_resolvable() {
        let info = CreatorInfo::new_for_caller("trusty-memory-mcp", CreatorSource::Mcp, None, None);
        assert_eq!(info.workstream, None);
        assert_eq!(info.cwd, None);
    }

    /// Why: an explicit-but-invalid caller workstream (unsafe chars,
    /// UUID-shaped, empty) is an explicit-but-bad signal — DOC-53 §4.3
    /// treats that as `None`, NOT a silent fallback to the cwd heuristic the
    /// caller didn't ask for (mirrors [`resolve_own_workstream_name`]'s
    /// identical rule for the env var).
    /// What: an invalid explicit workstream alongside a valid cwd fallback;
    /// asserts `None`, not the cwd-derived value.
    /// Test: itself.
    #[test]
    fn new_for_caller_invalid_explicit_workstream_returns_none_not_cwd_fallback() {
        let info = CreatorInfo::new_for_caller(
            "trusty-memory-mcp",
            CreatorSource::Mcp,
            Some("/x/.worktrees/cwd-derived-name"),
            Some("not a valid name!"),
        );
        assert_eq!(info.workstream, None);
    }

    /// Why (MEDIUM 1, DOC-53 §3.1): a hand-written claim drawer's own tags
    /// may already carry `ws:<name>` (and, less commonly, a literal
    /// `creator:workstream=<name>`) — `merge_into_deduped` must not append a
    /// second copy of either.
    /// What: seeds `dst` with `ws:feat-x` already present, merges a
    /// `CreatorInfo` whose rendered tags include `ws:feat-x`, asserts the
    /// tag appears exactly once while the OTHER rendered tags (which were
    /// not already present) still land.
    /// Test: itself.
    #[test]
    fn merge_into_deduped_skips_tags_already_present() {
        let mut tags = vec!["user-tag".to_string(), "ws:feat-x".to_string()];
        CreatorInfo {
            client: "trusty-memory-mcp".into(),
            version: "0.1.0".into(),
            source: CreatorSource::Mcp,
            cwd: None,
            workstream: Some("feat-x".into()),
        }
        .merge_into_deduped(&mut tags);
        assert_eq!(
            tags.iter().filter(|t| *t == "ws:feat-x").count(),
            1,
            "ws:feat-x must not be duplicated; got {tags:?}"
        );
        assert!(
            tags.contains(&"creator:client=trusty-memory-mcp".to_string()),
            "non-overlapping tags must still be appended; got {tags:?}"
        );
        assert!(
            tags.contains(&"creator:workstream=feat-x".to_string()),
            "creator:workstream= must still be appended (only ws: overlapped); got {tags:?}"
        );
    }

    /// Why: dedup must not become a no-append bug — when nothing overlaps,
    /// every rendered tag lands exactly as `merge_into` would produce.
    /// What: seeds `dst` with an unrelated tag only; asserts all rendered
    /// tags are present.
    /// Test: itself.
    #[test]
    fn merge_into_deduped_appends_when_no_overlap() {
        let mut tags = vec!["unrelated".to_string()];
        CreatorInfo {
            client: "trusty-memory-mcp".into(),
            version: "0.1.0".into(),
            source: CreatorSource::Mcp,
            cwd: None,
            workstream: Some("feat-x".into()),
        }
        .merge_into_deduped(&mut tags);
        assert_eq!(
            tags,
            vec![
                "unrelated".to_string(),
                "creator:client=trusty-memory-mcp".to_string(),
                "creator:version=0.1.0".to_string(),
                "creator:source=mcp".to_string(),
                "creator:workstream=feat-x".to_string(),
                "ws:feat-x".to_string(),
            ]
        );
    }

    /// Why: [`WORKSTREAM_NAME_ENV`] is the forward-compatible primary
    /// source (DOC-53 §4.3) and must win over the cwd fallback whenever
    /// set. Serialises on `env_test_lock` since it mutates process-wide
    /// state.
    /// What: sets the env var, passes an unrelated (even a `.worktrees`-
    /// bearing) cwd, and asserts the env value wins.
    /// Test: itself.
    #[tokio::test]
    async fn resolve_workstream_name_prefers_env_var() {
        let _guard = crate::commands::env_test_lock().lock().await;
        // SAFETY: serialised by env_test_lock; only this var is touched,
        // and it is always cleared before returning.
        unsafe {
            std::env::set_var(WORKSTREAM_NAME_ENV, "explicit-ws");
        }
        let resolved = resolve_own_workstream_name(Some("/x/.worktrees/other-name"));
        unsafe {
            std::env::remove_var(WORKSTREAM_NAME_ENV);
        }
        assert_eq!(resolved, Some("explicit-ws".to_string()));
    }

    /// Why: an env var that is SET but fails validation (DOC-53 §4.3) is an
    /// explicit-but-bad signal — it must resolve to `None`, not silently
    /// fall through to the cwd heuristic the writer never asked for.
    /// What: sets an unsafe env value with a plausible cwd fallback present
    /// and asserts `None`.
    /// Test: itself.
    #[tokio::test]
    async fn resolve_workstream_name_invalid_env_var_returns_none() {
        let _guard = crate::commands::env_test_lock().lock().await;
        // SAFETY: serialised by env_test_lock.
        unsafe {
            std::env::set_var(WORKSTREAM_NAME_ENV, "not a valid name!");
        }
        let resolved = resolve_own_workstream_name(Some("/x/.worktrees/other-name"));
        unsafe {
            std::env::remove_var(WORKSTREAM_NAME_ENV);
        }
        assert_eq!(resolved, None);
    }

    /// Why: the common case — a `tm`-managed PM session running directly in
    /// its own `.worktrees/<name>` checkout — must resolve without any `tm`
    /// changes (DOC-53 §4.3). [`resolve_workstream_name`] is pure cwd-based
    /// (no env var involved, see its doc comment), so no locking is needed.
    /// What: feeds a realistic worktree cwd, asserts the segment
    /// immediately after `.worktrees` is returned.
    /// Test: itself.
    #[test]
    fn resolve_workstream_name_falls_back_to_worktrees_cwd_segment() {
        let resolved = resolve_workstream_name(Some(
            "/Users/bob/trusty-tools/.base/.worktrees/feat-ws-memory-claims",
        ));
        assert_eq!(resolved, Some("feat-ws-memory-claims".to_string()));
    }

    /// Why: ephemeral/anonymous scratch worktrees are named by UUID, not by
    /// workstream (DOC-53 §4.3) — stamping one would produce noise
    /// indistinguishable from a real name, so it must resolve to `None`.
    /// What: feeds a UUID-named `.worktrees/<uuid>` cwd, asserts `None`.
    /// Test: itself.
    #[test]
    fn resolve_workstream_name_rejects_uuid_shaped_cwd_segment() {
        let resolved = resolve_workstream_name(Some(
            "/Users/bob/trusty-tools/.base/.worktrees/2eb72dca-de08-481b-8dfa-22ab7f81b1f9",
        ));
        assert_eq!(resolved, None);
    }

    /// Why: a cwd with no `.worktrees` component (or no cwd at all) is the
    /// honest "can't resolve this" case — DOC-53 §4.1's omit-cleanly rule,
    /// exercised at the resolver level.
    /// What: cwd `None`; then a cwd with no `.worktrees` segment. Both
    /// resolve to `None`.
    /// Test: itself.
    #[test]
    fn resolve_workstream_name_none_when_unresolvable() {
        assert_eq!(resolve_workstream_name(None), None);
        assert_eq!(
            resolve_workstream_name(Some("/Users/bob/some/other/project")),
            None
        );
    }

    /// Why: the validator is the single gate standing between untrusted
    /// input (env var or cwd segment) and a rendered tag — it must accept
    /// ordinary slugs.
    /// What: table of accepted names.
    /// Test: itself.
    #[test]
    fn is_valid_workstream_name_accepts_slugs() {
        for name in [
            "feat-ws-memory-claims",
            "tm_search_eviction_01",
            "a",
            "release.0.20.0",
        ] {
            assert!(is_valid_workstream_name(name), "expected valid: {name}");
        }
    }

    /// Why: the validator must reject both structurally-unsafe candidates
    /// (empty, overlong, unsafe characters, non-alnum leading char) and
    /// UUID-shaped candidates (DOC-53 §4.3's anonymous-worktree exclusion),
    /// since a caller (env var or cwd) is untrusted input.
    /// What: table of rejected names.
    /// Test: itself.
    #[test]
    fn is_valid_workstream_name_rejects_uuid_and_unsafe_names() {
        for name in [
            "",
            "2eb72dca-de08-481b-8dfa-22ab7f81b1f9",
            "-leading-dash",
            "has space",
            "has/slash",
            "has;semicolon",
            &"x".repeat(65),
        ] {
            assert!(!is_valid_workstream_name(name), "expected invalid: {name}");
        }
    }
}