@forwardimpact/outpost 3.10.0 → 3.11.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forwardimpact/outpost",
3
- "version": "3.10.0",
3
+ "version": "3.11.0",
4
4
  "description": "Personal operations center — context from email, calendar, and knowledge assembled so preparation is continuous, not a morning scramble.",
5
5
  "homepage": "https://www.forwardimpact.team",
6
6
  "repository": {
@@ -101,8 +101,8 @@ interactions.
101
101
  #### 5. Load type-specific context
102
102
 
103
103
  **Interviews:** read `Knowledge/Candidates/{Name}/{brief,screening,panel}.md`,
104
- look up the `Req` field's matching `Knowledge/Roles/*.md`, and load standard
105
- expectations:
104
+ look up the `Req` field's matching `Knowledge/Roles/*.md` file (check the
105
+ `**Status:**` field for context), and load standard expectations:
106
106
 
107
107
  ```bash
108
108
  bunx fit-pathway job {discipline} {level} --track={track}
@@ -104,7 +104,13 @@ gracefully — but the correct fix is to make the deck arrow-keys-only per
104
104
  **source line / column / context** for each highlight. (If the browser blocks
105
105
  folder access on `file://`, **Save** downloads the JSON instead — move it
106
106
  next to the deck.)
107
- 4. Navigation while reviewing is the deck's normal **← / →** (the overlay's own
107
+ 4. The connected folder is **remembered across reloads** (the directory handle
108
+ is stored in IndexedDB, keyed per deck). After a reload the tool reconnects
109
+ silently if the browser still grants access; otherwise the button reads
110
+ **Reconnect folder** and a single click re-grants permission without
111
+ re-picking the folder. (Browsers require a user gesture to re-grant, so the
112
+ one click can't be avoided; clearing site data forgets the folder.)
113
+ 5. Navigation while reviewing is the deck's normal **← / →** (the overlay's own
108
114
  keystrokes never leak to the deck).
109
115
 
110
116
  ## Acting on the feedback (the review loop)
@@ -13,6 +13,11 @@
13
13
  * (b) resolve each highlight to its SOURCE line/column + context lines by
14
14
  * reading the page’s own source file. No build step, no server, no deps.
15
15
  *
16
+ * The connected folder is remembered across reloads (the directory handle is
17
+ * stored in IndexedDB). On reload it reconnects silently if the browser still
18
+ * grants permission; otherwise the button reads “Reconnect folder” and one
19
+ * click re-grants access without re-picking the folder.
20
+ *
16
21
  * Optional host hook: if the page defines `window.deckGoto(index)` the panel’s
17
22
  * “Go” buttons will navigate to the right slide. Without it, the tool still
18
23
  * works (it falls back to scrollIntoView).
@@ -60,7 +65,8 @@
60
65
  /* ----------------------------- state ------------------------------------ */
61
66
  var state = loadLocal(); // { version, tool, target, annotations:[] }
62
67
  var review = false;
63
- var dirHandle = null; // File System Access directory handle
68
+ var dirHandle = null; // File System Access directory handle (active)
69
+ var rememberedHandle = null; // handle from a prior session, awaiting re-grant
64
70
  var sourceText = null; // page source (for line/column)
65
71
  var pending = null; // annotation awaiting note + confirm
66
72
  var ui = {}; // DOM refs
@@ -150,6 +156,7 @@
150
156
  ui.count = ui.panel.querySelector(".sa-count");
151
157
  ui.status = ui.panel.querySelector(".sa-status");
152
158
  ui.list = ui.panel.querySelector(".sa-list");
159
+ ui.connectBtn = ui.panel.querySelector('[data-act="connect"]');
153
160
  ui.note = ui.pop.querySelector("textarea");
154
161
  var onlyThisSlide = false;
155
162
 
@@ -435,28 +442,105 @@
435
442
  if (ui.count) ui.count.textContent = state.annotations.length;
436
443
  }
437
444
 
