Skip to content

Commit 5796f18

Browse files
committed
feat(shell, workspace, chat), skill(inbox-curation): pre-auth sign-in wall, didi persona + curator verbs, and a face (Build-Order Steps 7-8)
Steps 7 and 8 of the humain-vc unlock build order, plus the character art that both depend on — landed together since the wall and the chat header both needed the same avatar asset. Step 7 — pre-auth sign-in wall (services/workspace/src/server.ts, ws.ts, workspaces.ts, capabilities.ts; packages/workspace/src/state.svelte.ts, types.ts; shell/src/App.svelte, SignInWall.svelte): a single-tenant instance now shows a real sign-in wall to an unauthenticated visitor instead of a shell that renders remotes which all fail closed. Found and closed a real gap along the way — an anonymous WS upgrade against DIDI_AUTH=required is rejected (4401) BEFORE any session frame ships, so the session frame alone can never tell an anonymous visitor's shell to show the wall. Added a plain, unauthenticated GET /config the shell fetches alongside connect(). workspace.list also gained `pinned`, hiding the WorkspaceSwitcher on a single-tenant deploy. Verified live with headless Chromium: anonymous → full wall, no header/remotes/switcher; signed in → wall gone, real data loads. Step 8 — didi chat v0 (services/workspace/src/chat.ts; apps/chat/src/ App.svelte, app.css; context-v/agent-skills/inbox-curation/SKILL.md): the chat rail's system prompt now introduces didi by name, states both its jobs (enrichment + corpus curation), and stays explicit that it's scoped to augment-it only for now — the shared cross-service persona is its own initiative, per the Didi-sh-One-Login-One-Agent-Three-Services exploration. New CURATOR_CHAT_VERBS slab wires source.add/domain.create/ extract.add/tag.apply into the existing three-mode chat discipline, with a live existingCorporaSlab() (a domain.list read per turn) so didi resolves a name like "consumer-immunology" against what's actually in the workspace instead of guessing. Authored the first agent-skill, inbox-curation, following the decile-hub-interface format precedent. Verified live: "didi, file this link under consumer-immunology" ends with a real source in thesis:consumer-immunology, created_by matching the signed-in didi_id. Didi's face (shell/src/DidiBadge.svelte, SignInWall.svelte; apps/chat/ src/App.svelte; public/, shell/public/, apps/chat/public/): a character headshot (user-provided, transparent PNG) composited with the shell's actual brand-duotone gradient tokens, picked from four generated candidates. Cropped at higher resolution from the full character sheet than the original headshot offered. Wired into the header badge, the sign-in wall, and the chat rail's own header. Files changed: - services/workspace/src/server.ts - services/workspace/src/ws.ts - services/workspace/src/workspaces.ts - services/workspace/src/capabilities.ts - services/workspace/src/chat.ts - packages/workspace/src/state.svelte.ts - packages/workspace/src/types.ts - shell/src/App.svelte - shell/src/SignInWall.svelte - shell/src/DidiBadge.svelte - apps/chat/src/App.svelte - apps/chat/src/app.css - context-v/agent-skills/inbox-curation/SKILL.md - context-v/plans/Build-Order-Humain-VC-Unlock-Flow.md - changelog/2026-07-08_02_Pre-Auth-Sign-In-Wall-The-Anonymous-Visitor-Gap-The-Session-Frame-Couldnt-Close.md - changelog/2026-07-08_03_Didi-Gets-A-Face-And-A-Voice-Chat-v0-Curator-Verbs-And-Inbox-Curation.md
1 parent c6c3824 commit 5796f18

21 files changed

Lines changed: 711 additions & 46 deletions

apps/chat/public/didi-avatar.png

297 KB
Loading

apps/chat/src/App.svelte

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,12 @@
2929

3030
<div class="chat-app">
3131
<div class="chat-status" class:open={connectionStatus === 'open'} class:closed={connectionStatus !== 'open'}>
32-
chat · {connectionStatus}
32+
<img class="didi-avatar" src="/didi-avatar.png" alt="" aria-hidden="true" />
33+
<span class="didi-name">didi</span>
34+
<span class="chat-status-sep">·</span>
35+
<span class="chat-status-app">augment-it</span>
36+
<span class="chat-status-sep">·</span>
37+
<span class="chat-status-conn">{connectionStatus}</span>
3338
</div>
3439
<CharacterCastRow />
3540
<ChatSurface />

apps/chat/src/app.css

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,29 @@
1616
}
1717

