@aitne-sh/aitne 0.1.4 → 0.1.6

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 (87) hide show
  1. package/README.md +16 -0
  2. package/agent-assets/agent-profiles/_safety.md +29 -0
  3. package/agent-assets/agent-profiles/routine-fetch-window.md +75 -40
  4. package/agent-assets/agent-profiles/wiki-agent.md +19 -0
  5. package/agent-assets/docs/features/messaging/bang-commands.md +161 -0
  6. package/agent-assets/docs/features/messaging/overview.md +3 -0
  7. package/agent-assets/docs/features/wiki/commands.md +222 -0
  8. package/agent-assets/docs/features/wiki/overview.md +145 -0
  9. package/agent-assets/docs/getting-started/03-what-can-this-do.md +18 -0
  10. package/agent-assets/docs/glossary.md +34 -0
  11. package/agent-assets/docs/guides/budget-and-cost-for-wiki.md +123 -0
  12. package/agent-assets/docs/guides/build-your-wiki.md +99 -0
  13. package/agent-assets/docs/guides/explore-with-trace-and-connect.md +169 -0
  14. package/agent-assets/docs/guides/maintain-wiki-health.md +168 -0
  15. package/agent-assets/docs/guides/multiple-wikis-for-multiple-domains.md +192 -0
  16. package/agent-assets/docs/guides/pause-the-agent.md +10 -3
  17. package/agent-assets/docs/guides/use-an-existing-obsidian-vault.md +156 -0
  18. package/agent-assets/docs/reference/cli-commands.md +24 -1
  19. package/agent-assets/docs/troubleshooting/wiki-ingest-full-blocked.md +96 -0
  20. package/agent-assets/docs/troubleshooting/wiki-write-failed.md +82 -0
  21. package/agent-assets/skills/context/SKILL.md +288 -17
  22. package/agent-assets/skills/external-services/SKILL.delegated.claude.md +2 -2
  23. package/agent-assets/skills/external-services/SKILL.delegated.codex.md +3 -3
  24. package/agent-assets/skills/external-services/SKILL.delegated.gemini.md +6 -6
  25. package/agent-assets/skills/external-services/SKILL.md +5 -3
  26. package/agent-assets/skills/external-services/SKILL.native.claude.md +49 -58
  27. package/agent-assets/skills/external-services/SKILL.native.codex.md +50 -58
  28. package/agent-assets/skills/external-services/SKILL.native.gemini.md +53 -56
  29. package/agent-assets/skills/mail/SKILL.md +5 -5
  30. package/agent-assets/skills/mail/SKILL.native.claude.md +57 -65
  31. package/agent-assets/skills/mail/SKILL.native.codex.md +73 -75
  32. package/agent-assets/skills/mail/SKILL.native.gemini.md +80 -75
  33. package/agent-assets/skills/management-task-register/SKILL.md +3 -3
  34. package/agent-assets/skills/notion/SKILL.native.claude.md +78 -82
  35. package/agent-assets/skills/notion/SKILL.native.codex.md +78 -80
  36. package/agent-assets/skills/notion/SKILL.native.gemini.md +91 -90
  37. package/agent-assets/skills/observations/SKILL.md +123 -15
  38. package/agent-assets/skills/roadmap/SKILL.md +31 -4
  39. package/agent-assets/skills/schedule/SKILL.md +44 -3
  40. package/agent-assets/skills/today/SKILL.md +50 -11
  41. package/agent-assets/skills/travel-time/SKILL.md +9 -0
  42. package/agent-assets/skills/wiki/wiki-ask/SKILL.md +32 -0
  43. package/agent-assets/skills/wiki/wiki-compile/SKILL.md +126 -0
  44. package/agent-assets/skills/wiki/wiki-connect/SKILL.md +75 -0
  45. package/agent-assets/skills/wiki/wiki-graduate/SKILL.md +45 -0
  46. package/agent-assets/skills/wiki/wiki-ingest/SKILL.md +182 -0
  47. package/agent-assets/skills/wiki/wiki-lint/SKILL.md +90 -0
  48. package/agent-assets/skills/wiki/wiki-trace/SKILL.md +72 -0
  49. package/agent-assets/skills/wiki/wiki-vault-rules/SKILL.md +145 -0
  50. package/agent-assets/task-flows/_partials/calendar-acquire.google_calendar.md +28 -9
  51. package/agent-assets/task-flows/_partials/calendar-acquire.outlook_calendar.md +26 -9
  52. package/agent-assets/task-flows/_partials/mail-acquire.gmail.md +51 -24
  53. package/agent-assets/task-flows/_partials/mail-acquire.outlook_mail.md +46 -16
  54. package/agent-assets/task-flows/_partials/notion-acquire.notion.md +29 -9
  55. package/agent-assets/task-flows/message.received.dm.md +35 -2
  56. package/agent-assets/task-flows/message.received.dm.native.claude.md +25 -26
  57. package/agent-assets/task-flows/message.received.dm.native.codex.md +30 -24
  58. package/agent-assets/task-flows/message.received.dm.native.gemini.md +36 -36
  59. package/agent-assets/task-flows/message.received.dm_first.md +43 -4
  60. package/agent-assets/task-flows/message.received.dm_first.native.claude.md +20 -20
  61. package/agent-assets/task-flows/message.received.dm_first.native.codex.md +22 -19
  62. package/agent-assets/task-flows/message.received.dm_first.native.gemini.md +28 -24
  63. package/agent-assets/task-flows/routine.fetch_window.md +51 -36
  64. package/agent-assets/task-flows/routine.morning_routine.md +12 -3
  65. package/agent-assets/task-flows/routine.morning_routine_initial.md +22 -1
  66. package/agent-assets/task-flows/routine.roadmap_refresh.md +7 -3
  67. package/agent-assets/task-flows/scheduled.dm.md +477 -0
  68. package/agent-assets/task-flows/setup.initial.md +50 -23
  69. package/agent-assets/task-flows/wiki.ask.md +11 -0
  70. package/agent-assets/task-flows/wiki.compile.md +28 -0
  71. package/agent-assets/task-flows/wiki.connect.md +12 -0
  72. package/agent-assets/task-flows/wiki.ingest_url.md +35 -0
  73. package/agent-assets/task-flows/wiki.lint.md +13 -0
  74. package/agent-assets/task-flows/wiki.trace.md +13 -0
  75. package/agent-assets/wiki-seeds/schemas/output.md +12 -0
  76. package/agent-assets/wiki-seeds/schemas/raw.md +13 -0
  77. package/agent-assets/wiki-seeds/schemas/wiki.md +12 -0
  78. package/agent-assets/wiki-seeds/taxonomy.md +13 -0
  79. package/package.json +10 -6
  80. package/scripts/check-redaction-coverage.mjs +0 -109
  81. package/scripts/commands.md +0 -0
  82. package/scripts/message-discipline-digest.mjs +0 -535
  83. package/scripts/poc/google-connector-inheritance/REPORT.md +0 -197
  84. package/scripts/poc/google-connector-inheritance/claude-sdk-probe.mjs +0 -79
  85. package/scripts/regen-skill-fixtures.mjs +0 -39
  86. package/scripts/remint-roadmap-ids.mjs +0 -257
  87. package/scripts/smoke-obsidian-api.mjs +0 -166