445
+ /* Persist the connected directory handle across reloads. FileSystemHandles are
446
+ * structured-cloneable, so they live in IndexedDB (localStorage is strings
447
+ * only). Re-granting read/write permission after a reload needs a user
448
+ * gesture, so a remembered folder reconnects with one click via the same
449
+ * "Connect folder" button rather than the full directory picker. Keyed by
450
+ * cfg.target so distinct decks in a folder don't clobber each other. */
451
+ var IDB_NAME = "slide-annotator", IDB_STORE = "handles";
452
+ function idbOpen() {
453
+ return new Promise(function (resolve, reject) {
454
+ if (!window.indexedDB) { reject(new Error("no-idb")); return; }
455
+ var req = indexedDB.open(IDB_NAME, 1);
456
+ req.onupgradeneeded = function () { req.result.createObjectStore(IDB_STORE); };
457
+ req.onsuccess = function () { resolve(req.result); };
458
+ req.onerror = function () { reject(req.error); };
459
+ });
460
+ }
461
+ function idbGet(key) {
462
+ return idbOpen().then(function (db) {
463
+ return new Promise(function (resolve, reject) {
464
+ var r = db.transaction(IDB_STORE, "readonly").objectStore(IDB_STORE).get(key);
465
+ r.onsuccess = function () { resolve(r.result || null); };
466
+ r.onerror = function () { reject(r.error); };
467
+ });
468
+ }).catch(function () { return null; });
469
+ }
470
+ function idbSet(key, val) {
471
+ return idbOpen().then(function (db) {
472
+ return new Promise(function (resolve, reject) {
473
+ var tx = db.transaction(IDB_STORE, "readwrite");
474
+ tx.objectStore(IDB_STORE).put(val, key);
475
+ tx.oncomplete = function () { resolve(true); };
476
+ tx.onerror = function () { reject(tx.error); };
477
+ });
478
+ }).catch(function () { return false; });
479
+ }
480
+
481
+ function updateConnectLabel() {
482
+ if (!ui.connectBtn) return;
483
+ ui.connectBtn.textContent = dirHandle ? "Folder connected"
484
+ : rememberedHandle ? "Reconnect folder" : "Connect folder";
485
+ }
486
+
487
+ // Adopt a (freshly picked or re-granted) handle: link source, load any
488
+ // sidecar, and remember it for next time. Shared by connect and restore.
489
+ async function adoptFolder(handle) {
490
+ dirHandle = handle;
491
+ // read page source → enables source line/column resolution
492
+ try {
493
+ var fh = await dirHandle.getFileHandle(cfg.target);
494
+ sourceText = await (await fh.getFile()).text();
495
+ state.annotations.forEach(function (a) { a.source = locateInSource(a); });
496
+ } catch (e) { sourceText = null; setStatus("Connected, but '" + cfg.target + "' not found in folder — line numbers unavailable."); }
497
+ // load an existing sidecar if present
498
+ try {
499
+ var sh = await dirHandle.getFileHandle(SIDECAR);
500
+ mergeLoaded(JSON.parse(await (await sh.getFile()).text()));
501
+ } catch (e) { /* none yet */ }
502
+ await idbSet(cfg.target, dirHandle);
503
+ rememberedHandle = null;
504
+ updateConnectLabel();
505
+ persistLocal(); renderPanel(); renderHighlights();
506
+ setStatus("Connected. Saving to " + SIDECAR + (sourceText ? " · source linked" : ""));
507
+ }
508
+
438
509
  async function connectFolder() {
510
+ // Prefer a folder remembered from a prior session: re-granting permission is
511
+ // a single click (this click IS the required user gesture), no picker.
512
+ if (rememberedHandle) {
513
+ try {
514
+ var perm = await rememberedHandle.requestPermission({ mode: "readwrite" });
515
+ if (perm === "granted") { await adoptFolder(rememberedHandle); return; }
516
+ } catch (e) { /* fall through to the picker */ }
517
+ rememberedHandle = null; updateConnectLabel();
518
+ }
439
519
  if (!window.showDirectoryPicker) { setStatus("Folder access unsupported in this browser. Use Save (downloads the JSON)."); return; }
440
520
  try {
441
- dirHandle = await window.showDirectoryPicker({ mode: "readwrite" });
442
- // read page source → enables source line/column resolution
443
- try {
444
- var fh = await dirHandle.getFileHandle(cfg.target);
445
- sourceText = await (await fh.getFile()).text();
446
- state.annotations.forEach(function (a) { a.source = locateInSource(a); });
447
- } catch (e) { sourceText = null; setStatus("Connected, but '" + cfg.target + "' not found in folder — line numbers unavailable."); }
448
- // load an existing sidecar if present
449
- try {
450
- var sh = await dirHandle.getFileHandle(SIDECAR);
451
- mergeLoaded(JSON.parse(await (await sh.getFile()).text()));
452
- } catch (e) { /* none yet */ }
453
- persistLocal(); renderPanel(); renderHighlights();
454
- setStatus("Connected. Saving to " + SIDECAR + (sourceText ? " · source linked" : ""));
521
+ await adoptFolder(await window.showDirectoryPicker({ mode: "readwrite" }));
455
522
  } catch (e) {
456
523
  if (e && e.name !== "AbortError") setStatus("Folder access blocked (" + e.name + "). Use Save to download the JSON instead.");
457
524
  }
458
525
  }
