create-pmos 0.61.0 → 0.62.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.
Files changed (36) hide show
  1. package/package.json +1 -1
  2. package/payload/KIT-VERSION +1 -1
  3. package/payload/planning/evals/eval-calibration.md +1 -1
  4. package/payload/planning/okrs/health-budgets.md +1 -1
  5. package/payload/state/discoveries.md +1 -1
  6. package/workspace/first-paint-audit.js +8 -3
  7. package/workspace/guide/index.md +5 -3
  8. package/workspace/guide/layout.md +4 -3
  9. package/workspace/guide/people.md +70 -0
  10. package/workspace/guide/today.md +2 -2
  11. package/workspace/lib/calendar-memory.js +2 -1
  12. package/workspace/lib/people-cli.js +135 -0
  13. package/workspace/lib/people-store.js +123 -0
  14. package/workspace/lib/people.js +399 -0
  15. package/workspace/lib/pmos-verbs.js +3 -0
  16. package/workspace/package-lock.json +1 -1
  17. package/workspace/package.json +1 -1
  18. package/workspace/public/pm/app.js +2 -2
  19. package/workspace/public/pm/components.css +169 -0
  20. package/workspace/public/pm/dest-people.js +385 -0
  21. package/workspace/public/pm/dest-settings.js +44 -3
  22. package/workspace/public/pm/frame-copy.js +7 -0
  23. package/workspace/public/pm/initiative-sheet.js +5 -2
  24. package/workspace/public/pm/initiatives-copy.js +1 -0
  25. package/workspace/public/pm/item-sheet.js +18 -1
  26. package/workspace/public/pm/people-link.js +106 -0
  27. package/workspace/public/pm/registry.js +2 -0
  28. package/workspace/public/pm/today-agenda.js +3 -1
  29. package/workspace/public/pm/today-day.js +6 -3
  30. package/workspace/public/pm/topbar.js +4 -3
  31. package/workspace/server.js +27 -0
  32. package/workspace/skills/gather-requirements.skill +5 -3
  33. package/workspace/skills/jira-sync.skill +5 -4
  34. package/workspace/skills/live-notes.skill +3 -1
  35. package/workspace/skills/morning-brief.skill +5 -3
  36. package/workspace/test-lists/people-directory.json +6 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-pmos",
3
- "version": "0.61.0",
3
+ "version": "0.62.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@33595f8
1
+ kernel-source: pmos@ff2e983
2
2
  assembled: 2026-10-06
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-06T16:25:40Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-06T17:04:03Z"}
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-06T16:25:40Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-06T17:04:03Z"}
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-06T16:25:40Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-06T17:04:03Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -20,10 +20,15 @@
20
20
  // The budgets start at specimen A's counts. reports: 14 → 15 in Run 6 for Present, the one control the PM asked to keep
21
21
  // with the slides (2026-10-06). Every place +1 for Docs in the top bar (the PM, 2026-10-06, docs-three-way-sync): the
22
22
  // budget is for the whole screen and the bar is on every screen. The calibration stays specimen A's own count: the
23
- // specimen did not change.
23
+ // specimen did not change. Every place +1 again for People in the top bar (the PM, 2026-10-06, people-directory).
24
24
  const SPECIMEN_A = Object.freeze({ today: 17, initiatives: 24, board: 33, reports: 14 });
25
25
  const BAR_DOCS = 1;
26
- const EXPECT = Object.freeze({ today: 17 + BAR_DOCS, initiatives: 24 + BAR_DOCS, board: 33 + BAR_DOCS, reports: 15 + BAR_DOCS });
26
+ const BAR_PEOPLE = 1;
27
+ const BAR = BAR_DOCS + BAR_PEOPLE;
28
+ // Agenda opens on the month (the PM's Agenda mockup v5, 2026-10-06): the month adds its own controls to Today's budget,
29
+ // at most six week rows of a week button and seven days, the two month arrows and the five lenses.
30
+ const AGENDA_MONTH = 6 * 8 + 2 + 5;
31
+ const EXPECT = Object.freeze({ today: 17 + BAR + AGENDA_MONTH, initiatives: 24 + BAR, board: 33 + BAR, reports: 15 + BAR });
27
32
 
28
33
  /* ── shoot.js audit(), copied verbatim (scratchpad/specimens2/shoot.js, 2026-10-05) ─────────────────────────────── */
29
34
  // runs in the page
@@ -149,4 +154,4 @@ function installCounter(page) {
149
154
  return page.addInitScript({ content: 'window.__countIn = ' + countIn.toString() + ';' });
150
155
  }
151
156
 