@@ -0,0 +1,145 @@
1
+ ---
2
+ name: wiki-vault-rules
3
+ description: Load for every wiki.* process. Defines wiki vault layout, layer ownership, and the daemon Wiki API call convention (endpoints + canonical curl shape).
4
+ allowed-tools:
5
+ - Bash(curl *)
6
+ - Bash(jq *)
7
+ - Read
8
+ ---
9
+
10
+ # Wiki Vault Rules
11
+
12
+ The wiki vault is `{{vault_path}}` for workspace `{{workspace_name}}` (schema `{{schema_version}}`, language `{{language}}`).
13
+
14
+ Never write this directory directly. Use only the daemon Wiki API at `http://localhost:8321`:
15
+
16
+ ```
17
+ GET /api/wiki/{{workspace_name}}/index
18
+ GET /api/wiki/{{workspace_name}}/search?q=<query>
19
+ GET /api/wiki/{{workspace_name}}/files/<path>
20
+ POST /api/wiki/{{workspace_name}}/files/<path>
21
+ PATCH /api/wiki/{{workspace_name}}/files/<path>
22
+ ```
23
+
24
+ Every request must include `x-process-key` with your current process key.
25
+
26
+ ## Layers
27
+
28
+ - `00_inbox/` is human-only. Agents may read it but must never write it.
29
+ - `10_raw/` is create-only source capture. `wiki.ingest_url` may create root-level `<slug>.md`; existing raw notes are immutable.
30
+ - `10_raw/images/<slug>/<file>` is reserved for source images.
31
+ - `20_wiki/` is synthesized knowledge. `wiki.compile` may write root-level `<slug>.md` and `_index.md`.
32
+ - `30_outputs/` is answers and reports. `wiki.ask` may write root-level `<YYYY-MM-DD>-<slug>.md`.
33
+ - `90_meta/` holds taxonomy, schemas, and health notes. Treat schema files as read-mostly.
34
+ - `log.md` is append-only operational history (PATCH `mode: "append"` only).
35
+
36
+ Slugs are lowercase kebab-case (`^[a-z0-9][a-z0-9-]*$`). Preserve source URLs in raw and wiki notes.
37
+
38
+ ## Calling the Wiki API from Bash
39
+
40
+ The session installs a node-backed curl shim at `.pa/bin/curl` (first on `PATH`) which pins host/port and auto-attaches read-side auth. From your point of view it behaves like plain `curl` against `http://localhost:8321/api/*` — with the constraints below.
41
+
42
+ ### The command MUST start with the literal `curl` token
43
+
44
+ The session's allow-list is `Bash(curl *)`, **prefix-matched against the full command string**. These shapes are silently denied under `dontAsk` (no error, no `PA_API_ERROR`, no body — the call simply does not run):
45
+
46
+ ```
47
+ echo '{...}' | curl ... # starts with `echo`
48
+ cat <<JSON | curl ... -d @- # starts with `cat`
49
+ bash -c "curl ..." # starts with `bash`
50
+ ( curl ... ) # starts with `(`
51
+ curl1=... ; curl ... # starts with `curl1` then `;` — chained-curl deny
52
+ ```
53
+
54
+ Write a single, flat curl call. Multi-line / large bodies use a heredoc redirected directly to curl's stdin (the command still starts with `curl`):
55
+
56
+ ```
57
+ curl http://... -X POST -H 'content-type: application/json' \
58
+ -H 'x-process-key: ...' -d @- <<'JSON'
59
+ {"content":"... multi-line body ..."}
60
+ JSON
61
+ ```
62
+
63
+ The shim reads stdin when `-d @-` is passed, so the heredoc body lands as the request payload. For small single-line bodies, prefer `-d '<inline-json>'`.
64
+
65
+ ### Allowed curl flags
66
+
67
+ The shim only understands `-X/--request`, `-H/--header`, `-d/--data/--data-raw/--data-binary`, `-o/--output`, `-F/--form`, and silently ignores `-s/--silent/--show-error`. Any other flag exits with `Unsupported curl flag`.
68
+
69
+ ### Allowed headers
70
+
71
+ `content-type`, `x-process-key`, `x-lock-id`, `x-session-id`. Other authentication headers are managed by the shim — do not pass `Authorization`, `x-read-token`, or environment-variable expansions; they will be rejected.
72
+
73
+ ### Body-quoting cheat sheet
74
+
75
+ For inline `-d '<json>'` bodies, wrap the JSON in **outer single quotes** so the shell does not expand it, then follow JSON's own escapes inside:
76
+
77
+ | Need in `content` | Write in the shell-arg |
78
+ |---|---|
79
+ | `"` | `\"` |
80
+ | `\` | `\\` |
81
+ | newline | `\n` |
82
+ | `'` | `'\''` (close-escape-reopen), or substitute `’` (U+2019) |
83
+ | `$`, backticks | leave as-is |
84
+
85
+ For heredoc `-d @-` bodies the shell does NOT expand the heredoc body when the marker is single-quoted (`<<'JSON'`), so the JSON inside is verbatim — no shell escaping required, only JSON's own (`\"`, `\\`, `\n`). Use heredoc for any body that's more than a few lines or contains lots of `"`.
86
+
87
+ `-d @<path>` (file-read) is rejected by both the security hook and the shim — there is no agent-facing reason to load a body from disk.
88
+
89
+ ### What ELSE is silently denied (no `Bash(find|ls|cat|...)`)
90
+
91
+ Only `Bash(curl *)` and `Bash(jq *)` are on the allow-list. `Bash(find ...)`, `Bash(ls ...)`, `Bash(cat ...)`, `Bash(grep ...)`, `Bash(wc ...)`, and every other shell utility hit the same `dontAsk` denial — no useful tool result. Enumerate the workspace via `GET /api/wiki/<ws>/index` (returns `{ files: [{ path, mtime, sizeBytes }] }`); do not walk `{{vault_path}}` from disk.
92
+
93
+ `Write` and `Edit` tools are **stripped from the session allow-list** for every `wiki.*` process key — the SDK denies them silently. The Wiki API is the only legal write surface; `{{vault_path}}` is informational, not a target.
94
+
95
+ ### Canonical shapes
96
+
97
+ ```bash
98
+ # LIST every file in the workspace (returns JSON with path/mtime/sizeBytes per file)
99
+ curl http://localhost:8321/api/wiki/{{workspace_name}}/index \
100
+ -H 'x-process-key: <your-process-key>'
101
+
102
+ # SEARCH bodies for a substring (returns matches with snippet + mtime)
103
+ curl 'http://localhost:8321/api/wiki/{{workspace_name}}/search?q=<query>' \
104
+ -H 'x-process-key: <your-process-key>'
105
+
106
+ # READ a file
107
+ curl http://localhost:8321/api/wiki/{{workspace_name}}/files/<path> \
108
+ -H 'x-process-key: <your-process-key>'
109
+
110
+ # CREATE a file (POST — used by wiki.ingest_url, wiki.compile, wiki.ask)
111
+ curl http://localhost:8321/api/wiki/{{workspace_name}}/files/<path> \
112
+ -X POST \
113
+ -H 'content-type: application/json' \
114
+ -H 'x-process-key: <your-process-key>' \
115
+ -d '{"content":"..."}'
116
+
117
+ # APPEND/PREPEND to a file (PATCH — used for log.md, _index.md, taxonomy.md)
118
+ curl http://localhost:8321/api/wiki/{{workspace_name}}/files/<path> \
119
+ -X PATCH \
120
+ -H 'content-type: application/json' \
121
+ -H 'x-process-key: <your-process-key>' \
122
+ -d '{"mode":"append","content":"..."}'
123
+ ```
124
+
125
+ Filtering the index is a `jq` job — e.g. raw notes touched since a baseline ISO timestamp:
126
+
127
+ ```bash
128
+ curl http://localhost:8321/api/wiki/{{workspace_name}}/index \
129
+ -H 'x-process-key: <your-process-key>' \
130
+ | jq -r --arg since '<YYYY-MM-DDTHH:MM:SSZ>' \
131
+ '.files[] | select(.path | startswith("10_raw/")) | select(.mtime > $since) | .path'
132
+ ```
133
+
134
+ ### Common error codes
135
+
136
+ The shim emits `PA_API_ERROR {...}` to stderr on every non-2xx. React on `status` + `bodyPreview.error`:
137
+
138
+ - `403 missing_process_key` → add `-H 'x-process-key: ...'`.
139
+ - `403 raw_write_denied` / `wiki_write_denied` / `meta_write_denied` / `output_write_denied` / `log_write_denied` → your process key isn't authorized for that layer; fix the target path, not the header.
140
+ - `400 invalid_path` / `invalid_layer` → fix the slug shape or layer prefix.
141
+ - `400 invalid_body` → JSON shape wrong (e.g. `content` must be a string; PATCH `content` must be non-empty).
142
+ - `409 append_only` → file is immutable in this layer; either suffix the slug or use PATCH instead of POST.
143
+ - `413` → body exceeds the 512 KB cap.
144
+
145
+ If the Bash call returns to the prompt with no `PA_API_ERROR` and no body, your command did not start with literal `curl` and was denied under `dontAsk`. Rewrite as a flat curl invocation.
@@ -17,7 +17,18 @@ Note on coverage: routines whose calendar window is already covered by
17
17
  double-fetch. The catalog only emits drift / retrospective / imminent
18
18
  windows for the pre-pass.
19
19
 
20
- POST every returned event to `http://localhost:8321/api/observations`:
20
+ POST every returned event — for a whole window in **one** call — to
21
+ `http://localhost:8321/api/observations/batch`. Build the body as:
22
+
23
+ ```json
24
+ {"observations":[
25
+ {"source":"google_calendar:<calendarId>","ref":"<eventId>","changeType":"created","actor":"agent",
26
+ "payload":{"kind":"calendar","providerId":"<calendarId>","raw":{"title":"…","start":"…","end":"…","attendees":[…],"status":"…"}}},
27
+
28
+ ]}
29
+ ```
30
+
31
+ Field rules per element:
21
32
 
22
33
  - `source` = `"google_calendar:<calendarId>"` (use `"primary"` when the
23
34
  provider returns no explicit id)
@@ -31,13 +42,19 @@ POST every returned event to `http://localhost:8321/api/observations`:
31
42
  "raw": { "title": ..., "start": ..., "end": ...,
32
43
  "attendees": [...], "status": ... } }`
33
44
 
34
- Server computes the dedup hash from `(source, payload)`. Response shape:
45
+ Server computes the dedup hash from `(source, payload)`. The batch endpoint
46
+ always returns `200` with a JSON envelope `{ "results": [...], "fetched": N,
47
+ "posted": N, "duplicates": N, "errors": N }`. Per-item `results[*].status`:
48
+
49
+ - `"created"` / `"modified"` — rolled into `posted`.
50
+ - `"duplicate"` — rolled into `duplicates`.
51
+ - `"flip_locked"` — append `{type:"flip-locked","integration":"google_calendar"}`
52
+ to `errors` and continue.
53
+ - `"validation_error"` — append `{type:"validation-error","integration":"google_calendar","ref":"<ref>","detail":"<results[*].error>"}`
54
+ to `errors` and continue.
35
55
 
36
- - `200 {action: "created"|"modified"}` fresh / updated row; count in `posted`.
37
- - `409 {error: "duplicate"}` identical payload already pending; count in `duplicates`.
38
- - `409 {error: "integration_flip_in_progress"}` — append
39
- `{type:"flip-locked","integration":"google_calendar"}` to `errors`
40
- and move on.
56
+ Cap each batch at 200 entriessplit the window into multiple POSTs if the
57
+ upstream call returns more than that.
41
58
 
42
59
  <!-- mode:direct:google_calendar -->
43
60
  GET `http://localhost:8321/api/calendar/events<query>` where `<query>` is