459
526
 
527
+ // On load, try to restore a previously connected folder. queryPermission needs
528
+ // no gesture; if it's still "granted" we reconnect silently. If it's "prompt",
529
+ // we keep the handle so the next "Connect folder" click reconnects with one
530
+ // grant dialog (no picker). Handles cleared/denied storage are ignored.
531
+ function restoreFolder() {
532
+ idbGet(cfg.target).then(function (handle) {
533
+ if (!handle || typeof handle.queryPermission !== "function") return;
534
+ handle.queryPermission({ mode: "readwrite" }).then(function (perm) {
535
+ if (perm === "granted") { adoptFolder(handle); }
536
+ else {
537
+ rememberedHandle = handle; updateConnectLabel();
538
+ if (review) setStatus("Folder '" + cfg.target + "' remembered — click Reconnect folder.");
539
+ }
540
+ }).catch(function () {});
541
+ });
542
+ }
543
+
460
544
  async function saveDisk() {
461
545
  if (sourceText) state.annotations.forEach(function (a) { if (!a.source) a.source = locateInSource(a); });
462
546
  var json = JSON.stringify(serialize(), null, 2);
@@ -591,4 +675,5 @@
591
675
  };
592
676
 
593
677
  updateCount();
678
+ restoreFolder();
594
679
  })();
@@ -41,8 +41,9 @@ under `Knowledge/`. The core knowledge-graph builder.
41
41
  auto-created.
42
42
  - `Knowledge/Conditions/` — created when cross-cutting patterns are detected, or
43
43
  updated.
44
- - `Knowledge/Roles/`, `Knowledge/Candidates/*/brief.md` — enriched with inferred
45
- metadata.
44
+ - `Knowledge/Roles/*.md`, `Knowledge/Candidates/*/brief.md` — enriched with
45
+ inferred metadata. New role stubs created with `**Status:** open`; update to
46
+ `**Status:** closed` when a role closes.
46
47
  - `~/.cache/fit/outpost/state/graph_processed` — updated.
47
48
 
48
49
  <do_confirm_checklist goal="Verify the batch produced clean, linked,
@@ -3,6 +3,13 @@
3
3
  Reference for `extract-entities` Step 7b. Enrich `Knowledge/Roles/` and
4
4
  `Knowledge/Candidates/` with metadata that no single source carries.
5
5
 
