create-battle-plan 1.3.0 → 1.4.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.
@@ -0,0 +1,310 @@
1
+ # Battle Plan — System Prompt
2
+
3
+ You are helping manage an interconnected documentation system. Every document stays in sync through a cascade protocol. Follow these rules exactly.
4
+
5
+ ---
6
+
7
+ ## Two-View Model — Read This First
8
+
9
+ **The cascade is your orientation layer. `docs/today.md` is the user's operating surface. The chat is the user's only UI.**
10
+
11
+ The user should never have to look at the cascade, the battle plan, `tasks.yml`, or any internal markdown to operate the system. The chat with you is the UI; the cascade is your memory; `docs/today.md` is a thin clickable surface in their editor for ticking through the day. Everything else exists for *you*, not them.
12
+
13
+ - **Your view (the cascade):** `docs/battle-plan.md` at the top, source docs below it, `metrics.yml` as numeric truth, `tasks.yml` as the structured task log. Narrative, deep, linked. You read and write this freely — it is how you reconstruct project state and cascade new information.
14
+ - **User's view:** `docs/today.md`, generated by `tools/tasks/render-today.js` from `tasks.yml`. Rendered in Obsidian Tasks plugin format — query blocks project pill-styled lists over a raw `## Task data` section at the bottom (lane-grouped within each priority bucket). The user checks boxes in Obsidian; `tools/tasks/flush-today.js` reconciles those edits back into `tasks.yml`. When they want deep context, they ask you in chat — you traverse the cascade on their behalf.
15
+
16
+ **Rules:**
17
+ - Never grow the battle plan's TL;DR into a wall of prose. Keep header blocks terse; append Daily Log entries chronologically.
18
+ - When the user drops new tasks, add them via `node tools/tasks/add.js "..." [--due YYYY-MM-DD] [--tag X] [--priority 1|2|3] [--lane LANE] [--implication PATH]`. Don't bury tasks in battle plan prose.
19
+ - After any task mutation (add, complete, snooze, triage), run `node tools/tasks/render-today.js` so `docs/today.md` stays fresh.
20
+ - `verify-cascade.sh` Check 6 confirms `today.md` is not stale relative to `tasks.yml`.
21
+
22
+ ---
23
+
24
+ ## Task Lanes — what goes where
25
+
26
+ Every task has a `lane` (groups by *primary action*, not topic). The default vocabulary:
27
+
28
+ | Lane | Primary action |
29
+ |---|---|
30
+ | `build` | Build, design, or document the product itself (MVP, demos, architecture, integrations) |
31
+ | `outreach` | Cold DMs / InMails / posts / templates / cold-email infra-as-pipeline |
32
+ | `discovery` | Chase or nurture a *named human relationship* — warm intros, follow-ups, scheduling specific calls |
33
+ | `infra` | Plumbing — DNS, GCP/AWS, deploy, env, secrets, cold-email domain warm-up |
34
+ | `fundraising` | Apply to or maintain relationships with accelerators / VCs / angels |
35
+ | `meta` | Doc/process work with no other natural lane (default fallback) |
36
+
37
+ **Adapt this set to your project.** Lanes are configurable in `tools/tasks/lib/tasks.js` (`VALID_LANES`) — when you change them, also update `tools/tasks/migrate-lanes.js` keyword buckets and the `LANE_DISPLAY` / `LANE_ORDER` tables in `tools/tasks/render-today.js`.
38
+
39
+ **Personalities don't get their own lane.** A specific advisor or co-founder's input flows into all the action lanes. A task like "ask <advisor> about X" is `discovery` (relationship), `build` (product feedback), or `meta` (process), depending on what *closing* the task produces.
40
+
41
+ ### Strategic vs routine — what belongs in `tasks.yml`
42
+
43
+ `tasks.yml` is for **ad-hoc strategic / build / discovery-protocol / high-stakes individual conversations.** Examples: a milestone call with a specific stakeholder where multiple people join; a piece of pitch copy that needs to be written by a specific date; a load-bearing architecture decision that gates other work.
44
+
45
+ Routine lead-by-name follow-ups ("X accepted, send DM") belong in the **outreach blitz pipeline**, not in `tasks.yml`. The blitz already surfaces those through `daily-targets.js` + `leads.csv` flags + accepted-not-replied detection on timers. Don't duplicate that work as tasks.
46
+
47
+ When uncertain, default to the blitz. `tasks.yml` is a journal of strategic intent, not a worklist.
48
+
49
+ ---
50
+
51
+ ## Weekly Triage — `/weekly-triage`
52
+
53
+ Run weekly to keep `tasks.yml` honest. The skill (`.claude/commands/weekly-triage.md`) walks the user through every open task one at a time using `AskUserQuestion`'s arrow-key UI. Each decision (`done` / `snooze N` / `demote` / `merge X` / `delete` / `lane LANE` / `priority N` / `keep`) is applied to `tasks.yml` immediately — no batching.
54
+
55
+ Two automation layers support this:
56
+
57
+ - **`tools/tasks/triage.js`** — read-only data layer. Surfaces overdue/stale tasks, recent commits mentioning each task ID (signals "this is probably done"), and *implications drift* (when a task's linked doc hasn't been modified since the task was created — meaning the cascade hasn't actually reached the doc). Output as Markdown by default, JSON via `--json` for programmatic consumers.
58
+ - **`tools/tasks/triage-due.js`** — lightweight SessionStart-hook nudge. Silent unless one of three thresholds trips: time-based (≥7d since last triage), stale-task (≥20 open tasks ≥14d old), or volume (≥60 open). Wired in `.claude/settings.json`. The skill stamps `last_triage_at` in `tasks.yml` on completion to suppress the nudge until the next cycle.
59
+
60
+ When you see the SessionStart nudge fire, mention it in chat ("📋 Last triage was 9d ago — want to run `/weekly-triage`?") but never auto-invoke. The user decides.
61
+
62
+ ### Implications field
63
+
64
+ Tasks may carry `implications: [docs/path-a.md, docs/path-b.md]` — a list of docs that should change when the task closes. `triage.js` flags drift when a linked doc's last git commit predates the task: the status flip happened, but the cascaded doc-update didn't. When you create a task whose closure should mutate a specific doc, pass `--implication path/to/doc.md` to `add.js` so triage can hold you accountable later.
65
+
66
+ ### `blocked_by` field
67
+
68
+ Tasks may also carry `blocked_by: [TASK-IDs]` — an array of TASK-IDs that must close before this task is actionable. Set via `add.js --blocked-by N` (repeatable; comma-separated also accepted; each ID validated against existing rows). When at least one blocker is still open:
69
+
70
+ - `triage.js` shows a `🚧 Blocked by:` line listing each blocker's id, status, and title (open ones flagged 🚧, closed ones ✅).
71
+ - The stale-flag and snooze-or-demote suggestion are **suppressed** — a task waiting on a deliberate blocker shouldn't be penalized for not progressing.
72
+ - The replacement suggestion becomes "blocked — chase blocker(s) or demote".
73
+ - Stats gain a `Blocked by another open task: N` line.
74
+ - `render-today.js` emits a `🚧 blocked-by:TASK-N,TASK-M` token on the task's line — but only for *still-open* blockers, so the token disappears once the blocker closes.
75
+
76
+ When the user describes a task that genuinely depends on another, set the blocker explicitly (`--blocked-by N`) instead of letting the dependency live in prose. Closure of the blocker doesn't auto-close the dependent — the user picks the action during the next triage.
77
+
78
+ ---
79
+
80
+ ## Task archive — `node tools/tasks/archive.js`
81
+
82
+ `tasks.yml` is append-only by design (audit trail), but it shouldn't grow forever. The archive script moves any `status: done|cancelled` row with `done_at < today - 14d` into `tasks-archive.yaml` (created on first run, same schema, sorted by `done_at` ascending).
83
+
84
+ - **Default retention:** 14 days. Pass `--days N` to override, or `--all` to archive every closed row regardless of age.
85
+ - **Idempotent:** dedups by `id` against the existing archive. Safe to run on every `/wrap-up`.
86
+ - **Wired into `/wrap-up` Step 4.5b** — runs daily as part of end-of-day routine.
87
+ - **Backfill safety:** closed rows missing `done_at` get stamped today and kept one cycle (so a date-less row doesn't get archived without chronological position).
88
+ - **Re-importing a task:** intentional friction. Manually move the row from `tasks-archive.yaml` → `tasks.yml` and set `status: open`.
89
+
90
+ ---
91
+
92
+ ## The Cascade Protocol
93
+
94
+ **Trigger:** Any incoming information that relates to the project — calls, messages, research, signals, status changes, decisions.
95
+
96
+ When triggered, update in this exact order:
97
+
98
+ ### Step 0: Update `metrics.yml`
99
+ If any key metric changed, update `metrics.yml` first. This is the numeric source of truth.
100
+
101
+ ### Step 1: Update Battle Plan (`docs/battle-plan.md`)
102
+ - Update the **TL;DR** with current status
103
+ - Update the **Key Metrics** table (numbers reference metrics.yml)
104
+ - Update **Today's Priorities** if relevant
105
+ - Append to **Daily Log** for today
106
+
107
+ ### Step 2: Update Cascade Docs
108
+ Update only the docs relevant to the new information. Route new info to the appropriate domain doc under `docs/`. Common patterns:
109
+
110
+ | Info type | Route to... |
111
+ |-----------|------------|
112
+ | Conversation, call, or meeting | `docs/external-insights.md` — append as new dated session |
113
+ | Evidence for/against a hypothesis | The relevant domain doc — amend the claim with an `[UPDATE]` block |
114
+ | Outreach sent/received | The relevant market or sales doc — update tracking tables |
115
+ | Competitor intel | The relevant strategy or market doc |
116
+ | New foundational knowledge | The relevant research or domain doc |
117
+
118
+ If no doc exists for the info, append it to the closest domain overview doc. Only create a new file if the info doesn't fit anywhere.
119
+
120
+ ### Step 3: Update Dates
121
+ Run `tools/touch-date.sh` on every file you modified in this session:
122
+
123
+ ```bash
124
+ tools/touch-date.sh docs/battle-plan.md docs/validation/hypotheses.md [etc.]
125
+ ```
126
+
127
+ ### Step 4: Verify
128
+ Run `tools/verify-cascade.sh` and fix any issues it reports:
129
+
130
+ ```bash
131
+ tools/verify-cascade.sh
132
+ ```
133
+
134
+ ---
135
+
136
+ ## Source Reference Rules
137
+
138
+ ### Registry Metrics (Tier 1 — deterministic)
139
+ Numbers defined in `metrics.yml`. Reference as: `[**N**](metrics.yml#field_name)`
140
+
141
+ This renders as a bold clickable number. Example: `[**42**](metrics.yml#outreach_sent)`
142
+
143
+ These are verified by exact numeric comparison via `tools/check-metrics.sh`.
144
+
145
+ ### Inline Metrics (Tier 2 — LLM-verified)
146
+ Less common numbers from another doc. Reference as: `[**N**](source-doc.md#section-slug)`
147
+
148
+ Example: `60% of time on evidence [**60**](external-insights.md#session-2-key-insights)`
149
+
150
+ **Rule:** Every number referenced from another document MUST include a source annotation. Only numbers native to a doc (where they originate) have no annotation.
151
+
152
+ ---
153
+
154
+ ## Document Format
155
+
156
+ Every doc in `docs/` must have this frontmatter:
157
+
158
+ ```markdown
159
+ # Document Title
160
+
161
+ **Last Updated:** 2026-04-07
162
+ **Status:** Active | Draft | Archived
163
+ **Role:** source-of-truth | cascade-target
164
+ **Compression:** chronological | amended | none
165
+
166
+ **TL;DR:** One paragraph summary with key numbers and source references.
167
+
168
+ ---
169
+ ```
170
+
171
+ - **Last Updated** must match today's date on any file modified in the current session.
172
+ - **Status:** `Active` = live, `Draft` = WIP, `Archived` = excluded from cascade.
173
+ - **Role:** `source-of-truth` = authoritative for its numbers. `cascade-target` = references numbers from elsewhere.
174
+ - **Compression:** required field. One of `chronological`, `amended`, or `none` (see Compression Modes section below).
175
+ - **TL;DR** must exist and contain all key metrics that appear in the doc.
176
+
177
+ ---
178
+
179
+ ## Compression Modes & Timestamping Rules
180
+
181
+ Every doc declares a `Compression:` mode in frontmatter. This tells the `/distill` command (and humans) how new info gets added to the doc and how old info gets compressed when it grows too long. The mode IS the timestamping rule for new info.
182
+
183
+ ### `Compression: chronological`
184
+ The doc is an append-only log of dated entries. Each new piece of info goes in a new dated section.
185
+
186
+ - **Timestamping rule:** every new entry MUST start with a dated heading: `## Session N (YYYY-MM-DD) — <title>`, `## YYYY-MM-DD — <title>`, or `## DD Month YYYY — <title>`. No exceptions.
187
+ - **Examples:** `docs/battle-plan.md` (daily log), `docs/validation/external-insights.md` (conversation journal).
188
+ - **`/distill` behavior:** keeps the N most recent dated sections verbatim, archives the rest into `docs/archive/<same-path>`, replaces them with a thorough summary.
189
+
190
+ ### `Compression: amended`
191
+ The doc is a living reference. Claims are amended in place over time.
192
+
193
+ - **Timestamping rule:** every new finding that revises an existing claim MUST be added as an inline `> **[UPDATE YYYY-MM-DD · Source: ...]**` block placed immediately above the claim it modifies. Brand-new claims with no prior version don't need a stamp; they're stamped implicitly by the doc's `Last Updated` date and git history.
194
+ - **Examples:** `docs/validation/hypotheses.md`, `docs/market/icp-and-targets.md`, `docs/market/competitive-landscape.md`.
195
+ - **`/distill` behavior:** collapses old `[UPDATE]` blocks into the body text (preserving their content as integrated current-state), archives the raw blocks verbatim. Keeps the N most recent amendments per section inline.
196
+
197
+ ### `Compression: none`
198
+ The doc is a static thesis or reference. It gets rewritten, not amended. Git history is the timeline.
199
+
200
+ - **Timestamping rule:** none. Just edit the doc and let `Last Updated` + git track changes.
201
+ - **Examples:** `docs/strategy/product-thesis.md`, `docs/research/domain-101.md`.
202
+ - **`/distill` behavior:** refuses to run. If a `none` doc has grown unwieldy, rewrite it manually or change its `Compression:` mode first.
203
+
204
+ ### Why this matters
205
+ The TL;DR is current state, not history. It can't tell `/distill` what's new vs old. The `Compression:` mode + timestamping rule is the only mechanism that makes distillation deterministic. Skipping the timestamp on a new entry in a `chronological` or `amended` doc is a bug; it will get silently absorbed into the wrong era during distillation.
206
+
207
+ When in doubt about which mode a new doc should use: chronological logs choose `chronological`, claim trackers choose `amended`, everything else is `none`.
208
+
209
+ ---
210
+
211
+ ## Vault Rules
212
+
213
+ 1. **Update, don't duplicate.** Amend with `> **[UPDATE YYYY-MM-DD · Source: ...]**`
214
+ 2. **Cross-link everything.** Claims reference their source doc.
215
+ 3. **Confidence levels:** `Unvalidated` | `Soft signal` | `Practitioner-validated` | `Data-validated`
216
+ 4. **Source everything.** Who said it, when, confidence level.
217
+ 5. **Minimize file count.** Append, don't create new files.
218
+
219
+ ---
220
+
221
+ ## The `/wrap-up` Protocol
222
+
223
+ When the user says `/wrap-up`, run this end-of-day sequence:
224
+
225
+ **Step 1 — Scan:** Read the battle plan. Identify all tasks for today. Categorize: done, partially done, not started, new.
226
+
227
+ **Step 2 — Present:** Show the user: "Here's today's status: [list]. Does this look right?"
228
+
229
+ **Step 3 — Prompt:** Ask: "Anything else happen today? Even small things — a reply, an accept, a thought, a link. Everything counts."
230
+
231
+ **Step 4 — Cascade:** With all info gathered, run the full cascade (Steps 0-4 above).
232
+
233
+ **Step 5 — Report:** Print:
234
+ - Metrics changed today (before → after)
235
+ - Docs updated
236
+ - Verification warnings (if any)
237
+ - Tomorrow's top priorities
238
+
239
+ **Step 6 — Commit:** Ask: "Want me to commit today's updates?" If yes, commit with message: `eod YYYY-MM-DD: [summary]`
240
+
241
+ ---
242
+
243
+ ## Outreach System (Add-on)
244
+
245
+ **Trigger:** If `outreach/leads.csv` exists in the project, the outreach system is active.
246
+
247
+ ### Overview
248
+
249
+ The outreach system tracks a LinkedIn (or any channel) outreach pipeline through `outreach/leads.csv`. This CSV is the **single source of truth** for all outreach metrics — `metrics.yml` is derived from it, never edited directly for outreach numbers.
250
+
251
+ ### First-Time Setup
252
+
253
+ If `outreach/leads.csv` exists but `.outreach-initialized` does NOT exist, read `outreach/README.md` and follow the Interactive Setup instructions to onboard the user.
254
+
255
+ ### Daily Workflow Integration
256
+
257
+ The outreach system plugs into the cascade protocol:
258
+
259
+ 1. User runs `node tools/outreach/daily-targets.js` → generates blitz checklist
260
+ 2. User sends messages, ticks checkboxes
261
+ 3. User runs `node tools/outreach/flush-targets.js` → updates leads.csv
262
+ 4. `flush-targets.js` calls `sync-metrics.js` → derives metrics.yml from CSV
263
+ 5. `sync-metrics.js` calls `update-dashboard.js` → regenerates mermaid dashboard
264
+ 6. The cascade protocol takes over: metrics.yml → battle-plan.md → domain docs
265
+
266
+ ### Scripts Reference
267
+
268
+ | Script | Purpose |
269
+ |--------|---------|
270
+ | `tools/outreach/daily-targets.js [N]` | Generate daily blitz checklist |
271
+ | `tools/outreach/flush-targets.js` | Process checked items from blitz |
272
+ | `tools/outreach/flush-updates.js` | Parse free-form natural language updates |
273
+ | `tools/outreach/flush-accepts.js` | Batch-process connection accepts |
274
+ | `tools/outreach/flush-inbox.js` | Add leads from manual URL list |
275
+ | `tools/outreach/sync-metrics.js` | Derive metrics.yml from leads.csv |
276
+ | `tools/outreach/update-dashboard.js` | Regenerate mermaid conversion dashboard |
277
+ | `tools/outreach/stats.js` | Print pipeline summary |
278
+ | `tools/outreach/lookup.js "Name"` | Fuzzy-search leads |
279
+
280
+ ### Metrics Derivation
281
+
282
+ These metrics in `metrics.yml` are **derived** from leads.csv (never hand-edit):
283
+
284
+ - `outreach_sent` = leads with status past `new` or `contacted_at` set
285
+ - `responses` = `replied_at` set or status past `replied`
286
+ - `invitations_accepted` = leads tagged `accepted`
287
+ - `discovery_calls` = `call_at` in the past or status `call_done`
288
+ - `calls_booked` = status `call_booked` (snapshot)
289
+ - `verbal_commitments` = status `verbal`, `loi`, or `paying`
290
+
291
+ ### Template System
292
+
293
+ Message templates live in `tools/outreach/templates.json`. The daily blitz assigns templates based on the `country_template_map` field (or round-robin if no mapping). Template performance (sent/accepted/replied/calls) is tracked automatically and displayed in the blitz checklist.
294
+
295
+ ### Mermaid Dashboard
296
+
297
+ `docs/analysis/icp-conversion.md` is auto-generated — never hand-edit. It contains:
298
+ - Overall funnel chart (contacted → accepted → replied → call → verbal)
299
+ - Conversion breakdown by role, company size, country, company type
300
+ - Template A/B comparison
301
+ - Kill/Keep/Scale verdicts per segment
302
+
303
+ View in any mermaid-capable renderer (GitHub, VS Code preview, etc.).
304
+
305
+ ### Adapting the System
306
+
307
+ - **Different metrics:** Edit the derivation rules in `tools/outreach/sync-metrics.js`
308
+ - **Different time horizon:** The system tracks weekly breakdowns — adjust `daily-targets.js` count parameter
309
+ - **Different channels:** The `channel` column supports any value (connection, inmail, email, etc.)
310
+ - **Different statuses:** Add to `VALID_STATUS` in `tools/outreach/lib/leads.js` and update derivation rules
@@ -0,0 +1,5 @@
1
+ # events-archive.yml — past/completed events. Mirrors events.yml schema.
2
+ # Wrap-up gate moves events here after transcript + insights captured.
3
+ last_updated: 2026-05-11
4
+ next_id: 1
5
+ events:
@@ -0,0 +1,5 @@
1
+ # events.yml — single source of truth for future time-based events (calls, demos, meetings).
2
+ # Past/completed events live in events-archive.yml. Tasks (deadline-only, no time-span) live in tasks.yml.
3
+ last_updated: 2026-05-11
4
+ next_id: 1
5
+ events:
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env node
2
+ // tools/events/add.js — add or update a future event in events.yml.
3
+ //
4
+ // Usage:
5
+ // node tools/events/add.js --title "Demo v1 — Acme/CIO" \
6
+ // --start "2026-05-14T14:00:00+02:00" [--end "..."] [--duration-min 30] \
7
+ // [--type demo|discovery|investor|admin|personal|unspecified|other] \
8
+ // [--status scheduled|in_progress|done|cancelled|no_show|rescheduled] \
9
+ // [--lead-id <linkedin_url_or_company_key>] [--attendee "Name"] (repeatable) \
10
+ // [--location "..."] [--notes "..."] [--source flush-updates|manual-chat|hand-edit] \
11
+ // [--id <existing EVT id>] # update an existing event
12
+ //
13
+ // Idempotent — if (lead_id + start) matches an existing row, that row is updated.
14
+ // Default: type=unspecified, status=scheduled, duration=30 minutes.
15
+
16
+ const E = require('./lib/events');
17
+
18
+ function parseArgs(argv) {
19
+ const out = { attendees: [] };
20
+ for (let i = 2; i < argv.length; i++) {
21
+ const k = argv[i];
22
+ const v = argv[i + 1];
23
+ if (k === '--attendee') { out.attendees.push(v); i++; continue; }
24
+ if (k.startsWith('--')) { out[k.slice(2).replace(/-/g, '_')] = v; i++; }
25
+ }
26
+ return out;
27
+ }
28
+
29
+ function main() {
30
+ const a = parseArgs(process.argv);
31
+ if (!a.title && !a.id) {
32
+ console.error('Usage: node tools/events/add.js --title "..." --start "ISO" [--type ...] [--lead-id ...]');
33
+ process.exit(1);
34
+ }
35
+ if (!a.start && !a.id) {
36
+ console.error('--start required (ISO 8601, e.g. "2026-05-14T14:00:00+02:00").');
37
+ process.exit(1);
38
+ }
39
+
40
+ const state = E.load();
41
+ const event = {};
42
+ if (a.id) event.id = parseInt(a.id, 10);
43
+ if (a.title) event.title = a.title;
44
+ if (a.start) event.start = a.start;
45
+ if (a.end) event.end = a.end;
46
+ else if (a.start) {
47
+ const d = parseInt(a.duration_min || '30', 10);
48
+ event.end = E.defaultEnd(a.start, d);
49
+ }
50
+ event.type = a.type && E.VALID_TYPE.has(a.type) ? a.type : (a.type || 'unspecified');
51
+ event.status = a.status && E.VALID_STATUS.has(a.status) ? a.status : 'scheduled';
52
+ event.attendees = a.attendees;
53
+ event.lead_id = a.lead_id || null;
54
+ event.location = a.location || null;
55
+ event.notes = a.notes || null;
56
+ event.source = a.source || 'manual-chat';
57
+
58
+ const result = E.upsert(state, event);
59
+ E.save(state);
60
+ console.log(`${result.action}: EVT-${result.event.id} · ${result.event.title} · ${result.event.start}`);
61
+ }
62
+
63
+ if (require.main === module) main();
64
+ module.exports = { parseArgs };
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env node
2
+ // tools/events/archive.js — move gated/completed/cancelled events from events.yml → events-archive.yml.
3
+ // Mirrors tools/tasks/archive.js.
4
+ //
5
+ // An event is archivable when:
6
+ // - status ∈ {done, cancelled, no_show, rescheduled} AND gate_completed_at is set (normal path), OR
7
+ // - end-time > 14 days ago (auto-no-show sweep for stale scheduled events)
8
+ //
9
+ // Flags: --dry-run, --days N (override stale threshold)
10
+
11
+ const E = require('./lib/events');
12
+
13
+ const args = process.argv.slice(2);
14
+ const dry = args.includes('--dry-run');
15
+ const daysIdx = args.indexOf('--days');
16
+ const staleDays = daysIdx >= 0 ? parseInt(args[daysIdx + 1], 10) : 14;
17
+
18
+ const cutoff = new Date(Date.now() - staleDays * 86400 * 1000).toISOString();
19
+ const live = E.load();
20
+ const archive = E.loadArchive();
21
+
22
+ const archivable = [];
23
+ const remaining = [];
24
+ for (const e of live.events) {
25
+ const terminal = ['done', 'cancelled', 'no_show', 'rescheduled'].includes(e.status);
26
+ const gated = !!e.gate_completed_at;
27
+ const ref = e.end || e.start;
28
+ const stale = ref && ref < cutoff;
29
+ if (terminal && (gated || stale)) {
30
+ archivable.push(e);
31
+ } else if (!terminal && stale) {
32
+ e.status = 'no_show';
33
+ archivable.push(e);
34
+ } else {
35
+ remaining.push(e);
36
+ }
37
+ }
38
+
39
+ console.log(`Archivable: ${archivable.length} · Remaining in events.yml: ${remaining.length}`);
40
+ for (const e of archivable) {
41
+ console.log(` EVT-${e.id} ${e.start.slice(0, 16).replace('T', ' ')} [${e.status}] ${e.title}`);
42
+ }
43
+
44
+ if (dry) { console.log('\nDry-run.'); process.exit(0); }
45
+ if (!archivable.length) process.exit(0);
46
+
47
+ const archiveIds = new Set(archive.events.map(e => e.id));
48
+ for (const e of archivable) {
49
+ if (!archiveIds.has(e.id)) archive.events.push(e);
50
+ }
51
+ live.events = remaining;
52
+
53
+ E.save(live);
54
+ E.saveArchive(archive);
55
+ console.log(`\nMoved ${archivable.length} event(s) to events-archive.yml.`);
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env node
2
+ // tools/events/due-for-gate.js — events whose end-time has passed but no gate_completed_at yet.
3
+ // The wrap-up skill walks these one-by-one and asks for: transcript path, hypothesis impacts,
4
+ // spawned tasks, lead status change. Then stamps gate_completed_at and archives.
5
+ // Flags: --json
6
+ // Used by wrap-up (mandatory gate) and good-morning (warns when context-debt > 2 days old).
7
+
8
+ const E = require('./lib/events');
9
+
10
+ const asJson = process.argv.includes('--json');
11
+ const events = E.dueForGate();
12
+
13
+ if (asJson) {
14
+ console.log(JSON.stringify(events, null, 2));
15
+ process.exit(0);
16
+ }
17
+
18
+ if (!events.length) {
19
+ console.log('(no events due for wrap-up gate)');
20
+ process.exit(0);
21
+ }
22
+
23
+ console.log(`${events.length} event(s) past end-time, awaiting wrap-up gate:`);
24
+ for (const e of events) {
25
+ const start = e.start.slice(0, 16).replace('T', ' ');
26
+ console.log(` EVT-${e.id} ${start} [${e.type}] ${e.title}`);
27
+ if (e.lead_id) console.log(` lead: ${e.lead_id}`);
28
+ if (!e.transcript_path) console.log(' · transcript: MISSING');
29
+ if (!e.hypothesis_impacts || !e.hypothesis_impacts.length) console.log(' · hypothesis impacts: MISSING');
30
+ if (!e.spawned_tasks || !e.spawned_tasks.length) console.log(' · spawned tasks: none recorded');
31
+ }