@@ -45,7 +62,8 @@ the literal `query` attribute of the `<fetch>` row (e.g.
45
62
  `?date=2026-05-11&days=1` or `?date=2026-05-04&days=7`). The route
46
63
  accepts `date=YYYY-MM-DD` (or `today`) plus `days=N` (≤90); `timeMin`
47
64
  / `timeMax` are NOT recognised. The daemon returns `{ "events": [...] }`;
48
- map each event into one observation POST as specified above.
65
+ map every event into the `observations[]` array of a single
66
+ `POST /api/observations/batch` call.
49
67
  <!-- /mode:direct:google_calendar -->
50
68
 
51
69
  <!-- mode:delegated-same:google_calendar -->
@@ -100,7 +118,8 @@ following body (substitute the row's `query` into `task`):
100
118
  }
101
119
  ```
102
120
 
103
- Map each item in `result.events[]` to one observation POST.
121
+ Map all items in `result.events[]` into a single
122
+ `POST /api/observations/batch` call.
104
123
  <!-- /mode:delegated-cross:google_calendar -->
105
124
 
106
125
  <!-- mode:native:google_calendar -->
@@ -28,7 +28,18 @@ the window via `<calendar_events_*>` (multi-provider after §6.6), the
28
28
  catalog skips the pre-pass row to avoid double-fetching. The pre-pass
29
29
  only ships drift / retrospective / imminent windows.
30
30
 
31
- POST every returned event to `http://localhost:8321/api/observations`:
31
+ POST every returned event — for a whole window in **one** call — to
32
+ `http://localhost:8321/api/observations/batch`. Build the body as:
33
+
34
+ ```json
35
+ {"observations":[
36
+ {"source":"outlook_calendar:<calendarId>","ref":"<eventId>","changeType":"created","actor":"agent",
37
+ "payload":{"kind":"calendar","providerId":"<calendarId>","raw":{"title":"…","start":"…","end":"…","attendees":[…],"status":"…"}}},
38
+
39
+ ]}
40
+ ```
41
+
42
+ Field rules per element:
32
43
 
