create-pmos 0.87.0 → 0.88.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-pmos",
3
- "version": "0.87.0",
3
+ "version": "0.88.0",
4
4
  "description": "Scaffold the PMOS portable kit — run the PMOS product-management workflow (initiative → lane-fit spec → contract → build → three gates, with work-type lanes for features, research, maintenance, incidents, and content) entirely inside your own repo, with all state as git-versioned files. Includes `workflow-sweep`, which audits the state every board derives from and separates real defects from work you simply have not done. `create-pmos workspace` also installs + launches the local Terminal Workspace GUI. No database, no network, no telemetry.",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -1,3 +1,3 @@
1
- kernel-source: pmos@6a237b2
1
+ kernel-source: pmos@57df07b
2
2
  assembled: 2026-10-07
3
3
  mode: vendor
@@ -2,7 +2,7 @@
2
2
  type: Reference
3
3
  title: "Eval calibration log"
4
4
  description: "Append-only record of every PM override of an evaluator verdict."
5
- generated: {by: process:create-pmos, at: "2026-10-07T16:37:01Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-07T17:03:38Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -2,7 +2,7 @@
2
2
  type: Reference
3
3
  title: "Health budgets"
4
4
  description: "Closed registry of health-budget anchors (D59), edited only by the PM."
5
- generated: {by: process:create-pmos, at: "2026-10-07T16:37:01Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-07T17:03:38Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -2,7 +2,7 @@
2
2
  type: Reference
3
3
  title: "Discoveries and friction log"
4
4
  description: "Append-only record of what runs discovered and where the process rubbed."
5
- generated: {by: process:create-pmos, at: "2026-10-07T16:37:01Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-07T17:03:38Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -50,7 +50,7 @@ The dock holds your terminals, documents, conversations, skills and web pages. I
50
50
 
51
51
  - Press ⌘J (Ctrl-J), or the button on the rail, to open the dock. Press ⌘J again, or the fold button in the dock, to fold it to the rail.
52
52
  - The dock never opens by itself. A tab that opens while the dock is a rail waits in the dock until you open it.
53
- - The dock lists the sessions of the repository by initiative. A session is in the group of the initiative it is recorded on, or of its `initiative/<id>` branch. Then come **Not linked** (no initiative) and **Automatic** (started by the workspace itself, for example by a schedule or the day run). **Automatic** is hidden until you press **Show automatic** at the foot of the list (or beside the group select in the strip). A session in front, or one that waits on you, is always shown. Finished automatic sessions remove themselves a minute after a clean exit, and one with no output for 60 minutes is ended and removed. A failed one stays. The setting is in `state/session-policy.json`: `finish` (true or false) and `idleMinutes` (5 or more, or 0 for off). Then **Open** lists the tabs that are not sessions, for example documents and skills. Each one shows its title, what it is (for example PRD, Document or Web page) and its branch when it is not main. **✕** on an Open row closes the tab only. A terminal keeps running.
53
+ - The dock lists the sessions of the repository by initiative. A session is in the group of the initiative it is recorded on, or of its `initiative/<id>` branch. Then come **Not linked** (no initiative) and **Automatic** (anything you did not start yourself, for example a schedule, the day run, a note's summary or bullets, or a Jira push). **Automatic** is hidden until you press **Show automatic** at the foot of the list (or beside the group select in the strip). A session in front, or one held on a permission question, is always shown. An agent that has finished and reads "needs you" is hidden like the rest. Finished automatic sessions remove themselves a minute after a clean exit, and one with no output for 60 minutes is ended and removed. A failed one stays. The setting is in `state/session-policy.json`: `finish` (true or false) and `idleMinutes` (5 or more, or 0 for off). Then **Open** lists the tabs that are not sessions, for example documents and skills. Each one shows its title, what it is (for example PRD, Document or Web page) and its branch when it is not main. **✕** on an Open row closes the tab only. A terminal keeps running.
54
54
  - Each session row starts with a mark for its state: ● working, ◼ quiet, ◆ needs you, ◇ maybe blocked, ○ ended. Under the name, the row shows the state when the session is not working, the type of work when one is set, and the branch when it is not main and not the initiative's own branch. Running sessions come first and ended sessions last. Press the row to bring the session to the front.
55
55
  - Press the name of a group to fold it or open it. The number beside it counts its sessions. **Automatic** is folded while none of its sessions is running, because each run leaves an ended session. The group of the session in front always stays open, and the workspace keeps your choice for each group.
56
56
  - **⋯** and **✕** show on the row in front, on the row under the pointer and on the row with the keyboard focus. On a touch screen they always show.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  type: Guide
3
3
  title: "People: the people you work with in a repository, their interactions, tone, notes and shared work"
4
- timestamp: 2026-10-06
4
+ timestamp: 2026-10-07
5
5
  ---
6
6
 
7
7
  # People
@@ -37,16 +37,18 @@ A sighting of a new handle adds the person. A sighting with the full name of a p
37
37
 
38
38
  ## The list
39
39
 
40
- Type in **Find a person** to search names, handles and initiatives. **All**, **Team** and **External** filter the list. **Latest**, **Most** and **Name** sort it by the last interaction, the number of interactions or the name. Press a person to open their page beside the list.
40
+ People has the same two-card layout as the Agenda. The title says how many people there are. **All**, **Team** and **External** at the top right filter the list. The left card holds the list. Type in **Find a person** to search names, handles and initiatives. **Latest**, **Most** and **Name** sort it by the last interaction, the number of interactions or the name. Each row shows initials, the name, the role and organisation, and the tone as a coloured dot (a ? means you have not confirmed it). Press a person to open their page in the right card. Each card scrolls by itself.
41
41
 
42
42
  ## A person's page
43
43
 
44
+ The top of the page shows the person's name and role, a **Confirm** button when a tone is waiting, and a **⋯** menu. Three tiles under it give the number of interactions, the tone and the shared work.
45
+
44
46
  - **Interactions.** Newest first. Each line shows the date, the kind (Mail, Chat, Meeting, 1:1, Jira, Doc, Call or Mention), the line, and where it came from with its pointer and tone.
45
- - **Tone.** The mix of tones (warm, neutral, cautious, tense) and a proposed overall tone: the most frequent tone of the latest 10 interactions. When two tones are tied, the latest one wins. Press **Confirm** to keep the proposal, **Set another** to choose a different tone, or **Change** to clear your choice. Tone describes the exchanges, not the person.
47
+ - **Tone.** The mix of tones (warm, neutral, cautious, tense) and a proposed overall tone: the most frequent tone of the latest 10 interactions. When two tones are tied, the latest one wins. Press **Confirm** (at the top or here) to keep the proposal, **Set another** to choose a different tone, or **Change** to clear your choice. Tone describes the exchanges, not the person.
46
48
  - **Shared work.** The initiatives your interactions name, the plan items the person owns (with **Blocked** when the item is blocked), and the coming meetings that name them when calendar names are on. Nothing here is typed by hand.
47
- - **Notes.** Type a note and press **Add note**. Press **Remove** to remove one. Only you can add or remove a note. No command writes one.
49
+ - **Notes.** Type a note and press the **+** button (**Add note**). Press the **×** next to a note to remove it. Only you can add or remove a note. No command writes one.
48
50
  - **Identity and sources.** The person's handles, how many interactions each source gave, where People found them, and the file the person lives in.
49
- - **Remove from People.** Takes the person out. Later sightings of their handles stay out.
51
+ - **Remove from People.** In the **⋯** menu. Takes the person out. Later sightings of their handles stay out.
50
52
 
51
53
  ## Ways in from other places
52
54
 
@@ -16,6 +16,8 @@ Above the cards, Today shows the day in two columns.
16
16
 
17
17
  Press **Prepare my day** to start the day. It runs from the brief to the delivery of each agent job, and you do not press anything else until a draft is ready for you. The page [Guide: Today and the proactive engine](proactive-engine.md) describes each step.
18
18
 
19
+ The bar at the top of the day is one line. It shows the greeting, the date and how many goals are unfinished. While **Prepare my day** runs, it also shows the four steps (Read, Sort, Agents work, You approve) as a small progress bar; press a step to see its detail. When nothing runs, it says when the day was last prepared. **Sources** and **Check now** are icon buttons in the bar.
20
+
19
21
  If the morning-brief session stops before it submits a brief, the Read step says so and gives the reason. **Prepare my day** is then ready again, so press it to try once more. If the session says that its folder is not there, open the repository again from its current folder first.
20
22
 
21
23
  Each agent job runs in its own session, so you can open any job and follow or steer it. The workspace types the job's prompt into the session when the agent is ready to read, and presses Enter for you. When you approve or revise a job and its session is still open, the workspace uses that same session again. Prepare again works the same way: when the morning-brief session is still open and has finished its read, or has printed nothing for two minutes, the workspace types the read into that session and runs it again.
@@ -6,17 +6,22 @@
6
6
  * be done to them. Pure: the server passes in the rows and the clock, so the rules are testable without a server.
7
7
  *
8
8
  * Next step, Hand to agent, a skill draft and an alignment are NOT automatic: the PM pressed those, and often talks on.
9
+ * Any other origin is automatic.
9
10
  */
10
11
  'use strict';
11
12
 
12
- const AUTOMATIC_EXACT = Object.freeze(['day-task', 'day-check', 'my-work-read', 'docs:sync', 'docs:check']);
13
- const AUTOMATIC_PREFIX = Object.freeze(['schedule:']);
13
+ // The rule is "if the PM did not trigger it, it is automatic", so the list names what the PM DID trigger and everything
14
+ // else with an origin is automatic. A new background job is then hidden and cleaned up without anyone remembering to list it.
15
+ // Manual: a session opened with no origin, a Next step or Hand to agent press, a skill draft, an alignment, and the Whisper
16
+ // and final-pass setup shells (the PM pressed those and watches them install).
17
+ const MANUAL_EXACT = Object.freeze(['next-step', 'plan-item', 'notes-whisper-setup', 'notes-final-setup']);
18
+ const MANUAL_PREFIX = Object.freeze(['skill-draft:', 'align:']);
14
19
 
15
- /** True when the workspace, not the PM, started a session with this origin. */
20
+ /** True when the PM did not trigger a session with this origin: a schedule, the day run, a doc sync, a note's passes, Jira. */
16
21
  function isAutomatic(origin) {
17
22
  const o = String(origin || '');
18
23
  if (!o) return false;
19
- return AUTOMATIC_EXACT.includes(o) || AUTOMATIC_PREFIX.some((p) => o.startsWith(p));
24
+ return !(MANUAL_EXACT.includes(o) || MANUAL_PREFIX.some((p) => o.startsWith(p)));
20
25
  }
21
26
 
22
27
  const DEFAULT_POLICY = Object.freeze({ finish: true, idleMinutes: 60 });
@@ -57,9 +62,10 @@ function decide(s, policy, now) {
57
62
  return age >= FINISH_GRACE_MS ? 'remove' : null;
58
63
  }
59
64
  if (!policy.idleMinutes) return null;
60
- // waiting on a person is not idle: the session is asking something
61
- if (s.shown === 'needs' || s.shown === 'blocked') return null;
65
+ // A call waiting for permission is a real ask. 'needs' (the agent's turn is over) is NOT: a finished agent always reads
66
+ // 'needs you', so exempting it would keep every finished job for ever. The idle clock still gives a late question time.
67
+ if (s.shown === 'blocked') return null;
62
68
  return age >= policy.idleMinutes * 60 * 1000 ? 'end' : null;
63
69
  }
64
70
 
65
- module.exports = { isAutomatic, normalizePolicy, decide, endedCleanly, DEFAULT_POLICY, FINISH_GRACE_MS, AUTOMATIC_EXACT, AUTOMATIC_PREFIX };
71
+ module.exports = { isAutomatic, normalizePolicy, decide, endedCleanly, DEFAULT_POLICY, FINISH_GRACE_MS, MANUAL_EXACT, MANUAL_PREFIX };
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.87.0",
3
+ "version": "0.88.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pmos-workspace",
9
- "version": "0.87.0",
9
+ "version": "0.88.0",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@xterm/addon-fit": "^0.11.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.87.0",
3
+ "version": "0.88.0",
4
4
  "private": true,
5
5
  "description": "PMOS Terminal Workspace — local companion hosting real PTYs beside the library and gates (D62)",
6
6
  "license": "MIT",
@@ -3406,15 +3406,17 @@
3406
3406
  flex-wrap: wrap;
3407
3407
  align-items: center;
3408
3408
  gap: 8px 10px;
3409
- padding: 8px 12px;
3409
+ padding: 4px 8px 4px 12px;
3410
+ min-height: 40px;
3410
3411
  border: 1px solid var(--border);
3412
+ border-radius: 8px;
3411
3413
  background: var(--card);
3412
3414
  }
3413
- .td-day__hello { flex: 0 1 220px; min-width: 0; overflow: hidden; text-overflow: ellipsis; font-size: 14px; color: var(--foreground); white-space: nowrap; }
3415
+ .td-day__hello { flex: 0 0 auto; max-width: 100%; min-width: 0; overflow: hidden; text-overflow: ellipsis; font-size: 14px; color: var(--foreground); white-space: nowrap; }
3414
3416
  .td-day__dim { color: var(--muted-foreground); }
3415
3417
  .td-day__warn { color: var(--warning); }
3416
3418
  .td-day__mono { font-family: var(--mono); font-variant-numeric: tabular-nums; font-size: 12px; }
3417
- .td-day__steps { display: flex; flex-wrap: wrap; gap: 2px; list-style: none; margin: 0; padding: 0; }
3419
+ .td-day__steps { display: flex; flex-wrap: nowrap; gap: 3px; list-style: none; margin: 0; padding: 0; }
3418
3420
  .td-day__ts { border-radius: 6px; gap: 6px; padding: 0 6px; letter-spacing: 0.04em; color: var(--muted-foreground); }
3419
3421
  .td-day__ts[data-step-state="done"], .td-day__ts[data-step-state="now"] { color: var(--foreground); }
3420
3422
  .td-day__ts[data-step-state="now"] { background: var(--muted); }
@@ -3424,7 +3426,11 @@
3424
3426
  .td-day__ts[data-step-state="now"] .td-day__tsdot { background: var(--foreground); animation: td-day-pulse 1.2s infinite; }
3425
3427
  @keyframes td-day-pulse { 50% { opacity: 0.3; } }
3426
3428
  @media (prefers-reduced-motion: reduce) { .td-day__ts[data-step-state="now"] .td-day__tsdot { animation: none; } }
3427
- .td-day__status { flex: 1 1 0; min-width: 0; font-size: 12px; color: var(--muted-foreground); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
3429
+ .td-day__seg { display: block; width: 22px; height: 4px; border-radius: 6px; background: var(--border); }
3430
+ .td-day__ts[data-step-state="done"] .td-day__seg { background: currentColor; }
3431
+ .td-day__ts[data-step-state="now"] .td-day__seg { background: var(--foreground); animation: td-day-pulse 1.2s infinite; }
3432
+ @media (prefers-reduced-motion: reduce) { .td-day__ts[data-step-state="now"] .td-day__seg { animation: none; } }
3433
+ .td-day__status { flex: 1 1 0; min-width: 80px; text-align: right; font-size: 12px; color: var(--muted-foreground); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
3428
3434
  .td-day__check { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: var(--muted-foreground); }
3429
3435
  .td-day__run { margin: 0; font-size: 12px; line-height: 16px; color: var(--foreground); }
3430
3436
  .td-day__run--failed { color: var(--destructive); }
@@ -5343,6 +5349,9 @@
5343
5349
  .rp-doc__scroll {
5344
5350
  -webkit-overflow-scrolling: touch;
5345
5351
  }
5352
+ .rp-people-sec summary {
5353
+ min-height: 44px;
5354
+ }
5346
5355
  }
5347
5356
 
5348
5357
  /* Docs (docs-three-way-sync): one line per copy — a status dot, the title over the copy's link, one sentence, one action. */
@@ -5818,7 +5827,7 @@
5818
5827
  overflow-wrap: anywhere;
5819
5828
  }
5820
5829
 
5821
- /* People (people-directory): what waits for the PM, the list beside the chosen person's page. */
5830
+ /* People (people-directory): the PM's people directory — list on left, person's page on right. */
5822
5831
  .rp-people {
5823
5832
  display: flex;
5824
5833
  flex-direction: column;
@@ -5826,16 +5835,12 @@
5826
5835
  }
5827
5836
  .rp-people-top {
5828
5837
  display: flex;
5829
- flex-wrap: wrap;
5830
5838
  align-items: center;
5839
+ justify-content: space-between;
5831
5840
  gap: 12px;
5832
5841
  }
5833
5842
  .rp-people-top > .rp-head__title {
5834
- margin-right: auto;
5835
- }
5836
- .rp-people-find {
5837
- flex: 1 1 260px;
5838
- max-width: 420px;
5843
+ margin: 0;
5839
5844
  }
5840
5845
  .rp-people-body {
5841
5846
  display: flex;
@@ -5843,59 +5848,257 @@
5843
5848
  gap: 16px;
5844
5849
  }
5845
5850
  .rp-people-review {
5846
- padding: 12px;
5847
- border: 1px solid var(--border);
5851
+ display: flex;
5852
+ flex-direction: column;
5853
+ gap: 8px;
5854
+ }
5855
+ .rp-people-merge {
5856
+ display: flex;
5857
+ align-items: flex-start;
5858
+ gap: 12px;
5859
+ padding: 10px 12px;
5860
+ border-radius: 8px;
5861
+ background: var(--background);
5862
+ box-shadow: 0 0 0 1px var(--border);
5863
+ }
5864
+ .rp-people-merge__dot {
5865
+ width: 6px;
5866
+ height: 6px;
5867
+ border-radius: 50%;
5868
+ background: var(--foreground);
5869
+ flex: none;
5870
+ margin-top: 4px;
5871
+ }
5872
+ .rp-people-merge__text {
5873
+ flex: 1;
5874
+ min-width: 0;
5875
+ }
5876
+ .rp-people-merge__title {
5877
+ margin: 0;
5878
+ font-size: 14px;
5879
+ font-weight: 500;
5880
+ color: var(--foreground);
5881
+ }
5882
+ .rp-people-merge__why {
5883
+ margin: 2px 0 0;
5884
+ font-size: 12px;
5885
+ color: var(--muted-foreground);
5848
5886
  }
5849
5887
  .rp-people-grid {
5850
5888
  display: grid;
5851
- grid-template-columns: minmax(260px, 2fr) 3fr;
5852
- gap: 24px;
5889
+ grid-template-columns: minmax(300px, 2fr) 3fr;
5890
+ gap: 16px;
5853
5891
  align-items: start;
5892
+ height: calc(100vh - 220px);
5893
+ min-height: 420px;
5854
5894
  }
5855
- .rp-people-list {
5895
+ .rp-people-card {
5856
5896
  display: flex;
5857
5897
  flex-direction: column;
5858
- gap: 8px;
5859
- min-width: 0;
5898
+ gap: 10px;
5899
+ padding: 10px 12px;
5900
+ border-radius: 8px;
5901
+ background: var(--card);
5902
+ box-shadow: 0 0 0 1px var(--border);
5903
+ overflow: hidden;
5860
5904
  }
5861
- .rp-people-views {
5905
+ .rp-people-card__header {
5862
5906
  display: flex;
5863
- flex-wrap: wrap;
5864
- align-items: center;
5865
5907
  justify-content: space-between;
5866
- gap: 8px;
5867
- border-bottom: 1px solid var(--border);
5908
+ align-items: center;
5909
+ margin: 0;
5868
5910
  }
5869
- .rp-people-row {
5870
- border-top: 1px solid var(--border);
5911
+ .rp-people-card__label {
5912
+ display: flex;
5913
+ flex: 1;
5914
+ justify-content: space-between;
5915
+ font: 11px var(--mono);
5916
+ text-transform: uppercase;
5917
+ color: var(--muted-foreground);
5918
+ margin: 0;
5919
+ letter-spacing: 0.04em;
5871
5920
  }
5872
- .rp-people-row:last-child {
5873
- border-bottom: 1px solid var(--border);
5921
+ .rp-people-card__count {
5922
+ margin-left: auto;
5923
+ color: var(--muted-foreground);
5874
5924
  }
5875
- .rp-people-row--open {
5876
- box-shadow: inset 2px 0 0 var(--foreground);
5925
+ .rp-people-card__list {
5926
+ display: flex;
5927
+ flex-direction: column;
5928
+ gap: 2px;
5929
+ overflow: auto;
5930
+ min-width: 0;
5877
5931
  }
5878
- .rp-people-page {
5932
+ .rp-people-card__list [role="list"] {
5879
5933
  display: flex;
5880
5934
  flex-direction: column;
5881
- gap: 16px;
5935
+ gap: 2px;
5936
+ }
5937
+ .rp-people-item {
5938
+ border-radius: 6px;
5939
+ }
5940
+ .rp-people-item:has([aria-current="true"]) {
5941
+ background: var(--accent);
5942
+ box-shadow: 0 0 0 1px var(--ring);
5943
+ }
5944
+ .rp-people-row__avatar {
5945
+ width: 32px;
5946
+ height: 32px;
5947
+ border-radius: 6px;
5948
+ background: var(--muted);
5949
+ display: grid;
5950
+ place-items: center;
5951
+ font-size: 12px;
5952
+ font-weight: 600;
5953
+ flex: none;
5954
+ color: var(--foreground);
5955
+ }
5956
+ .rp-people-row__text {
5957
+ flex: 1;
5958
+ min-width: 0;
5959
+ }
5960
+ .rp-people-row__name {
5961
+ margin: 0;
5962
+ font-size: 14px;
5963
+ font-weight: 500;
5964
+ white-space: nowrap;
5965
+ overflow: hidden;
5966
+ text-overflow: ellipsis;
5967
+ }
5968
+ .rp-people-row__meta {
5969
+ margin: 2px 0 0;
5970
+ font-size: 12px;
5971
+ color: var(--muted-foreground);
5972
+ white-space: nowrap;
5973
+ overflow: hidden;
5974
+ text-overflow: ellipsis;
5975
+ }
5976
+ .rp-people-row__tone {
5977
+ display: inline-flex;
5978
+ align-items: center;
5979
+ gap: 5px;
5980
+ font-size: 12px;
5981
+ color: var(--muted-foreground);
5982
+ flex: none;
5983
+ }
5984
+ .rp-people-row__tone-dot {
5985
+ width: 8px;
5986
+ height: 8px;
5987
+ border-radius: 50%;
5988
+ background: var(--muted-foreground);
5989
+ flex: none;
5990
+ }
5991
+ .rp-people-row__tone-dot.warm { background: var(--foreground); }
5992
+ .rp-people-row__tone-dot.cautious { background: var(--warning); }
5993
+ .rp-people-row__tone-dot.tense { background: var(--destructive); }
5994
+ .rp-people-row__tone-dot--proposed {
5995
+ background: transparent;
5996
+ box-shadow: inset 0 0 0 1.5px currentColor;
5997
+ }
5998
+ .rp-people-page__header {
5999
+ display: flex;
6000
+ align-items: flex-start;
6001
+ justify-content: space-between;
6002
+ gap: 12px;
6003
+ }
6004
+ .rp-people-page__info {
6005
+ display: flex;
6006
+ align-items: flex-start;
6007
+ gap: 12px;
5882
6008
  min-width: 0;
5883
6009
  }
5884
- .rp-people-who__name {
6010
+ .rp-people-page__avatar {
6011
+ width: 48px;
6012
+ height: 48px;
6013
+ border-radius: 6px;
6014
+ background: var(--muted);
6015
+ display: grid;
6016
+ place-items: center;
6017
+ font-size: 16px;
6018
+ font-weight: 600;
6019
+ flex: none;
6020
+ color: var(--foreground);
6021
+ }
6022
+ .rp-people-page__text {
6023
+ flex: 1;
6024
+ min-width: 0;
6025
+ }
6026
+ .rp-people-page__name {
5885
6027
  margin: 0;
5886
6028
  font-size: 20px;
5887
6029
  font-weight: 500;
5888
6030
  }
6031
+ .rp-people-page__actions {
6032
+ display: flex;
6033
+ align-items: center;
6034
+ gap: 8px;
6035
+ flex: none;
6036
+ }
6037
+ .rp-people-page__tiles {
6038
+ display: grid;
6039
+ grid-template-columns: repeat(3, minmax(0, 1fr));
6040
+ gap: 6px;
6041
+ }
6042
+ .rp-people-tile {
6043
+ display: grid;
6044
+ gap: 2px;
6045
+ padding: 8px 10px;
6046
+ border-radius: 6px;
6047
+ background: var(--muted);
6048
+ min-width: 0;
6049
+ }
6050
+ .rp-people-tile__label {
6051
+ margin: 0;
6052
+ font: 11px var(--mono);
6053
+ text-transform: uppercase;
6054
+ color: var(--muted-foreground);
6055
+ letter-spacing: 0.04em;
6056
+ }
6057
+ .rp-people-tile b {
6058
+ font-size: 14px;
6059
+ font-weight: 500;
6060
+ white-space: nowrap;
6061
+ overflow: hidden;
6062
+ text-overflow: ellipsis;
6063
+ color: var(--foreground);
6064
+ display: flex;
6065
+ align-items: center;
6066
+ gap: 5px;
6067
+ }
6068
+ .rp-people-tile__dot {
6069
+ display: inline-block;
6070
+ vertical-align: baseline;
6071
+ width: 8px;
6072
+ height: 8px;
6073
+ border-radius: 50%;
6074
+ background: var(--muted-foreground);
6075
+ flex: none;
6076
+ }
6077
+ .rp-people-tile__dot--confirmed { background: currentColor; }
6078
+ .rp-people-tile__dot--proposed {
6079
+ background: transparent;
6080
+ box-shadow: inset 0 0 0 1.5px currentColor;
6081
+ }
6082
+ .rp-people-page__scroll {
6083
+ display: flex;
6084
+ flex-direction: column;
6085
+ gap: 14px;
6086
+ overflow: auto;
6087
+ min-width: 0;
6088
+ }
5889
6089
  .rp-people-sec {
5890
6090
  display: flex;
5891
6091
  flex-direction: column;
5892
- gap: 8px;
6092
+ gap: 6px;
6093
+ padding-top: 12px;
6094
+ border-top: 1px solid var(--border);
5893
6095
  }
5894
6096
  .rp-people-sec__title {
5895
6097
  margin: 0;
5896
- font-size: 12px;
5897
- font-weight: 500;
6098
+ font: 11px var(--mono);
6099
+ text-transform: uppercase;
5898
6100
  color: var(--muted-foreground);
6101
+ letter-spacing: 0.04em;
5899
6102
  }
5900
6103
  .rp-people-hint {
5901
6104
  margin: 0;
@@ -5915,36 +6118,66 @@
5915
6118
  .rp-people-list-plain {
5916
6119
  display: flex;
5917
6120
  flex-direction: column;
5918
- gap: 8px;
6121
+ gap: 6px;
5919
6122
  margin: 0;
5920
6123
  padding: 0;
5921
6124
  list-style: none;
5922
6125
  }
5923
6126
  .rp-people-int {
5924
6127
  display: flex;
5925
- flex-direction: column;
5926
- gap: 2px;
5927
- padding-bottom: 8px;
5928
- border-bottom: 1px solid var(--border);
6128
+ gap: 10px;
6129
+ align-items: flex-start;
6130
+ padding: 4px 0;
5929
6131
  }
5930
- .rp-people-int__when,
5931
- .rp-people-int__src {
5932
- font-size: 12px;
6132
+ .rp-people-int__icon {
6133
+ width: 28px;
6134
+ height: 28px;
6135
+ border-radius: 6px;
6136
+ background: var(--muted);
6137
+ display: grid;
6138
+ place-items: center;
5933
6139
  color: var(--muted-foreground);
6140
+ flex: none;
5934
6141
  }
5935
- .rp-people-int__src {
5936
- overflow-wrap: anywhere;
6142
+ .rp-people-int div {
6143
+ min-width: 0;
6144
+ flex: 1;
5937
6145
  }
5938
6146
  .rp-people-int__text,
5939
6147
  .rp-people-shared,
5940
6148
  .rp-people-handle {
6149
+ margin: 0;
5941
6150
  font-size: 14px;
5942
6151
  }
6152
+ .rp-people-int small {
6153
+ display: block;
6154
+ margin-top: 2px;
6155
+ font-size: 12px;
6156
+ color: var(--muted-foreground);
6157
+ }
5943
6158
  .rp-people-shared {
5944
6159
  display: flex;
5945
6160
  flex-wrap: wrap;
5946
6161
  align-items: center;
5947
- gap: 8px;
6162
+ gap: 6px;
6163
+ }
6164
+ .rp-people-chips {
6165
+ display: flex;
6166
+ flex-wrap: wrap;
6167
+ gap: 6px;
6168
+ }
6169
+ .rp-people-chip {
6170
+ display: inline-flex;
6171
+ gap: 6px;
6172
+ align-items: center;
6173
+ height: 26px;
6174
+ padding: 0 10px;
6175
+ border-radius: 6px;
6176
+ background: var(--muted);
6177
+ font-size: 12px;
6178
+ }
6179
+ .rp-people-chip.warn {
6180
+ color: var(--warning);
5948
6181
  }
5949
6182
  .rp-people-note {
5950
6183
  display: flex;
@@ -5961,29 +6194,43 @@
5961
6194
  }
5962
6195
  .rp-people-noteadd {
5963
6196
  display: flex;
5964
- flex-direction: column;
5965
6197
  gap: 8px;
5966
6198
  }
5967
- .rp-people-links {
5968
- display: flex;
5969
- flex-wrap: wrap;
5970
- gap: 4px;
5971
- }
5972
- .rp-people-links:empty {
5973
- display: none;
6199
+ .rp-people-noteadd > :first-child {
6200
+ flex: 1;
5974
6201
  }
5975
- .rp-people-rel {
5976
- display: flex;
5977
- flex-direction: column;
5978
- gap: 8px;
6202
+ .rp-people-noteadd textarea {
6203
+ min-height: 34px;
6204
+ height: 34px;
6205
+ resize: none;
5979
6206
  }
5980
6207
  .rp-people-file {
5981
6208
  font-family: var(--mono);
5982
6209
  overflow-wrap: anywhere;
5983
6210
  }
6211
+ .rp-people-sec summary {
6212
+ list-style: none;
6213
+ cursor: pointer;
6214
+ color: var(--muted-foreground);
6215
+ font-size: 12px;
6216
+ display: flex;
6217
+ gap: 6px;
6218
+ align-items: center;
6219
+ }
6220
+ .rp-people-sec summary::-webkit-details-marker {
6221
+ display: none;
6222
+ }
6223
+ .rp-people-sec summary:focus-visible {
6224
+ outline: 2px solid color-mix(in oklab, var(--ring) 50%, var(--foreground));
6225
+ outline-offset: 2px;
6226
+ }
5984
6227
  @media (max-width: 760px) {
5985
6228
  .rp-people-grid {
5986
6229
  grid-template-columns: 1fr;
6230
+ height: auto;
6231
+ }
6232
+ .rp-people-top {
6233
+ flex-wrap: wrap;
5987
6234
  }
5988
6235
  }
5989
6236
 
@@ -2,24 +2,22 @@
2
2
  * tone, notes and shared work (initiative people-directory, PRD R5 to R11, proposed D90).
3
3
  *
4
4
  * A DESTINATION MODULE (registry.js): mountDestination(host, ctx) on every render with the same host, disposeDestination(host).
5
- * It keeps ONE persistent root per host; the header (title and search) is built once, so typing never loses focus, and
6
- * only the body is rebuilt when its own data changed.
5
+ * It keeps ONE persistent root per host; the header is built once (h2 title on left, group switch on right), so typing never
6
+ * loses focus. The search field lives in the left card and is built once. Only the body is rebuilt when its own data changed.
7
7
  *
8
- * What it shows, for the repository the tab names (or the open one):
9
- * - Waiting for you: possible duplicates (Merge · Keep separate) and how many tones wait for a confirm. A new person
10
- * is added, not listed here (the PM, 2026-10-06): Remove from People on their page keeps them out;
11
- * - the list: search over names, handles and initiatives, All · Team · External, sort by latest, most or name;
12
- * - the person's page: who, interactions with where each came from, tone (Confirm · Set another · Change), shared
13
- * work (initiatives, items they own with Blocked shown, coming meetings), notes (Add · Remove), and identity and
14
- * sources (handles, how many interactions each source gave, the file the person lives in).
15
- * Every press is a page press to POST /api/people/decide. Nothing here reads a source: GET /api/people records the names
16
- * the workspace's own records hold, and agent sessions record what they saw through `pmos people record`. A link from another place opens a person through the tab's slug (people-link.js).
17
- * In fixture mode it reads nothing.
8
+ * Layout (desktop): header row, "Waiting for you" merge candidates as bordered rows, then two cards side-by-side (minmax(300px,2fr) 3fr,
9
+ * gap 16px, fixed height calc(100vh - 220px) min-height 420px, internal scrolling). Left card: "PEOPLE" label + count, search field,
10
+ * full-width sort tabs, list of people. Right card: person detail with avatar, name, meta, Confirm / ⋯ menu, 3 tiles (Interactions,
11
+ * Tone, Shared work), scrollable sections (Interactions, Shared work, Notes, Identity and sources).
12
+ *
13
+ * What it shows: waiting for you (possible merge candidates), the people list (search, filter, sort), and person's page
14
+ * (who, interactions, tone, shared work, notes, identity). Every press is a page press to POST /api/people/decide.
15
+ * A link from another place opens a person through the tab's slug (people-link.js). In fixture mode it reads nothing.
18
16
  */
19
17
 
20
18
  import { el } from './dom.js';
21
- import { button, row, field, toast, empty, tabs, badge, menu, ensureComponentsCss } from './primitives.js';
22
- import { ensureSprite } from './icons.js';
19
+ import { button, iconButton, row, field, toast, empty, tabs, badge, menu, ensureComponentsCss } from './primitives.js';
20
+ import { ensureSprite, icon } from './icons.js';
23
21
  import { pmGet, pmPost } from './http.js';
24
22
  import { wsOf } from './data.js';
25
23
  import { registerCommand, openDestination } from './registry.js';
@@ -174,22 +172,18 @@ function review(inst) {
174
172
  const rows = [];
175
173
  for (const m of r.merges) {
176
174
  const key = m.handle.kind + ':' + m.handle.value;
177
- rows.push(el('div', { class: 'rp-people-row', role: 'listitem', 'data-people-merge': key },
178
- row({
179
- title: COPY.same(m.handle.value, m.personName),
180
- meta: [m.why],
181
- trail: [
182
- button({ label: COPY.merge, variant: 'outline', size: 'sm', attrs: { 'data-people-act': 'merge', 'data-people-key': key },
183
- onClick: () => decide(inst, { act: 'merge', person: m.person, handle: m.handle }, COPY.merged) }),
184
- button({ label: COPY.keep, variant: 'ghost', size: 'sm', attrs: { 'data-people-act': 'keep-separate', 'data-people-key': key },
185
- onClick: () => decide(inst, { act: 'keep-separate', a: m.a, b: m.b }, COPY.kept) }),
186
- ],
187
- })));
175
+ rows.push(el('div', { class: 'rp-people-merge', role: 'listitem', 'data-people-merge': key },
176
+ el('div', { class: 'rp-people-merge__dot' }),
177
+ el('div', { class: 'rp-people-merge__text' },
178
+ el('div', { class: 'rp-people-merge__title' }, COPY.same(m.handle.value, m.personName)),
179
+ el('div', { class: 'rp-people-merge__why' }, m.why)),
180
+ button({ label: COPY.merge, icon: 'git-merge', variant: 'outline', size: 'sm', attrs: { 'data-people-act': 'merge', 'data-people-key': key },
181
+ onClick: () => decide(inst, { act: 'merge', person: m.person, handle: m.handle }, COPY.merged) }),
182
+ button({ label: COPY.keep, variant: 'ghost', size: 'sm', attrs: { 'data-people-act': 'keep-separate', 'data-people-key': key },
183
+ onClick: () => decide(inst, { act: 'keep-separate', a: m.a, b: m.b }, COPY.kept) })));
188
184
  }
189
185
  if (r.tones.length) rows.push(el('p', { class: 'rp-people-hint', 'data-people-tones': String(r.tones.length) }, COPY.tonesWait(r.tones.length)));
190
- return el('section', { class: 'rp-people-review', 'data-people-review': '' },
191
- el('h3', { class: 'rp-people-sec__title' }, `${COPY.waiting} · ${r.count}`),
192
- el('div', { role: 'list' }, rows));
186
+ return el('div', { class: 'rp-people-review', 'data-people-review': '' }, ...rows);
193
187
  }
194
188
 
195
189
  /* ── the list ────────────────────────────────────────────────────────────────────────────────────────────────────── */
@@ -212,11 +206,15 @@ function shownPeople(inst) {
212
206
  function personLine(inst, p) {
213
207
  const open = inst.open === p.id;
214
208
  const t = toneOf(p);
215
- return el('div', { class: 'rp-people-row' + (open ? ' rp-people-row--open' : ''), role: 'listitem', 'data-people-person': p.id },
209
+ const toneClass = t === 'tense' || t === 'cautious' || t === 'warm' ? t : '';
210
+ const initials = p.name.split(/\s+/).map((w) => w[0]).join('').toUpperCase();
211
+ return el('div', { class: 'rp-people-item', role: 'listitem', 'data-people-person': p.id },
216
212
  row({
217
213
  title: p.name,
218
- meta: [who(p), p.last ? `${p.count} · ${day(p.last)}` : String(p.count)],
219
- trail: t ? [badge({ text: p.tone.confirmed ? t : t + '?', tone: t === 'tense' ? 'destructive' : t === 'cautious' ? 'warning' : 'outline', attrs: { 'data-people-tone': t } })] : [],
214
+ meta: [[who(p), p.external ? COPY.external : null].filter(Boolean).join(' · ')],
215
+ lead: [el('span', { class: 'rp-people-row__avatar' }, initials)],
216
+ trail: t ? [el('span', { class: 'rp-people-row__tone' + (p.tone.confirmed ? '' : ' rp-people-row__tone--proposed'), 'data-people-tone': t },
217
+ el('i', { class: 'rp-people-row__tone-dot ' + toneClass }), p.tone.confirmed ? t : t + '?')] : [],
220
218
  onOpen: () => { inst.open = p.id; draw(inst); },
221
219
  openLabel: `${p.name}, ${who(p)}`,
222
220
  openAttrs: { 'aria-current': open ? 'true' : null, 'data-people-open': p.id },
@@ -226,20 +224,19 @@ function personLine(inst, p) {
226
224
  function listPane(inst) {
227
225
  const v = inst.view;
228
226
  const all = v.people.filter((p) => matches(p, inst.q.trim().toLowerCase()));
229
- const controls = el('div', { class: 'rp-people-views' },
230
- tabs({ variant: 'line', value: inst.group, ariaLabel: 'Group', attrs: { 'data-people-group': '' },
231
- items: [{ id: 'all', label: COPY.all, count: all.length }, { id: 'team', label: COPY.team, count: all.filter((p) => !p.external).length },
232
- { id: 'external', label: COPY.external, count: all.filter((p) => p.external).length }],
233
- onChange: (val) => { inst.group = val; draw(inst); } }),
234
- tabs({ variant: 'segmented', value: inst.sort, ariaLabel: 'Sort', attrs: { 'data-people-sort': '' },
235
- items: [{ id: 'latest', label: COPY.latest }, { id: 'most', label: COPY.most }, { id: 'name', label: COPY.name }],
236
- onChange: (val) => { inst.sort = val; draw(inst); } }));
237
227
  const list = shownPeople(inst);
238
228
  let rows;
239
229
  if (!v.people.length) rows = [el('p', { class: 'rp-section__empty' }, COPY.none)];
240
230
  else if (!list.length) rows = [el('p', { class: 'rp-section__empty' }, COPY.noMatch(inst.q.trim()))];
241
231
  else rows = [el('div', { role: 'list', 'data-people-list': '' }, list.map((p) => personLine(inst, p)))];
242
- return el('div', { class: 'rp-people-list' }, controls, ...rows);
232
+ return el('div', { class: 'rp-people-card' },
233
+ el('div', { class: 'rp-people-card__header' },
234
+ el('p', { class: 'rp-people-card__label' }, COPY.title, el('span', { class: 'rp-people-card__count' }, all.length))),
235
+ inst.head.find,
236
+ tabs({ variant: 'segmented', full: true, value: inst.sort, ariaLabel: 'Sort', attrs: { 'data-people-sort': '' },
237
+ items: [{ id: 'latest', label: COPY.latest }, { id: 'most', label: COPY.most }, { id: 'name', label: COPY.name }],
238
+ onChange: (val) => { inst.sort = val; draw(inst); } }),
239
+ el('div', { class: 'rp-people-card__list' }, ...rows));
243
240
  }
244
241
 
245
242
  /* ── the person's page ───────────────────────────────────────────────────────────────────────────────────────────── */
@@ -250,11 +247,16 @@ function section(title, kids, attrs) {
250
247
 
251
248
  function interactionsSec(p) {
252
249
  if (!p.interactions.length) return section(COPY.interactions, [el('p', { class: 'rp-people-hint' }, COPY.noInteractions)], { 'data-people-sec': 'interactions' });
253
- return section(`${COPY.interactions} · ${p.count}`, [el('ol', { class: 'rp-people-ints' }, p.interactions.slice(0, 50).map((i) =>
254
- el('li', { class: 'rp-people-int', 'data-people-int': i.date },
255
- el('span', { class: 'rp-people-int__when' }, `${day(i.date)} · ${i.kind}`),
256
- el('span', { class: 'rp-people-int__text' }, i.text),
257
- el('span', { class: 'rp-people-int__src' }, [SOURCE[i.source] || i.source, i.pointer, i.tone].filter(Boolean).join(' · ')))))],
250
+ const icons = { mail: 'mail', live: 'message-square', calendar: 'calendar', jira: 'ticket', gather: 'file-text', plan: 'file-text', notes: 'file-text' };
251
+ return section(`${COPY.interactions} · ${p.count}`, [el('div', { class: 'rp-people-ints' }, p.interactions.slice(0, 50).map((i) => {
252
+ const iconName = icons[i.source] || 'file-text';
253
+ return el('div', { class: 'rp-people-int', 'data-people-int': i.date },
254
+ el('span', { class: 'rp-people-int__icon' }, icon(iconName)),
255
+ el('div', {},
256
+ el('div', { class: 'rp-people-int__text' }, i.text),
257
+ el('small', {}, `${day(i.date)} · ${i.kind}`),
258
+ el('small', { class: 'rp-people-int__src' }, [SOURCE[i.source] || i.source, i.pointer, i.tone].filter(Boolean).join(' · '))));
259
+ }))],
258
260
  { 'data-people-sec': 'interactions' });
259
261
  }
260
262
 
@@ -267,8 +269,6 @@ function toneSec(inst, p) {
267
269
  else if (t.proposed) kids.push(el('p', { class: 'rp-people-tone', 'data-people-proposed': t.proposed }, COPY.proposed(t.proposed)));
268
270
  if (mix) kids.push(el('p', { class: 'rp-people-hint', 'data-people-mix': '' }, mix));
269
271
  const acts = [];
270
- if (!t.confirmed && t.proposed) acts.push(button({ label: COPY.confirm, variant: 'primary', size: 'sm', attrs: { 'data-people-act': 'tone-confirm' },
271
- onClick: () => decide(inst, { act: 'tone', person: p.id, tone: t.proposed }, COPY.toneSaved) }));
272
272
  const another = button({ label: COPY.setAnother, variant: 'outline', size: 'sm', iconEnd: 'chevron-down', attrs: { 'data-people-act': 'tone-set', 'aria-haspopup': 'menu' },
273
273
  onClick: () => menu({ anchor: another, ariaLabel: COPY.setAnother,
274
274
  items: TONES.map((x) => ({ label: x, onSelect: () => decide(inst, { act: 'tone', person: p.id, tone: x }, COPY.toneSaved) })) }) });
@@ -281,25 +281,24 @@ function toneSec(inst, p) {
281
281
 
282
282
  function sharedSec(p) {
283
283
  const s = p.shared;
284
- const lines = [];
285
- for (const g of s.initiatives) lines.push(el('li', { class: 'rp-people-shared', 'data-people-shared': 'initiative' }, `${g.id} · ${g.count} interaction${g.count === 1 ? '' : 's'}`));
284
+ const chips = [];
285
+ for (const g of s.initiatives) chips.push(el('span', { class: 'rp-people-chip', 'data-people-shared': 'initiative' }, icon('target'), `${g.id} · ${g.count} interaction${g.count === 1 ? '' : 's'}`));
286
286
  for (const it of s.items) {
287
- lines.push(el('li', { class: 'rp-people-shared', 'data-people-shared': 'item', 'data-people-item': it.id },
288
- `${COPY.owns} ${it.title || it.id}${it.initiative ? ' · ' + it.initiative : ''} · ${it.state}`,
289
- it.blocked ? badge({ text: COPY.blocked, tone: 'warning', attrs: { title: it.reason || COPY.blocked } }) : null));
287
+ const chip = el('span', { class: 'rp-people-chip', 'data-people-shared': 'item', 'data-people-item': it.id }, icon(it.blocked ? 'ban' : 'target'), `${COPY.owns} ${it.title || it.id}${it.blocked ? ' · ' + COPY.blocked : ''}`);
288
+ if (it.blocked) chip.classList.add('warn');
289
+ chips.push(chip);
290
290
  }
291
291
  for (const m of s.meetings) {
292
292
  const when = new Date(m.start);
293
- lines.push(el('li', { class: 'rp-people-shared', 'data-people-shared': 'meeting' },
294
- `${COPY.meeting} · ${when.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' })} ${when.toTimeString().slice(0, 5)} · ${m.title}`));
293
+ chips.push(el('span', { class: 'rp-people-chip', 'data-people-shared': 'meeting' }, icon('calendar'), `${when.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' })} · ${m.title}`));
295
294
  }
296
- return section(COPY.shared, lines.length ? [el('ul', { class: 'rp-people-list-plain' }, lines)] : [el('p', { class: 'rp-people-hint' }, COPY.noShared)], { 'data-people-sec': 'shared' });
295
+ return section(COPY.shared, chips.length ? [el('div', { class: 'rp-people-chips' }, chips)] : [el('p', { class: 'rp-people-hint' }, COPY.noShared)], { 'data-people-sec': 'shared' });
297
296
  }
298
297
 
299
298
  function notesSec(inst, p) {
300
- const list = p.notes.map((n) => el('li', { class: 'rp-people-note', 'data-people-note': n.id },
299
+ const list = p.notes.map((n) => el('div', { class: 'rp-people-note', 'data-people-note': n.id },
301
300
  el('span', { class: 'rp-people-note__text' }, n.text),
302
- button({ label: COPY.remove, variant: 'ghost', size: 'sm', attrs: { 'data-people-act': 'note-remove', 'data-people-key': n.id },
301
+ iconButton({ icon: 'x', ariaLabel: COPY.remove, size: 'sm', attrs: { 'data-people-act': 'note-remove', 'data-people-key': n.id },
303
302
  onClick: () => decide(inst, { act: 'note-remove', person: p.id, id: n.id }, COPY.noteRemoved) })));
304
303
  const f = field({ label: COPY.addNote, kind: 'textarea', hideLabel: true, placeholder: COPY.notePlaceholder, attrs: { 'data-people-field': 'note' } });
305
304
  const save = async () => {
@@ -310,8 +309,8 @@ function notesSec(inst, p) {
310
309
  if (r) f.control.value = '';
311
310
  };
312
311
  return section(COPY.notes, [
313
- list.length ? el('ul', { class: 'rp-people-list-plain' }, list) : el('p', { class: 'rp-people-hint' }, COPY.noNotes),
314
- el('div', { class: 'rp-people-noteadd' }, f, el('div', { class: 'rp-people-acts' }, button({ label: COPY.addNote, variant: 'outline', size: 'sm', attrs: { 'data-people-act': 'note-add' }, onClick: save }))),
312
+ list.length ? el('div', { class: 'rp-people-list-plain' }, list) : el('p', { class: 'rp-people-hint' }, COPY.noNotes),
313
+ el('div', { class: 'rp-people-noteadd' }, f, iconButton({ icon: 'plus', ariaLabel: COPY.addNote, size: 'sm', attrs: { 'data-people-act': 'note-add' }, onClick: save })),
315
314
  ], { 'data-people-sec': 'notes' });
316
315
  }
317
316
 
@@ -319,25 +318,52 @@ function identitySec(inst, p) {
319
318
  const by = {};
320
319
  for (const i of p.interactions) by[i.source] = (by[i.source] || 0) + 1;
321
320
  const file = inst.view.file ? String(inst.view.file).split('/').slice(-3).join('/') : 'people.jsonl';
322
- return section(COPY.identity, [
323
- el('ul', { class: 'rp-people-list-plain' },
324
- p.handles.map((h) => el('li', { class: 'rp-people-handle', 'data-people-handle': h.kind + ':' + h.value }, handleText(h))),
325
- Object.entries(by).map(([s, n]) => el('li', { class: 'rp-people-hint' }, `${SOURCE[s] || s}: ${n} interaction${n === 1 ? '' : 's'}`))),
326
- (p.sources || []).length ? el('p', { class: 'rp-people-hint', 'data-people-sources': p.sources.join(',') }, COPY.foundIn(p.sources.map((s) => SOURCE[s] || s).join(', '))) : null,
327
- el('p', { class: 'rp-people-hint rp-people-file' }, COPY.lives(file)),
328
- ], { 'data-people-sec': 'identity' });
321
+ const details = el('details', { class: 'rp-people-sec', 'data-people-sec': 'identity' },
322
+ el('summary', {},
323
+ icon('chevron-down'),
324
+ COPY.identity),
325
+ el('div', { class: 'rp-people-list-plain' },
326
+ p.handles.map((h) => el('div', { class: 'rp-people-handle', 'data-people-handle': h.kind + ':' + h.value }, handleText(h))),
327
+ Object.entries(by).map(([s, n]) => el('div', { class: 'rp-people-hint' }, `${SOURCE[s] || s}: ${n} interaction${n === 1 ? '' : 's'}`)),
328
+ (p.sources || []).length ? el('div', { class: 'rp-people-hint', 'data-people-sources': p.sources.join(',') }, COPY.foundIn(p.sources.map((s) => SOURCE[s] || s).join(', '))) : null,
329
+ el('div', { class: 'rp-people-hint rp-people-file' }, COPY.lives(file))));
330
+ return details;
329
331
  }
330
332
 
331
333
  function personPane(inst) {
332
334
  const p = inst.view.people.find((x) => x.id === inst.open);
333
- if (!p) return el('div', { class: 'rp-people-page' }, el('p', { class: 'rp-section__empty' }, COPY.pick));
334
- return el('article', { class: 'rp-people-page', 'data-people-page': p.id },
335
- el('header', { class: 'rp-people-who' },
336
- el('h3', { class: 'rp-people-who__name' }, p.name),
337
- el('p', { class: 'rp-people-hint' }, [who(p), p.external && (p.role || p.org) ? COPY.external : null].filter(Boolean).join(' · ')),
338
- el('div', { class: 'rp-people-acts' }, button({ label: COPY.removeFromPeople, variant: 'ghost', size: 'sm', attrs: { 'data-people-act': 'remove' },
339
- onClick: async () => { const r = await decide(inst, { act: 'remove', person: p.id }, COPY.removed(p.name)); if (r) { inst.open = null; draw(inst); } } }))),
340
- interactionsSec(p), toneSec(inst, p), sharedSec(p), notesSec(inst, p), identitySec(inst, p));
335
+ if (!p) return el('div', { class: 'rp-people-card' }, el('p', { class: 'rp-section__empty' }, COPY.pick));
336
+ const t = p.tone;
337
+ const confirmBtn = !t.confirmed && t.proposed ? button({ label: COPY.confirm + ' ' + t.proposed, icon: 'check', variant: 'primary', size: 'sm',
338
+ attrs: { 'data-people-act': 'tone-confirm' },
339
+ onClick: () => decide(inst, { act: 'tone', person: p.id, tone: t.proposed }, COPY.toneSaved) }) : null;
340
+ const moreBtn = iconButton({ icon: 'ellipsis', ariaLabel: 'More', attrs: { 'data-people-act': 'more' },
341
+ onClick: () => menu({ anchor: moreBtn, ariaLabel: 'Actions', items: [
342
+ { label: COPY.removeFromPeople, icon: 'x', attrs: { 'data-people-act': 'remove' }, onSelect: async () => { const r = await decide(inst, { act: 'remove', person: p.id }, COPY.removed(p.name)); if (r) { inst.open = null; draw(inst); } } }
343
+ ] }) });
344
+ return el('article', { class: 'rp-people-card', 'data-people-page': p.id },
345
+ el('div', { class: 'rp-people-page__header' },
346
+ el('div', { class: 'rp-people-page__info' },
347
+ el('span', { class: 'rp-people-page__avatar' }, p.name.split(/\s+/).map((w) => w[0]).join('').toUpperCase()),
348
+ el('div', { class: 'rp-people-page__text' },
349
+ el('h3', { class: 'rp-people-page__name' }, p.name),
350
+ el('p', { class: 'rp-people-hint' }, [who(p), p.external && (p.role || p.org) ? COPY.external : null].filter(Boolean).join(' · ')))),
351
+ el('div', { class: 'rp-people-page__actions' }, confirmBtn, moreBtn)),
352
+ el('div', { class: 'rp-people-page__tiles' },
353
+ el('div', { class: 'rp-people-tile' },
354
+ el('p', { class: 'rp-people-tile__label' }, COPY.interactions),
355
+ el('b', {}, p.count && p.last ? `${p.count} · ${day(p.last)}` : String(p.count || 0))),
356
+ el('div', { class: 'rp-people-tile' },
357
+ el('p', { class: 'rp-people-tile__label' }, COPY.tone),
358
+ el('b', {}, t.confirmed || t.proposed ? [
359
+ el('i', { class: 'rp-people-tile__dot ' + (t.confirmed ? 'rp-people-tile__dot--confirmed' : 'rp-people-tile__dot--proposed') }),
360
+ ' ', t.confirmed ? t.confirmed : (t.proposed + '?')
361
+ ] : 'None')),
362
+ el('div', { class: 'rp-people-tile' },
363
+ el('p', { class: 'rp-people-tile__label' }, COPY.shared),
364
+ el('b', {}, p.shared.initiatives.length ? `${p.shared.initiatives.length} ${p.shared.initiatives.length === 1 ? 'initiative' : 'initiatives'}` : 'None'))),
365
+ el('div', { class: 'rp-people-page__scroll' },
366
+ interactionsSec(p), toneSec(inst, p), sharedSec(p), notesSec(inst, p), identitySec(inst, p)));
341
367
  }
342
368
 
343
369
  /* ── the page ────────────────────────────────────────────────────────────────────────────────────────────────────── */
@@ -348,7 +374,15 @@ function header(inst) {
348
374
  const q = find.control;
349
375
  q.addEventListener('input', () => { inst.q = q.value; draw(inst); });
350
376
  q.addEventListener('keydown', (e) => { if (e.key === 'Escape') { q.resetCommitted(''); inst.q = ''; draw(inst); } });
351
- inst.head = el('div', { class: 'rp-people-top' }, el('h2', { class: 'rp-head__title' }, COPY.title), el('div', { class: 'rp-people-find' }, find));
377
+ const v = inst.view || { people: [] };
378
+ const all = v.people ? v.people.filter((p) => matches(p, inst.q.trim().toLowerCase())) : [];
379
+ const titleText = v.people && v.people.length ? `${v.people.length} ${v.people.length === 1 ? 'person' : 'people'}` : COPY.title;
380
+ inst.titleEl = el('h2', { class: 'rp-head__title' }, titleText);
381
+ inst.head = el('div', { class: 'rp-people-top' },
382
+ inst.titleEl,
383
+ tabs({ variant: 'segmented', value: inst.group, ariaLabel: 'Group', attrs: { 'data-people-group': '' },
384
+ items: [{ id: 'all', label: COPY.all }, { id: 'team', label: COPY.team }, { id: 'external', label: COPY.external }],
385
+ onChange: (val) => { inst.group = val; draw(inst); } }));
352
386
  inst.head.find = find;
353
387
  return inst.head;
354
388
  }
@@ -367,5 +401,9 @@ function draw(inst) {
367
401
  // Focus stays in a note being typed: a redraw of the page would drop it, so it waits until the field is left.
368
402
  const typing = document.activeElement && inst.body.contains(document.activeElement) && document.activeElement.matches('textarea');
369
403
  if (typing) { inst.last = ''; return; }
370
- inst.body.replaceChildren(...[review(inst), el('div', { class: 'rp-people-grid' }, listPane(inst), personPane(inst))].filter(Boolean));
404
+ const n = inst.view.people.length;
405
+ inst.titleEl.textContent = n ? `${n} ${n === 1 ? 'person' : 'people'}` : COPY.title;
406
+ const rev = review(inst);
407
+ const grid = el('div', { class: 'rp-people-grid' }, listPane(inst), personPane(inst));
408
+ inst.body.replaceChildren(...[rev, grid].filter(Boolean));
371
409
  }
@@ -89,9 +89,10 @@ export function groupsOf(repo, { all = false } = {}) {
89
89
  const out = [...by.keys()].sort((a, b) => rank(a) - rank(b) || a.localeCompare(b))
90
90
  .map((i) => ({ key: 'i:' + i, title: titleOf(i), initiative: i, automatic: false, sessions: by.get(i) }));
91
91
  if (none.length) out.push({ key: 'none', title: C().notLinked, initiative: null, automatic: false, sessions: none });
92
- // hidden by default; the session in front is never hidden, and neither is one that is waiting on the PM
92
+ // hidden by default; the session in front is never hidden, and neither is one held on a permission call (a finished agent reads
93
+ // 'needs you' too, so that state alone does not show it)
93
94
  const front = frontSessionId();
94
- const show = all || state.dsShowAuto || sched.some((s) => s.id === front || s.shown === 'needs' || s.shown === 'blocked');
95
+ const show = all || state.dsShowAuto || sched.some((s) => s.id === front || s.shown === 'blocked');
95
96
  if (sched.length && show) out.push({ key: 'auto', title: C().automatic, initiative: null, automatic: true, sessions: sched });
96
97
  return out;
97
98
  }
@@ -104,13 +104,15 @@ export const COPY = {
104
104
  toggle: (need) => (need ? `Your day · ${need} ${need === 1 ? 'needs' : 'need'} you` : 'Your day: calendar, jobs, goals and mentions'),
105
105
  fold: 'Fold your day',
106
106
  hello: { morning: 'Good morning.', afternoon: 'Good afternoon.', evening: 'Good evening.' },
107
+ unfinished: (open, all) => (!all ? 'No goals yet' : open ? `${open} unfinished ${open === 1 ? 'goal' : 'goals'}` : 'All goals done'),
107
108
  free: (min) => `${Math.floor(min / 60)} h ${min % 60} min free`,
108
109
  steps: ['Read', 'Sort', 'Agents work', 'You approve'],
109
110
  stepTitle: ['Read your sources', 'Make the jobs', 'Agents do the work', 'You approve'],
110
111
  stepHint: ['The session reads your calendar and your mentions.', 'Each job is tied to a goal or a meeting.', 'One session per job, at most 5 at once.', 'Only what sends or decides waits for you.'],
111
112
  stepOf: (n) => `Prepare my day · step ${n} of 4`,
112
113
  status: {
113
- idle: 'One press: the agent reads, makes the jobs and does them. You approve only what sends or decides.',
114
+ idle: 'Not prepared yet',
115
+ last: (at, n) => (at ? `Last prepared ${at}` : 'Prepared today') + (n ? ` · ${n} ${n === 1 ? 'needs' : 'need'} you` : ''),
114
116
  reading: 'Reading your calendar and mentions…',
115
117
  readFailed: (why) => `The read stopped: ${why}`,
116
118
  working: (w, n) => `${w} ${w === 1 ? 'agent' : 'agents'} working · ${n} ${n === 1 ? 'needs' : 'need'} you`,
@@ -389,34 +389,38 @@ function select(type, id) {
389
389
  function topBar(ctx) {
390
390
  const { m, ws } = ctx;
391
391
  const si = stepIndex(m.stage);
392
- const steps = el('ol', { class: 'td-day__steps', 'aria-label': D.prepare }, ...D.steps.map((t, i) => {
392
+ const press = runs.get(ws);
393
+ const busy = !!press && press.state === 'busy';
394
+ const bs = m.run && m.run.brief_session;
395
+ const reading = !!bs && !bs.ended_at && m.stage === 'reading' && !m.readFailed;
396
+ // the four steps show as a small progress bar only while Prepare my day is running; otherwise the bar says when it last ran
397
+ const inProgress = busy || reading || (!!m.run && m.stage === 'working');
398
+ const steps = inProgress ? el('ol', { class: 'td-day__steps', 'aria-label': D.prepare }, ...D.steps.map((t, i) => {
393
399
  const st = si < 0 ? 'todo' : i < si || si === 4 ? 'done' : i === si ? 'now' : 'todo';
394
- return el('li', null, button({ label: [el('span', { class: 'td-day__tsdot', 'aria-hidden': 'true' }), t], variant: 'ghost', size: 'sm', onClick: () => select('step', i),
395
- attrs: { class: 'td-day__ts', 'data-step-state': st, 'data-day-step': String(i), 'data-day-sel': 'step:' + i, 'aria-current': ctx.cur('step', String(i)), title: D.stepHint[i] } }));
396
- }));
400
+ return el('li', null, button({ label: [el('span', { class: 'td-day__seg', 'aria-hidden': 'true' })], variant: 'ghost', size: 'sm', onClick: () => select('step', i),
401
+ attrs: { class: 'td-day__ts', 'data-step-state': st, 'data-day-step': String(i), 'data-day-sel': 'step:' + i, 'aria-current': ctx.cur('step', String(i)), 'aria-label': t + ': ' + D.stepHint[i], title: t + ': ' + D.stepHint[i] } }));
402
+ })) : null;
397
403
  const working = m.tasks.filter((t) => t.status === 'working' || t.status === 'sending').length;
398
- const status = !m.run ? D.status.idle : m.readFailed ? D.status.readFailed(m.readFailed.why) : m.stage === 'reading' ? D.status.reading : m.stage === 'working' ? D.status.working(working, m.needs)
399
- : m.stage === 'review' ? D.status.review(m.needs, m.done) : D.status.done(m.done);
404
+ const started = m.run && m.run.started_at ? hhmm(m.run.started_at) : '';
405
+ const status = !m.run ? (busy ? D.starting : D.status.idle) : m.readFailed ? D.status.readFailed(m.readFailed.why) : m.stage === 'reading' ? D.status.reading : m.stage === 'working' ? D.status.working(working, m.needs)
406
+ : D.status.last(started, m.needs);
400
407
  let check = null;
401
408
  if (m.run && m.check) {
402
409
  const every = m.settings && m.settings.dayCheck && typeof m.settings.dayCheck.every === 'number' ? m.settings.dayCheck.every : m.check.every;
403
410
  const running = !!m.check.running || ui.busy.has('check');
404
- check = every === 0 ? el('span', { class: 'td-day__check', 'data-day': 'check' }, D.checkOff)
405
- : el('span', { class: 'td-day__check', 'data-day': 'check' }, el('span', null, running ? D.checking : D.checked(hhmm(m.check.last), hhmm(m.check.next))),
406
- button({ label: D.checkNow, size: 'sm', disabled: running, attrs: { 'data-day': 'check-now' }, onClick: () => checkNow(ws) }));
411
+ check = every === 0 ? el('span', { class: 'td-day__check', 'data-day': 'check', title: D.checkOff }, D.checkOff)
412
+ : el('span', { class: 'td-day__check', 'data-day': 'check', title: running ? D.checking : D.checked(hhmm(m.check.last), hhmm(m.check.next)) },
413
+ iconButton({ icon: 'refresh-cw', ariaLabel: D.checkNow, size: 'sm', attrs: { 'data-day': 'check-now', disabled: running ? 'true' : null }, onClick: () => checkNow(ws) }));
407
414
  }
408
- const press = runs.get(ws);
409
- const busy = !!press && press.state === 'busy';
410
- const bs = m.run && m.run.brief_session;
411
- const reading = !!bs && !bs.ended_at && m.stage === 'reading' && !m.readFailed;
412
415
  const label = busy ? D.starting : reading ? D.stillReading : m.run ? D.again : D.prepare;
416
+ const open = m.allGoals.filter((g) => g.status === 'open').length;
413
417
  return el('header', { class: 'td-day__bar' },
414
- (() => { const when = [dateLabel(m.now), m.free !== null && m.free !== undefined ? D.free(m.free) : null].filter(Boolean).join(' · ');
418
+ (() => { const when = [dateLabel(m.now), D.unfinished(open, m.allGoals.length)].filter(Boolean).join(' · ');
415
419
  return el('div', { class: 'td-day__hello', title: greet(m.now) + ' ' + when }, el('b', null, greet(m.now)), ' ', el('span', { class: 'td-day__dim' }, when)); })(),
416
420
  steps,
417
421
  el('span', { class: 'td-day__status', role: 'status', 'data-day': 'status', title: status }, status),
418
422
  check,
419
- button({ label: D.sources, size: 'sm', attrs: { 'data-day': 'sources', 'data-day-sel': 'sources:all', 'aria-current': ctx.cur('sources', 'all') }, onClick: () => select('sources', 'all') }),
423
+ iconButton({ icon: 'settings', ariaLabel: D.sources, size: 'sm', attrs: { 'data-day': 'sources', 'data-day-sel': 'sources:all', 'aria-current': ctx.cur('sources', 'all') }, onClick: () => select('sources', 'all') }),
420
424
  button({ label, variant: 'primary', size: 'sm', disabled: busy || reading, attrs: { 'data-day': 'prepare', 'data-goals': 'prepare-day', title: D.prepareHint, 'aria-busy': busy ? 'true' : 'false' }, onClick: () => prepareDay(ws) }),
421
425
  iconButton({ icon: 'chevron-down', ariaLabel: D.fold, size: 'sm', expanded: true, attrs: { 'data-day': 'toggle' }, onClick: () => fold(false) }));
422
426
  }
@@ -37,7 +37,7 @@ export function unsyncedOf(st) {
37
37
  }
38
38
 
39
39
  /** The lucide symbol each place wears in the bar (UI comfort, 2026-10-07: a word alone is slow to find; the phone's tab bar already wears these). */
40
- const NAV_ICON = Object.freeze({ today: 'house', mywork: 'list-checks', initiatives: 'target', board: 'square-kanban', reports: 'chart-column', docs: 'file-text', people: 'share-2', settings: 'settings' });
40
+ const NAV_ICON = Object.freeze({ today: 'house', mywork: 'list-checks', initiatives: 'target', board: 'square-kanban', reports: 'chart-column', docs: 'file-text', people: 'users', settings: 'settings' });
41
41
 
42
42
  export function createTopbar({ go, openPalette, repoMenu, sync } = {}) {
43
43
  const node = el('header', { class: 'fr-bar', 'data-shell': 'top', 'data-fr': 'bar' });
@@ -1,5 +1,5 @@
1
1
  /* lucide-sprite.js — GENERATED by workspace/third-party/build-lucide-sprite.js from lucide-static 1.52.0.
2
- * Do not edit by hand. The icons: grip-vertical, plus, check, square, square-check-big, x, undo-2, flag, bot, chevron-right, chevron-down, ellipsis, circle-alert, history, arrow-up, arrow-down, diamond, settings, search, refresh-cw, chevron-left, sun, moon, arrow-left, arrow-right, globe, columns-2, arrow-up-down, arrow-left-right, panel-top, panel-left, maximize-2, minimize-2, git-branch, clock, bold, italic, code, link, type, heading, list, list-checks, text-quote, file-code, git-compare, cloud, share-2, house, target, square-kanban, file-text, square-terminal, calendar, chart-column.
2
+ * Do not edit by hand. The icons: grip-vertical, plus, check, square, square-check-big, x, undo-2, flag, bot, chevron-right, chevron-down, ellipsis, circle-alert, history, arrow-up, arrow-down, diamond, settings, search, refresh-cw, chevron-left, sun, moon, arrow-left, arrow-right, globe, columns-2, arrow-up-down, arrow-left-right, panel-top, panel-left, maximize-2, minimize-2, git-branch, clock, bold, italic, code, link, type, heading, list, list-checks, text-quote, file-code, git-compare, cloud, share-2, house, target, square-kanban, file-text, square-terminal, calendar, chart-column, users, git-merge, mail, message-square, ticket, ban.
3
3
  *
4
4
  * ISC License
5
5
  *
@@ -101,5 +101,11 @@ export const SPRITE = '<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true
101
101
  + '<symbol id="lucide-square-terminal" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m7 11 2-2-2-2" /><path d="M11 13h4" /><rect width="18" height="18" x="3" y="3" rx="2" ry="2" /></symbol>'
102
102
  + '<symbol id="lucide-calendar" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2v3" /><path d="M16 2v3" /><rect x="3" y="3" width="18" height="18" rx="2" /><path d="M3 9h18" /></symbol>'
103
103
  + '<symbol id="lucide-chart-column" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3v16a2 2 0 0 0 2 2h16" /><path d="M18 17V9" /><path d="M13 17V5" /><path d="M8 17v-3" /></symbol>'
104
+ + '<symbol id="lucide-users" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" /><path d="M16 3.128a4 4 0 0 1 0 7.744" /><path d="M22 21v-2a4 4 0 0 0-3-3.87" /><circle cx="9" cy="7" r="4" /></symbol>'
105
+ + '<symbol id="lucide-git-merge" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="18" r="3" /><circle cx="6" cy="6" r="3" /><path d="M6 21V9a9 9 0 0 0 9 9" /></symbol>'
106
+ + '<symbol id="lucide-mail" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m22 7-8.991 5.727a2 2 0 0 1-2.009 0L2 7" /><rect x="2" y="4" width="20" height="16" rx="2" /></symbol>'
107
+ + '<symbol id="lucide-message-square" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z" /></symbol>'
108
+ + '<symbol id="lucide-ticket" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 9a3 3 0 0 1 0 6v2a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-2a3 3 0 0 1 0-6V7a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2Z" /><path d="M13 5v2" /><path d="M13 17v2" /><path d="M13 11v2" /></symbol>'
109
+ + '<symbol id="lucide-ban" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /><path d="M4.929 4.929 19.07 19.071" /></symbol>'
104
110
  + '</svg>';
105
- export const ICONS = ["grip-vertical","plus","check","square","square-check-big","x","undo-2","flag","bot","chevron-right","chevron-down","ellipsis","circle-alert","history","arrow-up","arrow-down","diamond","settings","search","refresh-cw","chevron-left","sun","moon","arrow-left","arrow-right","globe","columns-2","arrow-up-down","arrow-left-right","panel-top","panel-left","maximize-2","minimize-2","git-branch","clock","bold","italic","code","link","type","heading","list","list-checks","text-quote","file-code","git-compare","cloud","share-2","house","target","square-kanban","file-text","square-terminal","calendar","chart-column"];
111
+ export const ICONS = ["grip-vertical","plus","check","square","square-check-big","x","undo-2","flag","bot","chevron-right","chevron-down","ellipsis","circle-alert","history","arrow-up","arrow-down","diamond","settings","search","refresh-cw","chevron-left","sun","moon","arrow-left","arrow-right","globe","columns-2","arrow-up-down","arrow-left-right","panel-top","panel-left","maximize-2","minimize-2","git-branch","clock","bold","italic","code","link","type","heading","list","list-checks","text-quote","file-code","git-compare","cloud","share-2","house","target","square-kanban","file-text","square-terminal","calendar","chart-column","users","git-merge","mail","message-square","ticket","ban"];
@@ -5642,7 +5642,7 @@ async function handleRequest(req, res) {
5642
5642
  fs.mkdirSync(STATE, { recursive: true });
5643
5643
  fs.writeFileSync(SESSION_POLICY_FILE(), JSON.stringify(next, null, 2) + '\n');
5644
5644
  }
5645
- return json(res, { ok: true, policy: readSessionPolicy(), automatic_origins: [...sessionOrigin.AUTOMATIC_EXACT, ...sessionOrigin.AUTOMATIC_PREFIX.map((x) => x + '*')] });
5645
+ return json(res, { ok: true, policy: readSessionPolicy(), manual_origins: [...sessionOrigin.MANUAL_EXACT, ...sessionOrigin.MANUAL_PREFIX.map((x) => x + '*')] });
5646
5646
  }
5647
5647
  // End a session and remove it, with an answer. The WebSocket kill/remove messages return nothing,
5648
5648
  // so a refusal or a slow exit left the PM clicking a control that seemed to do nothing.
@@ -12,7 +12,7 @@ The workspace runs with no network (D62). Two third-party browser files are comm
12
12
  | Package | Version | Licence | Tarball | Tarball sha256 | Shipped file |
13
13
  |---|---|---|---|---|---|
14
14
  | sortablejs | 1.15.7 | MIT | https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.7.tgz | `66cbfc8fa18860783584fe0c346baf6cbf3399da72d23d21053425ba232abed4` | `public/vendor/Sortable.min.js`: an unmodified copy of `package/Sortable.min.js` (45 478 bytes, sha256 `bf4241bc73fef7f11c59a283a69fe8051cdd31c6d8ff5a2b9ba219e7831fcf76`; npm integrity `sha512-Kk8wLQPlS+yi1ZEf48a4+fzHa4yxjC30M/Sr2AnQu+f/MPwvvX9XjZ6OWejiz8crBsLwSq8GHqaxaET7u6ux0A==`) |
15
- | lucide-static | 1.52.0 | ISC, plus the Feather MIT notice | https://registry.npmjs.org/lucide-static/-/lucide-static-1.52.0.tgz | `4d2aa079171e4eba4c704844b8b0c9edb6e4cfb2fb8bd86f7b14353994f04a87` | `public/vendor/lucide-sprite.js`: generated by `build-lucide-sprite.js` (19 585 bytes, sha256 `405c8046d0954e3c942a4897eab7dcaca4cd3e2632b40788e4dbd4d9e5475461`). It holds 55 icons: grip-vertical, plus, check, square, square-check-big, x, undo-2, flag, bot, chevron-right, chevron-down, ellipsis, circle-alert, history, arrow-up, arrow-down, diamond, then the eight the frame's chrome draws (Run 3): settings, search, refresh-cw, chevron-left, sun, moon, arrow-left, arrow-right, then globe, columns-2, arrow-up-down and arrow-left-right for the dock strip's web, split and orientation chips (Run 3, C6), then panel-top, panel-left, maximize-2, minimize-2, git-branch and clock for the dock's icon controls and session rows (0.54.2), then bold, italic, code, link, type, heading, list, list-checks, text-quote, file-code, git-compare, cloud and share-2 for the document tab's in-place editor, its Confluence copy and its connections, then house, target, square-kanban, file-text, square-terminal, calendar and chart-column for the phone's tab bar and view switches |
15
+ | lucide-static | 1.52.0 | ISC, plus the Feather MIT notice | https://registry.npmjs.org/lucide-static/-/lucide-static-1.52.0.tgz | `4d2aa079171e4eba4c704844b8b0c9edb6e4cfb2fb8bd86f7b14353994f04a87` | `public/vendor/lucide-sprite.js`: generated by `build-lucide-sprite.js` (21 404 bytes, sha256 `b97ca7eba4a6f6d29614c41d3b050c96df67d77b36a5db5ee175b830d003edfc`). It holds 61 icons: grip-vertical, plus, check, square, square-check-big, x, undo-2, flag, bot, chevron-right, chevron-down, ellipsis, circle-alert, history, arrow-up, arrow-down, diamond, then the eight the frame's chrome draws (Run 3): settings, search, refresh-cw, chevron-left, sun, moon, arrow-left, arrow-right, then globe, columns-2, arrow-up-down and arrow-left-right for the dock strip's web, split and orientation chips (Run 3, C6), then panel-top, panel-left, maximize-2, minimize-2, git-branch and clock for the dock's icon controls and session rows (0.54.2), then bold, italic, code, link, type, heading, list, list-checks, text-quote, file-code, git-compare, cloud and share-2 for the document tab's in-place editor, its Confluence copy and its connections, then house, target, square-kanban, file-text, square-terminal, calendar and chart-column for the phone's tab bar and view switches, then users, git-merge, mail, message-square, ticket and ban for the People place's nav icon, merge button and interaction sources |
16
16
 
17
17
  The licences are in this folder: [sortablejs-1.15.7.LICENSE](sortablejs-1.15.7.LICENSE) and [lucide-static-1.52.0.LICENSE](lucide-static-1.52.0.LICENSE). The sprite also carries the lucide licence text as its first comment.
18
18