doco-cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +68 -0
- package/dist/index.js +5100 -0
- package/dist/schema.sql +362 -0
- package/package.json +44 -0
- package/templates/agent-bootstrap/.claude/bootstrap-fetch.sh +274 -0
- package/templates/agent-bootstrap/.claude/post-tool-use-check.sh +144 -0
- package/templates/agent-bootstrap/.claude/settings.json +56 -0
- package/templates/agent-bootstrap/.claude/stop-check.sh +206 -0
- package/templates/agent-bootstrap/.claude/user-prompt-fetch.sh +172 -0
- package/templates/agent-bootstrap/.env.example +21 -0
- package/templates/agent-bootstrap/AGENTS.md +233 -0
- package/templates/agent-bootstrap/CLAUDE.md +1 -0
- package/templates/glossary.yaml +52 -0
package/dist/schema.sql
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
-- Doco Postgres schema (decision_01KRKEVEE3RQGPWHAPMZ0MS9G9).
|
|
2
|
+
--
|
|
3
|
+
-- Source-of-truth + read-side index in one database per host. Replaces
|
|
4
|
+
-- the filesystem-and-git shape that ADR-023 / ADR-024 described.
|
|
5
|
+
--
|
|
6
|
+
-- Single-database, multi-tenant: every entity carries its `doco_id`
|
|
7
|
+
-- which scopes it to the owning Doco. Hosts can hold thousands of
|
|
8
|
+
-- Doco instances in one database.
|
|
9
|
+
--
|
|
10
|
+
-- Schema rules:
|
|
11
|
+
-- - Each entity type gets its own table; common columns live up top
|
|
12
|
+
-- in a consistent order (id, doco_id, summary, lifecycle, ...).
|
|
13
|
+
-- - Type-specific columns are appended.
|
|
14
|
+
-- - `body_md` is on the types that have a markdown narrative body.
|
|
15
|
+
-- - `edges` materializes cross-entity references for graph queries.
|
|
16
|
+
-- - `audit_events` is the structured history (decision_01KRKESCBTYG4005VMPKYNYR53).
|
|
17
|
+
|
|
18
|
+
-- Schema version. Tracked separately from app version so DB migrations
|
|
19
|
+
-- don't gate code releases. v1 = initial Phase 2 cut.
|
|
20
|
+
CREATE TABLE IF NOT EXISTS doco_meta (
|
|
21
|
+
key text PRIMARY KEY,
|
|
22
|
+
value text NOT NULL
|
|
23
|
+
);
|
|
24
|
+
INSERT INTO doco_meta (key, value) VALUES ('schema_version', '1') ON CONFLICT DO NOTHING;
|
|
25
|
+
|
|
26
|
+
-- Host config (singleton row at id='host'). Replaces <root>/host.yaml
|
|
27
|
+
-- (rule_01KRKQDHWNWJAF4YKTMCB2A0D9 — alpha forbids back-compat).
|
|
28
|
+
CREATE TABLE IF NOT EXISTS hosts (
|
|
29
|
+
id text PRIMARY KEY,
|
|
30
|
+
name text NOT NULL,
|
|
31
|
+
visibility text NOT NULL DEFAULT 'private' CHECK (visibility IN ('public', 'private')),
|
|
32
|
+
raw_yaml text NOT NULL,
|
|
33
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
34
|
+
updated_at timestamptz NOT NULL DEFAULT now()
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
-- Identity layer.
|
|
38
|
+
|
|
39
|
+
CREATE TABLE IF NOT EXISTS principals (
|
|
40
|
+
id text PRIMARY KEY,
|
|
41
|
+
username text NOT NULL UNIQUE,
|
|
42
|
+
type text NOT NULL CHECK (type IN ('human', 'agent')),
|
|
43
|
+
display_name text,
|
|
44
|
+
email text,
|
|
45
|
+
github_login text,
|
|
46
|
+
avatar_url text,
|
|
47
|
+
owner_id text REFERENCES principals(id) ON DELETE SET NULL,
|
|
48
|
+
raw_yaml text NOT NULL,
|
|
49
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
50
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
51
|
+
deactivated_at timestamptz
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
CREATE TABLE IF NOT EXISTS organizations (
|
|
55
|
+
id text PRIMARY KEY,
|
|
56
|
+
slug text NOT NULL UNIQUE,
|
|
57
|
+
name text NOT NULL,
|
|
58
|
+
raw_yaml text NOT NULL,
|
|
59
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
60
|
+
updated_at timestamptz NOT NULL DEFAULT now()
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
-- Organization membership (replaces members[] array inside organizations.yaml).
|
|
64
|
+
-- Must come after both `principals` and `organizations` — its FKs reference them.
|
|
65
|
+
CREATE TABLE IF NOT EXISTS org_members (
|
|
66
|
+
org_id text NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
|
|
67
|
+
principal_id text NOT NULL REFERENCES principals(id) ON DELETE CASCADE,
|
|
68
|
+
role text NOT NULL CHECK (role IN ('owner', 'admin', 'member')),
|
|
69
|
+
joined_at timestamptz NOT NULL DEFAULT now(),
|
|
70
|
+
PRIMARY KEY (org_id, principal_id)
|
|
71
|
+
);
|
|
72
|
+
CREATE INDEX IF NOT EXISTS org_members_principal_idx ON org_members (principal_id, role);
|
|
73
|
+
|
|
74
|
+
CREATE TABLE IF NOT EXISTS docos (
|
|
75
|
+
id text PRIMARY KEY,
|
|
76
|
+
owner_slug text NOT NULL,
|
|
77
|
+
doco_slug text NOT NULL,
|
|
78
|
+
owner_id text NOT NULL, -- principal_<ulid> OR organization_<ulid>
|
|
79
|
+
name text,
|
|
80
|
+
visibility text NOT NULL DEFAULT 'private' CHECK (visibility IN ('public', 'private')),
|
|
81
|
+
raw_yaml text NOT NULL,
|
|
82
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
83
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
84
|
+
UNIQUE (owner_slug, doco_slug)
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
-- Per-Doco entity tables. `body_md` carries the markdown narrative
|
|
88
|
+
-- on types that have one; the rest of the structured data lives in
|
|
89
|
+
-- `raw_yaml` (round-trippable to/from the legacy file format).
|
|
90
|
+
|
|
91
|
+
CREATE TABLE IF NOT EXISTS intents (
|
|
92
|
+
id text PRIMARY KEY,
|
|
93
|
+
doco_id text NOT NULL REFERENCES docos(id) ON DELETE CASCADE,
|
|
94
|
+
summary text,
|
|
95
|
+
lifecycle text,
|
|
96
|
+
body_md text,
|
|
97
|
+
raw_yaml text NOT NULL,
|
|
98
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
99
|
+
created_by text,
|
|
100
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
101
|
+
updated_by text
|
|
102
|
+
);
|
|
103
|
+
CREATE INDEX IF NOT EXISTS intents_doco_idx ON intents (doco_id, created_at DESC);
|
|
104
|
+
CREATE INDEX IF NOT EXISTS intents_lifecycle_idx ON intents (doco_id, lifecycle);
|
|
105
|
+
|
|
106
|
+
CREATE TABLE IF NOT EXISTS decisions (
|
|
107
|
+
id text PRIMARY KEY,
|
|
108
|
+
doco_id text NOT NULL REFERENCES docos(id) ON DELETE CASCADE,
|
|
109
|
+
summary text,
|
|
110
|
+
lifecycle text,
|
|
111
|
+
body_md text,
|
|
112
|
+
raw_yaml text NOT NULL,
|
|
113
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
114
|
+
created_by text,
|
|
115
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
116
|
+
updated_by text
|
|
117
|
+
);
|
|
118
|
+
CREATE INDEX IF NOT EXISTS decisions_doco_idx ON decisions (doco_id, created_at DESC);
|
|
119
|
+
CREATE INDEX IF NOT EXISTS decisions_lifecycle_idx ON decisions (doco_id, lifecycle);
|
|
120
|
+
|
|
121
|
+
CREATE TABLE IF NOT EXISTS rules (
|
|
122
|
+
id text PRIMARY KEY,
|
|
123
|
+
doco_id text NOT NULL REFERENCES docos(id) ON DELETE CASCADE,
|
|
124
|
+
summary text,
|
|
125
|
+
lifecycle text,
|
|
126
|
+
body_md text,
|
|
127
|
+
raw_yaml text NOT NULL,
|
|
128
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
129
|
+
created_by text,
|
|
130
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
131
|
+
updated_by text
|
|
132
|
+
);
|
|
133
|
+
CREATE INDEX IF NOT EXISTS rules_doco_idx ON rules (doco_id, created_at DESC);
|
|
134
|
+
CREATE INDEX IF NOT EXISTS rules_lifecycle_idx ON rules (doco_id, lifecycle);
|
|
135
|
+
|
|
136
|
+
CREATE TABLE IF NOT EXISTS actions (
|
|
137
|
+
id text PRIMARY KEY,
|
|
138
|
+
doco_id text NOT NULL REFERENCES docos(id) ON DELETE CASCADE,
|
|
139
|
+
summary text,
|
|
140
|
+
lifecycle text,
|
|
141
|
+
body_md text,
|
|
142
|
+
raw_yaml text NOT NULL,
|
|
143
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
144
|
+
created_by text,
|
|
145
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
146
|
+
updated_by text
|
|
147
|
+
);
|
|
148
|
+
CREATE INDEX IF NOT EXISTS actions_doco_idx ON actions (doco_id, created_at DESC);
|
|
149
|
+
CREATE INDEX IF NOT EXISTS actions_lifecycle_idx ON actions (doco_id, lifecycle);
|
|
150
|
+
|
|
151
|
+
CREATE TABLE IF NOT EXISTS logs (
|
|
152
|
+
id text PRIMARY KEY,
|
|
153
|
+
doco_id text NOT NULL REFERENCES docos(id) ON DELETE CASCADE,
|
|
154
|
+
summary text,
|
|
155
|
+
lifecycle text,
|
|
156
|
+
body_md text,
|
|
157
|
+
raw_yaml text NOT NULL,
|
|
158
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
159
|
+
created_by text,
|
|
160
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
161
|
+
updated_by text
|
|
162
|
+
);
|
|
163
|
+
CREATE INDEX IF NOT EXISTS logs_doco_idx ON logs (doco_id, created_at DESC);
|
|
164
|
+
CREATE INDEX IF NOT EXISTS logs_lifecycle_idx ON logs (doco_id, lifecycle);
|
|
165
|
+
|
|
166
|
+
CREATE TABLE IF NOT EXISTS evals (
|
|
167
|
+
id text PRIMARY KEY,
|
|
168
|
+
doco_id text NOT NULL REFERENCES docos(id) ON DELETE CASCADE,
|
|
169
|
+
summary text,
|
|
170
|
+
lifecycle text,
|
|
171
|
+
body_md text,
|
|
172
|
+
raw_yaml text NOT NULL,
|
|
173
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
174
|
+
created_by text,
|
|
175
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
176
|
+
updated_by text
|
|
177
|
+
);
|
|
178
|
+
CREATE INDEX IF NOT EXISTS evals_doco_idx ON evals (doco_id, created_at DESC);
|
|
179
|
+
CREATE INDEX IF NOT EXISTS evals_lifecycle_idx ON evals (doco_id, lifecycle);
|
|
180
|
+
|
|
181
|
+
CREATE TABLE IF NOT EXISTS scopes (
|
|
182
|
+
id text PRIMARY KEY,
|
|
183
|
+
doco_id text NOT NULL REFERENCES docos(id) ON DELETE CASCADE,
|
|
184
|
+
name text NOT NULL,
|
|
185
|
+
summary text,
|
|
186
|
+
lifecycle text,
|
|
187
|
+
raw_yaml text NOT NULL,
|
|
188
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
189
|
+
created_by text,
|
|
190
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
191
|
+
updated_by text,
|
|
192
|
+
UNIQUE (doco_id, name)
|
|
193
|
+
);
|
|
194
|
+
CREATE INDEX IF NOT EXISTS scopes_doco_idx ON scopes (doco_id, created_at DESC);
|
|
195
|
+
|
|
196
|
+
CREATE TABLE IF NOT EXISTS tags (
|
|
197
|
+
id text PRIMARY KEY,
|
|
198
|
+
doco_id text NOT NULL REFERENCES docos(id) ON DELETE CASCADE,
|
|
199
|
+
name text NOT NULL,
|
|
200
|
+
raw_yaml text NOT NULL,
|
|
201
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
202
|
+
UNIQUE (doco_id, name)
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
CREATE TABLE IF NOT EXISTS ideas (
|
|
206
|
+
id text PRIMARY KEY,
|
|
207
|
+
doco_id text NOT NULL REFERENCES docos(id) ON DELETE CASCADE,
|
|
208
|
+
summary text,
|
|
209
|
+
lifecycle text,
|
|
210
|
+
body_md text,
|
|
211
|
+
raw_yaml text NOT NULL,
|
|
212
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
213
|
+
created_by text,
|
|
214
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
215
|
+
updated_by text
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
CREATE TABLE IF NOT EXISTS reference_entities (
|
|
219
|
+
id text PRIMARY KEY,
|
|
220
|
+
doco_id text NOT NULL REFERENCES docos(id) ON DELETE CASCADE,
|
|
221
|
+
summary text,
|
|
222
|
+
lifecycle text,
|
|
223
|
+
raw_yaml text NOT NULL,
|
|
224
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
225
|
+
created_by text,
|
|
226
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
227
|
+
updated_by text
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
-- Doco-level Principal/Organization references (multi-tenant Principals
|
|
231
|
+
-- live in the host-level `principals` table above, but a Doco can record
|
|
232
|
+
-- which Principals are members for permissions). For Phase 2, we just
|
|
233
|
+
-- mirror the YAML files; full membership table comes later.
|
|
234
|
+
|
|
235
|
+
-- Audit events: structured replacement for git history
|
|
236
|
+
-- (decision_01KRKESCBTYG4005VMPKYNYR53). One row per mutation.
|
|
237
|
+
|
|
238
|
+
CREATE TABLE IF NOT EXISTS audit_events (
|
|
239
|
+
event_id text PRIMARY KEY,
|
|
240
|
+
at timestamptz NOT NULL,
|
|
241
|
+
by_principal text,
|
|
242
|
+
doco_id text NOT NULL REFERENCES docos(id) ON DELETE CASCADE,
|
|
243
|
+
entity_type text NOT NULL,
|
|
244
|
+
entity_id text NOT NULL,
|
|
245
|
+
op text NOT NULL CHECK (op IN ('entity.create', 'entity.update', 'entity.delete', 'lifecycle.transition', 'edge.add')),
|
|
246
|
+
before_json jsonb,
|
|
247
|
+
after_json jsonb,
|
|
248
|
+
reason text
|
|
249
|
+
);
|
|
250
|
+
CREATE INDEX IF NOT EXISTS audit_events_entity_idx ON audit_events (entity_id, at DESC);
|
|
251
|
+
CREATE INDEX IF NOT EXISTS audit_events_doco_idx ON audit_events (doco_id, at DESC);
|
|
252
|
+
CREATE INDEX IF NOT EXISTS audit_events_op_idx ON audit_events (doco_id, op, at DESC);
|
|
253
|
+
CREATE INDEX IF NOT EXISTS audit_events_actor_idx ON audit_events (by_principal, at DESC);
|
|
254
|
+
|
|
255
|
+
-- Indexing layer tables. These hold the derived-data the read side
|
|
256
|
+
-- consumes — graph edges, vector embeddings, denormalized rule targets,
|
|
257
|
+
-- and full-text search rows. Supersedes ADR-023 (tiered architecture)
|
|
258
|
+
-- and ADR-024 (SQLite + FTS5) — Postgres is now both source of truth
|
|
259
|
+
-- and read-side index.
|
|
260
|
+
|
|
261
|
+
-- Graph edges (ADR-025). Materialized from frontmatter ID-shaped fields
|
|
262
|
+
-- by the indexer. attribution=='explicit' means declared in source;
|
|
263
|
+
-- 'doco-auto' means LLM-detected. Doco-scoped via doco_id; both
|
|
264
|
+
-- endpoints can be any node_type so we can't FK them.
|
|
265
|
+
CREATE TABLE IF NOT EXISTS edges (
|
|
266
|
+
from_id text NOT NULL,
|
|
267
|
+
from_node_type text NOT NULL,
|
|
268
|
+
to_id text NOT NULL,
|
|
269
|
+
to_node_type text NOT NULL,
|
|
270
|
+
edge_type text NOT NULL,
|
|
271
|
+
doco_id text NOT NULL REFERENCES docos(id) ON DELETE CASCADE,
|
|
272
|
+
edge_props_json jsonb,
|
|
273
|
+
attribution text NOT NULL DEFAULT 'explicit' CHECK (attribution IN ('explicit', 'doco-auto')),
|
|
274
|
+
PRIMARY KEY (from_id, to_id, edge_type)
|
|
275
|
+
);
|
|
276
|
+
CREATE INDEX IF NOT EXISTS edges_doco_idx ON edges (doco_id);
|
|
277
|
+
CREATE INDEX IF NOT EXISTS edges_to_idx ON edges (to_id, edge_type);
|
|
278
|
+
CREATE INDEX IF NOT EXISTS edges_from_type_idx ON edges (from_id, edge_type);
|
|
279
|
+
CREATE INDEX IF NOT EXISTS edges_type_idx ON edges (edge_type);
|
|
280
|
+
CREATE INDEX IF NOT EXISTS edges_attribution_idx ON edges (attribution);
|
|
281
|
+
|
|
282
|
+
-- Vector embeddings (ADR-052). One row per entity. Storage is bytea
|
|
283
|
+
-- (Float32Array bytes, little-endian). pgvector + ivfflat/hnsw is an
|
|
284
|
+
-- additive optimization for Tier-C scale (currently Tier B per ADR-049,
|
|
285
|
+
-- where sequential cosine is microseconds). Switching to vector(N) later
|
|
286
|
+
-- is a column-type migration with no data reformat.
|
|
287
|
+
-- model_id + content_hash let the reindex hook skip work when nothing
|
|
288
|
+
-- changed; a model swap invalidates rows whose model_id differs.
|
|
289
|
+
CREATE TABLE IF NOT EXISTS embeddings (
|
|
290
|
+
entity_id text PRIMARY KEY,
|
|
291
|
+
doco_id text NOT NULL REFERENCES docos(id) ON DELETE CASCADE,
|
|
292
|
+
model_id text NOT NULL,
|
|
293
|
+
content_hash text NOT NULL,
|
|
294
|
+
embedding bytea NOT NULL,
|
|
295
|
+
updated_at timestamptz NOT NULL DEFAULT now()
|
|
296
|
+
);
|
|
297
|
+
CREATE INDEX IF NOT EXISTS embeddings_doco_idx ON embeddings (doco_id);
|
|
298
|
+
CREATE INDEX IF NOT EXISTS embeddings_model_idx ON embeddings (model_id);
|
|
299
|
+
|
|
300
|
+
-- Denormalized Rule.applies_to → matched targets (ADR-026). Populated
|
|
301
|
+
-- by the indexer at write time. Lets runtime checks look up "which
|
|
302
|
+
-- Rules apply to this target?" in O(1) without re-evaluating selectors.
|
|
303
|
+
CREATE TABLE IF NOT EXISTS scope_match (
|
|
304
|
+
source_id text NOT NULL,
|
|
305
|
+
source_node_type text NOT NULL,
|
|
306
|
+
target_id text NOT NULL,
|
|
307
|
+
target_node_type text NOT NULL,
|
|
308
|
+
doco_id text NOT NULL REFERENCES docos(id) ON DELETE CASCADE,
|
|
309
|
+
selector_rev integer NOT NULL DEFAULT 1,
|
|
310
|
+
PRIMARY KEY (source_id, target_id)
|
|
311
|
+
);
|
|
312
|
+
CREATE INDEX IF NOT EXISTS scope_match_target_idx ON scope_match (target_id);
|
|
313
|
+
CREATE INDEX IF NOT EXISTS scope_match_doco_idx ON scope_match (doco_id);
|
|
314
|
+
|
|
315
|
+
-- Full-text search. One row per
|
|
316
|
+
-- entity. The indexer populates summary + body; search_tsv is a
|
|
317
|
+
-- generated tsvector with English stemming and weighting (A=summary,
|
|
318
|
+
-- B=body). The GIN index handles `@@` queries efficiently.
|
|
319
|
+
CREATE TABLE IF NOT EXISTS entity_fts (
|
|
320
|
+
entity_id text PRIMARY KEY,
|
|
321
|
+
doco_id text NOT NULL REFERENCES docos(id) ON DELETE CASCADE,
|
|
322
|
+
node_type text NOT NULL,
|
|
323
|
+
summary text,
|
|
324
|
+
body text,
|
|
325
|
+
search_tsv tsvector GENERATED ALWAYS AS (
|
|
326
|
+
setweight(to_tsvector('english', coalesce(summary, '')), 'A') ||
|
|
327
|
+
setweight(to_tsvector('english', coalesce(body, '')), 'B')
|
|
328
|
+
) STORED
|
|
329
|
+
);
|
|
330
|
+
CREATE INDEX IF NOT EXISTS entity_fts_doco_idx ON entity_fts (doco_id);
|
|
331
|
+
CREATE INDEX IF NOT EXISTS entity_fts_tsv_idx ON entity_fts USING gin (search_tsv);
|
|
332
|
+
|
|
333
|
+
-- Drop the legacy `revision` column from entity tables. Was incremented
|
|
334
|
+
-- on every upsert but never read by any TS code (decision_01KRHBZMD0V35NAX94Y7N2MXVA
|
|
335
|
+
-- flagged it; never fully removed). Idempotent — no-op on fresh DBs.
|
|
336
|
+
ALTER TABLE intents DROP COLUMN IF EXISTS revision;
|
|
337
|
+
ALTER TABLE decisions DROP COLUMN IF EXISTS revision;
|
|
338
|
+
ALTER TABLE rules DROP COLUMN IF EXISTS revision;
|
|
339
|
+
ALTER TABLE actions DROP COLUMN IF EXISTS revision;
|
|
340
|
+
ALTER TABLE evals DROP COLUMN IF EXISTS revision;
|
|
341
|
+
ALTER TABLE scopes DROP COLUMN IF EXISTS revision;
|
|
342
|
+
ALTER TABLE ideas DROP COLUMN IF EXISTS revision;
|
|
343
|
+
ALTER TABLE reference_entities DROP COLUMN IF EXISTS revision;
|
|
344
|
+
|
|
345
|
+
-- Deprecation (2026-05-16): the `reasoning` node type was removed.
|
|
346
|
+
-- Drop the legacy table and purge any dangling edges / embeddings /
|
|
347
|
+
-- audit rows so existing databases converge to the new shape on boot.
|
|
348
|
+
-- Idempotent — no-op on fresh DBs.
|
|
349
|
+
DROP TABLE IF EXISTS reasoning CASCADE;
|
|
350
|
+
DELETE FROM edges WHERE from_id LIKE 'reasoning\_%' ESCAPE '\' OR to_id LIKE 'reasoning\_%' ESCAPE '\';
|
|
351
|
+
DELETE FROM embeddings WHERE entity_id LIKE 'reasoning\_%' ESCAPE '\';
|
|
352
|
+
DELETE FROM audit_events WHERE entity_id LIKE 'reasoning\_%' ESCAPE '\';
|
|
353
|
+
|
|
354
|
+
-- ──────────────────────────────────────────────────────────────────────────
|
|
355
|
+
-- Token store (session tokens + CLI authorizations). Alpha keeps this as a
|
|
356
|
+
-- host-scoped JSON blob; move to one-row-per-token tables once the surface
|
|
357
|
+
-- stabilizes.
|
|
358
|
+
CREATE TABLE IF NOT EXISTS tokens_blob (
|
|
359
|
+
key text PRIMARY KEY,
|
|
360
|
+
blob jsonb NOT NULL,
|
|
361
|
+
updated_at timestamptz NOT NULL DEFAULT now()
|
|
362
|
+
);
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "doco-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Doco CLI — init, bootstrap, search, capture, patch, lint, query.",
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/torrenegra/doco.git",
|
|
10
|
+
"directory": "packages/cli"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/torrenegra/doco#readme",
|
|
13
|
+
"publishConfig": {
|
|
14
|
+
"access": "public"
|
|
15
|
+
},
|
|
16
|
+
"main": "./dist/index.js",
|
|
17
|
+
"bin": {
|
|
18
|
+
"doco": "./dist/index.js"
|
|
19
|
+
},
|
|
20
|
+
"files": ["dist", "templates"],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsx build.ts",
|
|
23
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
24
|
+
"test": "vitest run --passWithNoTests",
|
|
25
|
+
"test:watch": "vitest",
|
|
26
|
+
"doco": "tsx src/index.ts",
|
|
27
|
+
"prepack": "pnpm build"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"citty": "^0.1.6",
|
|
31
|
+
"gray-matter": "^4.0.3",
|
|
32
|
+
"kleur": "^4.1.5",
|
|
33
|
+
"pg": "^8.13.1",
|
|
34
|
+
"yaml": "^2.6.1"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@doco/db": "workspace:*",
|
|
38
|
+
"@doco/host": "workspace:*",
|
|
39
|
+
"@doco/index": "workspace:*",
|
|
40
|
+
"@doco/lints": "workspace:*",
|
|
41
|
+
"@doco/shared": "workspace:*",
|
|
42
|
+
"esbuild": "^0.25.0"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# SessionStart hook: fetch the Doco host's agent-bootstrap and inject
|
|
3
|
+
# `canonical_instructions` into the agent's context.
|
|
4
|
+
#
|
|
5
|
+
# Wired from `.claude/settings.json`. Output is the JSON envelope Claude
|
|
6
|
+
# Code's hook runner expects:
|
|
7
|
+
# { "hookSpecificOutput": { "hookEventName": "SessionStart",
|
|
8
|
+
# "additionalContext": "<text>" } }
|
|
9
|
+
#
|
|
10
|
+
# On failure (host unreachable, env missing, jq absent) we still emit a
|
|
11
|
+
# valid JSON envelope with a disconnected indicator so the agent never
|
|
12
|
+
# presents as connected without proper access.
|
|
13
|
+
#
|
|
14
|
+
# Per the `agent-md-stays-a-pointer` Rule + the
|
|
15
|
+
# `claude-md-becomes-a-strong-bootstrap-stub` Decision (this commit).
|
|
16
|
+
|
|
17
|
+
set -u
|
|
18
|
+
|
|
19
|
+
# Load .env if present. The production host is fixed at doco.to; env
|
|
20
|
+
# only carries the DOCO_TOKEN secret. DOCO_ID lives in AGENTS.md
|
|
21
|
+
# (committed, non-secret) — we read it from there if it's not already
|
|
22
|
+
# in env. Legacy repos with DOCO_ID in .env keep working: env wins
|
|
23
|
+
# over the AGENTS.md fallback.
|
|
24
|
+
if [ -f "$PWD/.env" ]; then
|
|
25
|
+
# shellcheck disable=SC1091
|
|
26
|
+
set -a
|
|
27
|
+
source "$PWD/.env"
|
|
28
|
+
set +a
|
|
29
|
+
fi
|
|
30
|
+
if [ -z "${DOCO_ID:-}" ]; then
|
|
31
|
+
for f in "$PWD/AGENTS.md" "$PWD/CLAUDE.md"; do
|
|
32
|
+
if [ -f "$f" ]; then
|
|
33
|
+
DOCO_ID=$(grep -oE 'doco_[A-Za-z0-9]+' "$f" | head -1)
|
|
34
|
+
[ -n "$DOCO_ID" ] && export DOCO_ID && break
|
|
35
|
+
fi
|
|
36
|
+
done
|
|
37
|
+
fi
|
|
38
|
+
DOCO_BASE_URL="https://doco.to"
|
|
39
|
+
|
|
40
|
+
emit_disconnected() {
|
|
41
|
+
# $1 indicator_reason — single-line "Not connected yet: <reason>" reason
|
|
42
|
+
# $2 mode — picks the recovery suffix (see below); defaults to "default"
|
|
43
|
+
# $3 body_extra — optional multi-line content inserted between
|
|
44
|
+
# the indicator and the recovery (e.g. the host's
|
|
45
|
+
# structured missing_doco_guidance actions)
|
|
46
|
+
#
|
|
47
|
+
# Recovery mode never bakes `doco login` into a sandbox-block case —
|
|
48
|
+
# the token is fine, the request just didn't leave the runtime.
|
|
49
|
+
# default — credentials issue, do `doco login` to re-authorize
|
|
50
|
+
# network — sandbox / network policy, allowlist doco.to in the agent runtime
|
|
51
|
+
# tool — missing local tool (curl / jq); install it
|
|
52
|
+
# verbatim — body_extra is authoritative; no generic suffix
|
|
53
|
+
local indicator_reason=$1
|
|
54
|
+
local mode=${2:-default}
|
|
55
|
+
local body_extra=${3:-}
|
|
56
|
+
local recovery
|
|
57
|
+
case "$mode" in
|
|
58
|
+
network)
|
|
59
|
+
recovery=$'**This is a sandbox / network-policy issue, not a credentials problem — `doco login` will NOT help.** The bearer token is fine; the bootstrap request didn\'t leave your agent runtime. Tell the project owner to allowlist `doco.to` in your agent\'s network settings (Codex web, GitHub Coding Agent, Anthropic web sandbox, etc. each have their own settings page). For a one-off, they may also approve the call inline in the agent\'s UI. Once the host is reachable, restart or `/clear` so SessionStart runs again.'
|
|
60
|
+
;;
|
|
61
|
+
tool)
|
|
62
|
+
recovery=$'Install the missing local tool, then restart or `/clear` so SessionStart runs again.'
|
|
63
|
+
;;
|
|
64
|
+
verbatim)
|
|
65
|
+
recovery=""
|
|
66
|
+
;;
|
|
67
|
+
default|*)
|
|
68
|
+
recovery=$'Ask the project owner to authorize the agent with `doco login --host https://doco.to` (or `doco login --host https://doco.to --create <slug>` for a new Doco), then restart or `/clear` so SessionStart runs again.'
|
|
69
|
+
;;
|
|
70
|
+
esac
|
|
71
|
+
local body
|
|
72
|
+
body=$'🔒 Doco connection — not connected\n\n⚠️ **Every reply must start with this exact line until access is fixed:**\n\n [🔮 Doco] Not connected yet: '"${indicator_reason}"$'\n\nDo not render regular Doco query/count/footer/tally lines while disconnected.'
|
|
73
|
+
if [ -n "$body_extra" ]; then
|
|
74
|
+
body="${body}"$'\n\n'"${body_extra}"
|
|
75
|
+
fi
|
|
76
|
+
if [ -n "$recovery" ]; then
|
|
77
|
+
body="${body}"$'\n\n'"${recovery}"
|
|
78
|
+
fi
|
|
79
|
+
if command -v jq >/dev/null 2>&1; then
|
|
80
|
+
jq -nc --arg c "$body" \
|
|
81
|
+
'{hookSpecificOutput: {hookEventName: "SessionStart", additionalContext: $c}}'
|
|
82
|
+
else
|
|
83
|
+
# jq missing — emit a literal valid JSON. Escape only what matters.
|
|
84
|
+
printf '{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"[🔮 Doco] Not connected yet: %s. jq is also missing — install it before retrying."}}\n' \
|
|
85
|
+
"$indicator_reason"
|
|
86
|
+
fi
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if ! command -v curl >/dev/null 2>&1; then
|
|
90
|
+
emit_disconnected "curl is not installed" tool
|
|
91
|
+
exit 0
|
|
92
|
+
fi
|
|
93
|
+
if [ -z "${DOCO_ID:-}" ]; then
|
|
94
|
+
emit_disconnected "missing DOCO_ID — set the **This project's Doco ID** line at the top of AGENTS.md, or run \`doco login --host https://doco.to\` to stamp it" default
|
|
95
|
+
exit 0
|
|
96
|
+
fi
|
|
97
|
+
if [ -z "${DOCO_TOKEN:-}" ]; then
|
|
98
|
+
emit_disconnected "missing DOCO_TOKEN" default
|
|
99
|
+
exit 0
|
|
100
|
+
fi
|
|
101
|
+
|
|
102
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
103
|
+
emit_disconnected "jq is not installed" tool
|
|
104
|
+
exit 0
|
|
105
|
+
fi
|
|
106
|
+
|
|
107
|
+
DOCO_PARAM="?id=$(printf '%s' "$DOCO_ID" | jq -sRr @uri 2>/dev/null || printf '%s' "$DOCO_ID")"
|
|
108
|
+
TMP_RESP="${TMPDIR:-/tmp}/doco-bootstrap-$$.json"
|
|
109
|
+
HTTP_STATUS=$(curl -sS --max-time 8 -w '%{http_code}' -o "$TMP_RESP" \
|
|
110
|
+
-H "Authorization: Bearer ${DOCO_TOKEN}" \
|
|
111
|
+
"${DOCO_BASE_URL}/api/v1/agent-bootstrap${DOCO_PARAM}" 2>/dev/null || true)
|
|
112
|
+
RESP=$(cat "$TMP_RESP" 2>/dev/null || true)
|
|
113
|
+
rm -f "$TMP_RESP" 2>/dev/null || true
|
|
114
|
+
if [ -z "$RESP" ]; then
|
|
115
|
+
# Empty body OR curl exit non-zero means the request never reached
|
|
116
|
+
# the host (DNS, sandbox network gate, firewall). HTTP_STATUS:000 is
|
|
117
|
+
# the same case. Don't tell the project owner to `doco login` — the
|
|
118
|
+
# token is fine; the runtime is the blocker.
|
|
119
|
+
emit_disconnected "doco.to unreachable (HTTP_STATUS:000 / network blocked at the agent runtime)" network
|
|
120
|
+
exit 0
|
|
121
|
+
fi
|
|
122
|
+
if [ "$HTTP_STATUS" != "200" ]; then
|
|
123
|
+
case "$HTTP_STATUS" in
|
|
124
|
+
401) emit_disconnected "DOCO_TOKEN expired or invalid (HTTP 401)" default ;;
|
|
125
|
+
403) emit_disconnected "token cannot access this Doco (HTTP 403) — ask the project owner to add this agent as a member, or run \`doco login\` with an account that already has access. Don't run \`--create\` — there's already a Doco; you just can't reach it." default ;;
|
|
126
|
+
404) emit_disconnected "DOCO_ID \"${DOCO_ID}\" doesn't resolve on doco.to (HTTP 404)" default ;;
|
|
127
|
+
5*) emit_disconnected "doco.to returned ${HTTP_STATUS} — host outage; wait and retry. \`doco login\` won't help." network ;;
|
|
128
|
+
*) emit_disconnected "bootstrap failed with HTTP ${HTTP_STATUS}" default ;;
|
|
129
|
+
esac
|
|
130
|
+
exit 0
|
|
131
|
+
fi
|
|
132
|
+
|
|
133
|
+
INSTR=$(printf '%s' "$RESP" | jq -r '.canonical_instructions // empty')
|
|
134
|
+
if [ -z "$INSTR" ]; then
|
|
135
|
+
emit_disconnected "bootstrap response has no canonical_instructions field"
|
|
136
|
+
exit 0
|
|
137
|
+
fi
|
|
138
|
+
|
|
139
|
+
WARNING_TEXT=$(printf '%s' "$RESP" | jq -r '.warning // empty' 2>/dev/null)
|
|
140
|
+
RESP_DOCO_ID=$(printf '%s' "$RESP" | jq -r '.doco_id // empty' 2>/dev/null)
|
|
141
|
+
HAS_GUIDANCE=$(printf '%s' "$RESP" | jq -r '.missing_doco_guidance // empty | if type == "object" then "1" else "" end' 2>/dev/null)
|
|
142
|
+
|
|
143
|
+
# When the host says the DOCO_ID didn't resolve OR resolved-but-is-
|
|
144
|
+
# inaccessible, the bootstrap response carries structured recovery
|
|
145
|
+
# guidance under `missing_doco_guidance` plus a single-line summary
|
|
146
|
+
# under `warning`. Split title (indicator) from summary+actions (body)
|
|
147
|
+
# so the agent sees a clean "Not connected yet: <title>" line plus the
|
|
148
|
+
# numbered action list — instead of cramming the whole multi-paragraph
|
|
149
|
+
# guidance into the indicator slot.
|
|
150
|
+
if [ -n "$HAS_GUIDANCE" ]; then
|
|
151
|
+
GUIDANCE_TITLE=$(printf '%s' "$RESP" | jq -r '.missing_doco_guidance.title // empty' 2>/dev/null)
|
|
152
|
+
GUIDANCE_BODY=$(printf '%s' "$RESP" | jq -r '
|
|
153
|
+
.missing_doco_guidance as $g |
|
|
154
|
+
$g.summary + "\n\n"
|
|
155
|
+
+ (
|
|
156
|
+
($g.actions | to_entries | map(
|
|
157
|
+
((.key + 1) | tostring) + ". " + .value.label
|
|
158
|
+
+ (if .value.command then "\n $ " + .value.command else "" end)
|
|
159
|
+
+ "\n " + .value.explainer
|
|
160
|
+
)) | join("\n\n")
|
|
161
|
+
)
|
|
162
|
+
' 2>/dev/null)
|
|
163
|
+
emit_disconnected "$GUIDANCE_TITLE" verbatim "$GUIDANCE_BODY"
|
|
164
|
+
exit 0
|
|
165
|
+
fi
|
|
166
|
+
if [ -n "$WARNING_TEXT" ]; then
|
|
167
|
+
emit_disconnected "$WARNING_TEXT" verbatim
|
|
168
|
+
exit 0
|
|
169
|
+
fi
|
|
170
|
+
if [ "$RESP_DOCO_ID" != "$DOCO_ID" ]; then
|
|
171
|
+
emit_disconnected "bootstrap did not return context for ${DOCO_ID}" default
|
|
172
|
+
exit 0
|
|
173
|
+
fi
|
|
174
|
+
|
|
175
|
+
# Include the per-Doco code_map when the host returned one. Stringified
|
|
176
|
+
# as YAML-ish nested list under a clear header so the agent recognises it.
|
|
177
|
+
CODE_MAP_BLOCK=""
|
|
178
|
+
if printf '%s' "$RESP" | jq -e '.code_map and (.code_map | type == "object")' >/dev/null 2>&1; then
|
|
179
|
+
CODE_MAP_TEXT=$(printf '%s' "$RESP" | jq -r '
|
|
180
|
+
.code_map as $cm |
|
|
181
|
+
($cm | keys[]) as $k |
|
|
182
|
+
"### " + $k + "\n" + (($cm[$k] | if type == "array" then map("- " + .) | join("\n") else "- " + tostring end))
|
|
183
|
+
' 2>/dev/null)
|
|
184
|
+
if [ -n "$CODE_MAP_TEXT" ]; then
|
|
185
|
+
CODE_MAP_BLOCK=$(printf '\n\n---\n\n## code_map — where each feature\047s code lives\n\nSaves you a filesystem grep. Read these paths first when the user\047s task hits one of the listed features.\n\n%s\n' "$CODE_MAP_TEXT")
|
|
186
|
+
fi
|
|
187
|
+
fi
|
|
188
|
+
|
|
189
|
+
# Include the Constitution scope when the host returned one. The
|
|
190
|
+
# Constitution holds Doco-wide rules that block captures at the server
|
|
191
|
+
# boundary — surfacing it here means the agent sees what'll trip a
|
|
192
|
+
# 400 *before* drafting an entity, not after.
|
|
193
|
+
CONSTITUTION_BLOCK=""
|
|
194
|
+
if printf '%s' "$RESP" | jq -e '.constitution and (.constitution | type == "object")' >/dev/null 2>&1; then
|
|
195
|
+
CONSTITUTION_TEXT=$(printf '%s' "$RESP" | jq -r '
|
|
196
|
+
.constitution as $c |
|
|
197
|
+
"**Scope:** " + ($c.icon // "⚖️") + " `" + $c.name + "` (id: `" + $c.id + "`)\n\n"
|
|
198
|
+
+ (if ($c.purpose // "") != "" then "**Purpose:** " + $c.purpose + "\n\n" else "" end)
|
|
199
|
+
+ (if ($c.guidelines // "") != "" then "**Guidelines:**\n\n" + $c.guidelines + "\n\n" else "" end)
|
|
200
|
+
+ (if (($c.rules // []) | length) > 0
|
|
201
|
+
then "**Rules (capture aborts with 400 on a deterministic violation):**\n\n"
|
|
202
|
+
+ (($c.rules | map(
|
|
203
|
+
if .kind == "requires_edge" then
|
|
204
|
+
"- `requires_edge` " + .edge_type + (if .target_node_type then " → " + .target_node_type else "" end) + (if .reason then " — " + .reason else "" end)
|
|
205
|
+
elif .kind == "forbids_edge" then
|
|
206
|
+
"- `forbids_edge` " + .edge_type + (if .target_node_type then " → " + .target_node_type else "" end) + (if .reason then " — " + .reason else "" end)
|
|
207
|
+
elif .kind == "requires_field" then
|
|
208
|
+
"- `requires_field` `" + .field + "`" + (if .reason then " — " + .reason else "" end)
|
|
209
|
+
elif .kind == "forbids_field" then
|
|
210
|
+
"- `forbids_field` `" + .field + "`" + (if .reason then " — " + .reason else "" end)
|
|
211
|
+
elif .kind == "mandatory_scope" then
|
|
212
|
+
"- `mandatory_scope` → every node must list scope `" + .scope_id + "`" + (if .reason then " — " + .reason else "" end)
|
|
213
|
+
elif .kind == "probabilistic" then
|
|
214
|
+
"- `probabilistic` (LLM-judged) — " + (.spec // "")
|
|
215
|
+
else
|
|
216
|
+
"- `" + .kind + "`"
|
|
217
|
+
end
|
|
218
|
+
)) | join("\n"))
|
|
219
|
+
else ""
|
|
220
|
+
end)
|
|
221
|
+
' 2>/dev/null)
|
|
222
|
+
if [ -n "$CONSTITUTION_TEXT" ]; then
|
|
223
|
+
CONSTITUTION_BLOCK=$(printf '\n\n---\n\n## constitution — this Doco\047s load-bearing rules\n\nThese rules are enforced server-side at capture time. Read them BEFORE drafting a Decision/Rule/Intent so you don\047t draft something that\047ll trip a 400.\n\n%s\n' "$CONSTITUTION_TEXT")
|
|
224
|
+
fi
|
|
225
|
+
fi
|
|
226
|
+
|
|
227
|
+
# Include the scopes manifest when the host returned one. Splits into
|
|
228
|
+
# mandatory (forced by the Constitution onto every node — capture aborts
|
|
229
|
+
# without them) and optional (pick by content). When the manifest is
|
|
230
|
+
# empty the block is suppressed entirely.
|
|
231
|
+
SCOPES_BLOCK=""
|
|
232
|
+
if printf '%s' "$RESP" | jq -e '.scopes and (.scopes | type == "array") and ((.scopes | length) > 0)' >/dev/null 2>&1; then
|
|
233
|
+
SCOPES_TEXT=$(printf '%s' "$RESP" | jq -r '
|
|
234
|
+
(.scopes | map(select(.is_mandatory == true))) as $mand |
|
|
235
|
+
(.scopes | map(select(.is_mandatory != true))) as $opt |
|
|
236
|
+
"### Mandatory — every node must list these (capture aborts without them)\n\n"
|
|
237
|
+
+ (if ($mand | length) > 0
|
|
238
|
+
then (($mand | map(
|
|
239
|
+
"- " + (.icon // "🏷️") + " `" + .name + "` — " + (.purpose // "(no purpose set)")
|
|
240
|
+
)) | join("\n"))
|
|
241
|
+
else "_(none in this Doco — no `mandatory_scope` rule on the Constitution)_"
|
|
242
|
+
end)
|
|
243
|
+
+ "\n\n### Optional — pick by what the node is about\n\n"
|
|
244
|
+
+ (if ($opt | length) > 0
|
|
245
|
+
then (($opt | map(
|
|
246
|
+
"- " + (.icon // "🏷️") + " `" + .name + "` — " + (.purpose // "(no purpose set)")
|
|
247
|
+
+ (if .lifecycle == "deprecated" then " _(deprecated)_" else "" end)
|
|
248
|
+
)) | join("\n"))
|
|
249
|
+
else "_(none)_"
|
|
250
|
+
end)
|
|
251
|
+
' 2>/dev/null)
|
|
252
|
+
if [ -n "$SCOPES_TEXT" ]; then
|
|
253
|
+
SCOPES_BLOCK=$(printf '\n\n---\n\n## scopes — this Doco\047s topical neighborhoods\n\nEvery captured node must list at least one scope. Mandatory scopes apply to ALL nodes; optional scopes are picked by what the node is about. Full guidelines: open the scope\047s page or fetch /status.json.\n\n%s\n' "$SCOPES_TEXT")
|
|
254
|
+
fi
|
|
255
|
+
fi
|
|
256
|
+
|
|
257
|
+
# Pre-bake the session-load indicator line so the agent emits it as
|
|
258
|
+
# the literal first output of its first reply — BEFORE any prose,
|
|
259
|
+
# narration, or tool calls. Pick a random loading-verb from the
|
|
260
|
+
# canonical 1a list so the line is ready to paste verbatim.
|
|
261
|
+
LOADING_VERBS=("Connected to" "Tuned into" "Listening to" "Wired up to" "Synced with" "Plugged into" "Online with" "Reading" "Hooked into" "Eyes on" "Riding shotgun on" "Pinned to" "Threaded into" "Locked onto" "Channel open:" "Live on" "Mind-melded with" "Pulled up" "Holding the file on")
|
|
262
|
+
LOADING_VERB="${LOADING_VERBS[$RANDOM % ${#LOADING_VERBS[@]}]}"
|
|
263
|
+
DOCO_FOR_LINE="${DOCO_ID:-this Doco}"
|
|
264
|
+
SESSION_LOAD_LINE="[🔮 Doco] ${LOADING_VERB} ${DOCO_FOR_LINE}"
|
|
265
|
+
|
|
266
|
+
# Prepend a strong "do not re-fetch" header so the agent recognises the
|
|
267
|
+
# canonical is ALREADY in their context. The previous "if you see this,
|
|
268
|
+
# the hook worked" wording was too soft — agents re-fetched anyway. This
|
|
269
|
+
# version explicitly forbids re-fetching AND pre-bakes the session-load
|
|
270
|
+
# line the agent must emit as its first output.
|
|
271
|
+
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
272
|
+
HEADER=$'🔒 Doco canonical_instructions — auto-loaded by SessionStart hook at '"${TIMESTAMP}"$'\n\n⚠️ **The literal first line of your first reply must be the session-load indicator** — emitted BEFORE any prose, narration, or tool calls. Pre-built for you here (verb already randomized — paste verbatim):\n\n '"${SESSION_LOAD_LINE}"$'\n\nNo "let me read this first" preface. No "I see this repo has Doco" prose. The line IS the acknowledgement. Then your per-reply [🔮 Doco] querying / count lines, then prose. See canonical § 1a below.\n\nThis IS the canonical. **Do NOT re-fetch via raw curl** — re-read the block below instead, or use `doco bootstrap` if this block has genuinely fallen out of context. The protocol applies to every connected reply (query indicator at top, footer_lines after writes, tally at end). For deep reference (model walkthrough, scope onboarding, placement examples), the long form is at `/api/v1/agent-reference` — fetch only on demand.\n\n---\n\n'
|
|
273
|
+
printf '%s' "$RESP" | jq -nc --arg c "${HEADER}${INSTR}${CODE_MAP_BLOCK}${CONSTITUTION_BLOCK}${SCOPES_BLOCK}" \
|
|
274
|
+
'{hookSpecificOutput: {hookEventName: "SessionStart", additionalContext: $c}}'
|