33
44
  - `source` = `"outlook_calendar:<calendarId>"` (use `"primary"` when
34
45
  the provider returns no explicit id)
@@ -41,13 +52,19 @@ POST every returned event to `http://localhost:8321/api/observations`:
41
52
  "raw": { "title": ..., "start": ..., "end": ...,
42
53
  "attendees": [...], "status": ... } }`
43
54
 
44
- Server computes the dedup hash from `(source, payload)`. Response shape:
55
+ Server computes the dedup hash from `(source, payload)`. The batch endpoint
56
+ always returns `200` with a JSON envelope `{ "results": [...], "fetched": N,
57
+ "posted": N, "duplicates": N, "errors": N }`. Per-item `results[*].status`:
45
58
 
46
- - `200 {action: "created"|"modified"}` — fresh / updated row; count in `posted`.
47
- - `409 {error: "duplicate"}` — identical payload already pending; count in `duplicates`.
48
- - `409 {error: "integration_flip_in_progress"}` — append
49
- `{type:"flip-locked","integration":"outlook_calendar"}` to `errors`
50
- and move on.
59
+ - `"created"` / `"modified"` — rolled into `posted`.
60
+ - `"duplicate"` — rolled into `duplicates`.
61
+ - `"flip_locked"` — append `{type:"flip-locked","integration":"outlook_calendar"}`
62
+ to `errors` and continue.
63
+ - `"validation_error"` — append `{type:"validation-error","integration":"outlook_calendar","ref":"<ref>","detail":"<results[*].error>"}`
64
+ to `errors` and continue.
65
+
66
+ Cap each batch at 200 entries — split the window into multiple POSTs if the
67
+ upstream call returns more than that.
51
68
 
52
69
  <!-- mode:direct:outlook_calendar -->
53
70
  GET `http://localhost:8321/api/calendar/outlook/events<query>` where
@@ -55,8 +72,8 @@ GET `http://localhost:8321/api/calendar/outlook/events<query>` where
55
72
  `?date=2026-05-11&days=1` or `?date=2026-05-04&days=7`). The route
56
73
  accepts `date=YYYY-MM-DD` (or `today`) plus `days=N` (≤90); `timeMin`
57
74
  / `timeMax` are NOT recognised. The daemon returns
58
- `{ "events": [...] }`; map each event into one observation POST as
59
- specified above.
75
+ `{ "events": [...] }`; map every event into the `observations[]` array
76
+ of a single `POST /api/observations/batch` call.
60
77
  <!-- /mode:direct:outlook_calendar -->
61
78
 
62
79
  <!-- mode:delegated-same:outlook_calendar -->
@@ -8,37 +8,60 @@ spec: ROUTINE_DATA_ACQUISITION_DESIGN.md §6.8 / §8.1
8
8
 
9
9
  For every `<fetch integration="gmail" ...>` row in `<acquisition-plan>`, take
10
10
  the branch below that matches the row's `mode` attribute and acquire the