1818
.chat-app .chat-status {
19+
display: flex;
20+
align-items: center;
21+
gap: 0.35rem;
1922
padding: 0.4rem 0.8rem;
2023
font-size: 11px;
2124
font-family: var(--font-mono, ui-monospace, monospace);
2225
border-bottom: 1px solid var(--color-border);
2326
color: var(--color-text-muted);
2427
}
25-
.chat-app .chat-status.open { color: var(--color-accent); }
26-
.chat-app .chat-status.closed { color: var(--color-warn, #c66); }
28+
.chat-app .didi-avatar {
29+
width: 16px;
30+
height: 16px;
31+
border-radius: 50%;
32+
object-fit: cover;
33+
flex-shrink: 0;
34+
}
35+
.chat-app .didi-name {
36+
font-weight: 600;
37+
color: var(--color-text);
38+
}
39+
.chat-app .chat-status-sep { opacity: 0.5; }
40+
.chat-app .chat-status.open .chat-status-conn { color: var(--color-accent); }
41+
.chat-app .chat-status.closed .chat-status-conn { color: var(--color-warn, #c66); }
2742

2843
/* --- Character cast row --- */
2944
.chat-app .cast-row {
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
---
2+
date_created: 2026-07-08
3+
date_modified: 2026-07-08
4+
title: "Pre-auth sign-in wall — the anonymous-visitor gap the session frame couldn't close"
5+
lede: "Step 7 of the humain-vc unlock build order: a single-tenant deploy now shows a real sign-in wall instead of a half-broken shell to anyone without a didi.sh session. Building it surfaced a genuine architecture gap — an anonymous WS upgrade against a required instance is rejected before any session frame ships — closed with a small, unauthenticated config endpoint."
6+
publish: true
7+
authors:
8+
- Michael Staton
9+
augmented_with:
10+
- Claude Code on Claude Sonnet 5
11+
files_changed:
12+
- services/workspace/src/server.ts
13+
- services/workspace/src/ws.ts
14+
- services/workspace/src/workspaces.ts
15+
- services/workspace/src/capabilities.ts
16+
- packages/workspace/src/state.svelte.ts
17+
- packages/workspace/src/types.ts
18+
- shell/src/SignInWall.svelte
19+
- shell/src/App.svelte
20+
tags:
21+
- Progress-Update
22+
- Auth
23+
- Didi-Platform
24+
- Augment-It
25+
- humain-vc
26+
- Shell
27+
---
28+
29+
## Why Care?
30+
31+
A single-tenant deploy (the DO box waiting for humain-vc) needs to look like a real product to an unauthenticated visitor, not a broken app rendering remotes that all fail their capability calls closed. Step 7 makes that true — and along the way surfaced a real gap in how "does this instance require sign-in" gets communicated, one worth knowing about before it bites a real visitor.
32+
33+
## What's New?
34+
35+
- **The wall.** When `workspace.didi_auth_mode === 'required'` and the session has no `didi_id`, the shell now renders `SignInWall.svelte` — full-screen, no header, no remotes, no WorkspaceSwitcher — instead of mounting anything. Sign in and the wall disappears; sign out and it returns.
36+
- **The switcher hides on a pinned instance.** `workspace.list` now reports `pinned: true` whenever the instance booted with `ACTIVE_CLIENT_ID` set (a single-tenant deploy declaring its one client). No point offering a switch to a workspace that doesn't exist on that box.
37+
- **The real find: a plain `GET /config`.** An anonymous WS upgrade against a `required` instance is rejected with code 4401 *before* any session frame is ever sent — so a genuinely anonymous visitor's shell would never learn `didi_auth_mode` from the session frame alone, and `showWall` would silently stay false. Added an unauthenticated, CORS-open `GET /config` on workspace-service that the shell fetches alongside (not instead of) `connect()`, so the wall renders correctly for the actual case that matters.
38+
39+
## The Story
40+
41+
The session-frame approach looked complete until we asked "what does a fresh, never-visited browser actually see?" The answer: the WS handshake it fires gets closed before the server ever gets to say anything — the exact code path step 3's membership gate already relies on. Rather than weaken that gate (a real, tested, intentional 4401), the fix routes around it: a cheap, side-channel HTTP read that answers one question ("does this instance require sign-in?") without needing a session at all.
42+
43+
Verified with headless Chromium end to end, not just typechecked: a fresh anonymous context landed on the full wall with no header/remotes/switcher visible; filling the email and completing the local dev-token sign-in reloaded into the normal shell — header, WorkspaceSwitcher, and `record-collector` mounted with real data. `pinned: true`/`false` verified live via a temporary `ACTIVE_CLIENT_ID` override on the running container, no rebuild needed, then reverted.
44+
45+
## What's Next
46+
47+
Step 8 — didi chat v0 — landed right behind this one (see the next entry). The remaining work is the deploy tail: Step 9 puts augment-it on the prepped DigitalOcean box.
48+
49+
## Related
50+
51+
- `context-v/plans/Build-Order-Humain-VC-Unlock-Flow.md` — Step 7, now done
52+
- `context-v/plans/Unlock-Humain-VC-Team-Access-To-Augment-It.md` (ai-labs level) — item 6, "Instance posture flags"
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
---
2+
date_created: 2026-07-08
3+
date_modified: 2026-07-08
4+
title: "Didi gets a face and a voice — chat v0, curator verbs, and the inbox-curation skill"
5+
lede: "Step 8 of the humain-vc unlock build order: the chat rail is now didi, it can file a captured link under an existing thesis by name in one turn, and it has an actual face — a character headshot composited with the shell's own brand gradient. Deliberately scoped to augment-it alone; the cross-service didi vision has its own exploration doc and its own timeline."
6+
publish: true
7+
authors:
8+
- Michael Staton
9+
augmented_with:
10+
- Claude Code on Claude Sonnet 5
11+
files_changed:
12+
- services/workspace/src/chat.ts
13+
- apps/chat/src/App.svelte
14+
- apps/chat/src/app.css
15+
- shell/src/DidiBadge.svelte
16+
- shell/src/SignInWall.svelte
17+
- context-v/agent-skills/inbox-curation/SKILL.md
18+
- shell/public/didi-avatar.png
19+
- shell/public/didi-portrait.png
20+
- apps/chat/public/didi-avatar.png
21+
- public/illustrationOf_Didi--Headshot.png
22+
- public/illustrationOf_Didi_character_assortmant.png
23+
tags:
24+
- Progress-Update
25+
- Didi-Agent
26+
- Agent-Skills
27+
- Augment-It
28+
- humain-vc
29+
- Chat
30+
- Brand
31+
---
32+
33+
## Why Care?
34+
35+
Flow 1's fifth job is "DiDi assists" — the operator says "file this under consumer-immunology" and it just happens, attributed correctly, without a second click. Today that became real, and didi stopped being a generic "in-app assistant" with no face and became a character — the same one that'll eventually show up in dididecks and memopop too, just scoped to augment-it for now.
36+
37+
## What's New?
38+
39+
- **A persona.** `services/workspace/src/chat.ts`'s system prompt now introduces didi by name, states both of its jobs (enrichment + corpus curation), and is explicit that it's scoped to augment-it only — no cross-service claims, matching the [[Didi-sh-One-Login-One-Agent-Three-Services]] exploration's near-term posture (one shared persona *concept*, independent per-app runtimes, no shared code yet).
40+
- **Curator verbs, chat-invokable.** New `CURATOR_CHAT_VERBS` slab wires `source.add`, `domain.create`, `extract.add`, and `tag.apply` into the same three-mode (answer/propose/invoke) discipline the enrichment verbs already use. A new live `existingCorporaSlab()` runs a `domain.list` read on every turn and hands didi the workspace's real corpora as `Title → type:slug` — so "consumer-immunology" resolves against what's actually there, never a guess.
41+
- **The `inbox-curation` agent-skill**, `context-v/agent-skills/inbox-curation/SKILL.md` (the `decile-hub-interface` format precedent): the full decision tree — named + existing corpus → invoke directly; new corpus → propose first; unclear → propose-or-park — plus the never-fabricate-an-identifier rule and the boundary with `corpus.inbox.add` (untriaged parking, not filing).
42+
- **A face.** The character headshot (transparent PNG, user-provided) composited with the shell's actual `--color-accent`/`--color-accent-2` tokens as a soft brand-duotone gradient — picked from four generated candidates. Cropped at higher resolution from the full character sheet than the original headshot alone offered. Now showing in `DidiBadge`, `SignInWall`, and the chat rail's own header ("didi · augment-it · \<status\>").
43+
44+
## The Story
45+
46+
The acceptance line for this step was always going to be the real test: "didi, file this link under consumer-immunology" typed into a live chat turn, with nothing hand-waved. Ran it against the actual local stack — didi correctly resolved the thesis name from the live corpora list, chose `chat_invoke` (not a proposal, since the corpus was named explicitly — exactly the discipline the skill doc describes), and the source landed in `thesis:consumer-immunology`, `created_by` matching the signed-in didi_id. Verified again in the browser itself: didi's very first greeting lists the workspace's three real corpora by name, pulled from the same live slab a curious operator would see if they asked "what corpora do I have?"
47+
48+
The character art took its own small detour — four background treatments generated against the shell's real theme tokens (solid dark, brand duotone, copper glow echoing the glasses' gears, and a punchier single-accent glow), reviewed side by side before picking brand duotone, then re-cropped from the high-resolution character sheet rather than the smaller original headshot for a crisper result everywhere it's used.
49+
50+
```mermaid
51+
flowchart LR
52+
A["chat_turn: 'file this under\nconsumer-immunology'"] --> B[didi resolves name\nagainst live domain.list]
53+
B --> C[chat_invoke source.add]
54+
C --> D[source filed, created_by\n= signed-in didi_id]
55+
```
56+
57+
## What's Next
58+
59+
Steps 1–8 of the build order are now done. What's left is the deploy tail — Step 9 puts augment-it on the prepped DigitalOcean droplet, single-tenant for humain-vc.
60+
61+
## Related
62+
63+
- `context-v/plans/Build-Order-Humain-VC-Unlock-Flow.md` — Step 8, now done
64+
- `context-v/agent-skills/inbox-curation/SKILL.md` — the new skill
65+
- [[Didi-sh-One-Login-One-Agent-Three-Services]] (ai-labs level) — the cross-service vision this step is the augment-it-local first slice of
66+
- [[2026-07-08_02_Pre-Auth-Sign-In-Wall-The-Anonymous-Visitor-Gap-The-Session-Frame-Couldnt-Close]] — Step 7, landed just before this one
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
---
2+
name: inbox-curation
3+
description: didi's triage discipline for filing captured links and sources into the right corpus (thesis/strategy/topic) inside augment-it. Use whenever the user gives didi a URL to save, asks to "file this under <corpus name>", names an existing thesis/strategy by name for a link to go under, or it's unclear which corpus a captured link belongs to. Encodes the decision tree (named + existing corpus → source.add directly; new corpus → propose domain.create first; unclear → propose or park in the inbox), the domain-resolution discipline (resolve names against the live "Existing corpora" list, never fabricate a domain_type/domain_slug), and the boundary with corpus.inbox.add (the untriaged capture-first fallback). The capability catalog and args shapes live in services/workspace/src/chat.ts's CURATOR_CHAT_VERBS slab — this skill is the reasoning on top of it.
4+
---
5+
6+
# Inbox curation
7+
8+
didi's job, Flow 1: **a person captures a link while researching, and didi files
9+
it under the right corpus without becoming a second click for the operator to
10+
make.** "Corpus" here is augment-it's `domains` table — a typed grouping
11+
(`thesis`, `strategy`, `topic`, …) each workspace names for itself; `humain-vc`
12+
calls them theses, `reach-edu` calls them strategies. Same mechanism, different
13+
noun.
14+
15+
## The data model, in the order you reason about it
16+
17+
1. **A corpus is a `(type, slug)` pair** — e.g. `thesis:consumer-immunology`.
18+
Titles are for humans; `type` + `slug` is what every capability actually
19+
takes as an argument.
20+
2. **A source is a URL filed under one or more corpora**, identified by
21+
`source_uuid` once it's been added. `source.add` does the filing (DB
22+
registry row + Jina metadata fetch + on-disk file, per
23+
[[../../specs/Strategy-Curator-Entry-Point-for-Augment-It|Strategy-Curator-Entry-Point-for-Augment-It]]).
24+
3. **The inbox (`corpus.inbox.add`) is NOT a corpus.** It's a flat,
25+
untriaged holding pen — a markdown file with frontmatter, no `domain_type`/
26+
`domain_slug` at all. It exists for exactly the case this skill's decision
27+
tree below routes to it: a link with no clear corpus home yet.
28+
29+
## The decision tree (this is the whole skill)
30+
31+
Every turn where the user hands didi a URL, ask two questions in order:
32+
33+
**1. Did the user name an existing corpus?**
34+
35+
Check the name (or slug) they typed against the **"Existing corpora"**
36+
context slab (a live `domain.list` read, injected into every chat turn as
37+
`Title → type:slug`). Match on title OR slug, case- and
38+
punctuation-insensitive — "consumer-immunology", "the immunology thesis", and
39+
"Consumer Immunology" should all resolve to the same `thesis:consumer-immunology`
40+
row if it's in the list.
41+
42+
- **Match found**`chat_invoke source.add` directly, with the resolved
43+
`domain_type`/`domain_slug`. Do not propose first — the named corpus is
44+
unambiguous, and re-confirming a decision the user already made is the
45+
friction this flow exists to remove. This is the acceptance case: *"didi,
46+
file this link under consumer-immunology"* ends with a real source row in
47+
`thesis:consumer-immunology`, attributed to the signed-in user (rides
48+
automatically via the `via: 'didi-agent'` actor tag — no extra work needed
49+
here).
50+
- **No match, but the user clearly means a NEW corpus** (e.g. "start a new
51+
thesis on X and put this under it") → `chat_propose domain.create` first.
52+
Creating a corpus is a bigger, more visible decision than adding one more
53+
source to an existing one — it changes what the curator's sidebar shows
54+
for everyone on the workspace. Never `chat_invoke` it silently.
55+
56+
**2. Named nothing, or named something that doesn't match?**
57+
58+
`chat_propose`, offering the most plausible existing corpus (if the link's
59+
subject matter clearly matches one title) **and** `corpus.inbox.add` as the
60+
park-it-for-later option, side by side. Let the operator pick. **Never
61+
silently guess a corpus and file into it** — a wrong guess is worse than
62+
asking, because it makes the source invisible in the corpus the operator
63+
actually meant.
64+
65+
## Never fabricate identifiers
66+
67+
`domain_slug` must come from the live "Existing corpora" list (for filing
68+
into an existing one) or from a slug didi itself proposes for a NEW domain
69+
via `domain.create` (never invent one to hand to `source.add` directly —
70+
`source.add` requires the domain to already exist; `domain.create` is what
71+
makes it exist). Similarly, `extract.add` and `tag.apply` need a real
72+
`source_uuid` — if the conversation hasn't surfaced one (e.g. the user says
73+
"add a quote to that last source" and didi doesn't have its uuid in context),
74+
ask rather than guess. A wrong write here isn't reversible-by-refresh the way
75+
a UI mis-click is; the discipline matters more here than in the curator's own
76+
click-driven surface.
77+
78+
## Boundary with `corpus.inbox.add`
79+
80+
Do not treat the inbox as a lesser `source.add`. It has no `domain_type`,
81+
skips the Jina metadata fetch's role in a corpus's index, and its content
82+
lives in a different filesystem location
83+
(`clients/<client_id>/corpus/inbox/`, not
84+
`clients/<client_id>/corpus/<type-plural>/<slug>/sources/`). A link parked in
85+
the inbox needs a **second, separate** triage pass later — either a human
86+
walking `corpus/inbox/` and manually filing things, or a future didi capability
87+
that doesn't exist yet. Don't imply to the user that inboxing is equivalent to
88+
filing; say "parked in your inbox for later" or similar, not "saved."
89+
90+
## Worked examples
91+
92+
- *"didi, file this under consumer-immunology: https://example.com/paper"*
93+
→ "consumer-immunology" matches `thesis:consumer-immunology` in Existing
94+
corpora → `chat_invoke source.add` with `{ url, domain_type: "thesis",
95+
domain_slug: "consumer-immunology", client_slug: <active> }`.
96+
- *"save this for later, not sure where it goes: https://example.com/news"*
97+
→ no corpus named → `chat_propose` with one plausible existing-corpus
98+
guess (only if the URL's subject clearly matches a title) plus
99+
`corpus.inbox.add`.
100+
- *"start a new thesis on gut-brain axis research and put this in it"*
101+
→ no match, but a new corpus is explicitly requested → `chat_propose
102+
domain.create` with a slugified title, then (after acceptance) `source.add`
103+
as the natural follow-up turn.
104+
- *"add a note to that immunology source saying it's peer-reviewed"*
105+
`extract.add` needs `source_uuid`; if it's not already in the visible
106+
transcript/context, ask which source before calling anything.
107+
108+
## See also
109+
110+
- `services/workspace/src/chat.ts``CURATOR_CHAT_VERBS`, the capability
111+
catalog and args shapes this skill reasons on top of; `existingCorporaSlab`,
112+
the live `domain.list` read this skill's decision tree depends on.
113+
- [[../../specs/Strategy-Curator-Entry-Point-for-Augment-It|Strategy-Curator-Entry-Point-for-Augment-It]]
114+
— the click-driven UI this skill is the conversational equivalent of.
115+
- [[../../../context-v/plans/Unlock-Humain-VC-Team-Access-To-Augment-It|Unlock-Humain-VC-Team-Access-To-Augment-It]]
116+
(ai-labs level) — Flow 1 item 5, "DiDi assists," the flow this skill serves.
117+
- `decile-hub-interface` (sibling agent-skill) — the format precedent this
118+
doc follows.

0 commit comments

Comments
 (0)