@forwardimpact/outpost 3.12.1 → 3.13.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/package.json +3 -2
- package/src/kb-manager.js +58 -5
- package/src/kb-validator.js +762 -0
- package/src/outpost.js +73 -8
- package/templates/.claude/agents/chief-of-staff.md +14 -6
- package/templates/.claude/agents/concierge.md +9 -2
- package/templates/.claude/agents/head-hunter.md +9 -2
- package/templates/.claude/agents/librarian.md +11 -4
- package/templates/.claude/agents/postman.md +9 -2
- package/templates/.claude/agents/recruiter.md +9 -2
- package/templates/.claude/skills/anarlog-follow/SKILL.md +53 -29
- package/templates/.claude/skills/anarlog-follow/references/sessions.md +52 -0
- package/templates/.claude/skills/anarlog-process/SKILL.md +88 -45
- package/templates/.claude/skills/anarlog-process/references/extraction.md +19 -11
- package/templates/.claude/skills/anarlog-process/references/sessions.md +92 -58
- package/templates/.claude/skills/anarlog-process/scripts/scan.mjs +404 -163
- package/templates/.claude/skills/candidate-report/SKILL.md +12 -9
- package/templates/.claude/skills/changelog/SKILL.md +106 -56
- package/templates/.claude/skills/deck-create/SKILL.md +60 -2
- package/templates/.claude/skills/deck-review/SKILL.md +4 -1
- package/templates/.claude/skills/deck-summarize/SKILL.md +6 -2
- package/templates/.claude/skills/deck-summarize/references/brief-template.md +1 -1
- package/templates/.claude/skills/doc-collab/SKILL.md +16 -11
- package/templates/.claude/skills/doc-create/SKILL.md +6 -3
- package/templates/.claude/skills/draft-emails/SKILL.md +31 -25
- package/templates/.claude/skills/draft-emails/references/template.md +1 -1
- package/templates/.claude/skills/draft-emails/scripts/scan-emails.mjs +14 -8
- package/templates/.claude/skills/draft-emails/scripts/send-email.mjs +9 -3
- package/templates/.claude/skills/extract-entities/SKILL.md +17 -16
- package/templates/.claude/skills/extract-entities/references/TEMPLATES.md +2 -1
- package/templates/.claude/skills/extract-entities/references/conditions.md +6 -5
- package/templates/.claude/skills/extract-entities/references/links.md +34 -10
- package/templates/.claude/skills/extract-entities/references/recruitment.md +15 -10
- package/templates/.claude/skills/extract-entities/references/resolution.md +1 -1
- package/templates/.claude/skills/extract-entities/references/sources.md +1 -1
- package/templates/.claude/skills/extract-entities/references/templates-conditions.md +10 -4
- package/templates/.claude/skills/extract-entities/references/templates-people-orgs.md +29 -10
- package/templates/.claude/skills/extract-entities/references/templates-priorities.md +8 -2
- package/templates/.claude/skills/extract-entities/references/templates-projects-topics.md +19 -7
- package/templates/.claude/skills/meeting-prep/SKILL.md +23 -18
- package/templates/.claude/skills/organize-files/SKILL.md +3 -0
- package/templates/.claude/skills/person-identify/SKILL.md +58 -10
- package/templates/.claude/skills/person-identify/scripts/identify.sh +110 -11
- package/templates/.claude/skills/person-lookup/SKILL.md +8 -3
- package/templates/.claude/skills/person-lookup/scripts/lookup.sh +27 -1
- package/templates/.claude/skills/req-assess/SKILL.md +19 -11
- package/templates/.claude/skills/req-assess/references/interview-template.md +6 -1
- package/templates/.claude/skills/req-assess/references/panel-template.md +5 -1
- package/templates/.claude/skills/req-bundle/SKILL.md +164 -0
- package/templates/.claude/skills/req-bundle/references/matching.md +70 -0
- package/templates/.claude/skills/req-bundle/references/pdf-structure.md +74 -0
- package/templates/.claude/skills/req-bundle/scripts/split-bundle.mjs +377 -0
- package/templates/.claude/skills/req-decide/SKILL.md +26 -18
- package/templates/.claude/skills/req-decide/references/template.md +2 -2
- package/templates/.claude/skills/req-forget/SKILL.md +39 -22
- package/templates/.claude/skills/req-forget/references/classify.md +12 -10
- package/templates/.claude/skills/req-forget/references/locations.md +27 -22
- package/templates/.claude/skills/req-forget/references/report-template.md +23 -15
- package/templates/.claude/skills/req-scan/SKILL.md +6 -3
- package/templates/.claude/skills/req-scan/references/template.md +11 -1
- package/templates/.claude/skills/req-screen/SKILL.md +78 -34
- package/templates/.claude/skills/req-screen/references/rubric.md +38 -42
- package/templates/.claude/skills/req-screen/references/scoring.md +79 -0
- package/templates/.claude/skills/req-screen/references/template.md +26 -14
- package/templates/.claude/skills/req-track/SKILL.md +44 -44
- package/templates/.claude/skills/req-track/references/fields.md +18 -15
- package/templates/.claude/skills/req-track/references/overlays.md +42 -0
- package/templates/.claude/skills/req-track/references/signals.md +2 -2
- package/templates/.claude/skills/req-track/references/templates.md +28 -27
- package/templates/.claude/skills/req-workday/SKILL.md +49 -20
- package/templates/.claude/skills/req-workday/references/brief.md +81 -0
- package/templates/.claude/skills/req-workday/references/templates.md +6 -73
- package/templates/.claude/skills/req-workday/references/xlsx-format.md +19 -0
- package/templates/.claude/skills/req-workday/scripts/parse-workday.mjs +96 -20
- package/templates/.claude/skills/send-chat/SKILL.md +11 -11
- package/templates/.claude/skills/sync-apple-calendar/SKILL.md +3 -0
- package/templates/.claude/skills/sync-apple-mail/SKILL.md +3 -0
- package/templates/.claude/skills/sync-teams/SKILL.md +3 -0
- package/templates/.claude/skills/upstream-instructions/SKILL.md +8 -5
- package/templates/.claude/skills/upstream-instructions/references/examples.md +3 -3
- package/templates/CLAUDE.md +96 -87
- package/templates/MIGRATION.md +359 -0
- package/templates/registry.yaml +25 -0
- package/templates/.claude/skills/anarlog-follow/scripts/follow.mjs +0 -243
- package/templates/.claude/skills/anarlog-trim/SKILL.md +0 -186
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
# Outpost Knowledge Base Migration
|
|
2
|
+
|
|
3
|
+
This playbook moves a legacy knowledge base (`Knowledge/` wrapper plus a
|
|
4
|
+
personal `Drafts/` directory) into root-level tiers. Agents execute the
|
|
5
|
+
mechanical phases. You decide at four named gates. The validator arbitrates
|
|
6
|
+
completion. Do not migrate by hand: a mature vault holds thousands of notes
|
|
7
|
+
and more links than a person can rewrite correctly.
|
|
8
|
+
|
|
9
|
+
## The safety model
|
|
10
|
+
|
|
11
|
+
Follow these rules before any phase and during every phase:
|
|
12
|
+
|
|
13
|
+
1. **Stop the agent scheduler.** Scheduled agents write into the vault on
|
|
14
|
+
short cycles. A migration that races a writer produces a hybrid layout.
|
|
15
|
+
2. **Pause the sync client.** A mass rename inside a synced folder creates
|
|
16
|
+
conflicts and deletes files on teammates' replicas.
|
|
17
|
+
3. **Work on a copy.** Copy the vault to a local directory that no sync
|
|
18
|
+
client watches. Dereference symlinks during the copy (`cp -RL`). Exclude
|
|
19
|
+
dependency directories, nested repositories, and files that hold
|
|
20
|
+
credentials.
|
|
21
|
+
4. **Put the copy under version control.** Run `git init` and commit the
|
|
22
|
+
baseline. Commit at every phase boundary with the phase name. This is
|
|
23
|
+
your rollback path inside the copy.
|
|
24
|
+
5. **Do not touch the live share until cutover.** All phases except cutover
|
|
25
|
+
run in the copy.
|
|
26
|
+
6. **Narrowing is not un-sharing.** Moving a note to a narrower tier stops
|
|
27
|
+
future disclosure. It does not retract what recipients already read, and
|
|
28
|
+
the sync platform's version history may retain the old content. Where
|
|
29
|
+
past disclosure matters, use the platform's history controls; they are
|
|
30
|
+
outside Outpost's scope.
|
|
31
|
+
|
|
32
|
+
## The phases
|
|
33
|
+
|
|
34
|
+
Run the phases in order. Each phase has an exit test. Do not start a phase
|
|
35
|
+
before the previous exit test passes.
|
|
36
|
+
|
|
37
|
+
### Phase 0 — Freeze
|
|
38
|
+
|
|
39
|
+
Stop the scheduler. Pause sync. Make the working copy. Commit the baseline.
|
|
40
|
+
|
|
41
|
+
*Exit test:* no process writes into the vault or the copy; the baseline
|
|
42
|
+
commit exists.
|
|
43
|
+
|
|
44
|
+
### Phase 1 — Inventory (Gate 1: the tier map)
|
|
45
|
+
|
|
46
|
+
An agent walks the copy and produces a census:
|
|
47
|
+
|
|
48
|
+
- every top-level directory under the legacy wrapper, including the ones
|
|
49
|
+
the template never shipped (installations grow their own entity types);
|
|
50
|
+
- link statistics per directory pair, so you see which flows a tier split
|
|
51
|
+
would cut;
|
|
52
|
+
- duplicate basenames, duplicate identity files, and dangling links;
|
|
53
|
+
- every existing frontmatter key with its value shapes, every inline
|
|
54
|
+
bold-key Info field with its occurrence count, and every hashtag-shaped
|
|
55
|
+
token (most tokens are noise: hex colors, ticket identifiers, and UUID
|
|
56
|
+
fragments);
|
|
57
|
+
- every personal root entry, so tooling knows what it must never touch;
|
|
58
|
+
- a **proposed tier map**: one tier per top-level directory, with the link
|
|
59
|
+
evidence for each proposal.
|
|
60
|
+
|
|
61
|
+
**Gate 1 — you approve the tier map.** Migration blocks until every
|
|
62
|
+
top-level knowledge directory has an assigned tier. Commit the approved map
|
|
63
|
+
as the migration manifest; every later phase consumes it. The manifest also
|
|
64
|
+
carries three stamping maps built from the census: the directory-to-type
|
|
65
|
+
map (one `type` value per top-level directory), the status-variant map
|
|
66
|
+
(every observed free-text status value, mapped onto the registry vocabulary
|
|
67
|
+
or marked for the review queue), and the drift map (every observed
|
|
68
|
+
frontmatter key spelling, every ad-hoc `type` value, and every inline
|
|
69
|
+
Info-line key, each mapped onto one registry key or value, or marked to
|
|
70
|
+
stay body prose). The maps sit at this gate for one reason: Phase 3 stamps
|
|
71
|
+
frontmatter from them, so a later approval would block the mechanical pass.
|
|
72
|
+
Recurring hard cases:
|
|
73
|
+
|
|
74
|
+
- Per-person goal notes: no tier expresses "the person plus their manager."
|
|
75
|
+
Default them narrow; share by export.
|
|
76
|
+
- Deliverables addressed to named recipients: tier 0 plus export, never a
|
|
77
|
+
shared-tier placement.
|
|
78
|
+
- Asset and reference collections: the tier follows the audience. Mark
|
|
79
|
+
third-party copies and licensed assets no-redistribute; they are
|
|
80
|
+
ineligible for the public tier whatever their audience.
|
|
81
|
+
|
|
82
|
+
*Exit test:* the manifest maps every directory to a tier and a type; the
|
|
83
|
+
status and drift maps cover every observed variant; the census is
|
|
84
|
+
committed.
|
|
85
|
+
|
|
86
|
+
### Phase 2 — Hygiene
|
|
87
|
+
|
|
88
|
+
An agent fixes what would fail validation for reasons older than the
|
|
89
|
+
migration: merge duplicate identity notes, delete ingestion debris, and
|
|
90
|
+
resolve dangling links. A dangling link that is correct by design (a
|
|
91
|
+
scheduled skill mints links ahead of the target) goes on the proposed
|
|
92
|
+
baseline list instead.
|
|
93
|
+
|
|
94
|
+
*Exit test:* the only unresolved links left are on the proposed baseline
|
|
95
|
+
list.
|
|
96
|
+
|
|
97
|
+
### Phase 3 — Mechanical move and rewrite
|
|
98
|
+
|
|
99
|
+
Agents execute the manifest:
|
|
100
|
+
|
|
101
|
+
- create the tier directories at the root;
|
|
102
|
+
- move each top-level directory into its assigned tier; move folder-atomic
|
|
103
|
+
units (a per-entity subdirectory, an asset collection, a note with its
|
|
104
|
+
dataset) as single units, never split below folder level;
|
|
105
|
+
- rewrite every wiki link to the tier-prefixed, vault-absolute form and
|
|
106
|
+
convert every bare-basename link; keep display aliases; handle
|
|
107
|
+
escaped-pipe aliases inside tables, links inside front matter,
|
|
108
|
+
markdown-style relative links, URL-encoded links, and links to binary
|
|
109
|
+
targets. The rewriter and the stamper must cover every construct the
|
|
110
|
+
validator's link and frontmatter checks flag, or convergence never ends;
|
|
111
|
+
- leave relative links inside one entity subdirectory untouched;
|
|
112
|
+
- rewrite literal path strings and embedded commands that name legacy
|
|
113
|
+
paths, not only link syntax;
|
|
114
|
+
- stamp frontmatter in the same pass, with the deterministic rules below,
|
|
115
|
+
so two different agents produce byte-identical blocks on the same note:
|
|
116
|
+
- fix one run date when Phase 3 starts and pass it to every shard;
|
|
117
|
+
every date fallback below uses that fixed date, whatever day a shard
|
|
118
|
+
runs on;
|
|
119
|
+
- stamp `type` from the approved directory-to-type map; never guess;
|
|
120
|
+
- stamp `created` with the oldest date in the note's dated activity or
|
|
121
|
+
pipeline bullets; accept both bullet grammars (a bold date with a
|
|
122
|
+
source label, and a plain date); when the note has no dated bullet,
|
|
123
|
+
use the fixed run date;
|
|
124
|
+
- stamp `updated` with the newest such date, with the same fallback;
|
|
125
|
+
- lift the inline Aliases field into the `aliases` list, add the
|
|
126
|
+
reversed name-order variant (split the basename on its first comma
|
|
127
|
+
and swap the two parts; skip the variant when the basename holds no
|
|
128
|
+
comma or when the `type` is organization), deduplicate exact matches,
|
|
129
|
+
and remove the lifted line;
|
|
130
|
+
- lift every `**Key:** value` Info line whose key the approved drift
|
|
131
|
+
map names into its property (`email`, `role`, `organization`,
|
|
132
|
+
`status` through the status-variant map, `location`, `first_seen`,
|
|
133
|
+
`last_seen`, `source`); match keys through the drift map only, never
|
|
134
|
+
by guess; remove each lifted line; leave every unmapped bold key as
|
|
135
|
+
body prose;
|
|
136
|
+
- stamp `date` and `period` on briefing files from the filename;
|
|
137
|
+
- normalize the small legacy frontmatter batch through the drift map:
|
|
138
|
+
unify spelling-variant keys to one key, collapse the source-key
|
|
139
|
+
variants to `source`, and remap ad-hoc `type` values; a key or value
|
|
140
|
+
the drift map does not cover joins the review queue;
|
|
141
|
+
- rewrite wiki links inside frontmatter values in the same pass as body
|
|
142
|
+
links, quoted and vault-absolute (the syntax matrix already names
|
|
143
|
+
front-matter links);
|
|
144
|
+
- write the canonical key order, so every stamped block is byte-stable;
|
|
145
|
+
- move the draft-status ID ledgers (`Drafts/handled`, `Drafts/ignored`) to
|
|
146
|
+
`~/.cache/fit/outpost/drafts/` and verify the entry counts match.
|
|
147
|
+
|
|
148
|
+
The stamping pass touches nearly every file. That churn is one more reason
|
|
149
|
+
every phase runs in the version-controlled copy, with the scheduler
|
|
150
|
+
stopped, and never in the live share.
|
|
151
|
+
|
|
152
|
+
*Exit test:* the legacy wrapper and the old drafts directory are empty and
|
|
153
|
+
deleted in the copy; the validator reports no legacy-layout finding; no
|
|
154
|
+
shared-tier note lacks its required keys; a note the stamper cannot
|
|
155
|
+
complete joins the review queue.
|
|
156
|
+
|
|
157
|
+
### Phase 4 — Surgical split (Gate 2: narrow routing)
|
|
158
|
+
|
|
159
|
+
This phase is the reason the migration exists. Content does not only move;
|
|
160
|
+
it splits. An agent triages every note above a size threshold and every
|
|
161
|
+
note that matches sensitivity markers (compensation figures, requisition
|
|
162
|
+
identifiers, succession language) into a review queue, then **proposes**
|
|
163
|
+
splits using the deterministic rules below. Agents propose; you approve.
|
|
164
|
+
|
|
165
|
+
**The split rules:**
|
|
166
|
+
|
|
167
|
+
1. **Facet overlay.** A sensitive facet of an entity moves to an overlay
|
|
168
|
+
note in the narrower tier. The overlay declares itself by its one-way
|
|
169
|
+
link to the canonical note. The same relative path is the default; a
|
|
170
|
+
cross-entity overlay uses an explicit link instead. Stamp the overlay
|
|
171
|
+
with `canonical`: a quoted, tier-prefixed, vault-absolute link to the
|
|
172
|
+
canonical note. Add one audience-labeled alias. A cross-entity overlay
|
|
173
|
+
keeps its own `type`.
|
|
174
|
+
2. **Timeline split.** When sensitivity interleaves per dated entry inside
|
|
175
|
+
one activity log, the canonical note keeps the wide-audience entries and
|
|
176
|
+
the overlay holds the narrower entries under the same date keys. A
|
|
177
|
+
narrow-access reader merges the two logs chronologically. Stamp the
|
|
178
|
+
overlay as in rule 1.
|
|
179
|
+
3. **Link inversion.** A wider note's link into a narrower tier moves, with
|
|
180
|
+
its one-line context, into the narrower note. Leave no tombstone and no
|
|
181
|
+
forwarding prose in the wider note.
|
|
182
|
+
4. **Inverse stub.** When a note is narrow in its entirety but widely
|
|
183
|
+
linked, create a wider-tier stub that carries only shareable identity
|
|
184
|
+
facts. The narrow note links down to the stub. The narrow note is the
|
|
185
|
+
overlay: stamp it as in rule 1, with `canonical` pointing at the stub.
|
|
186
|
+
5. **Hire conversion.** When a recruitment subject joins the team, create
|
|
187
|
+
the team-tier note fresh. The recruitment record stays in its tier and
|
|
188
|
+
links up. Nothing links back down. Stamp the candidate note with the
|
|
189
|
+
`person` property: a quoted link up to the fresh team-tier note. The
|
|
190
|
+
person note carries no link back.
|
|
191
|
+
6. **Detach before promoting.** A note headed for the public tier first
|
|
192
|
+
loses or inlines its internal links, and passes the rights check.
|
|
193
|
+
|
|
194
|
+
**Gate 2 — you approve every routing into tier 1 or tier 0.** No agent
|
|
195
|
+
decides alone that content is management-only or owner-only. The gate does
|
|
196
|
+
not grow with the frontmatter work: audience decisions stay its only
|
|
197
|
+
subject, and the overlay stamps are mechanical.
|
|
198
|
+
|
|
199
|
+
*Exit test:* the review queue is empty; every approved split is applied and
|
|
200
|
+
committed.
|
|
201
|
+
|
|
202
|
+
### Phase 5 — Convergence (Gate 3: the baseline)
|
|
203
|
+
|
|
204
|
+
Run the validator in machine-readable mode in a loop. Partition each
|
|
205
|
+
finding: **mechanical** (the rewriter fixes it), **judgment** (append to
|
|
206
|
+
the review queue and return to phase 4), or **grandfathered** (append to
|
|
207
|
+
the baseline with a one-line reason). Frontmatter findings
|
|
208
|
+
(`frontmatter-missing`, `frontmatter-invalid`, `frontmatter-vocabulary`,
|
|
209
|
+
`overlay-undeclared`) partition the same way and enter the same baseline.
|
|
210
|
+
A metadata gap the stamper can fill is mechanical; it is never baselined.
|
|
211
|
+
A gap that needs a human fact goes to the review queue or onto the
|
|
212
|
+
baseline with a one-line reason. Examples: an unmappable status value, an
|
|
213
|
+
ambiguous alias, and a legitimate same-name duplicate flagged as
|
|
214
|
+
`overlay-undeclared`. Baseline keys are kind, file, and property, plus
|
|
215
|
+
value for vocabulary findings, never line. Later edits therefore do not
|
|
216
|
+
resurface grandfathered entries. Iterate until findings minus baseline
|
|
217
|
+
is empty.
|
|
218
|
+
|
|
219
|
+
**Gate 3 — you approve the baseline.** You approve the frontmatter entries
|
|
220
|
+
together with the link entries. Commit it as `validation-baseline.json` at
|
|
221
|
+
the vault root, so
|
|
222
|
+
every post-migration regression is a new finding, not noise.
|
|
223
|
+
|
|
224
|
+
A green validator proves structure, not content. Before the first share,
|
|
225
|
+
read through every note above the size threshold once: sensitive facts also
|
|
226
|
+
occur as plain prose that no link check can see.
|
|
227
|
+
|
|
228
|
+
*Exit test:* validation passes with the approved baseline; the content
|
|
229
|
+
audit is done.
|
|
230
|
+
|
|
231
|
+
### Phase 6 — Repoint
|
|
232
|
+
|
|
233
|
+
Rewrite every surface that writes into the vault, or the legacy layout
|
|
234
|
+
regrows on the next scheduled run:
|
|
235
|
+
|
|
236
|
+
- run `npx fit-outpost update` to install the tier-aware instructions;
|
|
237
|
+
- carry your local edits over: agent profiles, skill configuration,
|
|
238
|
+
embedded search commands inside notes, ignore rules (match case
|
|
239
|
+
exactly), and editor configuration;
|
|
240
|
+
- retarget every external generator and ingestion pipeline at the new tier
|
|
241
|
+
paths;
|
|
242
|
+
- verify every generator and skill emits conforming frontmatter: the
|
|
243
|
+
canonical key order, registry values, and quoted links;
|
|
244
|
+
- start one fresh changelog per shared tier; move the legacy changelog,
|
|
245
|
+
whole, into the narrowest tier its entries span, fix its inbound links,
|
|
246
|
+
and never append to it again. The root instruction changelog is a
|
|
247
|
+
different artifact and stays where it is.
|
|
248
|
+
|
|
249
|
+
*Exit test:* a dry-run agent session writes only tier-prefixed paths with
|
|
250
|
+
conforming frontmatter.
|
|
251
|
+
|
|
252
|
+
### Phase 7 — Cutover (Gate 4: the shares)
|
|
253
|
+
|
|
254
|
+
Sharing is per tier and cumulative. Folder permissions cannot express
|
|
255
|
+
"tier N and every wider tier" by inheritance, so use groups:
|
|
256
|
+
|
|
257
|
+
1. Per shared tier, create the cloud folder or Git remote. For Git, use one
|
|
258
|
+
repository per shared tier, never one repository that spans tiers:
|
|
259
|
+
history would carry narrow content into wide clones.
|
|
260
|
+
2. Per shared tier, create one group. A tier-N member joins the groups for
|
|
261
|
+
tier N through the widest tier. Revocation removes the member from all
|
|
262
|
+
of them.
|
|
263
|
+
3. Let each folder sync locally, then symlink it into the vault root under
|
|
264
|
+
its rank-prefixed name. The sync target's own name does not matter.
|
|
265
|
+
4. Copy the migrated content in, widest tier first. Recipients place each
|
|
266
|
+
received tier folder as a sibling under one local root, restore the
|
|
267
|
+
rank-prefixed name if their platform changed it, and run
|
|
268
|
+
`npx fit-outpost validate` on that root.
|
|
269
|
+
5. Flip the legacy share to read-only. Retire it after a stated retention
|
|
270
|
+
window, once teammates confirm the new tiers sync.
|
|
271
|
+
6. Restart the agent scheduler.
|
|
272
|
+
|
|
273
|
+
**Gate 4 — you grant the ACLs.** The validator cannot check who a platform
|
|
274
|
+
grants access to. Put a recurring audience audit on your calendar.
|
|
275
|
+
|
|
276
|
+
*Exit test:* validation passes on the live vault; a recipient's suffix
|
|
277
|
+
validates; the scheduler runs against tier paths only.
|
|
278
|
+
|
|
279
|
+
## Appendix A — the migration workflow prompt
|
|
280
|
+
|
|
281
|
+
Paste the prompt below into a Claude Code session at the root of the
|
|
282
|
+
**working copy** (never the live share). Fill the two placeholders. The
|
|
283
|
+
`ultracode` keyword opts the session into multi-agent orchestration; the
|
|
284
|
+
migration then runs as staged workflows with the gates above as stopping
|
|
285
|
+
points.
|
|
286
|
+
|
|
287
|
+
```text
|
|
288
|
+
ultracode
|
|
289
|
+
|
|
290
|
+
Migrate this Outpost knowledge base from the legacy layout (Knowledge/
|
|
291
|
+
wrapper plus Drafts/) to root-level tiers, following MIGRATION.md in this
|
|
292
|
+
directory. The tier manifest is at <path-to-approved-manifest>. My review
|
|
293
|
+
decisions go in <path-to-review-queue-file>. The property registry is
|
|
294
|
+
`registry.yaml` at the vault root.
|
|
295
|
+
|
|
296
|
+
Rules that bind every agent you spawn:
|
|
297
|
+
- Work only inside this directory. Never follow symlinks out of it. Never
|
|
298
|
+
read or write personal root entries other than the files MIGRATION.md
|
|
299
|
+
names (instruction files, ignore rules, editor configuration).
|
|
300
|
+
- Never decide an audience. Anything that routes into tier 1 or tier 0,
|
|
301
|
+
and every proposed split, goes into the review queue and stops there
|
|
302
|
+
until I mark it approved.
|
|
303
|
+
- Commit at every phase boundary with the phase name. Never use
|
|
304
|
+
destructive git commands.
|
|
305
|
+
- Apply the split rules from MIGRATION.md § Phase 4 exactly. Do not invent
|
|
306
|
+
alternative split shapes. Move the link plus its one-line context on
|
|
307
|
+
inversion; leave no tombstone.
|
|
308
|
+
- The link rewriter and the validator must agree: cover aliased links,
|
|
309
|
+
escaped pipes inside tables, front-matter links, markdown relative
|
|
310
|
+
links, URL-encoded links, binary targets, and literal path strings. The
|
|
311
|
+
stamper and the frontmatter checks must agree the same way, or
|
|
312
|
+
convergence never ends.
|
|
313
|
+
- Stamp frontmatter only from the approved manifest maps and
|
|
314
|
+
`registry.yaml`. Apply the stamping rules from MIGRATION.md § Phase 3
|
|
315
|
+
exactly. Never invent a key, a type value, a status value, or
|
|
316
|
+
a tag. Lifted Info lines leave the body.
|
|
317
|
+
- Folder-atomic units move whole. Never split below folder level.
|
|
318
|
+
|
|
319
|
+
Run these workflows in order, and stop for my gate review between them:
|
|
320
|
+
1. Inventory: fan out readers per top-level directory; produce the census
|
|
321
|
+
and the proposed tier map with link evidence; include the metadata
|
|
322
|
+
census and propose the directory-to-type map and the status-variant
|
|
323
|
+
map. Stop for Gate 1.
|
|
324
|
+
2. Hygiene: merge duplicate identities, fix dangling links, list
|
|
325
|
+
by-design dangles for the baseline.
|
|
326
|
+
3. Move and rewrite: execute the approved manifest; shard the link
|
|
327
|
+
rewrite by directory across parallel agents; run the stamping pass in
|
|
328
|
+
the same shards; verify with a full-vault link resolution pass.
|
|
329
|
+
4. Split: triage notes above the size threshold and notes matching
|
|
330
|
+
sensitivity markers; per flagged note, one splitter agent proposes the
|
|
331
|
+
split and an independent reviewer agent verifies it against the split
|
|
332
|
+
rules; write proposals to the review queue. Stop for Gate 2, then
|
|
333
|
+
apply only approved proposals.
|
|
334
|
+
5. Convergence: loop `npx fit-outpost validate` in JSON mode; auto-fix
|
|
335
|
+
mechanical findings; queue judgment findings; propose baseline
|
|
336
|
+
entries. Stop for Gate 3.
|
|
337
|
+
6. Repoint: rewrite instruction surfaces, embedded commands, ignore
|
|
338
|
+
rules, and generator configuration; prove with a dry-run session that
|
|
339
|
+
writes only tier-prefixed paths with conforming frontmatter.
|
|
340
|
+
|
|
341
|
+
Then hand back a migration report: what moved where, every split applied,
|
|
342
|
+
the final baseline, and the cutover checklist from MIGRATION.md § Phase 7
|
|
343
|
+
for me to execute against the sync platform.
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
## Appendix B — what the validator will and will not tell you
|
|
347
|
+
|
|
348
|
+
- It proves ranks, link direction, link resolution, and link format. It
|
|
349
|
+
follows symlinked tiers.
|
|
350
|
+
- On frontmatter, it proves the required keys, the closed vocabularies,
|
|
351
|
+
the tag grammar and tier bounds, and property-link legality. It does not
|
|
352
|
+
prove the freshness of `updated`, the completeness of `aliases`, the
|
|
353
|
+
truth of a `status`, or the retrieval quality of a tag. Instructions and
|
|
354
|
+
the content audit own those.
|
|
355
|
+
- It does not prove prose is audience-appropriate, and it cannot see the
|
|
356
|
+
sync platform's permissions. The content audit (phase 5) and the
|
|
357
|
+
audience audit (gate 4) own those.
|
|
358
|
+
- After migration, a legacy-layout finding means a writer you did not
|
|
359
|
+
repoint still produces old paths. Fix the writer, not the finding.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Metadata vocabularies for this knowledge base. Humans edit this file;
|
|
2
|
+
# agents only select from it. See CLAUDE.md § Note Metadata for the rules.
|
|
3
|
+
types: # directory name -> type value
|
|
4
|
+
People: person
|
|
5
|
+
Organizations: organization
|
|
6
|
+
Projects: project
|
|
7
|
+
Topics: topic
|
|
8
|
+
Teams: team
|
|
9
|
+
Candidates: candidate
|
|
10
|
+
Prospects: prospect
|
|
11
|
+
Roles: role
|
|
12
|
+
Priorities: priority
|
|
13
|
+
Conditions: condition
|
|
14
|
+
Tasks: task
|
|
15
|
+
Erasure: erasure
|
|
16
|
+
reserved: # file basenames with a fixed type, in any tier
|
|
17
|
+
CHANGELOG.md: changelog
|
|
18
|
+
status: # per-type closed vocabularies
|
|
19
|
+
candidate: [new, screening, interviewing, offer, hired, rejected, withdrawn]
|
|
20
|
+
prospect: [identified, contacted, responded, converted, closed]
|
|
21
|
+
tags: # closed topic/ taxonomy; bound = the widest tier a tag may reach
|
|
22
|
+
- { tag: topic/hiring, bound: 2, intent: recruitment pipeline retrieval }
|
|
23
|
+
- { tag: topic/planning, bound: 3, intent: project and priority retrieval }
|
|
24
|
+
rights:
|
|
25
|
+
- no-redistribute
|
|
@@ -1,243 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* follow.mjs — Read a live Anarlog transcript and output new content since
|
|
5
|
-
* the last read. Call it repeatedly during a live session.
|
|
6
|
-
*
|
|
7
|
-
* Usage:
|
|
8
|
-
* node follow.mjs <session-id> # First read — outputs everything
|
|
9
|
-
* node follow.mjs <session-id> --after <word-id> # Outputs words after the given word ID
|
|
10
|
-
* node follow.mjs <session-id> --after <word-id> --summary # Condensed summary mode
|
|
11
|
-
* node follow.mjs <session-id> --meta # Output session metadata only
|
|
12
|
-
* node follow.mjs --detect # Detect the most recently active session
|
|
13
|
-
*
|
|
14
|
-
* Output (JSON):
|
|
15
|
-
* {
|
|
16
|
-
* "session_id": "...",
|
|
17
|
-
* "title": "...",
|
|
18
|
-
* "total_words": 1234,
|
|
19
|
-
* "new_words": 56,
|
|
20
|
-
* "last_word_id": "...",
|
|
21
|
-
* "duration_ms": 123456,
|
|
22
|
-
* "channels": {
|
|
23
|
-
* "0": { "label": "user", "word_count": 600 },
|
|
24
|
-
* "1": { "label": "guest", "word_count": 634 }
|
|
25
|
-
* },
|
|
26
|
-
* "text": [
|
|
27
|
-
* { "channel": 0, "start_ms": 1000, "end_ms": 5000, "text": "Hello, how are you?" },
|
|
28
|
-
* ...
|
|
29
|
-
* ]
|
|
30
|
-
* }
|
|
31
|
-
*/
|
|
32
|
-
|
|
33
|
-
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
34
|
-
import { join } from "node:path";
|
|
35
|
-
import { homedir } from "node:os";
|
|
36
|
-
|
|
37
|
-
const SESSIONS_DIR = join(
|
|
38
|
-
homedir(),
|
|
39
|
-
"Library/Application Support/anarlog/sessions",
|
|
40
|
-
);
|
|
41
|
-
|
|
42
|
-
function parseArgs() {
|
|
43
|
-
const args = process.argv.slice(2);
|
|
44
|
-
const flags = {};
|
|
45
|
-
let positional = null;
|
|
46
|
-
|
|
47
|
-
for (let i = 0; i < args.length; i++) {
|
|
48
|
-
if (args[i] === "--after" && i + 1 < args.length) {
|
|
49
|
-
flags.after = args[++i];
|
|
50
|
-
} else if (args[i] === "--summary") {
|
|
51
|
-
flags.summary = true;
|
|
52
|
-
} else if (args[i] === "--meta") {
|
|
53
|
-
flags.meta = true;
|
|
54
|
-
} else if (args[i] === "--detect") {
|
|
55
|
-
flags.detect = true;
|
|
56
|
-
} else if (!args[i].startsWith("--")) {
|
|
57
|
-
positional = args[i];
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return { sessionId: positional, ...flags };
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function detectActiveSession() {
|
|
65
|
-
const entries = readdirSync(SESSIONS_DIR);
|
|
66
|
-
let best = null;
|
|
67
|
-
let bestMtime = 0;
|
|
68
|
-
|
|
69
|
-
for (const entry of entries) {
|
|
70
|
-
const transcriptPath = join(SESSIONS_DIR, entry, "transcript.json");
|
|
71
|
-
try {
|
|
72
|
-
const stat = statSync(transcriptPath);
|
|
73
|
-
if (stat.mtimeMs > bestMtime) {
|
|
74
|
-
bestMtime = stat.mtimeMs;
|
|
75
|
-
best = entry;
|
|
76
|
-
}
|
|
77
|
-
} catch {
|
|
78
|
-
// Skip when no transcript exists
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (!best) {
|
|
83
|
-
console.error("No active Anarlog session found.");
|
|
84
|
-
process.exit(1);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Check whether the transcript changed in the last 5 minutes
|
|
88
|
-
const ageMs = Date.now() - bestMtime;
|
|
89
|
-
const isLive = ageMs < 5 * 60 * 1000;
|
|
90
|
-
|
|
91
|
-
const metaPath = join(SESSIONS_DIR, best, "_meta.json");
|
|
92
|
-
let title = "Unknown";
|
|
93
|
-
try {
|
|
94
|
-
const meta = JSON.parse(readFileSync(metaPath, "utf-8"));
|
|
95
|
-
title = meta.title || "Untitled";
|
|
96
|
-
} catch {
|
|
97
|
-
// No meta
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
return {
|
|
101
|
-
session_id: best,
|
|
102
|
-
title,
|
|
103
|
-
is_live: isLive,
|
|
104
|
-
last_modified: new Date(bestMtime).toISOString(),
|
|
105
|
-
age_seconds: Math.round(ageMs / 1000),
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function readMeta(sessionId) {
|
|
110
|
-
const metaPath = join(SESSIONS_DIR, sessionId, "_meta.json");
|
|
111
|
-
try {
|
|
112
|
-
return JSON.parse(readFileSync(metaPath, "utf-8"));
|
|
113
|
-
} catch {
|
|
114
|
-
return { title: "Unknown", created_at: null, participants: [] };
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function readTranscript(sessionId) {
|
|
119
|
-
const path = join(SESSIONS_DIR, sessionId, "transcript.json");
|
|
120
|
-
try {
|
|
121
|
-
const data = JSON.parse(readFileSync(path, "utf-8"));
|
|
122
|
-
return data.transcripts?.[0]?.words || [];
|
|
123
|
-
} catch {
|
|
124
|
-
return [];
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function groupIntoSegments(words) {
|
|
129
|
-
if (words.length === 0) return [];
|
|
130
|
-
|
|
131
|
-
const segments = [];
|
|
132
|
-
let current = {
|
|
133
|
-
channel: words[0].channel,
|
|
134
|
-
start_ms: words[0].start_ms,
|
|
135
|
-
end_ms: words[0].end_ms,
|
|
136
|
-
texts: [words[0].text],
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
for (let i = 1; i < words.length; i++) {
|
|
140
|
-
const w = words[i];
|
|
141
|
-
const gap = w.start_ms - current.end_ms;
|
|
142
|
-
|
|
143
|
-
// New segment on channel change or >3s gap
|
|
144
|
-
if (w.channel !== current.channel || gap > 3000) {
|
|
145
|
-
segments.push({
|
|
146
|
-
channel: current.channel,
|
|
147
|
-
start_ms: current.start_ms,
|
|
148
|
-
end_ms: current.end_ms,
|
|
149
|
-
text: current.texts.join("").trim(),
|
|
150
|
-
});
|
|
151
|
-
current = {
|
|
152
|
-
channel: w.channel,
|
|
153
|
-
start_ms: w.start_ms,
|
|
154
|
-
end_ms: w.end_ms,
|
|
155
|
-
texts: [w.text],
|
|
156
|
-
};
|
|
157
|
-
} else {
|
|
158
|
-
current.end_ms = w.end_ms;
|
|
159
|
-
current.texts.push(w.text);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// Push final segment
|
|
164
|
-
segments.push({
|
|
165
|
-
channel: current.channel,
|
|
166
|
-
start_ms: current.start_ms,
|
|
167
|
-
end_ms: current.end_ms,
|
|
168
|
-
text: current.texts.join("").trim(),
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
return segments.filter((s) => s.text.length > 0);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function buildChannelStats(allWords) {
|
|
175
|
-
const channels = {};
|
|
176
|
-
for (const w of allWords) {
|
|
177
|
-
if (!channels[w.channel]) {
|
|
178
|
-
channels[w.channel] = {
|
|
179
|
-
label: w.channel === 0 ? "user" : `guest-${w.channel}`,
|
|
180
|
-
word_count: 0,
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
channels[w.channel].word_count++;
|
|
184
|
-
}
|
|
185
|
-
return channels;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function filterNewWords(allWords, afterId) {
|
|
189
|
-
if (!afterId) return allWords;
|
|
190
|
-
const idx = allWords.findIndex((w) => w.id === afterId);
|
|
191
|
-
// Return everything when the word ID is missing (safety fallback)
|
|
192
|
-
return idx >= 0 ? allWords.slice(idx + 1) : allWords;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function main() {
|
|
196
|
-
const opts = parseArgs();
|
|
197
|
-
|
|
198
|
-
// Detect mode
|
|
199
|
-
if (opts.detect) {
|
|
200
|
-
console.log(JSON.stringify(detectActiveSession(), null, 2));
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
if (!opts.sessionId) {
|
|
205
|
-
console.error(
|
|
206
|
-
"Usage: follow.mjs <session-id> [--after <word-id>] [--meta]",
|
|
207
|
-
);
|
|
208
|
-
process.exit(1);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
const meta = readMeta(opts.sessionId);
|
|
212
|
-
|
|
213
|
-
// Meta-only mode
|
|
214
|
-
if (opts.meta) {
|
|
215
|
-
console.log(JSON.stringify(meta, null, 2));
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// Read transcript
|
|
220
|
-
const allWords = readTranscript(opts.sessionId);
|
|
221
|
-
const words = filterNewWords(allWords, opts.after);
|
|
222
|
-
const lastWord = allWords[allWords.length - 1];
|
|
223
|
-
|
|
224
|
-
console.log(
|
|
225
|
-
JSON.stringify(
|
|
226
|
-
{
|
|
227
|
-
session_id: opts.sessionId,
|
|
228
|
-
title: meta.title,
|
|
229
|
-
created_at: meta.created_at,
|
|
230
|
-
total_words: allWords.length,
|
|
231
|
-
new_words: words.length,
|
|
232
|
-
last_word_id: lastWord?.id || null,
|
|
233
|
-
duration_ms: lastWord?.end_ms || 0,
|
|
234
|
-
channels: buildChannelStats(allWords),
|
|
235
|
-
text: groupIntoSegments(words),
|
|
236
|
-
},
|
|
237
|
-
null,
|
|
238
|
-
2,
|
|
239
|
-
),
|
|
240
|
-
);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
main();
|