6
+ All Role files are flat in `Knowledge/Roles/`. The `**Status:**` field
7
+ distinguishes:
8
+
9
+ - **`Status: open`** — active openings (use for new candidates and table
10
+ rebuilds).
11
+ - **`Status: closed`** — completed/withdrawn roles (read-only).
12
+
6
13
  ## Requisition number detection
7
14
 
8
15
  Scan email subjects and bodies for requisition numbers (e.g. 7-digit Workday
@@ -10,9 +17,12 @@ IDs).
10
17
 
11
18
  1. `ls Knowledge/Roles/ | grep "{req_number}"` — does a Role file exist?
12
19
  2. **No file:** create a stub using the Role-stub template in `req-track` Step
13
- 0b. Search `rg "{req_number}" Knowledge/` for context to enrich it.
14
- 3. **File exists:** check whether the email provides new metadata (hiring
15
- manager, recruiter, locations) and update the Role file.
20
+ 0b, with `**Status:** open`. Search `rg "{req_number}" Knowledge/` for
21
+ context to enrich it.
22
+ 3. **File exists:** check the `**Status:**` field. If `open`, check whether the
23
+ email provides new metadata (hiring manager, recruiter, locations) and update
24
+ the Role file. If `closed`, link for historical reference only; do not add
25
+ new candidates or rebuild tables.
16
26
 
17
27
  ## Hiring manager — calendar inference
18
28
 
@@ -28,7 +28,8 @@ meetings.
28
28
  - `Knowledge/Priorities/*.md` — active priorities and strategic context for
29
29
  framing
30
30
  - `Knowledge/Candidates/*/brief.md` — candidate context (for interview meetings)
31
- - `Knowledge/Roles/*.md` — role/requisition context (for interview meetings)
31
+ - `Knowledge/Roles/*.md` — role/requisition context (for interview meetings);
32
+ check the `**Status:**` field to distinguish active from historical reqs
32
33
 
33
34
  ## Outputs
34
35
 
@@ -145,7 +146,7 @@ When preparing for interview meetings (title contains "Interview", "Screening",
145
146
 
146
147
  1. **Read the candidate brief:** `Knowledge/Candidates/{Name}/brief.md`
147
148
  2. **Read the Role file:** Look up the `Req` field and read the corresponding
148
- `Knowledge/Roles/*.md` file.
149
+ `Knowledge/Roles/*.md` file; check the `**Status:**` field for context.
149
150
  3. **Include in the briefing:**
150
151
  - Candidate's current status, skills, and screening recommendation
151
152
  - Role context: hiring manager, domain lead, remaining positions
@@ -39,7 +39,8 @@ This is **Stage 3** of the three-stage pipeline:
39
39
  - Standard data via `fit-pathway`.
40
40
  - `Knowledge/Candidates/Insights.md` — cross-candidate context.
41
41
  - `Knowledge/Roles/*.md` — the candidate's requisition (positions, hiring
42
- manager, domain lead).
42
+ manager, domain lead). Check the `**Status:**` field to distinguish active
43
+ from historical reqs.
43
44
  - `Knowledge/Priorities/*.md` — strategic context.
44
45
  - Other active candidates at the same level — relative positioning.
45
46
 
@@ -118,7 +119,8 @@ ls Knowledge/Roles/ | grep "{req_number}"
118
119
  cat "Knowledge/Roles/{matching file}"
