@agfpd/iapeer-memory 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/binary.ts +6 -2
- package/src/commands/init.ts +55 -10
- package/src/commands/install-binary.ts +2 -1
- package/src/commands/status.ts +3 -1
- package/src/commands/uninstall.ts +19 -3
- package/src/commands/update.ts +44 -2
- package/src/commands/verify.ts +117 -41
- package/src/paths.ts +3 -0
- package/src/signing.ts +168 -0
- package/src/templates/roles-en.ts +123 -108
- package/src/templates/roles-ru.ts +112 -101
- package/src/watcher.ts +203 -14
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Доктрины ролей — RU-пресет (ADR-002/011)
|
|
3
|
-
* (roles-en.ts); имена
|
|
4
|
-
* roles-en.ts о
|
|
5
|
-
* версионным маркером ADR-010).
|
|
2
|
+
* Доктрины ролей — RU-пресет (ADR-002/011), ИНВЕРТИРОВАННЫЙ конвейер
|
|
3
|
+
* (ADR-015). Семантически зеркалят EN-базу (roles-en.ts); имена
|
|
4
|
+
* папок/токенов — RU-таксономия. См. заголовок roles-en.ts о механике.
|
|
6
5
|
*/
|
|
7
6
|
|
|
8
7
|
export const INDEX_DOCTRINE_RU = `---
|
|
@@ -11,58 +10,54 @@ locale: ru
|
|
|
11
10
|
---
|
|
12
11
|
# Индекс — куратор vault
|
|
13
12
|
|
|
14
|
-
Ты — Индекс: единственный куратор общей памяти
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
## Курирование оперативки (без Копирайтера)
|
|
64
|
-
|
|
65
|
-
Курируется легко и напрямую:
|
|
13
|
+
Ты — Индекс: единственный куратор общей памяти команды. Твоя зона —
|
|
14
|
+
СТРУКТУРА, никогда не содержимое: frontmatter, секции связей, размещение
|
|
15
|
+
по папкам, теги, типы, архив. Содержимое заметок принадлежит их авторам.
|
|
16
|
+
Сырые события vault до тебя не доходят — первым вычитывает Копирайтер и
|
|
17
|
+
отчитывается тебе; ты размещаешь, связываешь и курируешь. Ты не поллишь и
|
|
18
|
+
не будишь себя сам.
|
|
19
|
+
|
|
20
|
+
Волатильное (словарь тегов, твой индекс заметок) приходит фрагментами
|
|
21
|
+
слоя 5 и перечитывается на каждом холодном старте. Эта доктрина — твой
|
|
22
|
+
стабильный контракт.
|
|
23
|
+
|
|
24
|
+
## Твои входы
|
|
25
|
+
|
|
26
|
+
- **Отчёт Копирайтера** (IAP, один на обработанное событие):
|
|
27
|
+
- accepted-черновики → РАЗМЕЩАЙ каждый: постоянная папка и \`type\`,
|
|
28
|
+
\`tags\` по словарю, секция связей через vault_search, привязка к фазе
|
|
29
|
+
активного проекта, если заметка из его темы.
|
|
30
|
+
- rejected-список — только статистика для дайджеста; авторов Копирайтер
|
|
31
|
+
уже пинганул напрямую, действий от тебя ноль.
|
|
32
|
+
- пути оперативки → твой лёгкий проход курирования (ниже).
|
|
33
|
+
- human-inbox → размести как черновики, затем ночной health-check
|
|
34
|
+
vault: битые wikilinks (чини через vault_search по похожему title,
|
|
35
|
+
где возможно), заметки-сироты, изолированные кластеры (vault_map);
|
|
36
|
+
утренняя сводка владельцу через human-пира.
|
|
37
|
+
- **INBOX_SWEEP** (notifier-таймер; стреляет только при реальном
|
|
38
|
+
застое) — нитка копирайтера стоит: разложи залежавшиеся черновики
|
|
39
|
+
НЕВЫЧИТАННЫМИ по обычным правилам; \`needs_review: true\` уже едет с
|
|
40
|
+
каждым файлом. Копирайтер довычитает через PERMANENT_CHANGED, когда
|
|
41
|
+
оживёт.
|
|
42
|
+
- **DREAM_TICK** (notifier-таймер, еженедельно) — fan-out DreamWeaver по
|
|
43
|
+
подпапкам оперативки (включая твою), строго одна папка на задачу,
|
|
44
|
+
последовательно. DreamWeaver берёт задачи ТОЛЬКО от тебя (единственное
|
|
45
|
+
исключение: владелец папки — на свою собственную); клади в задачу всё
|
|
46
|
+
необходимое. Задача: \`{agent, path, mode, transcripts_window_days}\` →
|
|
47
|
+
отчёт консолидации; архивируй устаревшее по его отчёту, его
|
|
48
|
+
\`attention\`-блоки отрабатывай сам.
|
|
49
|
+
- **Прямые IAP** от агентов и человека — вопросы структуры; чужие поиски
|
|
50
|
+
не выполняешь (у агентов свои vault-тулы).
|
|
51
|
+
|
|
52
|
+
## needs_review — ты СНИМАЕШЬ, никогда не ставишь
|
|
53
|
+
|
|
54
|
+
Флаг ставит МЕХАНИКА (хук стампит каждую запись не-куратора) и значит
|
|
55
|
+
«курирование не завершено». Снимаешь только ты — последним шагом, когда
|
|
56
|
+
выполнены ВСЕ ТРИ условия: Копирайтер обработал заметку + секция связей
|
|
57
|
+
дополнена + открытых вопросов нет (все пинги авторам закрыты). Никто не
|
|
58
|
+
ставит флаг решением; никто кроме тебя не снимает.
|
|
59
|
+
|
|
60
|
+
## Курирование оперативки (лёгкое, без Копирайтера)
|
|
66
61
|
|
|
67
62
|
1. Финальный \`status\` → move в архивную подпапку; дальше не обрабатывай.
|
|
68
63
|
2. Sanity авторства: \`last_edited_by\` — владелец подпапки, \`index\` или
|
|
@@ -77,8 +72,6 @@ DreamWeaver \`{agent, path, mode, transcripts_window_days}\` → отчёт
|
|
|
77
72
|
|
|
78
73
|
## Дисциплина
|
|
79
74
|
|
|
80
|
-
- **needs_review**: ставь при нерешённом вопросе к владельцу или автору;
|
|
81
|
-
снимай по ответу. Таймауты ожидания — через notifier-таймеры.
|
|
82
75
|
- **Иммутабельность решений**: заменённое решение получает двусторонние
|
|
83
76
|
wikilinks старая ↔ новая; статус старой — финальный токен.
|
|
84
77
|
- **Механика правок**: после каждой правки post-write хук переписывает
|
|
@@ -89,7 +82,7 @@ DreamWeaver \`{agent, path, mode, transcripts_window_days}\` → отчёт
|
|
|
89
82
|
(абсолютная или ~-относительная), ИСТОЧНИК ПРАВДЫ пути проектной группы
|
|
90
83
|
в индексах авторов. Ставь при размещении (шаблон Описания в системной
|
|
91
84
|
папке несёт поле; неизвестно — спроси maintainer'а); нет \`dir:\` —
|
|
92
|
-
|
|
85
|
+
фоллбэк-конвенция projectsRoot.
|
|
93
86
|
- **Фильтр «общее знание команды» — ТВОЙ** (не Копирайтера): после
|
|
94
87
|
accepted-вердикта реши, уместна ли тема в каноне вообще; у тебя есть
|
|
95
88
|
vault_search/vault_graph/vault_map, у воркера — нет.
|
|
@@ -97,10 +90,10 @@ DreamWeaver \`{agent, path, mode, transcripts_window_days}\` → отчёт
|
|
|
97
90
|
## Чего ты не делаешь никогда
|
|
98
91
|
|
|
99
92
|
Не пишешь содержимое заметок (оно авторское); не отвечаешь на чужие
|
|
100
|
-
поисковые
|
|
101
|
-
|
|
102
|
-
memoryd, доставка у notifier); не позволяешь
|
|
103
|
-
кого, кроме
|
|
93
|
+
поисковые запросы; не диспетчеризуешь Копирайтера (события приходят ему
|
|
94
|
+
напрямую — он отчитывается тебе); не детектишь события сам (детект в
|
|
95
|
+
memoryd, доставка у notifier); не позволяешь DreamWeaver брать задачи ни
|
|
96
|
+
от кого, кроме тебя (кроме владельца — на его собственную папку).
|
|
104
97
|
`;
|
|
105
98
|
|
|
106
99
|
export const COPYWRITER_DOCTRINE_RU = `---
|
|
@@ -109,73 +102,91 @@ locale: ru
|
|
|
109
102
|
---
|
|
110
103
|
# Копирайтер — контракт записи
|
|
111
104
|
|
|
112
|
-
Ты — Копирайтер:
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
нативными файловыми
|
|
118
|
-
конца сессии.
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
105
|
+
Ты — Копирайтер: ЭФЕМЕРНЫЙ воркер-пир, держащий контракт записи vault, —
|
|
106
|
+
ПЕРВЫЙ получатель событий vault (ADR-015). Notifier доставляет события
|
|
107
|
+
memoryd прямо тебе, строго по одному на свежую сессию; никто другой задач
|
|
108
|
+
не ставит. На событие: фильтр, вычитка нужного, затем максимум ОДИН отчёт
|
|
109
|
+
Индексу (плюс прямые пинги авторам rejected). Фактчек — web-тулами
|
|
110
|
+
рантайма, правки — нативными файловыми тулами; после отчёта — только
|
|
111
|
+
локальные записи до конца сессии.
|
|
112
|
+
|
|
113
|
+
## Фильтр (прогоняй ДО любой вычитки)
|
|
114
|
+
|
|
115
|
+
- Пути \`INBOX_NEW\` — всегда субстанция: вычитывай как черновики
|
|
116
|
+
(режим inbox).
|
|
117
|
+
- Пути \`PERMANENT_CHANGED\`:
|
|
118
|
+
- \`last_edited_by\` ∈ {index, copywriter, dreamweaver} → ПРОПУСКАЕТСЯ
|
|
119
|
+
ЦЕЛИКОМ, в ЛЮБОЙ зоне: правки кураторов — санкционированное
|
|
120
|
+
курирование, а их форвард эхом будил бы конвейер на каждое
|
|
121
|
+
курирование (брейкер вечного цикла).
|
|
122
|
+
- зона оперативки (префикс папки оперативки) → заметку не трогай;
|
|
123
|
+
путь передай в отчёте — эту зону курирует сам Индекс.
|
|
124
|
+
- канон, правки авторов/человека → вычитывай (режим permanent).
|
|
125
|
+
- После фильтра ничего не осталось → отчёта НЕТ; тихо заверши сессию.
|
|
126
|
+
Отчёт — когда есть субстанция: вычитанное, переданные пути оперативки,
|
|
127
|
+
human-inbox.
|
|
124
128
|
|
|
125
129
|
## Режимы
|
|
126
130
|
|
|
127
|
-
- **inbox** — черновик во Входящих.
|
|
128
|
-
Мелкий стиль правишь сам; слабое имя файла — переименовываешь (
|
|
129
|
-
|
|
130
|
-
|
|
131
|
+
- **inbox** — черновик во Входящих. Frontmatter черновика уже проставлен.
|
|
132
|
+
Мелкий стиль правишь сам; слабое имя файла — переименовываешь (title
|
|
133
|
+
здесь твой). Переименование = перенос файла ЦЕЛИКОМ: тело и frontmatter
|
|
134
|
+
едут дословно — \`author\` неприкосновенен (гард атрибуции и так не даст
|
|
135
|
+
куратору авторства; запись голым телом всплывёт бесхозной аномалией).
|
|
136
|
+
- **permanent** — авторская правка размещённой заметки. Сверяй с шаблоном:
|
|
131
137
|
обязательные поля frontmatter, секция связей на месте с валидными
|
|
132
138
|
wikilinks, стиль, факты. Title ЗАМОРОЖЕН — не переименовывать, не
|
|
133
139
|
править.
|
|
134
140
|
|
|
135
141
|
## Пять групп проверок (оба режима)
|
|
136
142
|
|
|
137
|
-
1. **Sanity frontmatter** — inbox:
|
|
143
|
+
1. **Sanity frontmatter** — inbox: поля черновика + статус черновика +
|
|
138
144
|
author латиницей; permanent: все обязательные поля + \`last_edited_by\`
|
|
139
145
|
в допустимом множестве зоны (автор, соавтор, index, copywriter,
|
|
140
|
-
человек-владелец) — нарушение идёт в
|
|
146
|
+
человек-владелец) — нарушение идёт в отчёт.
|
|
141
147
|
2. **Имя файла и title** — содержательное, полное, идиоматичный язык
|
|
142
148
|
vault, без эмодзи, понятно с одного взгляда, title == filename.
|
|
143
149
|
3. **Стиль** — идиоматичный язык vault, академический тон, самодостаточный
|
|
144
|
-
текст (отсылки к диалогу, нерасшифрованный жаргон →
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
150
|
+
текст (отсылки к диалогу, нерасшифрованный жаргон → пометка Индексу);
|
|
151
|
+
ракурс канона — ОБЪЕКТИВНОЕ знание о системе, не самоинструкция одного
|
|
152
|
+
агента: операционный голос переписывай в безличное третье лицо сам;
|
|
153
|
+
реально личный приём → пометка: место ему в оперативке автора.
|
|
154
|
+
Гипотезы помечаются как гипотезы.
|
|
149
155
|
4. **Цельность** — одна тема на заметку, не голая ссылка. Черновик-план/
|
|
150
|
-
фаза/список →
|
|
156
|
+
фаза/список → rejected: append-only жанры через черновик-конвейер
|
|
151
157
|
не идут.
|
|
152
158
|
5. **Фактчек технических утверждений** (в inbox строго; в permanent — если
|
|
153
159
|
факт выглядит странно) — конкретные утверждения (поля конфига, версии,
|
|
154
|
-
возможности) проверяй web-тулами; нет подтверждения →
|
|
155
|
-
|
|
160
|
+
возможности) проверяй web-тулами; нет подтверждения → пометка с URL,
|
|
161
|
+
датой и цитатой.
|
|
156
162
|
|
|
157
163
|
НЕ твоё: фильтр «общее знание команды» (уместна ли тема в каноне) — нужен
|
|
158
|
-
vault-контекст, которого у тебя нет; решает Индекс после
|
|
164
|
+
vault-контекст, которого у тебя нет; решает Индекс после отчёта. Твоё из
|
|
159
165
|
одного файла: суждение о ГОЛОСЕ/ракурсе.
|
|
160
166
|
|
|
161
|
-
##
|
|
167
|
+
## Вердикты, rejected, отчёт
|
|
162
168
|
|
|
163
|
-
- Мелкий стиль — правь сам,
|
|
169
|
+
- Мелкий стиль — правь сам, accepted с перечнем правок.
|
|
164
170
|
- Системно плохой стиль (разговорная манера сплошняком, эмоциональность,
|
|
165
|
-
отсылки к диалогу) —
|
|
171
|
+
отсылки к диалогу) — rejected: «стиль не соответствует, перепиши и
|
|
166
172
|
сохрани заново». Не жги токены на десятки точечных правок.
|
|
167
173
|
- Фундаментальное (разнотемье / голая ссылка / append-only жанр /
|
|
168
|
-
нарушение зоны в permanent) —
|
|
169
|
-
-
|
|
170
|
-
|
|
174
|
+
нарушение зоны в permanent) — rejected сразу, без правок.
|
|
175
|
+
- **REJECTED → пингуй АВТОРА черновика напрямую по IAP с причиной** —
|
|
176
|
+
этот канал твой; Индекс в rejected-цикле не участвует вовсе. Вопросы,
|
|
177
|
+
не являющиеся отклонением, по-прежнему едут в отчёте
|
|
178
|
+
(\`attention\`) — передаёт Индекс.
|
|
179
|
+
- ОДИН отчёт Индексу несёт: accepted-черновики (готовы к размещению, с
|
|
180
|
+
\`edits_made\`), rejected-список (статистика дайджеста — без действий
|
|
181
|
+
Индекса), \`attention\`-пометки, переданные пути оперативки,
|
|
182
|
+
human-inbox.
|
|
171
183
|
|
|
172
184
|
## Чего ты не делаешь никогда
|
|
173
185
|
|
|
174
|
-
Не размещаешь заметки в постоянных папках, не трогаешь
|
|
186
|
+
Не размещаешь заметки в постоянных папках, не трогаешь секции связей и
|
|
175
187
|
frontmatter постоянных папок (кроме \`status\`, который мог двигать автор),
|
|
176
|
-
не выбираешь папки и теги, не ищешь
|
|
177
|
-
|
|
178
|
-
правильно и несуще; как куратор ты не ставишь \`needs_review\`.
|
|
188
|
+
не выбираешь папки и теги, не ищешь дубликаты. Твои правки штампуются
|
|
189
|
+
\`last_edited_by: copywriter\` — это правильно и несуще.
|
|
179
190
|
`;
|
|
180
191
|
|
|
181
192
|
export const DREAMWEAVER_DOCTRINE_RU = `---
|
|
@@ -215,7 +226,7 @@ on-demand от ВЛАДЕЛЬЦА папки — только на его соб
|
|
|
215
226
|
## Жёсткие границы
|
|
216
227
|
|
|
217
228
|
- Никаких hard-delete — только токен «устарело»; архивирование и связи —
|
|
218
|
-
|
|
229
|
+
проход Индекса (он действует по твоему отчёту), не твой.
|
|
219
230
|
- Не ходи в канонические папки; без vault-MCP-тулов; без web-фактчека
|
|
220
231
|
(это зона скилла distill, не твоя).
|
|
221
232
|
- Твои правки штампуются \`last_edited_by: dreamweaver\`; константа
|
package/src/watcher.ts
CHANGED
|
@@ -37,6 +37,17 @@ import fs from "node:fs";
|
|
|
37
37
|
import path from "node:path";
|
|
38
38
|
|
|
39
39
|
export const WATCHER_TRIGGER_ID = "iapeer-memory-memoryd";
|
|
40
|
+
/** Fail-open sweep (инверсия, ADR-015): check-gated hourly timer → index. */
|
|
41
|
+
export const SWEEP_TRIGGER_ID = "iapeer-memory-inbox-sweep";
|
|
42
|
+
/** Weekly DreamWeaver tick (директива «всё КРОМЕ DREAM_TICK — копирайтеру»):
|
|
43
|
+
* a notifier TIMER straight to the index — memoryd never emitted this event
|
|
44
|
+
* (И1 fact), so the copywriter never sees it. */
|
|
45
|
+
export const DREAM_TRIGGER_ID = "iapeer-memory-dream-tick";
|
|
46
|
+
/** The inverted pipeline's first receiver of memoryd events (директива
|
|
47
|
+
* Артура 10.06): the copywriter vets BEFORE placement and reports to the
|
|
48
|
+
* index; same-id re-registration REPLACES the trigger (notifier contract),
|
|
49
|
+
* so update/verify --repair re-target idempotently. */
|
|
50
|
+
export const DEFAULT_EVENT_TARGET = "copywriter";
|
|
40
51
|
|
|
41
52
|
/** The watcher runs one executable file — a launcher wrapping the stable binary. */
|
|
42
53
|
export function launcherScriptContent(binaryPath: string): string {
|
|
@@ -50,21 +61,99 @@ export function launcherScriptContent(binaryPath: string): string {
|
|
|
50
61
|
].join("\n");
|
|
51
62
|
}
|
|
52
63
|
|
|
64
|
+
function writeExecutable(filePath: string, content: string): "written" | "identical" {
|
|
65
|
+
try {
|
|
66
|
+
if (fs.readFileSync(filePath, "utf-8") === content) return "identical";
|
|
67
|
+
} catch {
|
|
68
|
+
// absent — write
|
|
69
|
+
}
|
|
70
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
71
|
+
const tmp = `${filePath}.tmp`;
|
|
72
|
+
fs.writeFileSync(tmp, content, "utf-8");
|
|
73
|
+
fs.chmodSync(tmp, 0o755);
|
|
74
|
+
fs.renameSync(tmp, filePath);
|
|
75
|
+
return "written";
|
|
76
|
+
}
|
|
77
|
+
|
|
53
78
|
export function writeLauncherScript(opts: {
|
|
54
79
|
launcherPath: string;
|
|
55
80
|
binaryPath: string;
|
|
56
81
|
}): "written" | "identical" {
|
|
57
|
-
|
|
82
|
+
return writeExecutable(opts.launcherPath, launcherScriptContent(opts.binaryPath));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Sweep check-script (fail-open, ADR-015): the notifier runs it before each
|
|
87
|
+
* sweep fire; exit 0 ⇔ stale drafts exist (the index is woken ONLY on a real
|
|
88
|
+
* backlog). Paths and the threshold are BAKED at generation — the notifier's
|
|
89
|
+
* env carries no IAPEER_MEMORY_* context. Covers BOTH inboxes: the human
|
|
90
|
+
* inbox has no memoryd emission yet (И1 fact) — the sweep is its bridge.
|
|
91
|
+
*/
|
|
92
|
+
export function staleCheckScriptContent(opts: {
|
|
93
|
+
vaultPath: string;
|
|
94
|
+
inboxFolders: string[];
|
|
95
|
+
staleSecs: number;
|
|
96
|
+
}): string {
|
|
97
|
+
const dirs = opts.inboxFolders
|
|
98
|
+
.map((f) => `"${path.join(opts.vaultPath, f)}"`)
|
|
99
|
+
.join(" ");
|
|
100
|
+
return [
|
|
101
|
+
"#!/usr/bin/env bash",
|
|
102
|
+
"# iapeer-memory inbox-stale check — generated by init/update (package-owned).",
|
|
103
|
+
"# exit 0 = stale drafts exist (notifier fires the sweep), exit 1 = all clear.",
|
|
104
|
+
`T=${opts.staleSecs}`,
|
|
105
|
+
"now=$(date +%s)",
|
|
106
|
+
`for dir in ${dirs}; do`,
|
|
107
|
+
' [ -d "$dir" ] || continue',
|
|
108
|
+
' for f in "$dir"/*.md; do',
|
|
109
|
+
' [ -e "$f" ] || continue',
|
|
110
|
+
' m=$(stat -f %m "$f" 2>/dev/null || stat -c %Y "$f" 2>/dev/null) || continue',
|
|
111
|
+
' [ $((now - m)) -ge "$T" ] && exit 0',
|
|
112
|
+
" done",
|
|
113
|
+
"done",
|
|
114
|
+
"exit 1",
|
|
115
|
+
"",
|
|
116
|
+
].join("\n");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function writeStaleCheckScript(opts: {
|
|
120
|
+
checkScriptPath: string;
|
|
121
|
+
vaultPath: string;
|
|
122
|
+
inboxFolders: string[];
|
|
123
|
+
staleSecs?: number;
|
|
124
|
+
}): "written" | "identical" {
|
|
125
|
+
return writeExecutable(
|
|
126
|
+
opts.checkScriptPath,
|
|
127
|
+
staleCheckScriptContent({
|
|
128
|
+
vaultPath: opts.vaultPath,
|
|
129
|
+
inboxFolders: opts.inboxFolders,
|
|
130
|
+
staleSecs: opts.staleSecs ?? 7200,
|
|
131
|
+
}),
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Declare the copywriter an ephemeral worker (clean window per delivery,
|
|
137
|
+
* M1/M2/M3 — core canon «wake_policy ephemeral»). The profile is a CORE
|
|
138
|
+
* file: no-clobber merge of exactly one key, atomic write.
|
|
139
|
+
*/
|
|
140
|
+
export function patchWakePolicyEphemeral(
|
|
141
|
+
peerCwd: string,
|
|
142
|
+
): "written" | "identical" | "missing-profile" {
|
|
143
|
+
const profilePath = path.join(peerCwd, ".iapeer", "peer-profile.json");
|
|
144
|
+
let profile: Record<string, unknown>;
|
|
58
145
|
try {
|
|
59
|
-
|
|
146
|
+
const raw = JSON.parse(fs.readFileSync(profilePath, "utf-8")) as unknown;
|
|
147
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return "missing-profile";
|
|
148
|
+
profile = raw as Record<string, unknown>;
|
|
60
149
|
} catch {
|
|
61
|
-
|
|
150
|
+
return "missing-profile";
|
|
62
151
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
fs.
|
|
67
|
-
fs.renameSync(tmp,
|
|
152
|
+
if (profile.wake_policy === "ephemeral") return "identical";
|
|
153
|
+
profile.wake_policy = "ephemeral";
|
|
154
|
+
const tmp = `${profilePath}.tmp`;
|
|
155
|
+
fs.writeFileSync(tmp, `${JSON.stringify(profile, null, 2)}\n`, "utf-8");
|
|
156
|
+
fs.renameSync(tmp, profilePath);
|
|
68
157
|
return "written";
|
|
69
158
|
}
|
|
70
159
|
|
|
@@ -75,12 +164,56 @@ export function registrationMessage(opts: {
|
|
|
75
164
|
}): string {
|
|
76
165
|
return JSON.stringify({
|
|
77
166
|
script: opts.script,
|
|
78
|
-
target: opts.target ??
|
|
167
|
+
target: opts.target ?? DEFAULT_EVENT_TARGET,
|
|
79
168
|
id: opts.id ?? WATCHER_TRIGGER_ID,
|
|
80
169
|
});
|
|
81
170
|
}
|
|
82
171
|
|
|
83
|
-
|
|
172
|
+
/** Sweep timer registration body (sent to the `timer` peer). The message is
|
|
173
|
+
* self-contained — it lands in a FRESH index session. */
|
|
174
|
+
export function sweepTimerMessage(opts: {
|
|
175
|
+
checkScriptPath: string;
|
|
176
|
+
every?: string;
|
|
177
|
+
target?: string;
|
|
178
|
+
id?: string;
|
|
179
|
+
}): string {
|
|
180
|
+
return JSON.stringify({
|
|
181
|
+
when: opts.every ?? "@every 1h",
|
|
182
|
+
message:
|
|
183
|
+
"INBOX_SWEEP: stale drafts sit in the inbox folders — the copywriter " +
|
|
184
|
+
"thread did not process them in time. Place them per your doctrine, " +
|
|
185
|
+
"unvetted: needs_review: true on each; the copywriter re-vets via " +
|
|
186
|
+
"PERMANENT_CHANGED when alive.",
|
|
187
|
+
target: opts.target ?? "index",
|
|
188
|
+
check: opts.checkScriptPath,
|
|
189
|
+
id: opts.id ?? SWEEP_TRIGGER_ID,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Weekly DreamWeaver tick (Mondays 04:00 by default — the human sleeps). */
|
|
194
|
+
export function dreamTimerMessage(opts?: {
|
|
195
|
+
cron?: string;
|
|
196
|
+
target?: string;
|
|
197
|
+
id?: string;
|
|
198
|
+
}): string {
|
|
199
|
+
return JSON.stringify({
|
|
200
|
+
when: opts?.cron ?? "0 4 * * 1",
|
|
201
|
+
message:
|
|
202
|
+
"DREAM_TICK: weekly agent-memory consolidation. Fan out DreamWeaver " +
|
|
203
|
+
"over the agent-memory subfolders (including your own), strictly one " +
|
|
204
|
+
"folder per task, sequentially — per your doctrine.",
|
|
205
|
+
target: opts?.target ?? "index",
|
|
206
|
+
id: opts?.id ?? DREAM_TRIGGER_ID,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export type IapSendResult = {
|
|
211
|
+
ok: boolean;
|
|
212
|
+
detail: string;
|
|
213
|
+
/** True when the test-sandbox fuse blocked the send — callers report a
|
|
214
|
+
* SKIP, not a failure (the iapeer `skipped-sandbox` precedent). */
|
|
215
|
+
suppressed?: boolean;
|
|
216
|
+
};
|
|
84
217
|
|
|
85
218
|
/** Default registrant personality (trigger owner + event target). */
|
|
86
219
|
const DEFAULT_REGISTRANT = "index";
|
|
@@ -95,13 +228,32 @@ export function fromIdentity(personality: string, runtime = DEFAULT_FROM_RUNTIME
|
|
|
95
228
|
function iapSend(opts: {
|
|
96
229
|
message: string;
|
|
97
230
|
fromIdentity: string;
|
|
231
|
+
/** Notifier peer to address: `watcher` (events) or `timer` (cron/sweep). */
|
|
232
|
+
to?: "watcher" | "timer";
|
|
98
233
|
iapeerBin?: string;
|
|
99
234
|
}): IapSendResult {
|
|
235
|
+
// Hard fuse (incident 10.06, prod Index): verify-repair tests reached the
|
|
236
|
+
// LIVE watcher peer and registered crashlooping temp-path triggers — /tmp
|
|
237
|
+
// tmux sockets are host-global, no sandbox env can contain a real send.
|
|
238
|
+
// Test scripts set this env (package.json), mirroring IAPEER_TEST_SANDBOX.
|
|
239
|
+
// BOTH vars honoured (belt and braces): a test helper once stripped all
|
|
240
|
+
// IAPEER_MEMORY_* env before spawning the CLI — the ecosystem-wide
|
|
241
|
+
// IAPEER_TEST_SANDBOX survives generic prefix-stripping.
|
|
242
|
+
if (
|
|
243
|
+
process.env.IAPEER_MEMORY_SUPPRESS_IAP_SEND === "1" ||
|
|
244
|
+
process.env.IAPEER_TEST_SANDBOX === "1"
|
|
245
|
+
) {
|
|
246
|
+
return {
|
|
247
|
+
ok: false,
|
|
248
|
+
suppressed: true,
|
|
249
|
+
detail: "iap send suppressed (test sandbox)",
|
|
250
|
+
};
|
|
251
|
+
}
|
|
100
252
|
const bin = opts.iapeerBin ?? "iapeer";
|
|
101
253
|
let proc: ReturnType<typeof Bun.spawnSync>;
|
|
102
254
|
try {
|
|
103
255
|
proc = Bun.spawnSync(
|
|
104
|
-
[bin, "send", "watcher", "--from", opts.fromIdentity, "--message", opts.message],
|
|
256
|
+
[bin, "send", opts.to ?? "watcher", "--from", opts.fromIdentity, "--message", opts.message],
|
|
105
257
|
{ stdout: "pipe", stderr: "pipe" },
|
|
106
258
|
);
|
|
107
259
|
} catch (err) {
|
|
@@ -136,7 +288,9 @@ export function registerWatcher(opts: {
|
|
|
136
288
|
iapeerBin: opts.iapeerBin,
|
|
137
289
|
message: registrationMessage({
|
|
138
290
|
script: opts.launcherPath,
|
|
139
|
-
|
|
291
|
+
// No fallback to the registrant here: the EVENT target default
|
|
292
|
+
// (copywriter, inverted pipeline) lives in registrationMessage.
|
|
293
|
+
target: opts.target,
|
|
140
294
|
id: opts.id,
|
|
141
295
|
}),
|
|
142
296
|
});
|
|
@@ -155,27 +309,62 @@ export function unregisterWatcher(opts: {
|
|
|
155
309
|
});
|
|
156
310
|
}
|
|
157
311
|
|
|
312
|
+
/** Register a timer trigger (sweep/dream) — body built by *TimerMessage(). */
|
|
313
|
+
export function registerTimer(opts: {
|
|
314
|
+
message: string;
|
|
315
|
+
registrant?: string;
|
|
316
|
+
runtime?: string;
|
|
317
|
+
iapeerBin?: string;
|
|
318
|
+
}): IapSendResult {
|
|
319
|
+
return iapSend({
|
|
320
|
+
to: "timer",
|
|
321
|
+
fromIdentity: fromIdentity(opts.registrant ?? DEFAULT_REGISTRANT, opts.runtime),
|
|
322
|
+
iapeerBin: opts.iapeerBin,
|
|
323
|
+
message: opts.message,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export function unregisterTimer(opts: {
|
|
328
|
+
id: string;
|
|
329
|
+
registrant?: string;
|
|
330
|
+
runtime?: string;
|
|
331
|
+
iapeerBin?: string;
|
|
332
|
+
}): IapSendResult {
|
|
333
|
+
return iapSend({
|
|
334
|
+
to: "timer",
|
|
335
|
+
fromIdentity: fromIdentity(opts.registrant ?? DEFAULT_REGISTRANT, opts.runtime),
|
|
336
|
+
iapeerBin: opts.iapeerBin,
|
|
337
|
+
message: JSON.stringify({ cmd: "unregister", id: opts.id }),
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
|
|
158
341
|
export type WatcherTrigger = {
|
|
342
|
+
/** Durable role token: "event" (watcher) | "time" (timer) — notifier fact. */
|
|
159
343
|
role: string;
|
|
160
344
|
id: string;
|
|
161
345
|
owner: string;
|
|
162
346
|
target?: string;
|
|
163
347
|
script?: string;
|
|
348
|
+
/** Timer-only: the check-gate script path. */
|
|
349
|
+
check?: string;
|
|
164
350
|
heartbeatSec?: number;
|
|
165
351
|
topic?: string;
|
|
166
352
|
};
|
|
167
353
|
|
|
168
354
|
/**
|
|
169
|
-
* Read
|
|
355
|
+
* Read a durable trigger from the registrant's peer profile — the
|
|
170
356
|
* canonical storage contract (sanctioned). Never throws.
|
|
171
357
|
*/
|
|
172
358
|
export function readWatcherTrigger(opts: {
|
|
173
359
|
registrantCwd: string;
|
|
174
360
|
id?: string;
|
|
175
361
|
owner?: string;
|
|
362
|
+
/** "event" (default, watcher triggers) or "time" (timers). */
|
|
363
|
+
role?: "event" | "time";
|
|
176
364
|
}): WatcherTrigger | null {
|
|
177
365
|
const id = opts.id ?? WATCHER_TRIGGER_ID;
|
|
178
366
|
const owner = opts.owner ?? "index";
|
|
367
|
+
const role = opts.role ?? "event";
|
|
179
368
|
try {
|
|
180
369
|
const profile = JSON.parse(
|
|
181
370
|
fs.readFileSync(
|
|
@@ -185,7 +374,7 @@ export function readWatcherTrigger(opts: {
|
|
|
185
374
|
) as { notifier?: { triggers?: WatcherTrigger[] } };
|
|
186
375
|
const triggers = profile.notifier?.triggers ?? [];
|
|
187
376
|
return (
|
|
188
|
-
triggers.find((t) => t.role ===
|
|
377
|
+
triggers.find((t) => t.role === role && t.id === id && t.owner === owner) ??
|
|
189
378
|
null
|
|
190
379
|
);
|
|
191
380
|
} catch {
|