@bli-cockpit/cli 0.2.291 → 0.2.293
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/dist/adapters/claude-attribution-discovery.js +22 -0
- package/dist/adapters/claude-attribution-memoized-reads.js +3 -3
- package/dist/adapters/claude-attribution.js +5 -2
- package/dist/adapters/codex-attribution-memoized-reads.js +3 -3
- package/dist/adapters/codex-attribution.js +30 -12
- package/dist/adapters/codex-segment-discovery.js +66 -0
- package/dist/adapters/codex-session-revision.js +139 -0
- package/dist/adapters/codex-session-signals.js +2 -2
- package/dist/adapters/pi-session-facts.js +28 -0
- package/dist/adapters/pi-session-reader.js +132 -0
- package/dist/adapters/raw-evidence-codex-reader.js +17 -7
- package/dist/adapters/raw-evidence-transcript-body.js +3 -0
- package/dist/adapters/raw-evidence-transcript-file.js +27 -4
- package/dist/adapters/raw-evidence-transcript-stream.js +33 -1
- package/dist/agent-plugins-converge.js +24 -5
- package/dist/collection-scope.js +2 -0
- package/dist/commands/backfill-checkpoint.js +3 -1
- package/dist/commands/backfill-history-scan.js +38 -0
- package/dist/commands/backfill-issues.js +8 -20
- package/dist/commands/backfill-scan.js +18 -40
- package/dist/commands/backfill-upload.js +1 -0
- package/dist/commands/books-usage.js +2 -1
- package/dist/commands/books.js +12 -4
- package/dist/commands/codex-hooks-trust-choice.js +51 -0
- package/dist/commands/codex-hooks-trust-prompt.js +172 -0
- package/dist/commands/codex-managed-hooks.js +226 -0
- package/dist/commands/codex-revision-facts.js +64 -0
- package/dist/commands/doctor-report.js +2 -2
- package/dist/commands/doctor-update.js +2 -0
- package/dist/commands/doctor.js +2 -0
- package/dist/commands/local-args-collector-setup.js +2 -0
- package/dist/commands/local-args-collector-status.js +5 -3
- package/dist/commands/local-args-notes-targets.js +35 -0
- package/dist/commands/local-args-tower-notes.js +7 -18
- package/dist/commands/local-help-commands-tower.js +3 -2
- package/dist/commands/local-help-commands.js +3 -2
- package/dist/commands/local-help-notes.js +2 -1
- package/dist/commands/local-help.js +1 -1
- package/dist/commands/memory-install-codex-hooks.js +95 -2
- package/dist/commands/memory-install-receipt.js +17 -1
- package/dist/commands/memory-install-report.js +5 -4
- package/dist/commands/memory-install.js +15 -6
- package/dist/commands/memory-log.js +13 -2
- package/dist/commands/memory-map.js +6 -2
- package/dist/commands/memory-verb-list.js +1 -1
- package/dist/commands/notes-writes.js +24 -0
- package/dist/commands/notes.js +3 -2
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/session-sync-record.js +3 -1
- package/dist/commands/session-sync-scan.js +14 -6
- package/dist/commands/session-sync-shelf.js +2 -0
- package/dist/commands/session-sync-upload.js +1 -0
- package/dist/commands/setup-receipt-lines.js +2 -8
- package/dist/commands/sources-discovery.js +63 -14
- package/dist/commands/sources-reconciliation.js +7 -5
- package/dist/commands/sources-verdict.js +3 -0
- package/dist/cursors/raw-evidence-cursor.js +1 -0
- package/dist/image-description-session.js +4 -2
- package/dist/native-session-roots.js +31 -0
- package/dist/t3-native-homes.js +64 -0
- package/package.json +4 -4
|
@@ -112,7 +112,7 @@ export const TOWER_COMMAND_HELP = [
|
|
|
112
112
|
"dm <email> [<email>...] — with several addresses, opens (or re-opens) the group message with exactly those people plus you; the same set always resolves to the same channel. channel <channel> [--json] — the channel's details: name, description, kind, archived, audience (a public channel is everyone in the org, not a member count), source workspace label (where it came from; it grants no access), whether you can manage it, and whether you hid it. channel <channel> --rename <name> | --describe <text> | --archive | --unarchive — changes it (one update; --rename and --describe can go together). Only the person who made the channel, its owners and its moderators can; anyone else is refused channel_manage_refused, a name in use channel_name_taken, a direct message dm_not_editable. Archiving keeps every message and link and stops new posts (channel_archived); unarchive restores posting. members <channel> [--json] — who is in a private channel or a direct message; a public channel says everyone in the org can read it. members <channel> --add <email> | --remove <email> — private channels only, by the same people who can manage it; a removed person's next read is refused. A direct message keeps the people it started with (dm_not_editable): start a new group with `msg dm a@x b@y`. hide <channel> / unhide <channel> — drops a PUBLIC channel from your own sidebar, or puts it back; it changes nothing for anyone else and nothing about reading, search, mentions or notifications. A private channel or direct message is refused hide_not_public.",
|
|
113
113
|
"read <channel> [--limit <n>] [--thread <id>] [--mark-read] — the channel's most recent top-level messages, oldest first; --thread <id> reads one thread's replies instead. The first page also says where the messages come from (for an imported Slack channel: how long ago the archive was checked, or that it is unknown) and where YOUR read line stands. Reading never moves the read line; --mark-read moves it to the newest message shown, and says plainly if that failed.",
|
|
114
114
|
"unread <channel> --from <message-id> — marks that message and everything after it unread, so the web sidebar badge and the NEW MESSAGES line start there. Opening or reading a channel never clears unseen history.",
|
|
115
|
-
"send <channel> [--thread <id>] [--request-id <uuid>] — posts a message. The content is never accepted on the command line: pipe it in, e.g. `echo \"hello\" | cockpit msg send general`. Every send carries a request id (minted when you give none); retry a send whose answer was lost with the SAME --request-id and the same text and it posts once. The same id with different text is refused request_id_reused, not applied as an edit. send <channel> --file <id> [--file <id>...] — attaches up to 10 ready files (from `msg file upload`) to the message. The text is optional with a file; it is read from stdin only when something is piped in. file upload <channel> <path|-> [--name <name>] — uploads a file of 1 byte to 25 MB straight to storage (reserve, PUT, finalize) and prints its id and state; `-` reads stdin and needs --name. A file is ready, not sent: attach it with `msg send <channel> --file <id>`. file show <id> — the descriptor (state reserved, ready, attached, cancelled or abandoned). file download <id> [--out <path>] — saves the bytes (60-second signed link; never overwrites). file cancel <id> — ends an unattached file and removes its bytes. file list <channel> — your own unsent uploads there, with coverage. A file is readable only by readers of its channel, and only once attached to a live message. Refusals keep the door's reason: file_too_large, file_not_found_or_unreadable, file_not_uploaded, file_not_ready, file_already_attached, file_wrong_channel, storage_unreachable.",
|
|
115
|
+
"send <channel> [--thread <id>] [--request-id <uuid>] — posts a message. The content is never accepted on the command line: pipe it in, e.g. `echo \"hello\" | cockpit msg send general`. Every send carries a request id (minted when you give none); retry a send whose answer was lost with the SAME --request-id and the same text and it posts once. The same id with different text is refused request_id_reused, not applied as an edit. send <channel> --file <id> [--file <id>...] — attaches up to 10 ready files (from `msg file upload`) to the message. The text is optional with a file; it is read from stdin only when something is piped in. file upload <channel> <path|-> [--name <name>] — uploads a file of 1 byte to 25 MB straight to storage (reserve, PUT, finalize) and prints its id and state; `-` reads stdin and needs --name. A file is ready, not sent: attach it with `msg send <channel> --file <id>`. file show <id> — the descriptor (state reserved, ready, attached, cancelled or abandoned). file download <id> [--out <path>] — saves the bytes (60-second signed link; never overwrites). file cancel <id> — ends an unattached file and removes its bytes. file list <channel> — your own unsent uploads there, with coverage. A file is readable only by readers of its channel, and only once attached to a live message. Refusals keep the door's reason: file_too_large, file_not_found_or_unreadable, file_not_uploaded, file_not_ready, file_already_attached, file_wrong_channel, storage_unreachable (no answer: try again), storage_refused (storage said no: retrying will not help; the sentence names the step, HTTP status and class).",
|
|
116
116
|
"edit <message-id> [--expected-revision <n>] [--request-id <uuid>] — replaces your own message's text (piped in). With --expected-revision, an edit of a message that has moved on is refused revision_conflict and nothing is saved over it; the current revision is printed. A message you did not write, or one that was deleted, is refused by name. delete <message-id> [--expected-revision <n>] [--request-id <uuid>] — deletes by tombstone: the text is cleared and every surface reads \"This message was deleted\", but the row stays so its thread, replies and reply count survive. Search and unread counts ignore it. A repeat answers already_deleted. outcome <request-uuid> — your own write receipt for a send, edit or delete: committed, not_committed (with the reason) or unknown_check_again. Run it after a timeout before retrying. draft [list|show|save|discard|restore|send] <channel> [--thread <id>] [--expected-revision <n>] [--request-id <uuid>] — the text you have not sent yet, one draft per channel and thread, the same drafts the browser autosaves. list shows all your unsent drafts (newest first, with coverage); show prints one; save takes the body on stdin (`echo \"half a thought\" | cockpit msg draft save general`) and with --expected-revision refuses draft_revision_conflict, naming the stored revision, instead of overwriting; discard keeps the text (state discarded) and restore brings it back; send posts the stored draft with a fresh request id (--request-id to retry the same send) and its revision, prints the receipt, and marks the draft sent only if it did not change meanwhile. Only you can read or change your drafts.",
|
|
117
117
|
"thread <id> --channel <channel> [--before <cursor>] [--limit <n>] — one thread's replies by the parent message's id.",
|
|
118
118
|
"show <message-id> — exact message, thread root and bounded neighbors. read/thread --before takes the preceding nextCursor; repeat until hasMore is false. activity [--mentions|--threads] [--unread] [--before <cursor>] [--limit <n>] [--mark-read] — your Mentions (messages that mention you) and Threads (replies in threads you follow: you started it, replied natively or followed it by hand), newest first, with unread counts and coverage; items whose message you can no longer read are left out and counted as dropped. Nothing is marked read by looking; --mark-read marks the unread items just printed, and the web marks one when its message is shown. Activity covers native Tower messages only, so the Slack import adds none. follow <message-id> / unfollow <message-id> — the id is the thread's top-level message; an explicit unfollow wins over a later auto follow, and earlier items stay. react <message-id> <emoji> [--remove] — adds or takes off your reaction (idempotent). Refusals: thread_root_not_found_or_unreadable, not_a_thread_root, reaction_not_found. notify — your message notification settings (the same ones the web header menu and Settings change): the default level (all, mentions or none; mentions is the default and covers direct messages, @mentions and replies in threads you follow), quiet hours and per-channel overrides. notify set --level <all|mentions|none> changes the default; add --channel <id|name> for one channel, with --mute / --unmute, or --clear to remove the override. notify quiet <HH:MM-HH:MM> [--timezone <zone>] sets quiet hours (24-hour, may wrap midnight; the zone defaults to this machine's) and notify quiet --off removes them. notify receipts [--message <id>] [--before <cursor>] [--limit <n>] lists what was delivered, suppressed or blocked and why (never message text): outcome shown means Tower told a browser tab to show a banner; push_accepted means the push service took the phone push, not that a phone showed it. Quiet hours and levels hold back banners and phone pushes, never the message or its unread badge. Refusals: invalid_notification_level, invalid_quiet_hours, invalid_timezone. search [words] [--from <email|me>]... [--in <channel>] [--since YYYY-MM-DD] [--until YYYY-MM-DD] [--timezone <IANA>] [--replies|--top-level] [--has-file] [--before <cursor>] [--limit <n>] — message search on your own access, newest first (not ranked; `cockpit search --kind msg` is the ranked bar). Each hit prints its id, channel, author, time and snippet, then `more: --before <cursor>` or `complete`; repeat with the cursor until complete. --from takes an email, a user id or me and repeats; --in takes a channel id or #name (exact after case folding, an ambiguous name is refused); --since and --until are whole local days in --timezone, both inclusive (this machine's zone when you give none). The words may be left out when a filter is set, so `msg search --has-file` finds attachment-only messages. Inline operators work in the words as in the web box: from:<email|me> in:#name after:<day> before:<day> (exclusive) on:<day> has:file is:reply is:root. Recognised but unsupported operators (has:link, has:pin, has:reaction, is:saved, is:dm, to:, with:, during:) are refused unsupported_search_operator, naming the operator; a bad day, zone, person or channel is refused invalid_search_filter. Messages imported from Slack carry no file links, so has:file does not find them. Deleted messages, channels you cannot read and files you cannot read never appear.",
|
|
@@ -480,7 +480,8 @@ export const TOWER_COMMAND_HELP = [
|
|
|
480
480
|
"log appends an opinion to ~/.codex/AGENT-EXPERIENCE.md and posts it to Tower.",
|
|
481
481
|
"Use --reason-stdin instead of a quoted reason; --json reports shipped status.",
|
|
482
482
|
"Reasons: one line, max 500 characters; no prompts, memory bodies or secrets.",
|
|
483
|
-
"
|
|
483
|
+
"--store is optional: bli (BLI Memory) is the only store. The retired supermemory and both",
|
|
484
|
+
"values are accepted and logged as retired: both records as bli, supermemory records nothing.",
|
|
484
485
|
"Offline entries stay queued; the next collector sync retries with the same ID.",
|
|
485
486
|
],
|
|
486
487
|
],
|
|
@@ -23,7 +23,7 @@ import { TOWER_COMMAND_HELP } from "./local-help-commands-tower.js";
|
|
|
23
23
|
import { TOWER_OPS_COMMAND_HELP } from "./local-help-commands-tower-ops.js";
|
|
24
24
|
/** One manual per collector command; the Tower nouns are spread in from their own file. */
|
|
25
25
|
const COMMAND_HELP = new Map([
|
|
26
|
-
["books", [BOOKS_USAGE, "Client Books: each engagement's record (timeline, actions, documents, meetings, ...). A member of a book reads it; a member with edit access, or a founder, writes it; only founders create books and manage members. put without --merge replaces the record's data; --merge changes only the fields given. --version <n> makes a write conditional: refused as version_conflict when someone changed the record since you read version n (0 = must not exist yet). delete is soft. export writes <dir>/<collection>/<doc_id>.json; import reads that layout and writes 50 records per batch. MCP twins: books_list, books_create, books_members, books_read, books_write, books_batch. Docs: docs/architecture/client-books.md."]],
|
|
26
|
+
["books", [BOOKS_USAGE, "Client Books: each engagement's record (timeline, actions, documents, meetings, ...). A member of a book reads it; a member with edit access, or a founder, writes it; only founders create books and manage members. put without --merge replaces the record's data; --merge changes only the fields given. --version <n> makes a write conditional: refused as version_conflict when someone changed the record since you read version n (0 = must not exist yet). delete is soft. export writes <dir>/<collection>/<doc_id>.json; import reads that layout and writes 50 records per batch. archive hides a book from the Client Books page; archive --restore returns it; nothing is deleted. list marks archived books. MCP twins: books_list, books_archive, books_create, books_members, books_read, books_write, books_batch. Docs: docs/architecture/client-books.md."]],
|
|
27
27
|
["glossary", ["Usage: cockpit glossary list|add <wrong> --to <name>|remove <wrong> [--json]", "Quote phrases. Removing a form prevents future corrections of it."]],
|
|
28
28
|
["skills", ["Usage: cockpit skills library [--weeks <1-52>] [--json] | cockpit skills refresh [--weeks <1-104>] [--json]", "The Skill Library: every agent skill and command our GitHub repos define (name, description, repo, path, versions) with weekly use by person, from the sessions Tower already collects. Uses that match no scanned skill are listed as unmatched, never dropped. The first line is the coverage: repos scanned, skills, invocations matched of total. You see your own usage; Vee, Jeetesh and Ed see everyone's except a founder's. refresh scans the repos and recomputes the weekly totals now (super_admin only; a stale library also refreshes itself when read). MCP twins: skills_library, skills_refresh. Web: TEAM > Skills."]],
|
|
29
29
|
["push", ["Usage: cockpit push status | test [--dashboard-url <url>] [--json]", "Phone notifications. status lists your saved devices and whether the server has its push keys; test sends a test push to every saved device. Turn a phone on from Settings on the phone."]],
|
|
@@ -105,7 +105,7 @@ const COMMAND_HELP = new Map([
|
|
|
105
105
|
[
|
|
106
106
|
"doctor",
|
|
107
107
|
[
|
|
108
|
-
"Usage: cockpit doctor [--workspace <path>] [--check | --no-repair] [--no-usage-telemetry] [--no-memory] [--lock-wait <seconds>] [--backfill-budget <seconds>] [--json]",
|
|
108
|
+
"Usage: cockpit doctor [--workspace <path>] [--check | --no-repair] [--no-usage-telemetry] [--no-memory] [--trust-codex-hooks] [--lock-wait <seconds>] [--backfill-budget <seconds>] [--json]",
|
|
109
109
|
"",
|
|
110
110
|
"Diagnose, repair, and verify this machine.",
|
|
111
111
|
"Repairs CLI, background sync, sign-in, roots, memory and hooks, agent rules, catch-up, uploads, and cleanup.",
|
|
@@ -113,6 +113,7 @@ const COMMAND_HELP = new Map([
|
|
|
113
113
|
"--no-usage-telemetry switches off Claude Code and Codex usage export, including later doctor runs.",
|
|
114
114
|
"--no-memory turns BLI Memory off on this machine (same as `cockpit memory off`), including later runs and the background sync.",
|
|
115
115
|
"In a terminal, the first doctor or onboard asks once whether BLI Memory may save your sessions; `cockpit memory on|off` changes the answer.",
|
|
116
|
+
"In a terminal, doctor also asks once \"Auto-trust the BLI Memory hooks in Codex? [Y/n]\" while Codex has not trusted them; yes (the default) writes them to Codex's system hooks folder with one sudo or Windows administrator prompt, so Codex never asks again. A no is remembered; --trust-codex-hooks trusts them without the question. Background and --json runs never ask.",
|
|
116
117
|
"--check or --no-repair diagnoses without repairs. --dry-run previews repairs.",
|
|
117
118
|
"--backfill-budget defaults to 900 seconds; catch-up repeats chunks until complete, with progress every 30 seconds.",
|
|
118
119
|
"Doctor never waits for the background sync: a running sync is reported as running (with how long ago it started) and doctor finishes; a sync that stopped responding or made no progress for 15 minutes is ended and doctor carries on. --lock-wait is accepted and changes nothing. Progress prints at most every 30 seconds and says what changed.",
|
|
@@ -3,7 +3,7 @@ export const NOTES_HELP = [
|
|
|
3
3
|
[
|
|
4
4
|
"notes",
|
|
5
5
|
[
|
|
6
|
-
"Usage: cockpit notes [list|show <id>|shelf|shelves|folders|mkdir <path>|rmdir <path>|rename <path>|upload <paths...> [--wait]|paste|share <id>|unshare <id>|summarize <id>|move <id>|accounts|connect|detach <id>|sync <id>|sync-status [<id>]|recordings <note-id>|recording <id>|record start|stop|status|cancel|app install|uninstall|status|review [<note-id>] [--submit|--accept-all]] [flags]",
|
|
6
|
+
"Usage: cockpit notes [list|show <id>|shelf|shelves|folders|mkdir <path>|rmdir <path>|rename <path>|upload <paths...> [--wait]|paste|share <id>|unshare <id>|correct <id> --from <term> --to <term>|summarize <id>|move <id>|accounts|connect|detach <id>|sync <id>|sync-status [<id>]|recordings <note-id>|recording <id>|record start|stop|status|cancel|app install|uninstall|status|review [<note-id>] [--submit|--accept-all]] [flags]",
|
|
7
7
|
"Audio uploads: MP3, M4A, WAV, WebM and OGG, up to 1 GB. Over 4 MB (a whole meeting) the file goes straight to storage and is transcribed in the background; --wait then polls for up to 4 hours and prints progress, otherwise for at most 330 seconds.",
|
|
8
8
|
"",
|
|
9
9
|
"record start|stop [<session-id>]|status|cancel — record a meeting in a room from this machine's microphone (Apple Silicon macOS and Windows; Linux says it is not supported). On Windows the microphone needs Settings > Privacy & security > Microphone on, including Let desktop apps access your microphone. Nothing leaves the machine while recording; the recorder stops by itself after 8 hours (under the 1 GB upload limit). `stop` uploads the whole original recording, silence included, and Tower writes a note with Speaker 1, Speaker 2... lines, or says no speech was detected (add --wait to watch the transcription). `cancel` stops and KEEPS the file; `status` lists what is recording and every session waiting for upload (`needs_upload`); `stop <session-id>` uploads a cancelled, crashed or auto-stopped session. The BLI Notetaker app uploads waiting sessions by itself. The recording also stays on this machine.",
|
|
@@ -24,6 +24,7 @@ export const NOTES_HELP = [
|
|
|
24
24
|
"share <id> [--yes] — let everyone signed in read the statements that are safe to share. Asks first in a terminal; --yes is required without one, and --json implies --yes.",
|
|
25
25
|
"summarize <id> — write a new summary (decisions, action items, key points) where every claim cites the speaker, timestamp and segment id (s12) it came from; claims the transcript does not support are dropped. Takes up to a minute; earlier summaries are kept. `show <id>` prints the newest one.",
|
|
26
26
|
"unshare <id> — take it back. Never asks: it only ever narrows who can read.",
|
|
27
|
+
"correct <id> --from \"<misheard>\" --to \"<right>\" [--transcript] — fix a misheard term: every whole-word mention in the note's notes and summary in one write, the previous text kept. The transcript stays as recorded unless you pass --transcript, and then each fix is marked beside the words as heard. Owner or admin only. Then `cockpit glossary add` makes future transcripts and incoming notes spell it right.",
|
|
27
28
|
"move <id> (--to \"<shelf>\"|--clear-shelf) — put it on a different shelf, or take the shelf off.",
|
|
28
29
|
"place <id> [--apply] — say which shelf this note belongs on and why, from the meeting it is part of, who was in the room and its own name. Moves nothing without --apply, and never overrules a shelf somebody typed.",
|
|
29
30
|
"",
|
|
@@ -152,7 +152,7 @@ const COMMAND_SUMMARY_LINES = [
|
|
|
152
152
|
" cockpit serve [--port <port>] [--workspace <path>]",
|
|
153
153
|
" cockpit autostart [install|uninstall|status] [--workspace <path>] [--dashboard-url <url>] [--interval-seconds <n>] [--json]",
|
|
154
154
|
" cockpit agent-rules [install|uninstall|status] [--host codex|claude|all] [--workspace <path>] [--json]",
|
|
155
|
-
" cockpit memory [search \"<question>\"|save|update <id>|forget <id>|profile|on|off|install|uninstall|status|log <win|loss|noise> --store
|
|
155
|
+
" cockpit memory [search \"<question>\"|save|update <id>|forget <id>|profile|on|off|install|uninstall|status|log <win|loss|noise> [--store bli] \"<reason>\"] [--container <tag>] [--limit <n>] [--no-recent] [--include-originals|--no-originals] [--include-source|--no-source] [--custom-id <id>] [--extract] [--content-stdin] [--reason <label>] [--file <path>] [--dashboard-url <url>] [--dry-run] [--json]",
|
|
156
156
|
" cockpit clean [--dry-run] [--all-committed] [--reconcile] [--dashboard-url <url>] [--json]",
|
|
157
157
|
" cockpit sources check [--reconcile] [--older-than <days>] [--dashboard-url <url>] [--json]",
|
|
158
158
|
" cockpit docs [list|tree|read <id|slug>|create --title <t>|update <id>|delete <id|slug>|trash|versions <id|slug>|restore <id>] [--parent <id>|root|--clear-parent] [--query <text> [--word]] [--limit <n>] [--visibility org|private] [--file <path>|--body-stdin] [--allow-empty] [--dashboard-url <url>] [--json]",
|
|
@@ -38,7 +38,16 @@
|
|
|
38
38
|
* `needs_trust` exists to name — and it is the one step an intern has to
|
|
39
39
|
* do by hand.
|
|
40
40
|
*
|
|
41
|
-
* ##
|
|
41
|
+
* ## Managed hooks (BLI-24349)
|
|
42
|
+
*
|
|
43
|
+
* Doctor's "Auto-trust the BLI Memory hooks in Codex?" puts the same hooks in
|
|
44
|
+
* Codex's SYSTEM folder (`codex-managed-hooks.ts`), where Codex runs them with
|
|
45
|
+
* no trust step. When that copy is there, the installer below takes the
|
|
46
|
+
* user-scope copy OUT instead of writing it (nothing runs twice), and the
|
|
47
|
+
* inspector reports the system file. Proven on a real Codex 0.166.0 run; see
|
|
48
|
+
* that module's header.
|
|
49
|
+
*
|
|
50
|
+
* ## What is NOT verified (user-scope route)
|
|
42
51
|
*
|
|
43
52
|
* Nobody has yet trusted these hooks on a real machine, so the wiring past the
|
|
44
53
|
* write is unproven end to end: the file shape, the event names and the trust
|
|
@@ -59,14 +68,36 @@
|
|
|
59
68
|
* A gauge that can only be too pessimistic is the right way round for this one.
|
|
60
69
|
*/
|
|
61
70
|
import path from "node:path";
|
|
62
|
-
import { applyHookGroups, applyJsonTarget, hookGroupsMatch, inspectJsonTarget, ourHookCount, } from "./memory-install-claude.js";
|
|
71
|
+
import { applyHookGroups, applyJsonTarget, hookGroupsMatch, inspectJsonTarget, ourHookCount, parseJsonRecord, serializeJson, stripOurHooks, } from "./memory-install-claude.js";
|
|
72
|
+
import { codexSystemHooksFile, readCodexManagedHooks, } from "./codex-managed-hooks.js";
|
|
63
73
|
export function codexHooksFile(homeDir) {
|
|
64
74
|
return path.join(homeDir, ".codex", "hooks.json");
|
|
65
75
|
}
|
|
66
76
|
export function codexConfigTomlFile(homeDir) {
|
|
67
77
|
return path.join(homeDir, ".codex", "config.toml");
|
|
68
78
|
}
|
|
79
|
+
function systemHooksFileFor(options) {
|
|
80
|
+
return options.systemHooksFile === undefined ? codexSystemHooksFile(process.platform) : options.systemHooksFile;
|
|
81
|
+
}
|
|
82
|
+
/** The managed reading, or null when nobody asked to look. Never throws. */
|
|
83
|
+
export async function readCodexManagedHooksFor(options) {
|
|
84
|
+
const file = systemHooksFileFor(options);
|
|
85
|
+
if (!file)
|
|
86
|
+
return null;
|
|
87
|
+
return readCodexManagedHooks({ config: options.config, io: options.io, file });
|
|
88
|
+
}
|
|
69
89
|
export async function installCodexMemoryHooks(options) {
|
|
90
|
+
// BLI-24349: once the hooks live in Codex's system folder (managed, run
|
|
91
|
+
// with no trust step), a user-scope copy would make every hook run twice
|
|
92
|
+
// the day somebody also trusts it in /hooks. So the user copy is taken out
|
|
93
|
+
// instead of written, and the target names the system file. A STALE
|
|
94
|
+
// system copy is treated the same way: the old managed hooks keep running,
|
|
95
|
+
// and doctor asks once to refresh them (the receipt says `needs_trust`,
|
|
96
|
+
// reason `managed_hooks_stale`).
|
|
97
|
+
const managed = await readCodexManagedHooksFor(options);
|
|
98
|
+
if (managed && (managed.state === "current" || managed.state === "stale")) {
|
|
99
|
+
return stripUserScopeCopy(options, managed);
|
|
100
|
+
}
|
|
70
101
|
return applyJsonTarget({
|
|
71
102
|
target: "codex_hooks",
|
|
72
103
|
file: codexHooksFile(options.homeDir),
|
|
@@ -76,6 +107,15 @@ export async function installCodexMemoryHooks(options) {
|
|
|
76
107
|
});
|
|
77
108
|
}
|
|
78
109
|
export async function inspectCodexMemoryHooks(options) {
|
|
110
|
+
const managed = await readCodexManagedHooksFor(options);
|
|
111
|
+
if (managed && (managed.state === "current" || managed.state === "stale")) {
|
|
112
|
+
return {
|
|
113
|
+
target: "codex_hooks",
|
|
114
|
+
status: "installed",
|
|
115
|
+
reason: managed.state === "current" ? "managed_by_system" : "managed_hooks_stale",
|
|
116
|
+
path: managed.file,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
79
119
|
return inspectJsonTarget({
|
|
80
120
|
target: "codex_hooks",
|
|
81
121
|
file: codexHooksFile(options.homeDir),
|
|
@@ -84,6 +124,59 @@ export async function inspectCodexMemoryHooks(options) {
|
|
|
84
124
|
matches: (root) => hookGroupsMatch(root, options.config.hooks),
|
|
85
125
|
});
|
|
86
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* Takes OUR groups out of `~/.codex/hooks.json` (a person's own hooks stay),
|
|
129
|
+
* because the system copy now carries them. Same read-back rule as every
|
|
130
|
+
* other write here.
|
|
131
|
+
*/
|
|
132
|
+
async function stripUserScopeCopy(options, managed) {
|
|
133
|
+
const reason = managed.state === "current" ? "managed_by_system" : "managed_hooks_stale";
|
|
134
|
+
const file = codexHooksFile(options.homeDir);
|
|
135
|
+
let raw;
|
|
136
|
+
try {
|
|
137
|
+
raw = await options.io.readText(file);
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
return { target: "codex_hooks", status: "failed", reason: "user_copy_unreadable", path: file, detail: error instanceof Error ? error.name : "unknown" };
|
|
141
|
+
}
|
|
142
|
+
const root = raw === null || !raw.trim() ? null : parseJsonRecord(raw);
|
|
143
|
+
if (!root || ourHookCount(root) === 0) {
|
|
144
|
+
return { target: "codex_hooks", status: "already", reason, path: managed.file };
|
|
145
|
+
}
|
|
146
|
+
if (options.dryRun) {
|
|
147
|
+
return { target: "codex_hooks", status: "would_install", reason: "dry_run", path: file, detail: "would remove the user-scope copy; the system copy carries the hooks" };
|
|
148
|
+
}
|
|
149
|
+
const next = removeOurGroups(structuredClone(root));
|
|
150
|
+
try {
|
|
151
|
+
await options.io.writeText(file, serializeJson(next, raw ?? ""));
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
return { target: "codex_hooks", status: "failed", reason: "user_copy_write_failed", path: file, detail: error instanceof Error ? error.name : "unknown" };
|
|
155
|
+
}
|
|
156
|
+
const storedRaw = await options.io.readText(file);
|
|
157
|
+
const stored = storedRaw === null ? null : parseJsonRecord(storedRaw);
|
|
158
|
+
if (!stored || ourHookCount(stored) > 0) {
|
|
159
|
+
return { target: "codex_hooks", status: "failed", reason: "read_back_mismatch", path: file, detail: "the user-scope copy is still there after the write" };
|
|
160
|
+
}
|
|
161
|
+
console.error("[codex-hooks-trust] user-scope copy removed; Codex runs the system copy", JSON.stringify({ reason: "user_copy_removed", managed_state: managed.state }));
|
|
162
|
+
return { target: "codex_hooks", status: "installed", reason, path: managed.file, detail: "user-scope copy removed" };
|
|
163
|
+
}
|
|
164
|
+
function removeOurGroups(root) {
|
|
165
|
+
const hooks = root["hooks"];
|
|
166
|
+
if (!hooks || typeof hooks !== "object" || Array.isArray(hooks))
|
|
167
|
+
return root;
|
|
168
|
+
const record = hooks;
|
|
169
|
+
for (const [event, groups] of Object.entries(record)) {
|
|
170
|
+
if (!Array.isArray(groups))
|
|
171
|
+
continue;
|
|
172
|
+
const kept = stripOurHooks(groups).groups;
|
|
173
|
+
if (kept.length === 0)
|
|
174
|
+
delete record[event];
|
|
175
|
+
else
|
|
176
|
+
record[event] = kept;
|
|
177
|
+
}
|
|
178
|
+
return root;
|
|
179
|
+
}
|
|
87
180
|
/**
|
|
88
181
|
* Reads the trust state out of `~/.codex/config.toml` WITHOUT parsing the
|
|
89
182
|
* whole file.
|
|
@@ -86,6 +86,18 @@ function judgeCodexHooks(input, reasons) {
|
|
|
86
86
|
reasons[key] = "hooks_disabled_in_config";
|
|
87
87
|
return "unsupported";
|
|
88
88
|
}
|
|
89
|
+
// BLI-24349: hooks in Codex's system folder are managed and run with no
|
|
90
|
+
// trust step. `ok` carries its reason so the board can say HOW it is ok.
|
|
91
|
+
if (input.codexManaged === "current") {
|
|
92
|
+
reasons[key] = "managed_by_system";
|
|
93
|
+
return "ok";
|
|
94
|
+
}
|
|
95
|
+
if (input.codexManaged === "stale") {
|
|
96
|
+
// The old managed copy may still run, but not today's hooks; doctor asks
|
|
97
|
+
// once to refresh it, so the action is the same as an untrusted machine's.
|
|
98
|
+
reasons[key] = "managed_hooks_stale";
|
|
99
|
+
return "needs_trust";
|
|
100
|
+
}
|
|
89
101
|
const target = input.targets.find((entry) => entry.target === "codex_hooks");
|
|
90
102
|
if (!target) {
|
|
91
103
|
reasons[key] = input.binFound ? "target_not_checked" : "bin_missing";
|
|
@@ -105,7 +117,11 @@ function judgeCodexHooks(input, reasons) {
|
|
|
105
117
|
if (input.codexTrust.trustedRows >= input.expectedTrustRows)
|
|
106
118
|
return "ok";
|
|
107
119
|
reasons[key] =
|
|
108
|
-
input.codexTrust.disabledRows > 0
|
|
120
|
+
input.codexTrust.disabledRows > 0
|
|
121
|
+
? "trust_rows_disabled"
|
|
122
|
+
: input.codexTrustDeclined
|
|
123
|
+
? "trust_declined"
|
|
124
|
+
: "no_trust_rows";
|
|
109
125
|
return "needs_trust";
|
|
110
126
|
}
|
|
111
127
|
/**
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* one outcome, and rendering that outcome for a log line or a terminal.
|
|
4
4
|
* Sibling of `memory-install.ts`, named in its header.
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
6
|
+
import { codexHooksTrustAction, memoryInstallGaps } from "@bli-cockpit/telemetry-core";
|
|
7
7
|
import { memoryDaemonLine } from "./memory-daemon-probe.js";
|
|
8
8
|
import { memoryReceiptLine } from "./memory-install-receipt.js";
|
|
9
9
|
import { DEFAULT_DASHBOARD_URL, getCollectorRuntimePaths, readLocalCollectorConfig, } from "../local-state.js";
|
|
@@ -102,9 +102,10 @@ export function memoryOutcomeLines(outcome) {
|
|
|
102
102
|
// The trust step is the one thing a person has to do by hand, so it is said
|
|
103
103
|
// here in words rather than left as a status word they have to look up.
|
|
104
104
|
if (outcome.receipt.codex.hooks === "needs_trust") {
|
|
105
|
-
lines.push(
|
|
106
|
-
|
|
107
|
-
|
|
105
|
+
lines.push(` Codex will NOT run these hooks until they are trusted. ${codexHooksTrustAction(outcome.receipt.reasons?.["codex.hooks"])}`);
|
|
106
|
+
}
|
|
107
|
+
if (outcome.receipt.codex.hooks === "ok" && outcome.receipt.reasons?.["codex.hooks"] === "managed_by_system") {
|
|
108
|
+
lines.push(" Codex runs these hooks from its system hooks folder (managed), so no /hooks trust step is needed.");
|
|
108
109
|
}
|
|
109
110
|
if (outcome.receipt.codex.hooks === "unsupported") {
|
|
110
111
|
lines.push(" Codex hooks are turned off in your ~/.codex/config.toml ([features] hooks = false), " +
|
|
@@ -52,7 +52,9 @@ import { removeMemoryIntegration } from "./memory-uninstall.js";
|
|
|
52
52
|
import { readEffectiveMemoryCapture, readMemoryCaptureChoice, writeMemoryCaptureChoice } from "../memory-capture-choice.js";
|
|
53
53
|
import { envWithNodeRuntimeOnPath } from "../scheduled-self-update.js";
|
|
54
54
|
import { probeMemoryDaemon } from "./memory-daemon-probe.js";
|
|
55
|
-
import { expectedCodexTrustRows, readCodexHookTrustFromDisk, } from "./memory-install-codex-hooks.js";
|
|
55
|
+
import { expectedCodexTrustRows, readCodexHookTrustFromDisk, readCodexManagedHooksFor, } from "./memory-install-codex-hooks.js";
|
|
56
|
+
import { codexSystemHooksFileFor } from "./codex-managed-hooks.js";
|
|
57
|
+
import { readCodexHooksTrustChoice } from "./codex-hooks-trust-choice.js";
|
|
56
58
|
import { buildMemoryInstallReceipt, writeMemoryReceiptFile, } from "./memory-install-receipt.js";
|
|
57
59
|
import { getCollectorRuntimePaths } from "../local-state.js";
|
|
58
60
|
import { aggregate, logMemoryOutcome, memoryOutcomeLines, resolveDashboardUrl } from "./memory-install-report.js";
|
|
@@ -128,8 +130,9 @@ export async function installMemoryIntegration(command, io, deps = {}) {
|
|
|
128
130
|
const targets = [resolved.binTarget];
|
|
129
131
|
// `no_bin_no_write`: with no resolved server there is nothing safe to
|
|
130
132
|
// register, so not one file is opened. The bin target carries the reason.
|
|
133
|
+
const systemHooksFile = codexSystemHooksFileFor(deps.codexSystemHooksFile, platform, io.env);
|
|
131
134
|
if (resolved.config) {
|
|
132
|
-
const host = { homeDir, config: resolved.config, dryRun: command.dryRun, io: fileIo };
|
|
135
|
+
const host = { homeDir, config: resolved.config, dryRun: command.dryRun, io: fileIo, systemHooksFile };
|
|
133
136
|
targets.push(...(await installClaudeMemoryIntegration(host)));
|
|
134
137
|
targets.push(...(await installCodexMemoryIntegration(host)));
|
|
135
138
|
}
|
|
@@ -142,7 +145,7 @@ export async function installMemoryIntegration(command, io, deps = {}) {
|
|
|
142
145
|
const outcome = {
|
|
143
146
|
action: "install",
|
|
144
147
|
...(capture.ignored ? { opt_out_ignored: "opt_out_ignored_not_founder" } : {}),
|
|
145
|
-
...(await readBackOutcome(targets, resolved, homeDir, fileIo)),
|
|
148
|
+
...(await readBackOutcome(targets, resolved, homeDir, fileIo, systemHooksFile)),
|
|
146
149
|
};
|
|
147
150
|
logMemoryOutcome(outcome, platform);
|
|
148
151
|
// The heartbeat reads the cache, not the machine — see the receipt module's
|
|
@@ -223,7 +226,10 @@ async function installWhileOptedOut(command, io, deps, scope) {
|
|
|
223
226
|
return outcome;
|
|
224
227
|
}
|
|
225
228
|
/** What both `install` and `status` report once the targets are known: their verdict and the receipt. */
|
|
226
|
-
async function readBackOutcome(targets, resolved, homeDir, fileIo) {
|
|
229
|
+
async function readBackOutcome(targets, resolved, homeDir, fileIo, systemHooksFile) {
|
|
230
|
+
// BLI-24349: the managed copy and the person's "no" both shape `codex.hooks`.
|
|
231
|
+
const managed = resolved.config ? await readCodexManagedHooksFor({ config: resolved.config, io: fileIo, systemHooksFile }) : null;
|
|
232
|
+
const choice = await readCodexHooksTrustChoice(getCollectorRuntimePaths(homeDir));
|
|
227
233
|
return {
|
|
228
234
|
...aggregate(targets),
|
|
229
235
|
config_source: resolved.source,
|
|
@@ -235,6 +241,8 @@ async function readBackOutcome(targets, resolved, homeDir, fileIo) {
|
|
|
235
241
|
binFound: resolved.config !== null,
|
|
236
242
|
codexTrust: await codexTrustFor(homeDir, fileIo),
|
|
237
243
|
expectedTrustRows: resolved.config ? expectedCodexTrustRows(resolved.config) : 0,
|
|
244
|
+
codexManaged: managed?.state ?? null,
|
|
245
|
+
codexTrustDeclined: choice.choice === "declined",
|
|
238
246
|
}),
|
|
239
247
|
};
|
|
240
248
|
}
|
|
@@ -274,8 +282,9 @@ export async function inspectMemoryIntegration(command, io, deps = {}) {
|
|
|
274
282
|
// Without a resolved server there is no shape to compare the stored config
|
|
275
283
|
// against, so `status` says the one true thing — the server is not here —
|
|
276
284
|
// rather than reporting four targets against a shape we invented.
|
|
285
|
+
const systemHooksFile = codexSystemHooksFileFor(deps.codexSystemHooksFile, platform, io.env);
|
|
277
286
|
if (resolved.config) {
|
|
278
|
-
const host = { homeDir, config: resolved.config, io: fileIo };
|
|
287
|
+
const host = { homeDir, config: resolved.config, io: fileIo, systemHooksFile };
|
|
279
288
|
targets.push(...(await inspectClaudeMemoryIntegration(host)));
|
|
280
289
|
targets.push(...(await inspectCodexMemoryIntegration(host)));
|
|
281
290
|
}
|
|
@@ -302,7 +311,7 @@ export async function inspectMemoryIntegration(command, io, deps = {}) {
|
|
|
302
311
|
// bounded, it opens nothing but a local socket, and a machine without one
|
|
303
312
|
// is reported as exactly that rather than as a problem.
|
|
304
313
|
daemon: await probeMemoryDaemon({ ...(homeDir ? { homeDir } : {}) }),
|
|
305
|
-
...(await readBackOutcome(targets, resolved, homeDir, fileIo)),
|
|
314
|
+
...(await readBackOutcome(targets, resolved, homeDir, fileIo, systemHooksFile)),
|
|
306
315
|
};
|
|
307
316
|
// `status` is a pure read and its reading is exactly as good as an install's
|
|
308
317
|
// — both re-parse the stored files — so it refreshes the cache too. A person
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import { appendExperience, drainExperiences, shipExperience, validateExperience } from "@bli-cockpit/telemetry-core/memory-experience";
|
|
2
|
+
import { appendExperience, drainExperiences, normaliseExperienceStore, shipExperience, validateExperience } from "@bli-cockpit/telemetry-core/memory-experience";
|
|
3
3
|
import { readPipedText, writeLine } from "./cli-io.js";
|
|
4
4
|
import { callTower, openTower } from "./tower-command.js";
|
|
5
5
|
function sender(command, io) {
|
|
@@ -25,7 +25,18 @@ export async function runMemoryLog(command, io) {
|
|
|
25
25
|
? (await readPipedText(io.stdin, { maxChars: 1002 })).trim()
|
|
26
26
|
: command.reason;
|
|
27
27
|
validateExperience(command.store, command.verdict, reason);
|
|
28
|
-
|
|
28
|
+
// BLI-24380: BLI Memory is the only store. A retired store is accepted and
|
|
29
|
+
// logged (the normaliser logs the line); `supermemory` records nothing.
|
|
30
|
+
const store = normaliseExperienceStore(command.store);
|
|
31
|
+
if (store.store === null) {
|
|
32
|
+
const receipt = { ok: true, recorded: false, reason: store.reason, given: store.given };
|
|
33
|
+
writeLine(io.stderr, `[memory experience] not recorded ${JSON.stringify(receipt)}`);
|
|
34
|
+
writeLine(io.stdout, command.json
|
|
35
|
+
? JSON.stringify(receipt)
|
|
36
|
+
: "Not recorded: Supermemory is retired (store_retired). BLI Memory is the only store; use --store bli.");
|
|
37
|
+
return 0;
|
|
38
|
+
}
|
|
39
|
+
const entry = await appendExperience({ store: store.store, verdict: command.verdict, reason }, {
|
|
29
40
|
homeDir: command.homeDir,
|
|
30
41
|
agent: "cockpit",
|
|
31
42
|
project: path.basename(process.cwd()),
|
|
@@ -14,7 +14,7 @@ export async function runMemoryMap(door) {
|
|
|
14
14
|
if (!answer.ok)
|
|
15
15
|
return failAgentDoor(door, "[memory map cli]", answer.reason, answer.detail);
|
|
16
16
|
const body = answer.body;
|
|
17
|
-
writeLine(door.io.stderr, `[memory map cli] answered ${JSON.stringify({ reason: body.capped ? "capped" : "exhausted", examined: body.examined, total: body.total })}`);
|
|
17
|
+
writeLine(door.io.stderr, `[memory map cli] answered ${JSON.stringify({ reason: body.capped ? "capped" : "exhausted", examined: body.examined, total: body.total, total_source: body.totalSource ?? null })}`);
|
|
18
18
|
if (door.json)
|
|
19
19
|
return emitAgentDoor(door, body);
|
|
20
20
|
for (const project of body.projects) {
|
|
@@ -22,7 +22,11 @@ export async function runMemoryMap(door) {
|
|
|
22
22
|
for (const topic of project.topics)
|
|
23
23
|
writeLine(door.io.stdout, ` ${topic.label}: ${topic.count}${topic.averageImportance === null ? " (unscored)" : ` (importance ${Math.round(topic.averageImportance)}/100)`}`);
|
|
24
24
|
}
|
|
25
|
-
|
|
25
|
+
// BLI-24124: the total is the stored count with its time, or the rows read when none is stored (an older server sends neither field).
|
|
26
|
+
// BLI-24122: a server that sends `words` is printed as-is, so the page, this command and MCP say one thing.
|
|
27
|
+
const totalAge = body.totalSource === "stored_count" && body.totalAsOf ? ` Total counted ${body.totalAsOf}.`
|
|
28
|
+
: body.totalSource === "rows_read" ? " No stored total yet, so the total is the memories read." : "";
|
|
29
|
+
writeLine(door.io.stdout, body.words ?? `${body.total.toLocaleString("en-US")} visible memories; the map draws ${body.examined.toLocaleString("en-US")} of them; as of ${body.computedAt}.${totalAge}`);
|
|
26
30
|
if (!body.projects.length)
|
|
27
31
|
writeLine(door.io.stdout, "No project knowledge yet.");
|
|
28
32
|
return 0;
|
|
@@ -55,7 +55,7 @@ const VERB_LIST_LINES = [
|
|
|
55
55
|
" cockpit memory status [--json] read the stored config back",
|
|
56
56
|
"",
|
|
57
57
|
"The experience log:",
|
|
58
|
-
" cockpit memory log <win|loss|noise> --store
|
|
58
|
+
" cockpit memory log <win|loss|noise> [--store bli] \"<reason>\"",
|
|
59
59
|
"",
|
|
60
60
|
"Search: --limit caps the MEMORIES (default 5, at most 20). Up to 3 Tower pages and",
|
|
61
61
|
"tickets ride beside them whatever --limit says; for more, use `cockpit search`.",
|
|
@@ -223,6 +223,30 @@ export async function shareNote(command, door) {
|
|
|
223
223
|
sayUpload(door, body);
|
|
224
224
|
return 0;
|
|
225
225
|
}
|
|
226
|
+
/**
|
|
227
|
+
* `cockpit notes correct <note-id> --from <term> --to <term> [--transcript]` (BLI-24437): one call to
|
|
228
|
+
* `POST /api/notes/correct`, the door the browser, MCP and JARVIS use. The server counts; this prints its sentences.
|
|
229
|
+
* A refusal, including "nothing_to_correct" (no mention found, nothing changed), prints the server's sentence and exits 1.
|
|
230
|
+
*/
|
|
231
|
+
export async function correctNote(command, door) {
|
|
232
|
+
const scope = command.transcript ? "notes_and_transcript" : "notes";
|
|
233
|
+
const answer = await ask(door, {
|
|
234
|
+
path: "/api/notes/correct",
|
|
235
|
+
method: "POST",
|
|
236
|
+
label: "notes correct",
|
|
237
|
+
timeoutMs: READ_DEADLINE_MS,
|
|
238
|
+
body: { note_id: command.noteId, from: command.from, to: command.to, scope },
|
|
239
|
+
});
|
|
240
|
+
if (!answer.ok) {
|
|
241
|
+
return fail(door, answer.reason, answer.detail);
|
|
242
|
+
}
|
|
243
|
+
const body = answer.body;
|
|
244
|
+
writeLine(door.io.stderr, `${TAG} corrected ${JSON.stringify({ note_id: command.noteId ?? null, scope, notes: body.counts?.notes ?? null, transcript: body.counts?.transcript ?? null, summary: body.counts?.summary ?? null })}`);
|
|
245
|
+
if (door.json)
|
|
246
|
+
return emit(door, body);
|
|
247
|
+
sayUpload(door, body);
|
|
248
|
+
return 0;
|
|
249
|
+
}
|
|
226
250
|
/** A summary run is two model calls over a long transcript: give it room (BLI-20882). */
|
|
227
251
|
const SUMMARIZE_DEADLINE_MS = 330_000;
|
|
228
252
|
/**
|
package/dist/commands/notes.js
CHANGED
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
import os from "node:os";
|
|
49
49
|
import { loadPairedSession } from "../tower-client.js";
|
|
50
50
|
import { listNotes, listShelves, showNote, showShelf } from "./notes-reads.js";
|
|
51
|
-
import { uploadNotes, pasteNote, shareNote, summarizeNote, moveNote, placeNote } from "./notes-writes.js";
|
|
51
|
+
import { uploadNotes, pasteNote, shareNote, summarizeNote, moveNote, placeNote, correctNote } from "./notes-writes.js";
|
|
52
52
|
import { runFolderCommand } from "./notes-folders.js";
|
|
53
53
|
import { armNotesWebhook, connectNotetaker, detachNotetaker, listNotesAccounts, showNotesWebhook, syncNotetaker, syncStatusNotetakers, } from "./notes-accounts.js";
|
|
54
54
|
import { showBotUpcoming } from "./notes-bot-upcoming.js";
|
|
@@ -60,7 +60,7 @@ import { defaultRecordDeps, runNotesRecord } from "./notes-record.js";
|
|
|
60
60
|
import { runNotesApp } from "./notes-app.js";
|
|
61
61
|
import { defaultExec, errorMessage } from "./cli-io.js";
|
|
62
62
|
export { listNotes, listShelves, showNote, showShelf } from "./notes-reads.js";
|
|
63
|
-
export { uploadNotes, pasteNote, shareNote, summarizeNote, moveNote, placeNote } from "./notes-writes.js";
|
|
63
|
+
export { uploadNotes, pasteNote, shareNote, summarizeNote, moveNote, placeNote, correctNote } from "./notes-writes.js";
|
|
64
64
|
export { armNotesWebhook, connectNotetaker, detachNotetaker, listNotesAccounts, showNotesWebhook, syncNotetaker, syncStatusNotetakers, } from "./notes-accounts.js";
|
|
65
65
|
export { showGranolaStatus } from "./notes-granola-status.js";
|
|
66
66
|
export { showTranscriptionStats } from "./notes-transcription-stats.js";
|
|
@@ -80,6 +80,7 @@ const NOTES_VERBS = {
|
|
|
80
80
|
paste: pasteNote,
|
|
81
81
|
share: shareNote,
|
|
82
82
|
unshare: shareNote,
|
|
83
|
+
correct: correctNote,
|
|
83
84
|
summarize: summarizeNote,
|
|
84
85
|
move: moveNote,
|
|
85
86
|
place: placeNote,
|
|
@@ -21,7 +21,7 @@ export async function runCockpitCli(argv, io) {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
if (command === "--version" || command === "-V" || command === "version") {
|
|
24
|
-
writeLine(io?.stdout ?? process.stdout, "0.2.
|
|
24
|
+
writeLine(io?.stdout ?? process.stdout, "0.2.293");
|
|
25
25
|
return 0;
|
|
26
26
|
}
|
|
27
27
|
|
|
@@ -317,11 +317,13 @@ function recordSourceObservation(options, result) {
|
|
|
317
317
|
const uploadedAt = uploadedThisSync
|
|
318
318
|
? options.now.toISOString()
|
|
319
319
|
: (prior?.uploaded_at ?? (durableThisSync ? options.now.toISOString() : null));
|
|
320
|
-
const uploadedByteSize = uploadedThisSync
|
|
320
|
+
const uploadedByteSize = uploadedThisSync || (durableThisSync && options.source === "codex")
|
|
321
321
|
? result.byte_size
|
|
322
322
|
: (prior?.uploaded_byte_size ?? (durableThisSync ? result.byte_size : null));
|
|
323
323
|
const entry = {
|
|
324
324
|
file_hash_sha256: result.content_hash_sha256,
|
|
325
|
+
uploaded_source_revision: durableThisSync && !isRedactionStubObjectKey(uploadedObjectKey)
|
|
326
|
+
? result.content_hash_sha256 : prior?.uploaded_source_revision ?? null,
|
|
325
327
|
file_mtime_ms: result.session_file_mtime_ms,
|
|
326
328
|
byte_size: result.byte_size,
|
|
327
329
|
// Durable byte offset reflects how many bytes are durable remotely (the
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { nativeSessionRoots } from "../native-session-roots.js";
|
|
1
2
|
/**
|
|
2
3
|
* Reading both session stores, and settling what the reading owes.
|
|
3
4
|
*
|
|
@@ -9,7 +10,7 @@
|
|
|
9
10
|
*/
|
|
10
11
|
import path from "node:path";
|
|
11
12
|
import { saveRootWorkScan } from "../root-work.js";
|
|
12
|
-
import { CODEX_ATTRIBUTION_SCAN_WINDOW_SESSION_LIMIT, CODEX_ATTRIBUTION_SCAN_WINDOW_MINUTES,
|
|
13
|
+
import { CODEX_ATTRIBUTION_SCAN_WINDOW_SESSION_LIMIT, CODEX_ATTRIBUTION_SCAN_WINDOW_MINUTES, scanAndAttributeCodexSessions, } from "../adapters/codex-attribution.js";
|
|
13
14
|
import { scanAndAttributeClaudeSessions, } from "../adapters/claude-attribution.js";
|
|
14
15
|
import { CLAUDE_CURSOR_FILENAME } from "../cursors/raw-evidence-cursor.js";
|
|
15
16
|
import { isRedactionStubObjectKey } from "../cursors/raw-evidence-stub-entry.js";
|
|
@@ -31,9 +32,10 @@ const CLAUDE_DEFAULT_WINDOW_MINUTES = 24 * 60;
|
|
|
31
32
|
*/
|
|
32
33
|
export async function scanAndAttributeBothSources(options) {
|
|
33
34
|
const { plan } = options;
|
|
35
|
+
const roots = nativeSessionRoots(options.homeDir);
|
|
34
36
|
const codexScanned = await scanCodexSessionsForSync({
|
|
35
37
|
codexEnabled: plan.codexEnabled,
|
|
36
|
-
|
|
38
|
+
roots,
|
|
37
39
|
worktrees: options.worktrees,
|
|
38
40
|
now: options.now,
|
|
39
41
|
collectionRoots: plan.collectionRoots,
|
|
@@ -48,7 +50,7 @@ export async function scanAndAttributeBothSources(options) {
|
|
|
48
50
|
const firstRunBackfill = plan.claudeEnabled && !claudeCursorExists;
|
|
49
51
|
const claudeScanned = await scanClaudeSessionsForSync({
|
|
50
52
|
claudeEnabled: plan.claudeEnabled,
|
|
51
|
-
|
|
53
|
+
roots,
|
|
52
54
|
worktrees: options.worktrees,
|
|
53
55
|
now: options.now,
|
|
54
56
|
collectionRoots: plan.collectionRoots,
|
|
@@ -58,6 +60,10 @@ export async function scanAndAttributeBothSources(options) {
|
|
|
58
60
|
outsideRootCatchUpPending: plan.outsideRootCatchUpPending,
|
|
59
61
|
stateDir: options.paths.state_dir,
|
|
60
62
|
});
|
|
63
|
+
if (roots.discoveryFailed) {
|
|
64
|
+
codexScanned.directory_read_failed_count += 1;
|
|
65
|
+
claudeScanned.project_dir_read_failed_count += 1;
|
|
66
|
+
}
|
|
61
67
|
// BLI-23590: under `roots_only` a session outside every approved root leaves the result HERE, before the retry
|
|
62
68
|
// bookkeeping, the local work scan, the upload, the report or the git scan can see it.
|
|
63
69
|
let codexAttribution = codexScanned;
|
|
@@ -117,7 +123,7 @@ async function scanCodexSessionsForSync(options) {
|
|
|
117
123
|
const readMemo = await openTranscriptReadMemo(options.stateDir, "codex_attribution").catch(() => undefined);
|
|
118
124
|
try {
|
|
119
125
|
let scan = await scanAndAttributeCodexSessions({
|
|
120
|
-
sessionsDirs:
|
|
126
|
+
sessionsDirs: options.roots.codex,
|
|
121
127
|
worktrees: options.worktrees,
|
|
122
128
|
now: options.now,
|
|
123
129
|
sinceMinutes,
|
|
@@ -130,7 +136,7 @@ async function scanCodexSessionsForSync(options) {
|
|
|
130
136
|
// capped scan still returns every session it found instead of silently
|
|
131
137
|
// truncating at the window's default limit.
|
|
132
138
|
scan = await scanAndAttributeCodexSessions({
|
|
133
|
-
sessionsDirs:
|
|
139
|
+
sessionsDirs: options.roots.codex,
|
|
134
140
|
worktrees: options.worktrees,
|
|
135
141
|
now: options.now,
|
|
136
142
|
sinceMinutes,
|
|
@@ -159,12 +165,13 @@ async function scanClaudeSessionsForSync(options) {
|
|
|
159
165
|
: options.firstRunBackfill
|
|
160
166
|
? CLAUDE_FIRST_RUN_BACKFILL_MINUTES
|
|
161
167
|
: undefined, options.outsideRootCatchUpPending, CLAUDE_DEFAULT_WINDOW_MINUTES);
|
|
162
|
-
const projectsDir =
|
|
168
|
+
const { claudeProjects: projectsDir, claudeProjectDirs: projectsDirs } = options.roots;
|
|
163
169
|
// BLI-22797: one memo for both passes, settled once, so an unchanged file is never opened.
|
|
164
170
|
const readMemo = await openTranscriptReadMemo(options.stateDir, "attribution").catch(() => undefined);
|
|
165
171
|
try {
|
|
166
172
|
let scan = await scanAndAttributeClaudeSessions({
|
|
167
173
|
projectsDir,
|
|
174
|
+
projectsDirs,
|
|
168
175
|
worktrees: options.worktrees,
|
|
169
176
|
now: options.now,
|
|
170
177
|
collectionRoots: options.collectionRoots,
|
|
@@ -174,6 +181,7 @@ async function scanClaudeSessionsForSync(options) {
|
|
|
174
181
|
if (scan.session_limit_applied) {
|
|
175
182
|
scan = await scanAndAttributeClaudeSessions({
|
|
176
183
|
projectsDir,
|
|
184
|
+
projectsDirs,
|
|
177
185
|
worktrees: options.worktrees,
|
|
178
186
|
now: options.now,
|
|
179
187
|
collectionRoots: options.collectionRoots,
|
|
@@ -34,6 +34,8 @@ export function sessionShelf(result, rules) {
|
|
|
34
34
|
return { shelf: "team", reason: "routing_inactive" };
|
|
35
35
|
if (rules.failed)
|
|
36
36
|
return { shelf: "drawer", reason: "shelf_resolve_failed" };
|
|
37
|
+
if (result.local_cwds && (result.local_cwds.length === 0 || result.local_cwds.some(cwd => !cwd || resolveShelfWith(rules, cwd).shelf !== "team")))
|
|
38
|
+
return { shelf: "drawer", reason: "shelf_identity_mismatch" };
|
|
37
39
|
if (result.state === "ambiguous")
|
|
38
40
|
return { shelf: "drawer", reason: "ambiguous_attribution" };
|
|
39
41
|
const byWorktree = result.worktree && !isOutsideRootsWorkspace(result.worktree)
|
|
@@ -180,6 +180,7 @@ function ambientEnvelopeForWorktree(options) {
|
|
|
180
180
|
.map((result) => ({
|
|
181
181
|
local_path: result.file_path,
|
|
182
182
|
codex_session_id: result.codex_session_id,
|
|
183
|
+
...(result.segment_paths ? { segment_paths: result.segment_paths, revision_hash: result.content_hash_sha256 ?? undefined, revision_bytes: result.byte_size } : {}),
|
|
183
184
|
})),
|
|
184
185
|
codexAttributionScan: scan.codexAttribution,
|
|
185
186
|
claudeSessionFiles,
|