119
120
  ```
120
121
 
121
- Capture remaining positions, hiring manager, domain lead, goal alignment, other
122
+ Check the `**Status:**` field. Capture remaining positions, hiring manager,
123
+ domain lead, goal alignment, other
122
124
  candidates on the same req, and channel (hr / vendor). Frame the recommendation
123
125
  in terms of strategic impact.
124
126
 
@@ -39,7 +39,8 @@ This is **Stage 1** of a three-stage hiring pipeline:
39
39
  - Target role (optional).
40
40
  - Existing `Knowledge/Candidates/{Name}/brief.md`, if any.
41
41
  - `Knowledge/Roles/*.md` matching the candidate's `Req` (provides `Level`,
42
- `Discipline`, `Hiring manager`, `Domain lead`).
42
+ `Discipline`, `Hiring manager`, `Domain lead`, and the `**Status:**` field).
43
+ Look up by Req number or filename substring.
43
44
 
44
45
  ## Outputs
45
46
 
@@ -82,7 +83,8 @@ cat "Knowledge/Roles/{matching file}"
82
83
 
83
84
  Use the Role's `Level` and `Discipline` as the target unless the user specified
84
85
  a different target. Capture `Hiring manager` and `Domain lead` for the screening
85
- header.
86
+ header. Note the `**Status:**` field, but don't let historical (`closed`) roles
87
+ block screening of still-active candidates.
86
88
 
87
89
  If no target is available, estimate one using the level heuristics in
88
90
  [references/rubric.md](references/rubric.md#level-estimation-heuristics).
@@ -29,7 +29,8 @@ pipeline from scattered email threads.
29
29
  - `~/.cache/fit/outpost/apple_mail/attachments/` — CV/resume attachments.
30
30
  - `~/.cache/fit/outpost/apple_calendar/*.json` — calendar events (for
31
31
  cross-source inference).
32
- - `Knowledge/Roles/*.md` — open role/requisition files (metadata inheritance).
32
+ - `Knowledge/Roles/*.md` — role/requisition files (check the `**Status:**`
33
+ field: `open` for active roles, `closed` for historical).
33
34
  - `~/.cache/fit/outpost/state/graph_processed` — processed-file index (shared
34
35
  with `extract-entities`).
35
36
  - `~/.cache/fit/outpost/state/identity.md` — user identity for self-exclusion
@@ -40,8 +41,8 @@ pipeline from scattered email threads.
40
41
  - `Knowledge/Candidates/{Full Name}/brief.md` — candidate profile note.
41
42
  - `Knowledge/Candidates/{Full Name}/CV.pdf` (or `CV.docx`) — local CV copy.
42
43
  - `Knowledge/Candidates/{Full Name}/headshot.jpeg` — candidate photo.
43
- - `Knowledge/Roles/*.md` — created/updated role files (Candidates tables
44
- rebuilt).
44
+ - `Knowledge/Roles/*.md` — role files created/updated; the `**Status:**` field
45
+ determines visibility (open/closed).
45
46
  - `~/.cache/fit/outpost/state/graph_processed` — updated with processed threads.
46
47
 
47
48
  <do_confirm_checklist goal="Verify candidate processing batch is complete and
@@ -94,21 +95,24 @@ links.
94
95
 
95
96
  ### 3. Sync `Knowledge/Roles/`
96
97
 
97
- This keeps role metadata current and enables inheritance.
98
+ Role files are flat here; the `**Status:**` field is `open` or `closed`.
98
99
 
99
100
  1. Read each Role file's Info block to map Req → Role file path, Hiring manager,
100
- Domain lead, recruiter, Channel.
101
- 2. Find Reqs referenced by candidate briefs but missing a Role file:
102
- `rg "^\*\*Req:\*\*" Knowledge/Candidates/*/brief.md`. For each missing Req,
103
- create a stub using the **Role file stub** in
104
- [references/templates.md](references/templates.md), then enrich by searching
105
- the graph: `rg "{req_number}" Knowledge/`.
106
- 3. Rebuild each Role file's `## Candidates` table by scanning briefs:
101
+ Domain lead, recruiter, Channel. Look up by filename substring so a req-less
102
+ role stays findable: `ls Knowledge/Roles/ | grep "{partial_name_or_req}"`.
103
+ 2. Find Reqs in candidate briefs missing a Role file:
104
+ `rg "^\*\*Req:\*\*" Knowledge/Candidates/*/brief.md`. Check all Role files
105
+ first; for a genuinely missing open role, create a stub with `**Status:**
106
+ open` using the **Role file stub** in
107
+ [references/templates.md](references/templates.md), then enrich:
108
+ `rg "{req_number}" Knowledge/`.
109
+ 3. Rebuild each **open** Role file's `## Candidates` table by scanning briefs:
107
110
  `rg -l "Req:.*{req_number}" Knowledge/Candidates/*/brief.md`. Use the **Role