152
- module.exports = { EXPECT, SPECIMEN_A, audit, countIn, stableCount, installCounter };
157
+ module.exports = { AGENDA_MONTH, EXPECT, SPECIMEN_A, audit, countIn, stableCount, installCounter };
@@ -12,8 +12,8 @@ This guide describes the PMOS workspace as it works today. It ships with the app
12
12
 
13
13
  On a desktop the workspace is one frame. [Layout](layout.md) explains each part.
14
14
 
15
- 1. **The top bar.** Repository ▾, the five places (Docs included), search (⌘K), Sync when the repository is not in sync, and Settings.
16
- 2. **The place.** One of Agenda, Initiatives, Board or Reports, or Docs (⌘K or `g d`). It fills the screen under the bar.
15
+ 1. **The top bar.** Repository ▾, the six places (Docs and People included), search (⌘K), Sync when the repository is not in sync, and Settings.
16
+ 2. **The place.** One of Agenda, Initiatives, Board or Reports, or Docs (⌘K or `g d`), or People (⌘K or `g p`). It fills the screen under the bar.
17
17
  3. **The detail.** The item you opened: an initiative with its status, next steps and artifacts, or the profiles. It opens beside the place and closes with ×.
18
18
  4. **The agent dock.** Terminals, documents, conversations and web pages. It is open on Agenda and a thin rail elsewhere.
19
19
 
@@ -27,7 +27,8 @@ On a desktop the workspace is one frame. [Layout](layout.md) explains each part.
27
27
  | The items of an initiative | Board, or Breakdown in the initiative detail |
28
28
  | Run reports | Reports |
29
29
  | Whether a document is the same as its Google Docs or Confluence copy | Docs: ⌘K → **Docs**, or press `g d` |
30
- | Repositories, Profiles, New sessions (the default CLI and model), Validate consent, Theme | Settings |
30
+ | The people you work with: who they are, your interactions, their tone, your notes and shared work | People: ⌘K → **People**, or press `g p` |
31
+ | Repositories, Profiles, New sessions (the default CLI and model), Validate consent, People (calendar names), Theme | Settings |
31
32
  | Validate | Settings, under Validate consent: **Open Validate** |
32
33
  | A document, a decision record, a memory entry, a page of this guide | ⌘K, with its scope: Knowledge, Memory or Guide |
33
34
  | A report across every repository | ⌘K: Portfolio report |
@@ -37,6 +38,7 @@ On a desktop the workspace is one frame. [Layout](layout.md) explains each part.
37
38
 
38
39
  - [Start the workspace without a terminal](launcher.md): the PMOS app, the login item and `pmos launcher`.
39
40
  - [Jira tickets](jira.md): Jira tickets drafted from the PRD, pushed when you select them, and their status.
41
+ - [People](people.md): the people you work with in a repository, how they get there, and `pmos people`.
40
42
  - [Docs](docs.md): whether each linked document is the same as, ahead of or behind its Google Docs and Confluence copy.
41
43
  - [Layout](layout.md): the frame, the keys and the rule for where a list or a detail opens.
42
44
  - [Statuses](statuses.md): what each status means, where it comes from, and what you can change.
@@ -15,7 +15,7 @@ The top bar has these parts, from left to right:
15
15
 
16
16
  - **Repository ▾** names the current repository. Press it to choose another repository, or **All repositories** to see the work of every repository at once. All repositories replaces the old Portfolio mode.
17
17
  - **Branches and sessions** is in ⌘K. It opens a sheet with every branch of the repository (main first) and how many sessions run in each. Press a branch to see its sessions, open one, end one, or start a session there. **+ branch** makes a branch, and **+ session** starts one with a branch, an agent and a profile. With All repositories, the sheet lists every repository.
18
- - **Agenda, Initiatives, Board, Reports and Docs** are the five places. Press one to show it. The Agenda count shows how many items need you. Docs shows whether each document is the same as its Google Docs and Confluence copy.
18
+ - **Agenda, Initiatives, Board, Reports, Docs and People** are the six places. Press one to show it. The Agenda count shows how many items need you. Docs shows whether each document is the same as its Google Docs and Confluence copy. People shows the people you work with in the repository ([People](people.md)).
19
19
  - **Search or capture ⌘K** opens the command palette. Type to find an initiative, a repository, an open tab or a command.
20
20
  - **Sync** shows only when the current repository is not in sync with its remote. Press it to fetch, bring in the work of the other machine, record your changes and push. If the repository has not fetched in the last 10 minutes, Sync also shows, because a push from another machine is not visible until a fetch. When the repository fetched recently and is in sync, Sync is not shown. If Sync stops, the message names the step that stopped.