11
- window described by `query`. Each row carries `account="<accountId>"` — apply
12
- the query against that account only, do not pool across accounts.
11
+ window described by `query`.
13
12
 
14
- POST every returned message to `http://localhost:8321/api/observations` per
15
- the contract in your agent profile (the response from the upstream call IS
16
- the payload; do not summarise or rank). Use:
13
+ **Account attribution.** In `direct` mode each row carries
14
+ `account="<accountId>"` apply the query against THAT account only, do not
15
+ pool across accounts. In `delegated-same` / `delegated-cross` / `native` modes
16
+ the daemon emits a single row WITHOUT an `account` attribute because the
17
+ bound Gmail MCP authenticates as a single user; substitute the literal
18
+ string `default` wherever the observation contract below references
19
+ `<accountId>`. Never invent an accountId from the message body — `default`
20
+ is the canonical placeholder.
17
21
 
18
- - `source` = `"gmail:<accountId>"`
22
+ POST every returned message — for a whole window in **one** call — to
23
+ `http://localhost:8321/api/observations/batch` (the response from the
24
+ upstream call IS the payload; do not summarise or rank). Build the body as:
25
+
26
+ ```json
27
+ {"observations":[
28
+ {"source":"gmail:<accountId>","ref":"<messageId>","changeType":"created","actor":"agent",
29
+ "payload":{"kind":"mail","providerId":"<accountId>","raw":{"subject":"…","from":"…","snippet":"…","date":"…"}}},
30
+
31
+ ]}
32
+ ```
33
+
34
+ Field rules per element:
35
+
36
+ - `source` = `"gmail:<accountId>"` (use `"gmail:default"` when the
37
+ `<fetch>` row has no `account` attribute — see Account attribution above)
19
38
  - `ref` = provider-side stable message id
20
39
  - `changeType` = `"created"` for fresh items; `"modified"` when the row updates
21
40
  a payload the server already has under the same `(source, ref)`
22
41
  - `actor` = `"agent"`
23
42
  - `payload` = `{ "kind": "mail", "providerId": "<accountId>", "raw": {
24
43
  "subject": ..., "from": ..., "snippet": ...,
25
- "date": ... } }`
44
+ "date": ... } }` (providerId is `"default"` when no
45
+ `account` attribute)
26
46
 
27
47
  Do NOT compute the dedup hash — the server derives it from `(source, payload)`.
28
- The response body shape distinguishes three outcomes:
29
-
30
- - `200 {action: "created"}` fresh row inserted; count it in `posted`.
31
- - `200 {action: "modified"}` same `(source, ref)` pending row existed
32
- with a different payload; the row was updated and re-summarized.
33
- Count it in `posted`.
34
- - `409 {error: "duplicate"}` — same `(source, ref)` pending row already
35
- stores an identical payload. Nothing was written; count it in
36
- `duplicates` and move on.
37
- - `409 {error: "integration_flip_in_progress"}` — a mode flip is
38
- draining for this integration. Record
48
+
49
+ The batch endpoint always returns `200` with a JSON envelope `{ "results": [...],
50
+ "fetched": N, "posted": N, "duplicates": N, "errors": N }`. Add each
51
+ field-count into your top-level totals. Per-item `results[*].status` values:
52
+
53
+ - `"created"` / `"modified"` fresh or updated row; rolled into `posted`.
54
+ - `"duplicate"` — identical pending row already exists; rolled into `duplicates`.
55
+ - `"flip_locked"` a mode flip is draining for this integration. Append
39
56
  `{type:"flip-locked","integration":"gmail","account":"<accountId>"}`
40
- in `errors` and continue with the next row (the parent routine will
41
- retry on the next tick).
57
+ (use `"default"` when no `account` attribute) to your `errors` array
58
+ and continue (the parent routine will retry on the next tick).
59
+ - `"validation_error"` — a malformed item slipped through. Append
60
+ `{type:"validation-error","integration":"gmail","account":"<accountId>","ref":"<ref>","detail":"<results[*].error>"}`
61
+ to `errors` and continue.
62
+
63
+ Cap each batch at 200 entries — split the window into multiple POSTs if the
64
+ upstream call returns more than that.
42
65
 
43
66
  <!-- mode:direct:gmail -->
44
67
  GET `http://localhost:8321/api/mail/<accountId>/messages<query>` where
@@ -48,7 +71,8 @@ GET `http://localhost:8321/api/mail/<accountId>/messages<query>` where
48
71
  `?folder=sent&since=2026-05-11T00:00:00.000Z&limit=30`). The route
49
72
  accepts `since` (ISO 8601 datetime), `limit`, `folder`, `q`,
50
73
  `unreadOnly` — `days=…` is NOT recognised. The daemon returns
51
- `{ "messages": [...] }`; map each item to one POST as specified above.
74
+ `{ "messages": [...] }`; map every message into the `observations[]`
75
+ array of a single `POST /api/observations/batch` call.
52
76
  <!-- /mode:direct:gmail -->
53
77
 
54
78
  <!-- mode:delegated-same:gmail -->
@@ -64,11 +88,13 @@ does not proxy in this branch.
64
88
  The connector is bound to a different backend than this session, so reach it
65
89
  through the daemon's delegation proxy. POST to
66
90
  `http://localhost:8321/api/integrations/gmail/exec` with the following body