108
- Candidates table** format from `references/templates.md`. Sort by First seen,
109
- newest first.
111
+ Candidates table** format from `references/templates.md`, newest first.
110
112
  4. If a Role file has a hiring manager but no domain lead, walk the
111
113
  `**Reports to:**` chain in `Knowledge/People/` to a VP or senior leader.
114
+ 5. When a role closes (filled, cancelled, or frozen 6+ months), set its
115
+ `**Status:**` field to `closed` — only on a clear signal.
112
116
 
113
117
  ### 4. Identify recruitment threads
114
118
 
@@ -43,8 +43,10 @@ Field map and resolution rules for Step 2 of `req-track`.
43
43
 
44
44
  Stop at the first match:
45
45
 
46
- 1. **Req-first inheritance** — look up `Knowledge/Roles/*.md` for the matching
47
- Req; inherit Hiring manager and Domain lead from the Role file.
46
+ 1. **Req-first inheritance** — search `Knowledge/Roles/` for a file matching the
47
+ Req number or role description (use filename substring lookup for req-less
48
+ roles); check the `**Status:**` field (open/closed), then inherit Hiring
49
+ manager and Domain lead from the Role file.
48
50
  2. **Calendar inference** —
49
51
  `rg -l "{Candidate Name}" ~/.cache/fit/outpost/apple_calendar/`. The non-user
50
52
  organizer of an interview event is likely the hiring manager.
@@ -38,7 +38,8 @@ integrate with the `req-track` pipeline format.
38
38
 
39
39
  - `Knowledge/Candidates/{Clean Name}/brief.md` — candidate profile.
40
40
  - `Knowledge/Candidates/{Clean Name}/CV.md` — resume text as markdown.
41
- - `Knowledge/Roles/{Req ID} — {Title}.md` — created or updated.
41
+ - `Knowledge/Roles/{Req ID} — {Title}.md` — created or updated with
42
+ `**Status:** open`. Update to `**Status:** closed` when the role closes.
42
43
  - Updated existing briefs when a candidate already exists.
43
44
 
44
45
  <do_confirm_checklist goal="Verify the Workday import is consistent with
@@ -96,7 +97,8 @@ header indices, name annotations) are in
96
97
  ls Knowledge/Roles/ | grep "{Req ID}"
97
98
  ```
98
99
 
99
- Use the **Role file stub** in
100
+ New role files go in `Knowledge/Roles/` with `**Status:** open`. Use the
101
+ **Role file stub** in
100
102
  [references/templates.md](references/templates.md). Resolve the domain lead by:
101
103
 
102
104
  1. `rg "{Req ID}" Knowledge/` — look in project timelines, People notes, Topics
@@ -5,13 +5,15 @@ Reference templates for `req-workday` Steps 1b, 4, and 5.
5
5
  ## Role file stub
6
6
 
7
7
  Use when no Role file exists for the requisition. Filename:
8
- `Knowledge/Roles/{Req ID} — {Short Title}.md`.
8
+ `Knowledge/Roles/{Req ID} — {Short Title}.md`. The `**Status:**` field
9
+ (`open`/`closed`) is updated in place — never move files.
9
10
 
10
11
  ```markdown
11
12
  # {Requisition Title}
12
13
 
13
14
  ## Info
14
15
  **Req:** {Req ID}
16
+ **Status:** open
15
17
  **Title:** {Full title from export}
16
18
  **Level:** {Infer from title: "Principal" → J100, "Staff" → J090, "Director" → J100 M-track, "Senior" → J070}
17
19
  **Track:** {P-track for IC roles, M-track for Director/Manager roles}