21
21
  - **Sync on its own.** If you forget to press Sync, the workspace syncs for you. It syncs each repository once when the workspace starts. It also syncs a repository that has records not yet pushed when no record changed for 5 minutes. In a document repository it syncs every changed file on any branch. In a code repository it syncs only on the default branch (for example `main`), and only when every commit not yet pushed changes records and no code. If you close the tab while records are not yet pushed, the browser asks you first. To turn automatic sync off, start the workspace with `PMOS_AUTO_SYNC=0`.
@@ -25,12 +25,13 @@ Each place is drawn when its run is built. Until then the place says which run b
25
25
 
26
26
  ## Settings
27
27
 
28
- Settings holds settings only, in seven sections:
28
+ Settings holds settings only, in eight sections:
29
29
 
30
30
  - **Repositories**: every registered repository. Press a repository to make it the current one. Press its ⋯ for **Rename** or **Remove from PMOS**. Each asks in a sheet, and a remove asks you to confirm. **Add repository** shows the scan of a folder before you add it.
31
31
  - **Profiles**: the Claude profiles a session can start with. They open in the detail.
32
32
  - **New sessions**: the default CLI and the default model for a new session in the dock.
33
33
  - **Validate consent**: the consent of the current repository. Turn it on or off here. **Open Validate** opens the Validate page as a tab.
34
+ - **People**: whether the morning brief keeps attendee names from the calendar for the current repository. It is off until you turn it on. Turning it off also hides the names recorded before.
34
35
  - **Online**: puts this workspace at your own address, such as `pmos.pm-os.app`, through a Cloudflare tunnel. Enter the public address, the tunnel name, your Cloudflare team domain, the Access audience (AUD) tag and your email, then press **Turn on**. The workspace runs the tunnel and remembers the switch when it restarts. On your phone, open the address and sign in with Cloudflare Access (Google or an email code). The workspace does not ask for its token there. Only the computer that runs the workspace can change this section. A phone sees whether it is on. When a sign-in at the address is refused, this section names the check that failed, such as the email or the AUD tag. [REMOTE.md](../REMOTE.md) has the one-time Cloudflare steps.
35
36
  - **Theme**: Dark or Light.
36
37
  - **Updates**: the version of the workspace app and the version of the kit in the current repository. Press **Check for updates** to compare them with the latest published version of PMOS. This is the only time the workspace asks the internet for a version, and it asks only when you press. When a newer version is available, one button shows: **Update to** and the version. It updates every part that is older, in this order:
@@ -75,7 +76,7 @@ The workspace is neutral. A primary button is the text colour on the page colour
75
76
 
76
77
  | Key | What it does |
77
78
  |---|---|
78
- | g t, g i, g b, g r, g d | Show Agenda, Initiatives, Board, Reports or Docs. |
79
+ | g t, g i, g b, g r, g d, g p | Show Agenda, Initiatives, Board, Reports, Docs or People. |
79
80
  | ⌘K (Ctrl-K) | Open or close the command palette. |
80
81
  | ⌘J (Ctrl-J) | Open or fold the dock. |
81
82
  | ⌘⇧J (Ctrl-Shift-J) | Focus: give the dock the whole window, or show the place again. |