67
- (substitute the row's `query` and `accountId` into `task`):
91
+ (substitute the row's `query` into `task`). The `<fetch>` row in this mode
92
+ carries no `account` attribute — the proxy's bound MCP authenticates as a
93
+ single user, so the task is account-implicit:
68
94
 
69
95
  ```json
70
96
  {
71
- "task": "On account <accountId>, search Gmail with the query expression <query> and return id, subject, from, snippet, date for each message. Up to 30 messages.",
97
+ "task": "Search Gmail with the query expression <query> and return id, subject, from, snippet, date for each message. Up to 30 messages.",
72
98
  "outputSchema": {
73
99
  "type": "object",
74
100
  "required": ["messages"],
@@ -94,7 +120,8 @@ through the daemon's delegation proxy. POST to
94
120
  }
95
121
  ```
96
122
 
97
- Map each item in `result.messages[]` to one observation POST.
123
+ Map all items in `result.messages[]` into a single
124
+ `POST /api/observations/batch` call.
98
125
  <!-- /mode:delegated-cross:gmail -->
99
126
 
100
127
  <!-- mode:native:gmail -->
@@ -7,8 +7,16 @@ spec: ROUTINE_DATA_ACQUISITION_DESIGN.md §6.8 / §8.2
7
7
  # Outlook Mail acquisition
8
8
 
9
9
  For every `<fetch integration="outlook_mail" ...>` row in `<acquisition-plan>`,
10
- take the branch below that matches the row's `mode` attribute. Each row
11
- carries `account="<accountId>"` — apply the query to that account only.
10
+ take the branch below that matches the row's `mode` attribute.
11
+
12
+ **Account attribution.** In `direct` mode each row carries
13
+ `account="<accountId>"` — apply the query against THAT account only. In
14
+ `delegated-same` / `delegated-cross` / `native` modes the daemon emits a
15
+ single row WITHOUT an `account` attribute (the bound MCP authenticates as
16
+ one user); substitute the literal string `default` wherever the observation
17
+ contract below references `<accountId>`. The `userManagedConnector`
18
+ collapse (see below) means `delegated-cross` never carries an account
19
+ either.
12
20
 
13
21
  Outlook Mail is a **user-managed** integration: the daemon has no
14
22
  delegation proxy for it (no `/api/integrations/outlook_mail/exec` exists).
@@ -22,25 +30,47 @@ The four non-disabled branches therefore split into two real flows:
22
30
  this partial states the intent, not specific tool names. If no surface
23
31
  is bound, record an error and continue.
24
32
 
25
- POST every returned message to `http://localhost:8321/api/observations`:
33
+ POST every returned message — for a whole window in **one** call — to
34
+ `http://localhost:8321/api/observations/batch`. Build the body as:
35
+
36
+ ```json
37
+ {"observations":[
38
+ {"source":"outlook_mail:<accountId>","ref":"<messageId>","changeType":"created","actor":"agent",
39
+ "payload":{"kind":"mail","providerId":"<accountId>","raw":{"subject":"…","from":"…","snippet":"…","date":"…"}}},
40
+
41
+ ]}
42
+ ```
26
43
 
27
- - `source` = `"outlook_mail:<accountId>"`
44
+ Field rules per element:
45
+
46
+ - `source` = `"outlook_mail:<accountId>"` (use `"outlook_mail:default"`
47
+ when the `<fetch>` row has no `account` attribute — see Account
48
+ attribution above)
28
49
  - `ref` = provider-side stable message id
29
50
  - `changeType` = `"created"` for fresh items; `"modified"` when the row
30
51
  updates a payload already known under `(source, ref)`
31
52
  - `actor` = `"agent"`
32
53
  - `payload` = `{ "kind": "mail", "providerId": "<accountId>", "raw": {
33
54
  "subject": ..., "from": ..., "snippet": ...,
34
- "date": ... } }`
55
+ "date": ... } }` (providerId is `"default"` when no
56
+ `account` attribute)
35
57
 
36
- The server computes the dedup hash from `(source, payload)`. Response
37
- shape:
58
+ The server computes the dedup hash from `(source, payload)`. The batch
59
+ endpoint always returns `200` with `{ "results": [...], "fetched": N,
60
+ "posted": N, "duplicates": N, "errors": N }`. Per-item `results[*].status`:
38
61
 
39
- - `200 {action: "created"|"modified"}` — fresh / updated row; count in `posted`.
40
- - `409 {error: "duplicate"}` — identical payload already pending; count in `duplicates`.
41
- - `409 {error: "integration_flip_in_progress"}` — append
62
+ - `"created"` / `"modified"` — rolled into `posted`.
63
+ - `"duplicate"` — rolled into `duplicates`.
64
+ - `"flip_locked"` — append
42
65
  `{type:"flip-locked","integration":"outlook_mail","account":"<accountId>"}`
43
- to `errors` and move on; do not retry inline.
66
+ (use `"default"` when no `account` attribute) to `errors` and
67
+ continue; do not retry inline.
68
+ - `"validation_error"` — append
69
+ `{type:"validation-error","integration":"outlook_mail","account":"<accountId>","ref":"<ref>","detail":"<results[*].error>"}`
70
+ to `errors` and continue.
71
+
72
+ Cap each batch at 200 entries — split the window into multiple POSTs if the
73
+ upstream call returns more than that.
44
74
 
45
75
  <!-- mode:direct:outlook_mail -->
46
76
  GET `http://localhost:8321/api/mail/<accountId>/messages<query>` where
@@ -49,8 +79,8 @@ GET `http://localhost:8321/api/mail/<accountId>/messages<query>` where
49
79
  `?folder=sent&since=2026-05-11T00:00:00.000Z&limit=30`). The route
50
80
  accepts `since` (ISO 8601), `limit`, `folder`, `q`, `unreadOnly` — it
51
81
  does NOT accept `days=…`. The daemon returns `{ "messages": [...] }`
52
- regardless of the underlying provider; map each item into one
53
- observation POST as specified above.
82
+ regardless of the underlying provider; map every message into the
83
+ `observations[]` array of a single `POST /api/observations/batch` call.
54
84
  <!-- /mode:direct:outlook_mail -->
55
85
 
56
86
  <!-- mode:delegated-same:outlook_mail -->
@@ -62,7 +92,7 @@ args your bound surface accepts. POST every returned message as specified
62
92
  above.
63
93
 
64
94
  If no Outlook Mail surface is bound on this backend, append
65
- `{"type":"no-surface","integration":"outlook_mail","account":"<accountId>"}`
95
+ `{"type":"no-surface","integration":"outlook_mail","account":"<accountId>"}` (use `"default"` when no `account` attribute)
66
96
  to your `errors` array and continue with the next row. Do NOT halt the
67
97
  pre-pass; the parent routine continues with whatever observations the rest
