@forwardimpact/outpost 3.12.0 → 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.
Files changed (129) hide show
  1. package/README.md +2 -2
  2. package/bin/fit-outpost.js +3 -2
  3. package/package.json +6 -5
  4. package/src/agent-path.js +10 -6
  5. package/src/agent-runner.js +28 -24
  6. package/src/index.js +2 -3
  7. package/src/kb-manager.js +75 -20
  8. package/src/kb-validator.js +762 -0
  9. package/src/outpost.js +108 -39
  10. package/src/posture.js +3 -3
  11. package/src/privilege.js +11 -11
  12. package/src/scheduler.js +14 -12
  13. package/src/socket-server.js +19 -18
  14. package/src/spawn-env.js +7 -6
  15. package/src/state-manager.js +2 -2
  16. package/templates/.claude/agents/chief-of-staff.md +32 -24
  17. package/templates/.claude/agents/concierge.md +26 -17
  18. package/templates/.claude/agents/head-hunter.md +34 -26
  19. package/templates/.claude/agents/librarian.md +26 -18
  20. package/templates/.claude/agents/postman.md +26 -18
  21. package/templates/.claude/agents/recruiter.md +38 -27
  22. package/templates/.claude/skills/anarlog-follow/SKILL.md +91 -66
  23. package/templates/.claude/skills/anarlog-follow/references/coaching.md +22 -21
  24. package/templates/.claude/skills/anarlog-follow/references/sessions.md +52 -0
  25. package/templates/.claude/skills/anarlog-process/SKILL.md +108 -62
  26. package/templates/.claude/skills/anarlog-process/references/extraction.md +41 -29
  27. package/templates/.claude/skills/anarlog-process/references/sessions.md +92 -58
  28. package/templates/.claude/skills/anarlog-process/scripts/scan.mjs +402 -160
  29. package/templates/.claude/skills/candidate-report/SKILL.md +41 -36
  30. package/templates/.claude/skills/candidate-report/references/report-template.html +10 -10
  31. package/templates/.claude/skills/candidate-report/references/report.css +4 -4
  32. package/templates/.claude/skills/candidate-report/references/rubric.md +15 -14
  33. package/templates/.claude/skills/candidate-report/scripts/render-pdf.mjs +2 -2
  34. package/templates/.claude/skills/changelog/SKILL.md +122 -67
  35. package/templates/.claude/skills/deck-create/SKILL.md +90 -32
  36. package/templates/.claude/skills/deck-create/references/slide.css +4 -4
  37. package/templates/.claude/skills/deck-create/scripts/convert-to-pdf.mjs +6 -6
  38. package/templates/.claude/skills/deck-review/SKILL.md +63 -58
  39. package/templates/.claude/skills/deck-review/assets/slide-annotator.js +58 -49
  40. package/templates/.claude/skills/deck-summarize/SKILL.md +55 -45
  41. package/templates/.claude/skills/deck-summarize/references/brief-template.md +5 -4
  42. package/templates/.claude/skills/deck-summarize/scripts/extract-pptx.mjs +8 -7
  43. package/templates/.claude/skills/doc-collab/SKILL.md +39 -34
  44. package/templates/.claude/skills/doc-create/SKILL.md +27 -23
  45. package/templates/.claude/skills/doc-create/scripts/convert-to-pdf.mjs +6 -6
  46. package/templates/.claude/skills/draft-emails/SKILL.md +63 -53
  47. package/templates/.claude/skills/draft-emails/references/template.md +2 -2
  48. package/templates/.claude/skills/draft-emails/scripts/scan-emails.mjs +15 -8
  49. package/templates/.claude/skills/draft-emails/scripts/send-email.mjs +19 -11
  50. package/templates/.claude/skills/extract-entities/SKILL.md +76 -75
  51. package/templates/.claude/skills/extract-entities/references/TEMPLATES.md +6 -5
  52. package/templates/.claude/skills/extract-entities/references/conditions.md +20 -18
  53. package/templates/.claude/skills/extract-entities/references/content.md +10 -9
  54. package/templates/.claude/skills/extract-entities/references/links.md +37 -13
  55. package/templates/.claude/skills/extract-entities/references/recruitment.md +30 -24
  56. package/templates/.claude/skills/extract-entities/references/resolution.md +14 -14
  57. package/templates/.claude/skills/extract-entities/references/sources.md +9 -9
  58. package/templates/.claude/skills/extract-entities/references/templates-conditions.md +12 -6
  59. package/templates/.claude/skills/extract-entities/references/templates-people-orgs.md +29 -10
  60. package/templates/.claude/skills/extract-entities/references/templates-priorities.md +10 -4
  61. package/templates/.claude/skills/extract-entities/references/templates-projects-topics.md +19 -7
  62. package/templates/.claude/skills/extract-entities/scripts/state.mjs +9 -8
  63. package/templates/.claude/skills/meeting-prep/SKILL.md +42 -36
  64. package/templates/.claude/skills/organize-files/SKILL.md +27 -22
  65. package/templates/.claude/skills/organize-files/scripts/organize-by-type.mjs +5 -4
  66. package/templates/.claude/skills/organize-files/scripts/summarize.mjs +4 -4
  67. package/templates/.claude/skills/person-identify/SKILL.md +77 -27
  68. package/templates/.claude/skills/person-identify/scripts/identify.sh +125 -26
  69. package/templates/.claude/skills/person-lookup/SKILL.md +34 -30
  70. package/templates/.claude/skills/person-lookup/scripts/lookup.sh +46 -17
  71. package/templates/.claude/skills/req-assess/SKILL.md +44 -36
  72. package/templates/.claude/skills/req-assess/references/interview-template.md +7 -2
  73. package/templates/.claude/skills/req-assess/references/panel-template.md +7 -2
  74. package/templates/.claude/skills/req-assess/references/rubric.md +13 -13
  75. package/templates/.claude/skills/req-bundle/SKILL.md +164 -0
  76. package/templates/.claude/skills/req-bundle/references/matching.md +70 -0
  77. package/templates/.claude/skills/req-bundle/references/pdf-structure.md +74 -0
  78. package/templates/.claude/skills/req-bundle/scripts/split-bundle.mjs +377 -0
  79. package/templates/.claude/skills/req-decide/SKILL.md +52 -41
  80. package/templates/.claude/skills/req-decide/references/rubric.md +11 -10
  81. package/templates/.claude/skills/req-decide/references/template.md +2 -2
  82. package/templates/.claude/skills/req-forget/SKILL.md +58 -39
  83. package/templates/.claude/skills/req-forget/references/classify.md +15 -13
  84. package/templates/.claude/skills/req-forget/references/locations.md +27 -22
  85. package/templates/.claude/skills/req-forget/references/report-template.md +24 -16
  86. package/templates/.claude/skills/req-scan/SKILL.md +27 -25
  87. package/templates/.claude/skills/req-scan/references/fallbacks.md +9 -9
  88. package/templates/.claude/skills/req-scan/references/filters.md +13 -13
  89. package/templates/.claude/skills/req-scan/references/sources.md +7 -7
  90. package/templates/.claude/skills/req-scan/references/state.md +1 -1
  91. package/templates/.claude/skills/req-scan/references/template.md +11 -1
  92. package/templates/.claude/skills/req-scan/scripts/state.mjs +3 -2
  93. package/templates/.claude/skills/req-screen/SKILL.md +96 -50
  94. package/templates/.claude/skills/req-screen/references/rubric.md +46 -50
  95. package/templates/.claude/skills/req-screen/references/scoring.md +79 -0
  96. package/templates/.claude/skills/req-screen/references/template.md +29 -17
  97. package/templates/.claude/skills/req-track/SKILL.md +76 -76
  98. package/templates/.claude/skills/req-track/references/fields.md +31 -27
  99. package/templates/.claude/skills/req-track/references/overlays.md +42 -0
  100. package/templates/.claude/skills/req-track/references/signals.md +12 -12
  101. package/templates/.claude/skills/req-track/references/statuses.md +3 -2
  102. package/templates/.claude/skills/req-track/references/templates.md +28 -27
  103. package/templates/.claude/skills/req-workday/SKILL.md +69 -40
  104. package/templates/.claude/skills/req-workday/references/brief.md +81 -0
  105. package/templates/.claude/skills/req-workday/references/status-mapping.md +5 -5
  106. package/templates/.claude/skills/req-workday/references/templates.md +11 -78
  107. package/templates/.claude/skills/req-workday/references/xlsx-format.md +26 -7
  108. package/templates/.claude/skills/req-workday/scripts/parse-workday.mjs +113 -35
  109. package/templates/.claude/skills/send-chat/SKILL.md +58 -54
  110. package/templates/.claude/skills/sync-apple-calendar/SKILL.md +23 -19
  111. package/templates/.claude/skills/sync-apple-calendar/references/SCHEMA.md +7 -7
  112. package/templates/.claude/skills/sync-apple-calendar/scripts/query.mjs +6 -6
  113. package/templates/.claude/skills/sync-apple-calendar/scripts/sync.mjs +9 -8
  114. package/templates/.claude/skills/sync-apple-mail/SKILL.md +20 -14
  115. package/templates/.claude/skills/sync-apple-mail/references/SCHEMA.md +10 -10
  116. package/templates/.claude/skills/sync-apple-mail/scripts/parse-emlx.mjs +9 -9
  117. package/templates/.claude/skills/sync-apple-mail/scripts/sync-helpers.mjs +16 -13
  118. package/templates/.claude/skills/sync-apple-mail/scripts/sync.mjs +3 -3
  119. package/templates/.claude/skills/sync-teams/SKILL.md +46 -43
  120. package/templates/.claude/skills/sync-teams/scripts/idb-reader.mjs +22 -21
  121. package/templates/.claude/skills/sync-teams/scripts/leveldb-reader.mjs +4 -4
  122. package/templates/.claude/skills/sync-teams/scripts/sync.mjs +4 -4
  123. package/templates/.claude/skills/upstream-instructions/SKILL.md +43 -41
  124. package/templates/.claude/skills/upstream-instructions/references/examples.md +5 -5
  125. package/templates/CLAUDE.md +97 -85
  126. package/templates/MIGRATION.md +359 -0
  127. package/templates/registry.yaml +25 -0
  128. package/templates/.claude/skills/anarlog-follow/scripts/follow.mjs +0 -243
  129. package/templates/.claude/skills/anarlog-trim/SKILL.md +0 -183
@@ -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. Designed to be called 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
- // No transcript — skip
79
- }
80
- }
81
-
82
- if (!best) {
83
- console.error("No active Anarlog session found.");
84
- process.exit(1);
85
- }
86
-
87
- // Check if the transcript was modified recently (within 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
- // If word ID not found, return everything (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();