@@ -0,0 +1,70 @@
1
+ ---
2
+ type: Guide
3
+ title: "People: the people you work with in a repository, their interactions, tone, notes and shared work"
4
+ timestamp: 2026-10-06
5
+ ---
6
+
7
+ # People
8
+
9
+ Open People from the top bar, from ⌘K (**People**), or press `g p`.
10
+
11
+ People lists the people you work with in the current repository. For each person it shows who they are, your interactions with them and where each one came from, the tone of those interactions, your own notes, and the work you share.
12
+
13
+ ## How people get into People
14
+
15
+ The workspace does not read your mail, chat, calendar or Jira. Agent sessions that already read them with your own connectors record the people they see:
16
+
17
+ - **Gather requirements** records the author of each item it gathers.
18
+ - **Live notes** records each named speaker.
19
+ - **Jira sync** records assignees and reporters by their Jira name.
20
+ - **Morning brief** records meeting attendees, but only when **Keep attendee names from the calendar** is on (see Settings below).
21
+
22
+ Each record is a sighting: a name, a handle (a mail address, a chat or Jira name, a transcript speaker, or only a name), and at most one interaction line in the agent's words with a pointer to where it came from. No message body is kept. Private, personal, confidential and HR material is skipped.
23
+
24
+ A sighting does not make a person. New people wait for you in **Waiting for you** at the top of People.
25
+
26
+ ## Waiting for you
27
+
28
+ - **A new person.** Press **Add to People** to add them, or **Ignore** to hide them. Later sightings of an ignored handle stay hidden.
29
+ - **A possible duplicate.** When a session thinks two handles are one person, it says why. Press **Merge** to join the handle to that person, or **Keep separate** to close the question.
30
+ - **Tones to confirm.** The count of people whose tone you have not confirmed. Open the person to confirm it.
31
+
32
+ ## The list
33
+
34
+ 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.
35
+
36
+ ## A person's page
37
+
38
+ - **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.
39
+ - **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.
40
+ - **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.
41
+ - **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.
42
+ - **Identity and sources.** The person's handles, how many interactions each source gave, and the file the person lives in.
43
+
44
+ ## Ways in from other places
45
+
46
+ - **Agenda.** A meeting's detail shows a link for each person its attendee line names.
47
+ - **Initiative sheet.** The **People** tab lists the people whose interactions name the initiative or who own one of its items.
48
+ - **Item sheet.** When an item's owner is a person in People, the owner is a link to their page.
49
+
50
+ A link matches a full name, a first name of three letters or more, or a handle, as a whole word. Two people with the same first name both match.
51
+
52
+ ## Settings
53
+
54
+ Settings has a **People** section for the current repository. **Keep attendee names from the calendar** is off until you turn it on. While it is off, the morning brief records nobody from the calendar, `pmos people record` refuses calendar sightings, and names recorded before stay hidden.
55
+
56
+ ## Where People keeps its records
57
+
58
+ People keeps one append-only file, `people.jsonl`, in the repository's state directory (`pmos/state/` in a product repository). Sync sends it with your other records, so People is the same on every machine that syncs the repository. Every clone of the repository can read your tone choices and notes. Keep them to work context.
59
+
60
+ ## Commands
61
+
62
+ - `pmos people list` lists each person with their role, interactions and tone, and the new people waiting for you. Add `--json` for the whole view.
63
+ - `pmos people show <person>` shows one person's page: interactions, tone, shared work, notes and handles. `<person>` is the id from the list or the full name.
64
+ - `pmos people record --file <sightings.json>` records the people a session saw. Agent sessions run it. It must name who recorded it: in a workspace session the session token does this; outside the workspace, add `--by <profile>`. A sighting already recorded is skipped and counted.
65
+ - `pmos people settings` prints `calendar names: on` or `calendar names: off`. The morning brief reads it before it records attendees.
66
+ - `pmos people help` prints the commands.
67
+
68
+ The sightings file holds `{ "sightings": [...], "merges": [...] }`. A sighting is `{ "source": "gather" | "calendar" | "live" | "jira", "handle": { "kind": "mail" | "slack" | "jira" | "confluence" | "speaker" | "name", "value": "…" }, "name": "…", "role"?, "org"?, "external"?: true, "interaction"?: { "date": "YYYY-MM-DD", "kind": "Mail", "text": "one line, at most 200 characters", "pointer": "where it came from, at most 200 characters", "tone"?: "warm", "initiative"?: "<id>" } }`. A merge is `{ "a": <handle>, "b": <handle>, "why": "one line" }`.
69
+
70
+ No command adds a person, ignores or merges a handle, sets a tone or writes a note. Do those in People.
@@ -30,11 +30,11 @@ An agent job can stop with its session still open, for example when the agent as
30
30
 
31
31
  Each job session is named "Job ·" and the first words of the job, so the session list stays short.
32
32
 
33
- **Agent results**, at the top of the left column, counts the jobs and lists each finished one with its draft summary or the reason it failed. Your goals for today come under it, above the calendar. The other things that wait for you, such as outcome checks, report drafts and sessions, are in **Also waiting for you** at the end of the left column. When the day is folded, they show under it.
33
+ The Agenda month is at the top of the left column. **Agent results** comes under it, counts the jobs and lists each finished one with its draft summary or the reason it failed. Your goals for today come under Agent results. The day is open when you go to Agenda, also before you press **Prepare my day**. The other things that wait for you, such as outcome checks, report drafts and sessions, are in **Also waiting for you** at the end of the left column. When the day is folded, they show under it.
34
34
 
35
35
  ### The Agenda month
36
36
 
37
- The menu calls this page **Agenda**. At the top of the left column is the month, with today marked. Each week starts with its number, for example W41.
37
+ The menu calls this page **Agenda**. At the top of the left column is the month, with today marked. When a day shows no meetings yet, press **Prepare my day** (or **Prepare again**): the morning-brief session reads your calendar and fills the month. Each week starts with its number, for example W41.
38
38
 
39
39
  - Press a day to see its agenda under the month. Three tiles open the agenda: the time in meetings, the meeting that matters most, and your prep.
40
40
  - Press a week number to see the agenda of that week, day by day.