68
98
  of the plan produced.
@@ -74,7 +104,7 @@ proxy. The dispatcher should not have emitted a `delegated-cross` row for
74
104
  this integration — if you see one, treat it exactly like
75
105
  `delegated-same`: use whichever in-session surface your skills document
76
106
  for Outlook Mail. If nothing is bound, append
77
- `{"type":"no-surface","integration":"outlook_mail","account":"<accountId>"}`
107
+ `{"type":"no-surface","integration":"outlook_mail","account":"<accountId>"}` (use `"default"` when no `account` attribute)
78
108
  to `errors` and continue.
79
109
  <!-- /mode:delegated-cross:outlook_mail -->
80
110
 
@@ -84,7 +114,7 @@ in-session connector surface your skills document for Outlook Mail —
84
114
  same call shape as `delegated-same`. The daemon does not proxy.
85
115
 
86
116
  If no Outlook Mail surface is bound, append
87
- `{"type":"no-surface","integration":"outlook_mail","account":"<accountId>"}`
117
+ `{"type":"no-surface","integration":"outlook_mail","account":"<accountId>"}` (use `"default"` when no `account` attribute)
88
118
  to `errors` and continue.
89
119
  <!-- /mode:native:outlook_mail -->
90
120
 
@@ -10,7 +10,18 @@ For every `<fetch integration="notion" ...>` row in `<acquisition-plan>`,
10
10
  take the branch below that matches the row's `mode` attribute. Notion rows
11
11
  do not fan out per account — the dispatcher emits one row per workspace.
12
12
 
13
- POST every returned page to `http://localhost:8321/api/observations`:
13
+ POST every returned page — for a whole window in **one** call — to
14
+ `http://localhost:8321/api/observations/batch`. Build the body as:
15
+
16
+ ```json
17
+ {"observations":[
18
+ {"source":"notion:<workspaceId>","ref":"<pageId>","changeType":"created","actor":"agent",
19
+ "payload":{"kind":"notion","providerId":"<workspaceId>","raw":{"title":"…","last_edited":"…","parent":"…","url":"…"}}},
20
+
21
+ ]}
22
+ ```
23
+
24
+ Field rules per element:
14
25
 
15
26
  - `source` = `"notion:<workspaceId>"` (use `"default"` when the daemon
16
27
  reports no explicit workspace id)
@@ -22,12 +33,19 @@ POST every returned page to `http://localhost:8321/api/observations`:
22
33
  "raw": { "title": ..., "last_edited": ...,
23
34
  "parent": ..., "url": ... } }`
24
35
 
25
- The server computes the dedup hash from `(source, payload)`. Response shape:
36
+ The server computes the dedup hash from `(source, payload)`. The batch
37
+ endpoint always returns `200` with `{ "results": [...], "fetched": N,
38
+ "posted": N, "duplicates": N, "errors": N }`. Per-item `results[*].status`:
39
+
40
+ - `"created"` / `"modified"` — rolled into `posted`.
41
+ - `"duplicate"` — rolled into `duplicates`.
42
+ - `"flip_locked"` — append `{type:"flip-locked","integration":"notion"}`
43
+ to `errors` and continue.
44
+ - `"validation_error"` — append `{type:"validation-error","integration":"notion","ref":"<ref>","detail":"<results[*].error>"}`
45
+ to `errors` and continue.
26
46
 
27
- - `200 {action: "created"|"modified"}` fresh / updated row; count in `posted`.
28
- - `409 {error: "duplicate"}` identical payload already pending; count in `duplicates`.
29
- - `409 {error: "integration_flip_in_progress"}` — append
30
- `{type:"flip-locked","integration":"notion"}` to `errors` and move on.
47
+ Cap each batch at 200 entriessplit the window into multiple POSTs if the
48
+ upstream call returns more than that.
31
49
 
32
50
  <!-- mode:direct:notion -->
33
51
  GET `http://localhost:8321/api/notion/search<query>` where `<query>` is
@@ -39,8 +57,9 @@ window cutoff client-side. The daemon returns `{ "results": [...] }`
39
57
  sorted by `last_edited_time` descending; filter to entries whose
40
58
  `last_edited_time` is at or after the window the `<fetch>` row's
41
59
  window symbol implies (`updated_24h` → today's agent-day start,
42
- `updated_1h` → the current hour boundary), then map each surviving
43
- page into one observation POST as specified above.
60
+ `updated_1h` → the current hour boundary), then map every surviving
61
+ page into the `observations[]` array of a single
62
+ `POST /api/observations/batch` call.
44
63
  <!-- /mode:direct:notion -->
45
64
 
46
65
  <!-- mode:delegated-same:notion -->
@@ -85,7 +104,8 @@ body (substitute the row's `query` into `task`):
85
104
  }