@@ -90,6 +90,7 @@ function validateMemory(raw, { now = Date.now() } = {}) {
90
90
  if (e.series !== undefined && !str(e.series, 200)) return { ok: false, error: `${at}: series is a text of at most 200 characters` };
91
91
  if (e.attendees !== undefined && !(Number.isInteger(e.attendees) && e.attendees >= 0 && e.attendees <= 1000)) return { ok: false, error: `${at}: attendees is a whole number from 0 to 1000` };
92
92
  for (const b of ['external', 'exec']) if (e[b] !== undefined && typeof e[b] !== 'boolean') return { ok: false, error: `${at}: ${b} is true or false` };
93
+ if (e.with !== undefined && !str(e.with, 120)) return { ok: false, error: `${at}: with is one line of at most 120 characters` };
93
94
  if (e.role !== undefined && !ROLES.includes(e.role)) return { ok: false, error: `${at}: role is one of ${ROLES.join(', ')}` };
94
95
  const past = f <= now;
95
96
  if (e.notes !== undefined) {
@@ -107,7 +108,7 @@ function validateMemory(raw, { now = Date.now() } = {}) {
107
108
  if (ids.has(id)) return { ok: false, error: `${at}: two events have the same start and title` };
108
109
  ids.add(id);
109
110
  const out = { id, start: new Date(s).toISOString(), end: new Date(f).toISOString(), title };
110
- for (const k of ['series', 'attendees', 'external', 'exec', 'role', 'notes', 'open', 'prep', 'prep_done']) if (e[k] !== undefined) out[k] = e[k];
111
+ for (const k of ['series', 'attendees', 'external', 'exec', 'role', 'with', 'notes', 'open', 'prep', 'prep_done']) if (e[k] !== undefined) out[k] = e[k];
111
112
  events.push(out);
112
113
  }
113
114
  events.sort((a, b) => Date.parse(a.start) - Date.parse(b.start));
@@ -0,0 +1,135 @@
1
+ 'use strict';
2
+ /* people-cli.js — `pmos people`: the people the PM works with in this repository, from a terminal
3
+ * (initiative people-directory, PRD R2, R9, R14, proposed D90).
4
+ *
5
+ * pmos people [list] [--json] each person: name, role, last interaction, tone; and the review list
6
+ * pmos people show <person> [--json] one person's page: interactions, tone, shared work, notes, handles
7
+ * pmos people record --file <sightings.json> (agent session) append what the session saw
8
+ * pmos people settings [--json] whether calendar attendee names are kept
9
+ *
10
+ * The same module the People place reads (lib/people-store.js), so a terminal line and the page cannot disagree.
11
+ * No command adds a person, merges, sets a tone or writes a note: those are the PM's presses in People (D90).
12
+ * `record` must name who recorded: the session's PMOS_SESSION token, or `--by <profile>` outside the workspace.
13
+ *
14
+ * Exit codes: 0 done, 1 refused, 2 usage.
15
+ */
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+ const store = require('./people-store');
19
+ const { parseArgs } = require('./schedule-cli');
20
+
21
+ const COMMANDS = { people: ['list', 'show', 'record', 'settings'] };
22
+ const USAGE = [
23
+ 'usage: pmos people [list|show|record|settings] — the people you work with in this repository',
24
+ ' pmos people [list] [--json] each person, and what waits for you in the review list',
25
+ ' pmos people show <person> [--json] one person: interactions, tone, shared work, notes and handles',
26
+ ' pmos people record --file <sightings.json> (agent session) record the people a session saw',
27
+ ' pmos people settings [--json] whether calendar attendee names are kept (calendar names: on|off)',
28
+ 'Nothing here adds a person, merges, sets a tone or writes a note. Do that in People.',
29
+ ];
30
+ const BOOL = new Set(['json', 'help']);
31
+
32
+ function parse(argv) {
33
+ const rest = []; const bools = {};
34
+ for (const a of argv) {
35
+ if (a.startsWith('--') && BOOL.has(a.slice(2))) bools[a.slice(2)] = true;
36
+ else if (a === '-h') bools.help = true;
37
+ else rest.push(a);
38
+ }
39
+ const { pos, flags } = parseArgs(rest);
40
+ return { pos, flags: { ...flags, ...bools } };
41
+ }
42
+ /** The repository holding `cwd`: the nearest folder with a .git entry. No process is started (R16). */
43
+ function repoRoot(cwd) {
44
+ for (let d = path.resolve(cwd); ; d = path.dirname(d)) {
45
+ if (fs.existsSync(path.join(d, '.git'))) return d;
46
+ if (path.dirname(d) === d) return cwd;
47
+ }
48
+ }
49
+ /** Who is recording: the workspace session's token names it; outside the workspace, the profile the PM names. */
50
+ function whoOf(flags, env) {
51
+ const tok = String(env.PMOS_SESSION || '').trim();
52
+ if (tok) return 'session:' + tok.split('.')[0].slice(0, 60);
53
+ if (flags.by) return 'profile:' + String(flags.by).slice(0, 60);
54
+ return null;
55
+ }
56
+ const toneWord = (t) => (t.confirmed ? t.confirmed : t.proposed ? `${t.proposed} (proposed)` : 'no tone yet');
57
+ const handleText = (h) => `${h.kind} ${h.value}`;
58
+
59
+ async function run(argv, { say = console.log, cwd = process.cwd(), env = process.env, now = Date.now() } = {}) {
60
+ const { pos, flags } = parse(argv.slice(1));
61
+ const sub = pos[0] || 'list';
62
+ if (flags.help || sub === 'help') { USAGE.forEach((l) => say(l)); return 0; }
63
+ const json = !!flags.json;
64
+ const root = flags.root ? path.resolve(cwd, flags.root) : repoRoot(cwd);
65
+ const fail = (error, code = 1) => { if (json) say(JSON.stringify({ ok: false, error })); else say(`✗ ${error}`); return code; };
66
+
67
+ if (sub === 'list') {
68
+ const v = store.view(root, { now });
69
+ if (!v.ok) return fail(v.error);
70
+ if (json) { say(JSON.stringify({ ok: true, people: v.people, review: v.review, settings: v.settings, counts: v.counts })); return 0; }
71
+ const r = v.review;
72
+ say(`People · ${v.people.length} ${v.people.length === 1 ? 'person' : 'people'} · ${r.new.length} new · ${r.merges.length} possible duplicate${r.merges.length === 1 ? '' : 's'} · ${r.tones.length} tone${r.tones.length === 1 ? '' : 's'} to confirm`);
73
+ if (!v.people.length && !r.new.length) { say('Nobody yet. Agent sessions record the people they see; you add them in People.'); return 0; }
74
+ for (const p of v.people) {
75
+ say(`${p.name} (${p.id}) ${[p.role, p.org].filter(Boolean).join(' · ') || p.group} · ${p.count} interaction${p.count === 1 ? '' : 's'}${p.last ? ' · last ' + p.last : ''} · ${toneWord(p.tone)}`);
76
+ }
77
+ if (r.new.length) {
78
+ say('New, waiting for you in People:');
79
+ for (const f of r.new) say(` ${f.name} ${handleText(f.handle)} · ${f.count} sighting${f.count === 1 ? '' : 's'} from ${f.sources.join(', ')}`);
80
+ }
81
+ return 0;
82
+ }
83
+
84
+ if (sub === 'show') {
85
+ const id = pos[1];
86
+ if (!id) return fail('usage: pmos people show <person>', 2);
87
+ const v = store.view(root, { now });
88
+ if (!v.ok) return fail(v.error);
89
+ const q = String(id).toLowerCase();
90
+ const p = v.people.find((x) => x.id === q) || v.people.find((x) => x.name.toLowerCase() === q);
91
+ if (!p) return fail(`no person ${id} — pmos people list names everyone`);
92
+ if (json) { say(JSON.stringify({ ok: true, person: p })); return 0; }
93
+ say(`${p.name} (${p.id}) ${[p.role, p.org].filter(Boolean).join(' · ')} ${p.group}`);
94
+ const mix = Object.entries(p.tone.mix).map(([t, n]) => `${n} ${t}`).join(', ');
95
+ say(`Tone: ${toneWord(p.tone)}${mix ? ' · ' + mix : ''}`);
96
+ say(`Interactions (${p.count}):`);
97
+ for (const i of p.interactions.slice(0, 20)) say(` ${i.date} ${i.kind.padEnd(7)} ${i.text}${i.tone ? ' [' + i.tone + ']' : ''} — ${i.source}: ${i.pointer}`);
98
+ const s = p.shared;
99
+ if (s.initiatives.length) say(`Initiatives: ${s.initiatives.map((g) => `${g.id} (${g.count})`).join(', ')}`);
100
+ for (const it of s.items) say(` Owns ${it.id} ${it.title || ''} · ${it.state}${it.blocked ? ' · Blocked' + (it.reason ? ': ' + it.reason : '') : ''}`);
101
+ for (const m of s.meetings) say(` Meeting ${String(m.start).slice(0, 16).replace('T', ' ')} ${m.title}`);
102
+ if (p.notes.length) { say('Notes:'); for (const n of p.notes) say(` ${n.text}`); }
103
+ say(`Handles: ${p.handles.map(handleText).join(' · ')}`);
104
+ return 0;
105
+ }
106
+
107
+ if (sub === 'settings') {
108
+ const s = store.settings(root);
109
+ if (!s.ok) return fail(s.error);
110
+ if (json) say(JSON.stringify({ ok: true, calendar_names: s.calendar_names }));
111
+ else say(`calendar names: ${s.calendar_names ? 'on' : 'off'}`);
112
+ return 0;
113
+ }
114
+
115
+ if (sub === 'record') {
116
+ if (!flags.file) return fail('usage: pmos people record --file <sightings.json>', 2);
117
+ const by = whoOf(flags, env);
118
+ if (!by) return fail('a sighting must name who recorded it: run it in a workspace session, or add --by <profile>');
119
+ let parsed;
120
+ try { parsed = JSON.parse(fs.readFileSync(path.resolve(cwd, flags.file), 'utf8')); }
121
+ catch (e) { return fail(`could not read ${flags.file}: ${String((e && e.message) || e).slice(0, 160)}`); }
122
+ const sightings = Array.isArray(parsed) ? parsed : (parsed && parsed.sightings) || [];
123
+ const merges = (!Array.isArray(parsed) && parsed && parsed.merges) || [];
124
+ const r = store.record(root, { by, sightings, merges, now });
125
+ if (json) { say(JSON.stringify(r)); return r.ok ? 0 : 1; }
126
+ if (!r.ok && r.recorded === undefined) return fail(r.error);
127
+ say(`${r.recorded || 0} sighting(s) recorded · ${r.duplicates || 0} already recorded · ${r.proposals || 0} merge proposal(s) · ${(r.refused || []).length} refused`);
128
+ for (const x of r.refused || []) say(`✗ ${x.index !== undefined ? 'sighting ' + x.index : 'merge ' + x.merge}: ${x.error}`);
129
+ if (!r.ok) say(`✗ ${r.error}`);
130
+ return r.ok ? 0 : 1;
131
+ }
132
+ return fail(`pmos people ${sub} is not a command. ${USAGE[0]}`, 2);
133
+ }
134
+
135
+ module.exports = { run, COMMANDS, USAGE };
@@ -0,0 +1,123 @@
1
+ 'use strict';
2
+ /* people-store.js — the file side of People (initiative people-directory, PRD R1 to R10, proposed D90).
3
+ *
4
+ * lib/people.js decides; this module reads and appends. The `pmos people` verb and the /api/people routes both call
5
+ * it, so the terminal and the People place cannot hold two opinions about a person.
6
+ *
7
+ * view(root, { now, meetings }) → { ok, people, review, settings, counts, file }
8
+ * record(root, { by, sightings, merges, now }) → { ok, recorded, duplicates, refused[], error? }
9
+ * decide(root, { actor, body, now }) → { ok, row } | { ok:false, error } one PM page press
10
+ * settings(root) → { ok, calendar_names, file }
11
+ *
12
+ * Every write is an append through people.js's validators, so a refused row is never on disk. No network, no process.
13
+ */
14
+ const crypto = require('crypto');
15
+ const P = require('./people');
16
+ const fourPanel = require('./four-panel');
17
+ const consoleState = require('./console-state');
18
+
19
+ const MAX_PER_RECORD = 500;
20
+
21
+ function stateDir(root) {
22
+ const e = consoleState.loadEngine();
23
+ if (!e.ok) return { ok: false, error: e.error };
24
+ const dir = fourPanel.storeDir(e.engine, root);
25
+ return dir ? { ok: true, dir } : { ok: false, error: 'this repository has no file-mode state directory' };
26
+ }
27
+ function readRows(root) {
28
+ const s = stateDir(root);
29
+ if (!s.ok) return s;
30
+ const r = fourPanel.readDeclarations(s.dir, P.LEDGER);
31
+ if (!r.ok) return { ok: false, error: r.error };
32
+ return { ok: true, dir: s.dir, file: r.file, rows: r.rows };
33
+ }
34
+ /** The plan items, for shared work (R10). A repository without a plan gives none. */
35
+ function planItems(root) {
36
+ try { return Object.values(require('./plan-items').readFold(root).items || {}); } catch { return []; }
37
+ }
38
+
39
+ function view(root, { now = Date.now(), meetings = [] } = {}) {
40
+ const r = readRows(root);
41
+ if (!r.ok) return r;
42
+ const v = P.fold(r.rows, { now, items: planItems(root), meetings });
43
+ return { ok: true, ...v, file: r.file };
44
+ }
45
+
46
+ function settings(root) {
47
+ const r = readRows(root);
48
+ if (!r.ok) return r;
49
+ return { ok: true, calendar_names: P.fold(r.rows).settings.calendar_names, file: r.file };
50
+ }
51
+
52
+ const stamp = (row, by, now) => ({ v: 1, ...row, by, at: new Date(now).toISOString() });
53
+ const sightKey = (r) => [P.handleKey(r.handle), r.interaction ? r.interaction.date : '', r.interaction ? r.interaction.pointer : '', r.interaction ? r.interaction.text : ''].join('\u0000');
54
+
55
+ /**
56
+ * Append what an agent session saw. `by` names who recorded it (a session token or a profile); it is required.
57
+ * A calendar sighting is refused while the PM's calendar setting is off (R3). A sighting already on disk is skipped.
58
+ */
59
+ function record(root, { by, sightings = [], merges = [], now = Date.now() } = {}) {
60
+ if (typeof by !== 'string' || !/^(session|profile):\S{1,60}$/.test(by)) return { ok: false, error: 'a sighting must name who recorded it' };
61
+ if (!Array.isArray(sightings) || !Array.isArray(merges)) return { ok: false, error: 'sightings and merges must be lists' };
62
+ if (sightings.length + merges.length > MAX_PER_RECORD) return { ok: false, error: `at most ${MAX_PER_RECORD} rows in one record` };
63
+ const r = readRows(root);
64
+ if (!r.ok) return r;
65
+ const calendarOn = P.fold(r.rows).settings.calendar_names;
66
+ const seen = new Set(r.rows.filter((x) => x && x.kind === 'sighting').map(sightKey));
67
+ const refused = [];
68
+ let recorded = 0; let duplicates = 0;
69
+ for (let i = 0; i < sightings.length; i++) {
70
+ const c = P.checkSighting(sightings[i]);
71
+ if (!c.ok) { refused.push({ index: i, error: c.error }); continue; }
72
+ if (c.row.source === 'calendar' && !calendarOn) { refused.push({ index: i, error: 'calendar names are off for this repository (Settings → People)' }); continue; }
73
+ const k = sightKey(c.row);
74
+ if (seen.has(k)) { duplicates++; continue; }
75
+ const w = fourPanel.appendDeclaration(r.dir, P.LEDGER, stamp(c.row, by, now));
76
+ if (!w.ok) return { ok: false, error: w.error, recorded, duplicates, refused };
77
+ seen.add(k); recorded++;
78
+ }
79
+ let proposals = 0;
80
+ for (let i = 0; i < merges.length; i++) {
81
+ const c = P.checkMergeProposal(merges[i]);
82
+ if (!c.ok) { refused.push({ merge: i, error: c.error }); continue; }
83
+ const w = fourPanel.appendDeclaration(r.dir, P.LEDGER, stamp(c.row, by, now));
84
+ if (!w.ok) return { ok: false, error: w.error, recorded, duplicates, refused };
85
+ proposals++;
86
+ }
87
+ return { ok: true, recorded, proposals, duplicates, refused };
88
+ }
89
+
90
+ /** A new note id: n- and eight letters or digits. */
91
+ const noteId = () => 'n-' + crypto.randomBytes(6).toString('base64').replace(/[^0-9a-z]/gi, '').toLowerCase().slice(0, 8).padEnd(8, '0');
92
+
93
+ /**
94
+ * One PM press. `actor` is requirePagePress's actor ({ by: 'pm:<slug>' }); the route has already checked the press.
95
+ * Add refuses a handle that a person already holds; merge, tone and notes refuse a person who does not exist.
96
+ */
97
+ function decide(root, { actor, body, now = Date.now() } = {}) {
98
+ const by = actor && typeof actor.by === 'string' ? actor.by : null;
99
+ if (!by || !/^pm:/.test(by)) return { ok: false, error: 'only the PM decides who is a person' };
100
+ const b = body && typeof body === 'object' ? { ...body } : {};
101
+ if (b.act === 'note' && !b.id) b.id = noteId();
102
+ const c = P.checkDecision(b);
103
+ if (!c.ok) return c;
104
+ const r = readRows(root);
105
+ if (!r.ok) return r;
106
+ const v = P.fold(r.rows);
107
+ const row = c.row;
108
+ if (row.person && !v.people.some((p) => p.id === row.person)) return { ok: false, error: `no person ${row.person}` };
109
+ if (row.kind === 'add' && v.people.some((p) => p.handles.some((h) => P.handleKey(h) === P.handleKey(row.handle)))) {
110
+ return { ok: false, error: 'a person already holds that handle' };
111
+ }
112
+ const w = fourPanel.appendDeclaration(r.dir, P.LEDGER, stamp(row, by, now));
113
+ if (!w.ok) return { ok: false, error: w.error };
114
+ let person = row.person || null;
115
+ if (row.kind === 'add') {
116
+ const after = P.fold([...r.rows, stamp(row, by, now)]);
117
+ const p = after.people.find((x) => x.handles.some((h) => P.handleKey(h) === P.handleKey(row.handle)));
118
+ person = p ? p.id : null;
119
+ }
120
+ return { ok: true, row: w.recorded, person };
121
+ }
122
+
123
+ module.exports = { view, record, decide, settings, stateDir, readRows };