86
105
  ```
87
106
 
88
- Map each item in `result.pages[]` to one observation POST.
107
+ Map all items in `result.pages[]` into a single
108
+ `POST /api/observations/batch` call.
89
109
  <!-- /mode:delegated-cross:notion -->
90
110
 
91
111
  <!-- mode:native:notion -->
@@ -36,6 +36,22 @@ If any gate fails, leave the latent entry untouched. The user must never feel th
36
36
 
37
37
  Apply the conversational profile's "speak as one agent" rule: phrase your knowledge as your own memory; never name internal storage, sections, files, or routines in user-visible text. The user-facing message discipline (awareness, no ceremony, no readback, compactness) is owned by the notify skill.
38
38
 
39
+ #### No topic-pivoting trailing question (universal, non-negotiable)
40
+
41
+ Never append a question that **changes the topic** of the user's message. A topic-continuing question (clarifier, follow-up, "want me to track this as X?" that flows from what the user just said) is fine when it reads as a natural beat in the same thread. A topic-pivoting question — about an unrelated task, deadline, or profile slot — is forbidden in the same reply, even if a gate's conditions for that ask are technically met.
42
+
43
+ If the reply does not naturally invite a question of its own, end with a statement. The agent has other surfaces (the morning briefing, the `scheduled.dm` confirm sub-flow, observation alerts) for non-conversational asks — the inbound DM reply is not the only channel, and pushing them here costs the conversation more than it saves the agent.
44
+
45
+ Worked examples (illustrate the topic-continuing vs. topic-pivoting distinction; tone follows persona / Character, NOT these example phrasings):
46
+
47
+ | User opener | Acceptable trailing question | Forbidden trailing question |
48
+ |---|---|---|
49
+ | "I quit IBM Japan, moved to LA for a PM master's." | *"Want me to start tracking the program — syllabus, deadlines — as a project?"* (continues the share) | *"By the way, your 23:59 PT 407632 midterm is tomorrow — how's prep?"* (pivots to an unrelated deadline) |
50
+ | "Feeling pretty drained today." | *(no trailing question — acknowledge the mood)* | *"What time should I remind you about the design review?"* (ignores the mood) |
51
+ | "What's on today?" | *"Want a heads-up 15 min before the 2pm review?"* (continues the orientation) | *"Also — what city are you in these days?"* (latent-profile weave on a tight factual ask) |
52
+
53
+ This rule covers the latent-profile-question weave in Step 2 and any future opportunistic ask. **When a gate would have asked here but this rule suppresses it, the gate SHOULD instead schedule a `confirm:` sub-flow row** (see `scheduled.dm.md` ## Confirmation follow-up) so the question lands at a natural moment without violating the thread.
54
+
39
55
  **Day-type filter.** Parse line 2 of <today>. For any category whose focus is `off` (map via the today skill's "Category → focus-dimension mapping"), do not volunteer items in that category.
40
56
 
41
57
  **Resolved User Tasks.** When the user reports completing one of their tasks, mark it `[x]` per the today / context skill. Do NOT modify the agent's internal Agent Plan rows from this handler — those flip in `scheduled.task` handlers and Evening Review only.
@@ -116,6 +132,23 @@ If a tool you genuinely need is denied, or no available tool can handle the file
116
132
 
117
133
  These dispatchers are not exclusive — multiple may apply to one message.
118
134
 
135
+ **Confirm-reply continuation.** Before evaluating the per-domain
136
+ dispatchers below, scan `<conversation_history>` for the most recent
137
+ assistant message. If that message asked a question that the user is
138
+ now answering (typical shape: a short, single-question DM with no
139
+ project/task name embedded — emitted by the `scheduled.dm.md` ## Confirmation
140
+ follow-up sub-flow), route the user's reply to the originating gate's
141
+ **reply branches** based on the topic of the question — *not* on the
142
+ literal text of the user's reply. A bare "yeah" or "no" from the user
143
+ will not match the named-workstream / commitment shape the per-domain
144
+ dispatchers expect, so without this routing rule the reply would
145
+ silently miss the gate. Concretely: a confirm DM about *"track the LA
146
+ PM master's as a project?"* → user replies "yeah" / "no" / "actually
147
+ call it la-pm" → route through the context skill's "Project DM-intent
148
+ detection" §"Reply branches", carrying the user's reply shape (yes /
149
+ counter-proposal / no) into that handler. Apply the same pattern to
150
+ any other gate that opts into the confirm sub-flow in the future.
151
+
119
152
  **Scheduling.** Recurring ("every morning at 9", "weekly") → `POST /api/recurring-schedules`. One-shot ("tomorrow 3pm", "in 30 min") → `POST /api/schedule/dm` (pre-composed; default) or `POST /api/schedule` (wake-up that must look something up at fire time). Edit / cancel / list use the same endpoints. Load the schedule skill for the request shape, dedup pre-check, DM-vs-wake decision, and description contract. Use `<current_time>` for timezone resolution.
120
153
 
121
154
  Schedules go through this daemon — never through any cloud-hosted scheduled-agent feature your CLI may expose. Cloud routines cannot reach `localhost:8321`, so they cannot deliver via the user's chat platforms or use any integration registered here. Do not propose one as a tradeoff.
@@ -130,9 +163,9 @@ Schedules go through this daemon — never through any cloud-hosted scheduled-ag
130
163
  - **Re-enable** → `PATCH /api/recurring-schedules/:id` `{"enabled": true}`.
131
164
  3. Confirm to the user in persona voice. Keep it short — never name internal mechanisms ("recurring schedule", "pin_to_quiet_hours_end", row IDs) in user-visible text.
132
165
 
133
- **Long-horizon intent** (commitment, trip, deliverable, learning target beyond today) → route through the roadmap skill's "Long-horizon DM-intent detection". Ambiguous or speculative items belong in `agent-journal.md` as a candidate line for the next morning routine to confirm — do **not** write directly to `roadmap.md` without a clear positive signal.
166
+ **Long-horizon intent** (commitment, trip, deliverable, learning target beyond today) → route through the roadmap skill's "Long-horizon DM-intent detection". Ambiguous or speculative items belong in `agent/journal.md` as a candidate line for the next morning routine to confirm — do **not** write directly to `roadmap.md` without a clear positive signal.
134
167
 
135
- **Project intent** (state, progress, milestone, blocker, or a new-project request for a named workstream) → route through the context skill's "Project DM-intent detection". A new project requires the DM-confirm gate before any write; silently inferring a slug is forbidden. A project update tied to a dated milestone runs both this dispatcher and the long-horizon one (see the context skill's "Tie-breakers"). Future durable-state domains (e.g. git) follow the same shape — per-domain skill block, thin dispatcher here.
168
+ **Project intent** (state, progress, milestone, blocker, or a new-project request for a named workstream) → route through the context skill's "Project DM-intent detection". A new project requires the project-creation gate before any write; the gate's "No match" path schedules a `confirm:` DM rather than asking inline (see the context skill Step 3 and `scheduled.dm.md` ## Confirmation follow-up). Silently inferring a slug is forbidden. A project update tied to a dated milestone runs both this dispatcher and the long-horizon one (see the context skill's "Tie-breakers"). Future durable-state domains (e.g. git) follow the same shape — per-domain skill block, thin dispatcher here.
136
169
 
137
170
  ## User Message
138
171
  Platform: {event_data[platform]}