@ghostpaw/questlog 0.10.1 → 0.11.1

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/README.md CHANGED
@@ -81,6 +81,9 @@ questlog list --view quests.resolved --json
81
81
  `--questline <id>`, and `--due` as YYYY-MM-DD, DD.MM.YYYY, `today`, `tomorrow`,
82
82
  `+3d`, an ISO date-time with timezone, or milliseconds since epoch. Date-only
83
83
  values use local end of day.
84
+ `--objective` is the one flag that reads a file: `@path`, `@-` for stdin, and
85
+ `@@text` for a literal `@text` (help marks it `(@file, @- stdin, @@ literal @)`). Every
86
+ other value, including titles, `--outcome` and per-tool flags, is literal.
84
87
  `list` defaults to available quests; `list --help` lists all supported views.
85
88
  Its `--limit` caps text output; JSON preserves the full review tool result.
86
89
  `search --limit` caps matches in both formats. `show` prints full quest detail.
@@ -88,10 +91,14 @@ Lists show local due dates, overdue markers, and recorded outcomes. Plain JSON
88
91
  adds ISO UTC siblings for numeric timestamp fields ending in `At`.
89
92
  `done` stores `--outcome` with the finished quest; without it the command asks for one and exits 3.
90
93
 
91
- Plain commands print short text by default. Use `--json` for the full tool result
94
+ Commands print text by default, including hooks, delivery views and each watch tick; `call` and direct tool commands return the raw tool result as JSON. Use `--json` for the full tool result
92
95
  and `--compact` for compact JSON. `--db path` works before or after the command.
93
96
  Each command accepts `--help`. Exit codes are 0 for success, 1 for invalid input
94
- or a tool error, 2 for usage errors, and 3 when clarification is needed.
97
+ or a tool error, 2 for usage errors, and 3 when clarification is needed or `hook remove`/`hook test` addresses a missing target.
98
+ Missing quests and deliveries exit 1. `hook label` names the database, not an individual target.
99
+
100
+ `questlog skill --short` prints the compact cheat sheet; `questlog skill --list` lists guides.
101
+ ISO date-times without a zone (for example `2026-10-29T09:00`) use the local time zone.
95
102
 
96
103
  The `call` command and the per-tool form still reach every tool and parameter:
97
104
 
@@ -124,7 +131,7 @@ ghostpaw-questlog skill > SKILL.md
124
131
 
125
132
  Each line downloads the latest release asset, checks it against the release’s SHA256SUMS and installs it on the PATH (~/.local/bin on Linux/macOS; the Windows script adds its folder to the user PATH).
126
133
 
127
- These lines never edit ~/.profile or other shell startup files: `export PATH` applies to the current shell only. To keep it, add `export PATH="$HOME/.local/bin:$PATH"` to your shell profile yourself.
134
+ These lines never edit ~/.profile or other shell startup files: `export PATH` applies to the current shell only. To keep it in new shells, use the `~/.profile` line below.
128
135
 
129
136
  linux-x64:
130
137
 
@@ -159,7 +166,13 @@ irm https://raw.githubusercontent.com/GhostPawJS/questlog/main/install.ps1 | iex
159
166
  Pinned to a release:
160
167
 
161
168
  ```sh
162
- curl -fsSL https://raw.githubusercontent.com/GhostPawJS/questlog/main/install.sh | sh -s -- --target linux-x64 --version 0.10.1 && export PATH="$HOME/.local/bin:$PATH"
169
+ curl -fsSL https://raw.githubusercontent.com/GhostPawJS/questlog/main/install.sh | sh -s -- --target linux-x64 --version 0.11.1 && export PATH="$HOME/.local/bin:$PATH"
170
+ ```
171
+
172
+ Also adding ~/.local/bin to ~/.profile (edits ~/.profile once; skipped if it already mentions ~/.local/bin; zsh login shells read ~/.zprofile instead, so use that file name there):
173
+
174
+ ```sh
175
+ curl -fsSL https://raw.githubusercontent.com/GhostPawJS/questlog/main/install.sh | sh -s -- --target linux-x64 && { grep -qs '.local/bin' ~/.profile || echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.profile; } && export PATH="$HOME/.local/bin:$PATH"
163
176
  ```
164
177
 
165
178
  To skip the persistent Windows user PATH edit (the installer still installs and prints the folder to add):
@@ -170,7 +183,7 @@ $env:GHOSTPAW_NO_MODIFY_PATH=1; irm https://raw.githubusercontent.com/GhostPawJS
170
183
 
171
184
  ```text
172
185
  $ ghostpaw-questlog --version
173
- questlog 0.10.1 (binary, linux-x64)
186
+ questlog 0.11.1 (binary, linux-x64)
174
187
  ```
175
188
 
176
189
  Scripts that compare versions should use `--version --short` or `--version --json`.
@@ -536,6 +549,22 @@ questlog hook remove 1
536
549
 
537
550
  Save the signing secret printed once by `hook add` (including its `--json` result).
538
551
  `--secret-stdin` reads a supplied secret from standard input; there is no secret flag.
552
+ Secrets and custom header values are stored in the database. On POSIX, new database files
553
+ and their WAL/SHM siblings use mode 0600, and directories created for them use 0700.
554
+ Existing database permissions are preserved; Windows uses its existing access controls.
555
+
556
+ Add repeatable custom headers with `--header "Authorization: Bearer value"` or
557
+ `--header-stdin` (one `Name: value` line per header, keeping values out of shell history).
558
+ For example: `questlog hook add https://example.org/wake --header "X-Key: value"`.
559
+ Headers accompany every delivery and test. Header names are shown with values replaced by
560
+ `***`; values are never included in list, show, test, tick, watch, or tool output.
561
+ Names must be HTTP tokens; `host`, `content-length`, `content-type`, and `x-questlog-*`
562
+ are reserved (case insensitive). To change headers, remove the hook and add it again.
563
+ Use separate invocations for `--header-stdin` and `--secret-stdin`.
564
+ URL userinfo is rejected; use a custom header or query key instead.
565
+ `questlog show d_…` prints delivery status, attempts, HTTP status, next attempt,
566
+ redacted target URL, and event ids; add `--json` for JSON.
567
+
539
568
  List output redacts URL userinfo and query strings. Removal deletes the target and its deliveries.
540
569
  A target covers the database unless `--tag` restricts it to quests with that tag;
541
570
  rumors have no tags and reach database targets only. `--events` defaults to all five events.
package/SKILL.md CHANGED
@@ -28,15 +28,19 @@ Run tools as `ghostpaw-questlog ...`; use `~/.local/bin/ghostpaw-questlog` if it
28
28
  `questlog` and `ghostpaw-questlog` are the same binary.
29
29
 
30
30
  - `questlog search <words>` before `add` to avoid duplicates.
31
- - `ghostpaw-questlog add "..." --tag a,b --due tomorrow`
31
+ - `ghostpaw-questlog add "Write the report" --tag work --due tomorrow`
32
32
  - `questlog list [--view <view>]`: deliveries, questlines, quests.active, quests.available, quests.blocked, quests.deferred, quests.due_soon, quests.in_progress, quests.missed_scheduled, quests.open, quests.overdue, quests.resolved, quests.scheduled_for_day, quests.scheduled_now, repeatables.due_anchors, rumors.
33
- - `questlog done <id> --outcome "..."`
34
- - `questlog search <words>`
35
- - `questlog show <id>`
33
+ - `questlog done 12 --outcome "Report sent"`
34
+ - `questlog search "release"`
35
+ - `questlog show 12`
36
+ - `questlog list --view quests.available`
36
37
  - Use `--json` on any plain command.
37
- - Exit codes: 0 ok / 1 error / 2 usage / 3 needs input.
38
+ - Exit codes: 0 ok / 1 error / 2 usage / 3 needs input or hook target not found.
38
39
  - Database: `~/.ghostpaw/questlog.db`; override with `--db` or `GHOSTPAW_QUESTLOG_DB`.
39
- - Use one database per agent or project.
40
+ - Use one database per project.
41
+ - `questlog --version` prints the installed version.
42
+ - `questlog skill --short` prints this compact cheat sheet.
43
+ - `questlog skill --list` lists guides; `questlog skill webhooks` reads one.
40
44
  - Webhooks: `questlog hook add <url>`, `questlog tick --dry-run`, `questlog watch`. Read `questlog skill webhooks` for events, signing, verification, and cron setup.
41
45
 
42
46
  ## Appendix: MCP and tool calls
@@ -175,16 +179,39 @@ Writes: writes_state.
175
179
  | action | string | yes | attach_quest_to_questline, create_quest, create_questline, detach_quest_from_questline, dismiss_rumor, reopen_rumor, settle_rumor, update_rumor. Action to perform. |
176
180
  | questId | integer | no | Quest to attach or detach. |
177
181
  | questlineId | integer | no | Questline that should receive the quest. |
178
- | quest | object | no | |
182
+ | quest | object | no | Object: title (required), objective (required), questlineId, tags, rewards, dueAt, notBeforeAt, scheduledStartAt, scheduledEndAt, allDay, estimateSeconds |
179
183
  | quest.title | string | yes | Title for the new quest. |
180
184
  | quest.objective | string | yes | Objective for the new quest. |
181
- | questline | object | no | |
185
+ | quest.questlineId | integer | no | Optional questline to place the quest in. |
186
+ | quest.tags | array | no | Optional tags to apply at creation. |
187
+ | quest.rewards | array | no | Optional rewards to attach at creation. |
188
+ | quest.rewards[].kind | string | yes | Reward kind. |
189
+ | quest.rewards[].name | string | yes | Reward name. |
190
+ | quest.dueAt | integer | no | Optional latest acceptable completion timestamp. |
191
+ | quest.notBeforeAt | integer | no | Optional earliest actionable timestamp. |
192
+ | quest.scheduledStartAt | integer | no | Optional planned schedule window start. |
193
+ | quest.scheduledEndAt | integer | no | Optional planned schedule window end. |
194
+ | quest.allDay | boolean | no | Whether the scheduled window is all-day. |
195
+ | quest.estimateSeconds | integer | no | Optional active-effort estimate in seconds. |
196
+ | questline | object | no | Object: title (required), description |
182
197
  | questline.title | string | yes | Questline title. |
183
198
  | questline.description | string | no | Optional questline description. |
184
199
  | rumorId | integer | no | Rumor to settle, dismiss, or reopen. |
185
200
  | dismissedAt | integer | no | Optional dismissal timestamp. |
186
201
  | settledAt | integer | no | Optional settlement timestamp. |
187
202
  | quests | array | no | Quests to create during rumor settlement. Include timing and tags directly. |
203
+ | quests[].title | string | yes | Title for a created quest. |
204
+ | quests[].objective | string | yes | Objective for a created quest. |
205
+ | quests[].tags | array | no | Optional tags to apply at creation. |
206
+ | quests[].rewards | array | no | Optional rewards to attach at creation. |
207
+ | quests[].rewards[].kind | string | yes | Reward kind. |
208
+ | quests[].rewards[].name | string | yes | Reward name. |
209
+ | quests[].dueAt | integer | no | Optional latest acceptable completion timestamp. |
210
+ | quests[].notBeforeAt | integer | no | Optional earliest actionable timestamp. |
211
+ | quests[].scheduledStartAt | integer | no | Optional planned schedule window start. |
212
+ | quests[].scheduledEndAt | integer | no | Optional planned schedule window end. |
213
+ | quests[].allDay | boolean | no | Whether the scheduled window is all-day. |
214
+ | quests[].estimateSeconds | integer | no | Optional active-effort estimate in seconds. |
188
215
  | now | integer | no | Optional timestamp for membership, reopen, or rumor update. |
189
216
  | title | string | no | New title for the update_rumor action. |
190
217
  | details | string | no | New or enriched context text for the update_rumor action. |
@@ -244,6 +271,8 @@ Writes: writes_state.
244
271
  | outcome | string | no | Terminal outcome text. Required for finish, abandon, and abandon_and_spawn_followups. |
245
272
  | resolvedAt | integer | no | Optional resolved timestamp for finish or abandon actions. |
246
273
  | followups | array | no | Follow-up quests to create atomically. Required for abandon_and_spawn_followups. |
274
+ | followups[].title | string | yes | Title for a follow-up quest. |
275
+ | followups[].objective | string | yes | Objective for a follow-up quest. |
247
276
 
248
277
  ```sh
249
278
  ghostpaw-questlog call run_quest '{"action":"start","questId":1}'
@@ -295,7 +324,7 @@ Writes: writes_state.
295
324
  | --- | --- | --- | --- |
296
325
  | action | string | yes | archive, create, preview_due, spawn_due, update. Action to perform. |
297
326
  | repeatableQuestId | integer | no | Repeatable quest to archive or update. |
298
- | repeatableQuest | object | no | |
327
+ | repeatableQuest | object | no | Object: title (required), objective (required), rrule (required), anchorAt (required) |
299
328
  | repeatableQuest.title | string | yes | Title for the new repeatable quest. |
300
329
  | repeatableQuest.objective | string | yes | Objective for the new repeatable quest. |
301
330
  | repeatableQuest.rrule | string | yes | RRULE describing recurrence. |
@@ -333,7 +362,7 @@ Writes: writes_state.
333
362
  | Parameter | Type | Required | Values / description |
334
363
  | --- | --- | --- | --- |
335
364
  | action | string | yes | add, remove, replace. Action to perform. |
336
- | target | object | yes | |
365
+ | target | object | yes | Object: id (required), kind (required) |
337
366
  | target.id | integer | yes | Target entity id. |
338
367
  | target.kind | string | yes | quest, repeatable_quest. Target kind. |
339
368
  | tagNames | array | yes | Tag names to add, remove, or replace. |
@@ -358,14 +387,16 @@ Writes: writes_state.
358
387
  | Parameter | Type | Required | Values / description |
359
388
  | --- | --- | --- | --- |
360
389
  | action | string | yes | add, claim, remove, replace_repeatable_template, update. Action to perform. |
361
- | target | object | yes | |
390
+ | target | object | yes | Object: id (required), kind (required) |
362
391
  | target.id | integer | yes | Target entity id. |
363
392
  | target.kind | string | yes | quest, reward, repeatable_quest. Target kind. |
364
- | reward | object | no | |
393
+ | reward | object | no | Object: kind (required), name (required), now |
365
394
  | reward.kind | string | yes | Reward kind. |
366
395
  | reward.name | string | yes | Reward name. |
367
396
  | reward.now | integer | no | Optional update timestamp for the update action. |
368
397
  | rewards | array | no | Full future reward template set. Required for replace_repeatable_template. |
398
+ | rewards[].kind | string | yes | Reward kind. |
399
+ | rewards[].name | string | yes | Reward name. |
369
400
  | claimedAt | integer | no | Optional claim timestamp. |
370
401
  | now | integer | no | Optional timestamp for removal or template replacement. |
371
402
 
@@ -399,32 +430,32 @@ ghostpaw-questlog call retire_work '{"action":"hide","target":{"kind":"quest","i
399
430
 
400
431
  ### Workflow guides
401
432
 
402
- - `ghostpaw-questlog skill webhooks` ([webhooks](skills/questlog/references/webhooks.md)): Configure signed outbound event webhooks and verify deliveries.
403
- - `ghostpaw-questlog skill archive-delete-and-prune` ([archive-delete-and-prune](skills/questlog/references/archive-delete-and-prune.md)): Choose the right retirement action for each kind of work so history stays honest and hiding does not get confused with archival or terminal execution truth.
404
- - `ghostpaw-questlog skill choose-work-shape` ([choose-work-shape](skills/questlog/references/choose-work-shape.md)): Choose the right structure for new work by separating uncertainty, one-shot execution, multi-step arcs, and recurring patterns.
405
- - `ghostpaw-questlog skill clarify-ambiguous-requests` ([clarify-ambiguous-requests](skills/questlog/references/clarify-ambiguous-requests.md)): Treat ambiguity as a real state, capture uncertainty honestly, and only commit work structure once the next executable shape is clear.
406
- - `ghostpaw-questlog skill close-work-and-claim-outcomes` ([close-work-and-claim-outcomes](skills/questlog/references/close-work-and-claim-outcomes.md)): Close work with the correct terminal lifecycle action, then handle any concrete reward claim as a separate explicit step.
407
- - `ghostpaw-questlog skill create-concrete-quest` ([create-concrete-quest](skills/questlog/references/create-concrete-quest.md)): Create real, bounded executable quests only when the objective is concrete enough to execute without interpretive guesswork.
408
- - `ghostpaw-questlog skill detect-slips-and-staleness` ([detect-slips-and-staleness](skills/questlog/references/detect-slips-and-staleness.md)): Detect work that is slipping or going stale by reading overdue, deferred, in-progress, and related operational views before choosing one recovery move.
409
- - `ghostpaw-questlog skill escalate-and-unblock` ([escalate-and-unblock](skills/questlog/references/escalate-and-unblock.md)): Diagnose blockers honestly, then remove or realign hard gates when the model is wrong instead of treating every delay as generic escalation theater.
410
- - `ghostpaw-questlog skill frame-initiative-questline` ([frame-initiative-questline](skills/questlog/references/frame-initiative-questline.md)): Use questlines for shared initiative context, then attach concrete quests and maintain the structure without confusing one quest with one arc.
411
- - `ghostpaw-questlog skill handle-exceptions-and-weird-cases` ([handle-exceptions-and-weird-cases](skills/questlog/references/handle-exceptions-and-weird-cases.md)): Handle weird cases by diagnosing the actual tool outcome, routing into clarification or recovery when needed, and refusing to fake a clean normal-state story.
412
- - `ghostpaw-questlog skill intake-triage` ([intake-triage](skills/questlog/references/intake-triage.md)): Triage new asks, ideas, and vague incoming work into the right next step without prematurely committing execution structure.
413
- - `ghostpaw-questlog skill maintain-clear-ownership` ([maintain-clear-ownership](skills/questlog/references/maintain-clear-ownership.md)): Keep ownership explicit through quest content and classification because Questlog does not provide a separate assignee field or ownership tool.
414
- - `ghostpaw-questlog skill manage-approvals-and-promises` ([manage-approvals-and-promises](skills/questlog/references/manage-approvals-and-promises.md)): Treat approvals and commitments as modeled work with timing and dependencies instead of burying them as vague narrative promises.
415
- - `ghostpaw-questlog skill plan-successor-work` ([plan-successor-work](skills/questlog/references/plan-successor-work.md)): Create explicit successor work after closure by spawning follow-ups or shaping new work, while keeping uncertain next steps in intake instead of faking certainty.
416
- - `ghostpaw-questlog skill plan-time-correctly` ([plan-time-correctly](skills/questlog/references/plan-time-correctly.md)): Use quest planning fields honestly so availability, due pressure, and schedule intent stay meaningful instead of getting collapsed into one generic date.
417
- - `ghostpaw-questlog skill prioritize-across-streams` ([prioritize-across-streams](skills/questlog/references/prioritize-across-streams.md)): Prioritize across competing work streams by reading real candidate sets, distinguishing available from blocked work, and then reflecting the choice in planning.
418
- - `ghostpaw-questlog skill protect-capacity-and-focus` ([protect-capacity-and-focus](skills/questlog/references/protect-capacity-and-focus.md)): Protect capacity and focus by reading the real in-progress and scheduled-now load, then using planning to rebalance instead of silently overcommitting.
419
- - `ghostpaw-questlog skill recover-from-modeling-mistakes` ([recover-from-modeling-mistakes](skills/questlog/references/recover-from-modeling-mistakes.md)): Recover from modeling mistakes by diagnosing the exact current state first, then applying the smallest honest corrective tool instead of rewriting history blindly.
420
- - `ghostpaw-questlog skill report-status-and-manage-expectations` ([report-status-and-manage-expectations](skills/questlog/references/report-status-and-manage-expectations.md)): Build status reporting from the real questlog state, separating available, blocked, in-progress, slipping, and resolved work without relying on memory.
421
- - `ghostpaw-questlog skill revise-or-reshape-commitment` ([revise-or-reshape-commitment](skills/questlog/references/revise-or-reshape-commitment.md)): Revise a quest objective before execution starts, but reshape commitments honestly with lifecycle tools once real execution history exists.
422
- - `ghostpaw-questlog skill run-operational-review` ([run-operational-review](skills/questlog/references/run-operational-review.md)): Drive daily or weekly operational review from structured list views, then move from diagnosis into actual planning or execution changes.
423
- - `ghostpaw-questlog skill run-recurring-rhythms` ([run-recurring-rhythms](skills/questlog/references/run-recurring-rhythms.md)): Model recurring work as repeatable definitions that spawn fresh concrete quests over time instead of living forever as one immortal task.
424
- - `ghostpaw-questlog skill search-and-retrieve-context` ([search-and-retrieve-context](skills/questlog/references/search-and-retrieve-context.md)): Find relevant questlog context quickly, then switch from discovery to exact inspection without confusing free-text search with structured review surfaces.
425
- - `ghostpaw-questlog skill sequence-with-unlocks` ([sequence-with-unlocks](skills/questlog/references/sequence-with-unlocks.md)): Model hard sequencing with unlocks between concrete quests so blocked and available work stay truthful and reviewable.
426
- - `ghostpaw-questlog skill settle-rumor-into-structure` ([settle-rumor-into-structure](skills/questlog/references/settle-rumor-into-structure.md)): Turn a ready rumor into committed quest or questline structure without losing provenance or pretending vague intake is settled.
427
- - `ghostpaw-questlog skill tag-and-filter-portfolios` ([tag-and-filter-portfolios](skills/questlog/references/tag-and-filter-portfolios.md)): Use tags for cross-cutting classification and portfolio filtering without confusing tags for status, timing, or dependency modeling.
433
+ - `ghostpaw-questlog skill webhooks`: Configure signed outbound event webhooks and verify deliveries.
434
+ - `ghostpaw-questlog skill archive-delete-and-prune`: Choose the right retirement action for each kind of work so history stays honest and hiding does not get confused with archival or terminal execution truth.
435
+ - `ghostpaw-questlog skill choose-work-shape`: Choose the right structure for new work by separating uncertainty, one-shot execution, multi-step arcs, and recurring patterns.
436
+ - `ghostpaw-questlog skill clarify-ambiguous-requests`: Treat ambiguity as a real state, capture uncertainty honestly, and only commit work structure once the next executable shape is clear.
437
+ - `ghostpaw-questlog skill close-work-and-claim-outcomes`: Close work with the correct terminal lifecycle action, then handle any concrete reward claim as a separate explicit step.
438
+ - `ghostpaw-questlog skill create-concrete-quest`: Create real, bounded executable quests only when the objective is concrete enough to execute without interpretive guesswork.
439
+ - `ghostpaw-questlog skill detect-slips-and-staleness`: Detect work that is slipping or going stale by reading overdue, deferred, in-progress, and related operational views before choosing one recovery move.
440
+ - `ghostpaw-questlog skill escalate-and-unblock`: Diagnose blockers honestly, then remove or realign hard gates when the model is wrong instead of treating every delay as generic escalation theater.
441
+ - `ghostpaw-questlog skill frame-initiative-questline`: Use questlines for shared initiative context, then attach concrete quests and maintain the structure without confusing one quest with one arc.
442
+ - `ghostpaw-questlog skill handle-exceptions-and-weird-cases`: Handle weird cases by diagnosing the actual tool outcome, routing into clarification or recovery when needed, and refusing to fake a clean normal-state story.
443
+ - `ghostpaw-questlog skill intake-triage`: Triage new asks, ideas, and vague incoming work into the right next step without prematurely committing execution structure.
444
+ - `ghostpaw-questlog skill maintain-clear-ownership`: Keep ownership explicit through quest content and classification because Questlog does not provide a separate assignee field or ownership tool.
445
+ - `ghostpaw-questlog skill manage-approvals-and-promises`: Treat approvals and commitments as modeled work with timing and dependencies instead of burying them as vague narrative promises.
446
+ - `ghostpaw-questlog skill plan-successor-work`: Create explicit successor work after closure by spawning follow-ups or shaping new work, while keeping uncertain next steps in intake instead of faking certainty.
447
+ - `ghostpaw-questlog skill plan-time-correctly`: Use quest planning fields honestly so availability, due pressure, and schedule intent stay meaningful instead of getting collapsed into one generic date.
448
+ - `ghostpaw-questlog skill prioritize-across-streams`: Prioritize across competing work streams by reading real candidate sets, distinguishing available from blocked work, and then reflecting the choice in planning.
449
+ - `ghostpaw-questlog skill protect-capacity-and-focus`: Protect capacity and focus by reading the real in-progress and scheduled-now load, then using planning to rebalance instead of silently overcommitting.
450
+ - `ghostpaw-questlog skill recover-from-modeling-mistakes`: Recover from modeling mistakes by diagnosing the exact current state first, then applying the smallest honest corrective tool instead of rewriting history blindly.
451
+ - `ghostpaw-questlog skill report-status-and-manage-expectations`: Build status reporting from the real questlog state, separating available, blocked, in-progress, slipping, and resolved work without relying on memory.
452
+ - `ghostpaw-questlog skill revise-or-reshape-commitment`: Revise a quest objective before execution starts, but reshape commitments honestly with lifecycle tools once real execution history exists.
453
+ - `ghostpaw-questlog skill run-operational-review`: Drive daily or weekly operational review from structured list views, then move from diagnosis into actual planning or execution changes.
454
+ - `ghostpaw-questlog skill run-recurring-rhythms`: Model recurring work as repeatable definitions that spawn fresh concrete quests over time instead of living forever as one immortal task.
455
+ - `ghostpaw-questlog skill search-and-retrieve-context`: Find relevant questlog context quickly, then switch from discovery to exact inspection without confusing free-text search with structured review surfaces.
456
+ - `ghostpaw-questlog skill sequence-with-unlocks`: Model hard sequencing with unlocks between concrete quests so blocked and available work stay truthful and reviewable.
457
+ - `ghostpaw-questlog skill settle-rumor-into-structure`: Turn a ready rumor into committed quest or questline structure without losing provenance or pretending vague intake is settled.
458
+ - `ghostpaw-questlog skill tag-and-filter-portfolios`: Use tags for cross-cutting classification and portfolio filtering without confusing tags for status, timing, or dependency modeling.
428
459
 
429
460
  ### Principles
430
461
 
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env node
2
+ // src/surfaces/schema_rows.ts
3
+ function schemaRows(schema, prefix = "") {
4
+ const properties = { ...schema.properties };
5
+ const required = new Set(schema.required);
6
+ for (const branch of schema.oneOf ?? []) {
7
+ Object.assign(properties, branch.properties);
8
+ for (const key of branch.required ?? []) required.add(key);
9
+ }
10
+ return Object.entries(properties).flatMap(([key, property]) => {
11
+ const nested = property.items ?? property;
12
+ const children = nested.properties ?? Object.assign({}, ...(nested.oneOf ?? []).map((b) => b.properties));
13
+ const description = property.description || (Object.keys(children).length ? `Object: ${Object.keys(children).map((name) => `${name}${nested.required?.includes(name) ? " (required)" : ""}`).join(", ")}` : property.type === "array" ? `Array of ${property.items?.type ?? "values"}` : `Value for ${key}`);
14
+ return [
15
+ `${prefix}${key} | ${property.type ?? "oneOf"} | ${required.has(key) ? "yes" : "no"} | ${(property.enum ? `${property.enum.join(", ")}. ` : "") + description.replaceAll("|", "\\|").replaceAll("\n", " ")}`,
16
+ ...schemaRows(nested, `${prefix}${key}${property.items ? "[]" : ""}.`)
17
+ ];
18
+ });
19
+ }
20
+
21
+ export { schemaRows };
@@ -4,20 +4,26 @@ import { readFileSync } from 'node:fs';
4
4
  var CliUsageError = class extends Error {
5
5
  exitCode = 2;
6
6
  };
7
+ function readFileValue(raw, name) {
8
+ if (!raw.startsWith("@")) return raw;
9
+ if (raw.startsWith("@@")) return raw.slice(1);
10
+ const path = raw.slice(1);
11
+ if (!path) throw new CliUsageError(`Empty file path for ${name}; use @@ for a literal leading @`);
12
+ try {
13
+ return readFileSync(path === "-" ? 0 : path, "utf8");
14
+ } catch {
15
+ throw new CliUsageError(
16
+ `Cannot read value file for ${name}: ${path} (use @@ for a literal leading @)`
17
+ );
18
+ }
19
+ }
7
20
  var keyOf = (name) => name.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
8
21
  function parseCliArgs(argv, spec) {
9
22
  const values = {};
10
23
  const positions = [];
11
24
  const flags = spec.flags ?? [];
12
25
  const convert = (raw, arg) => {
13
- let value = raw;
14
- if (raw.startsWith("@")) {
15
- try {
16
- value = readFileSync(raw.slice(1), "utf8");
17
- } catch {
18
- throw new CliUsageError(`Cannot read value file: ${raw.slice(1)}`);
19
- }
20
- }
26
+ const value = arg.file ? readFileValue(raw, arg.name) : raw;
21
27
  let result = value;
22
28
  if (arg.type === "number" || arg.type === "integer") {
23
29
  result = Number(value);
@@ -94,7 +100,7 @@ function parseCliArgs(argv, spec) {
94
100
  return values;
95
101
  }
96
102
  function renderCliHelp(spec) {
97
- const describe = (arg) => `${arg.type}${arg.enum ? ` (${arg.enum.join("|")})` : ""}${arg.required ? " required" : ""} ${arg.description ?? ""}`;
103
+ const describe = (arg) => `${arg.type}${arg.enum ? ` (${arg.enum.join("|")})` : ""}${arg.required ? " required" : ""}${arg.file ? " (@file, @- stdin, @@ literal @)" : ""} ${arg.description ?? ""}`;
98
104
  return `Usage: ${spec.command} ${(spec.positionals ?? []).map((arg) => `<${arg.name}${arg.variadic ? "..." : ""}>`).join(" ")} [flags]
99
105
  ${spec.description ?? ""}
100
106
 
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env node
2
+ // src/hooks/list_deliveries.ts
3
+ function listDeliveries(db, options = {}) {
4
+ return db.prepare(
5
+ `SELECT * FROM hook_deliveries ${options.includeDelivered ? "" : "WHERE status <> 'delivered'"} ORDER BY target_id, next_attempt_at, id`
6
+ ).all().map(
7
+ (row) => ({
8
+ id: String(row.id),
9
+ targetId: Number(row.target_id),
10
+ events: JSON.parse(String(row.events)),
11
+ status: row.status,
12
+ attempts: Number(row.attempts),
13
+ lastAttemptAt: row.last_attempt_at == null ? null : Number(row.last_attempt_at),
14
+ httpStatus: row.http_status == null ? null : Number(row.http_status),
15
+ errorClass: row.error_class == null ? null : String(row.error_class),
16
+ nextAttemptAt: Number(row.next_attempt_at)
17
+ })
18
+ ).filter(
19
+ (d) => options.questId === void 0 || d.events.some((e) => e.questId === options.questId)
20
+ );
21
+ }
22
+
23
+ export { listDeliveries };
@@ -1,7 +1,44 @@
1
1
  #!/usr/bin/env node
2
+ import { listDeliveries } from './chunk-D6EGZCT7.js';
3
+ import { listHooks } from './chunk-NBCCA6AX.js';
2
4
  import { randomBytes, createHmac } from 'node:crypto';
3
5
  import { basename, extname } from 'node:path';
4
6
 
7
+ // src/with_transaction.ts
8
+ var transactionDepth = /* @__PURE__ */ new WeakMap();
9
+ function withTransaction(db, fn) {
10
+ const depth = transactionDepth.get(db) ?? 0;
11
+ const savepoint = `questlog_${depth}`;
12
+ if (depth === 0) {
13
+ db.exec("BEGIN IMMEDIATE");
14
+ } else {
15
+ db.exec(`SAVEPOINT ${savepoint}`);
16
+ }
17
+ transactionDepth.set(db, depth + 1);
18
+ try {
19
+ const result = fn();
20
+ if (depth === 0) {
21
+ db.exec("COMMIT");
22
+ } else {
23
+ db.exec(`RELEASE SAVEPOINT ${savepoint}`);
24
+ }
25
+ transactionDepth.set(db, depth);
26
+ return result;
27
+ } catch (error) {
28
+ try {
29
+ if (depth === 0) {
30
+ db.exec("ROLLBACK");
31
+ } else {
32
+ db.exec(`ROLLBACK TO SAVEPOINT ${savepoint}`);
33
+ db.exec(`RELEASE SAVEPOINT ${savepoint}`);
34
+ }
35
+ } catch {
36
+ }
37
+ transactionDepth.set(db, depth);
38
+ throw error;
39
+ }
40
+ }
41
+
5
42
  // src/tags/normalize_tag_name.ts
6
43
  function normalizeTagName(name) {
7
44
  return name.trim().toLowerCase().replace(/\s+/g, " ");
@@ -24,13 +61,20 @@ function addHook(db, input) {
24
61
  } catch {
25
62
  throw new Error("Expected an HTTP or HTTPS URL");
26
63
  }
27
- if (!["http:", "https:"].includes(url.protocol)) throw new Error("Expected an HTTP or HTTPS URL");
64
+ if (!["http:", "https:"].includes(url.protocol))
65
+ throw new Error(`unsupported scheme ${url.protocol}; use http or https`);
66
+ if (url.username || url.password)
67
+ throw new Error(
68
+ "URL userinfo is not supported; use a custom header (--header or --header-stdin) or a query key instead"
69
+ );
28
70
  const minGap = input.minGap ?? 60;
29
71
  if (!Number.isSafeInteger(minGap) || minGap < 0)
30
72
  throw new Error("Min gap must be a nonnegative integer");
31
73
  const events = input.events ?? [...hookEventTypes];
32
74
  if (!events.length || events.some((e) => !hookEventTypes.includes(e)))
33
- throw new Error("Unknown event filter");
75
+ throw new Error(
76
+ `unknown event ${events.find((e) => !hookEventTypes.includes(e)) ?? "(empty)"}; valid: ${hookEventTypes.join(", ")}`
77
+ );
34
78
  if (input.tag !== void 0 && !normalizeTagName(input.tag))
35
79
  throw new Error("Expected a tag name");
36
80
  const secret = input.secret ?? randomBytes(32).toString("hex");
@@ -62,67 +106,6 @@ function hookLabel(db, name) {
62
106
  return path ? basename(path, extname(path)) : "questlog";
63
107
  }
64
108
 
65
- // src/hooks/list_hooks.ts
66
- function listHooks(db) {
67
- return db.prepare(`SELECT id, url, tag, events, min_gap, created_at, last_wake,
68
- (SELECT COUNT(*) FROM hook_assignments a LEFT JOIN hook_deliveries d ON d.id = a.delivery_id WHERE a.target_id = t.id AND (a.delivery_id IS NULL OR d.status = 'pending')) AS pending,
69
- (SELECT COUNT(*) FROM hook_deliveries d WHERE d.target_id = t.id AND d.status = 'failed') AS failed
70
- FROM hook_targets t ORDER BY id`).all().map((row) => {
71
- const url = new URL(String(row.url));
72
- url.username = "";
73
- url.password = "";
74
- url.search = "";
75
- url.hash = "";
76
- return {
77
- id: Number(row.id),
78
- scope: row.tag == null ? "database" : "tag",
79
- url: url.href,
80
- tag: row.tag == null ? null : String(row.tag),
81
- events: JSON.parse(String(row.events)),
82
- minGap: Number(row.min_gap),
83
- createdAt: Number(row.created_at),
84
- lastWake: row.last_wake == null ? null : Number(row.last_wake),
85
- pending: Number(row.pending),
86
- failed: Number(row.failed)
87
- };
88
- });
89
- }
90
-
91
- // src/with_transaction.ts
92
- var transactionDepth = /* @__PURE__ */ new WeakMap();
93
- function withTransaction(db, fn) {
94
- const depth = transactionDepth.get(db) ?? 0;
95
- const savepoint = `questlog_${depth}`;
96
- if (depth === 0) {
97
- db.exec("BEGIN IMMEDIATE");
98
- } else {
99
- db.exec(`SAVEPOINT ${savepoint}`);
100
- }
101
- transactionDepth.set(db, depth + 1);
102
- try {
103
- const result = fn();
104
- if (depth === 0) {
105
- db.exec("COMMIT");
106
- } else {
107
- db.exec(`RELEASE SAVEPOINT ${savepoint}`);
108
- }
109
- transactionDepth.set(db, depth);
110
- return result;
111
- } catch (error) {
112
- try {
113
- if (depth === 0) {
114
- db.exec("ROLLBACK");
115
- } else {
116
- db.exec(`ROLLBACK TO SAVEPOINT ${savepoint}`);
117
- db.exec(`RELEASE SAVEPOINT ${savepoint}`);
118
- }
119
- } catch {
120
- }
121
- transactionDepth.set(db, depth);
122
- throw error;
123
- }
124
- }
125
-
126
109
  // src/hooks/remove_hook.ts
127
110
  function removeHook(db, id) {
128
111
  withTransaction(db, () => {
@@ -133,7 +116,7 @@ function removeHook(db, id) {
133
116
  }
134
117
 
135
118
  // src/version.ts
136
- var questlogVersion = "0.10.1";
119
+ var questlogVersion = "0.11.1";
137
120
 
138
121
  // src/hooks/send_hook_delivery.ts
139
122
  async function sendHookDelivery(db, delivery, target, options) {
@@ -154,8 +137,13 @@ async function sendHookDelivery(db, delivery, target, options) {
154
137
  method: "POST",
155
138
  redirect: "error",
156
139
  headers: {
157
- "content-type": "application/json",
158
140
  "user-agent": `questlog/${questlogVersion}`,
141
+ ...JSON.parse(
142
+ String(
143
+ db.prepare("SELECT headers FROM hook_targets WHERE id = ?").get(target.id)?.headers ?? "{}"
144
+ )
145
+ ),
146
+ "content-type": "application/json",
159
147
  "x-questlog-delivery": delivery.id,
160
148
  "x-questlog-timestamp": timestamp,
161
149
  "x-questlog-signature": `sha256=${signature}`
@@ -815,27 +803,6 @@ function detectHookEvents(db, now) {
815
803
  );
816
804
  }
817
805
 
818
- // src/hooks/list_deliveries.ts
819
- function listDeliveries(db, options = {}) {
820
- return db.prepare(
821
- `SELECT * FROM hook_deliveries ${options.includeDelivered ? "" : "WHERE status <> 'delivered'"} ORDER BY target_id, next_attempt_at, id`
822
- ).all().map(
823
- (row) => ({
824
- id: String(row.id),
825
- targetId: Number(row.target_id),
826
- events: JSON.parse(String(row.events)),
827
- status: row.status,
828
- attempts: Number(row.attempts),
829
- lastAttemptAt: row.last_attempt_at == null ? null : Number(row.last_attempt_at),
830
- httpStatus: row.http_status == null ? null : Number(row.http_status),
831
- errorClass: row.error_class == null ? null : String(row.error_class),
832
- nextAttemptAt: Number(row.next_attempt_at)
833
- })
834
- ).filter(
835
- (d) => options.questId === void 0 || d.events.some((e) => e.questId === options.questId)
836
- );
837
- }
838
-
839
806
  // src/hooks/tick_questlog.ts
840
807
  async function tickQuestlog(db, options = {}) {
841
808
  const now = options.now ?? options.clock?.() ?? Date.now();
@@ -979,4 +946,4 @@ async function watchQuestlog(db, options) {
979
946
  }
980
947
  }
981
948
 
982
- export { addHook, deriveQuestMarkerId, deriveQuestState, getQuestOrThrow, getRepeatableQuestOrThrow, hookLabel, listDeliveries, listDueRepeatableQuestAnchors, listHooks, mapQuestRewardRow, normalizeTagName, parseRRule, queryQuestDetailRecords, questlogVersion, removeHook, resolveNow, spawnDueRepeatableQuests, testHook, tickQuestlog, watchQuestlog, withTransaction };
949
+ export { addHook, deriveQuestMarkerId, deriveQuestState, getQuestOrThrow, getRepeatableQuestOrThrow, hookLabel, listDueRepeatableQuestAnchors, mapQuestRewardRow, normalizeTagName, parseRRule, queryQuestDetailRecords, questlogVersion, removeHook, resolveNow, spawnDueRepeatableQuests, testHook, tickQuestlog, watchQuestlog, withTransaction };
@@ -728,7 +728,7 @@ Failure handling:
728
728
  var webhooksSkill = defineQuestlogSkill({
729
729
  name: "webhooks",
730
730
  description: "Configure signed outbound event webhooks and verify deliveries.",
731
- content: '# Webhooks\n\nUse `questlog` to configure opt-in outbound automation. No server or scheduler is added.\nNo targets means no network calls. `manage_hooks` supports list, remove, tick with\n`dry_run`, and deliveries; add/test are CLI-only. `review_questlog` view `deliveries`\nand `questlog show <id>` expose pending/failed deliveries.\n\n```sh\nquestlog hook add https://example.org/wake --min-gap 60\nquestlog hook add https://example.org/team --tag work --events quest.due,quest.overdue --secret-stdin\nquestlog hook label work\nquestlog hook list\nquestlog tick --dry-run\nquestlog tick\nquestlog watch --interval 60\nquestlog hook test 1\nquestlog review deliveries\nquestlog show 12\nquestlog hook remove 1\n```\n\n`hook add` prints its random 32-byte signing secret once, also as `secret` in `--json`.\nSave it securely. `--secret-stdin` supplies one via stdin; there is no secret flag.\nList output redacts userinfo/query strings. Removal deletes the target and deliveries.\nDefault scope is the database; `--tag` matches quest tags, never rumors.\n`--events` defaults to all types. The DB label defaults to the filename without its\npath/extension (`questlog` in memory); set it to a name suitable for disclosure.\n\n```cron\n* * * * * questlog tick\n```\n\nCron\'s PATH may need `$HOME/.local/bin/questlog tick --db /path/to/quests.db`.\nWatch defaults to 60 seconds (minimum 5); SIGINT/SIGTERM finishes the current tick.\nTick exits 0 even when deliveries need retries. Dry-run records nothing and sends nothing.\n\nEvents are detected from the model at tick time:\n\n- `quest.due`: an unresolved, undeleted quest reaches its effective due time (own `due_at`, otherwise its undeleted questline\'s due time).\n- `quest.overdue`: that effective deadline is strictly earlier than now and the quest remains unresolved.\n- `quest.unblocked`: an unresolved quest is available, all active prerequisites have succeeded or been deleted, and at least one prerequisite succeeded after its unlock was installed. Occurrence time is the latest active prerequisite success or not-before time, whichever is later.\n- `repeatable.spawned`: a concrete repeatable instance was created; occurrence time is its creation time. Tick first materializes due anchors through the existing spawn operation.\n- `rumor.captured`: an undeleted rumor was created; occurrence time is its creation time, even if subsequently settled or dismissed.\n\nPersistent event keys contain type, entity ids, and occurrence time. Restarts and repeated\nticks do not repeat occurrences. Removed transitions between ticks cannot be reconstructed.\nOnly occurrences at/after target creation are assigned; tags are evaluated at assignment.\nTick materializes due repeatables using the existing spawn path, including during preview\nwith rollback. Batches contain only ids, event types, timestamps, and the DB label:\n\n```json\n{"type":"questlog.events","db":"work","deliveryId":"d_\u2026","sentAt":"2026-10-28T10:00:00.000Z","events":[{"id":"e_\u2026","type":"quest.due","questId":12,"occurredAt":"2026-10-28T09:59:00.000Z"}]}\n```\n\nHeaders are `content-type: application/json`, `user-agent: questlog/<version>`,\n`x-questlog-delivery: <deliveryId>`, `x-questlog-timestamp: <unix seconds>`, and\n`x-questlog-signature: sha256=<hex HMAC-SHA256(secret, timestamp + "." + rawBody)>`.\nNo titles, objectives, details, or tag text are sent. `hook test` sends a logged `hook.test`\nevent immediately, ignoring the min gap. Add and test are CLI-only in the tool facade;\n`manage_hooks` supports list, remove, tick (with `dry_run`), and deliveries.\n\n2xx delivers. Timeout is 5 seconds. Failures retry after 30 s, 1 m, 2 m, 4 m, doubling\nto 1 h plus <1 s jitter, subject to the target\'s min gap. Eight failures mark failed.\nRetries keep the delivery id/events; new events queue behind them. Keep the last 500\ndelivered log entries per target and all pending/failed entries. Transactional claims\nlease work for two minutes. A crash after receipt may retry: dedupe at the receiver.\n\n### Verifying a delivery\n\nCompute HMAC-SHA256 over `timestamp + "." + rawBody` with the secret and compare in constant\ntime. Reject timestamps older than 5 minutes (and timestamps too far in the future), then\ndedupe on `x-questlog-delivery`. Use durable deduplication storage in a production receiver.\nThis 15-line Node example uses an in-memory set:\n\n```js\nimport { createHmac, timingSafeEqual } from \'node:crypto\';\nimport { createServer } from \'node:http\';\nconst secret = process.env.WEBHOOK_SECRET, seen = new Set();\ncreateServer(async (req, res) => {\n const chunks = []; for await (const chunk of req) chunks.push(chunk);\n const rawBody = Buffer.concat(chunks), h = req.headers;\n const timestamp = h[\'x-questlog-timestamp\'], id = h[\'x-questlog-delivery\'];\n const age = Date.now() / 1000 - Number(timestamp);\n const expected = createHmac(\'sha256\', secret).update(timestamp + \'.\').update(rawBody).digest();\n const signature = String(h[\'x-questlog-signature\'] || \'\');\n const actual = Buffer.from(signature.slice(7), \'hex\');\n if (!id || !/^\\d+$/.test(timestamp) || age > 300 || age < -300 || !/^sha256=[a-f0-9]{64}$/.test(signature) || actual.length !== expected.length || !timingSafeEqual(actual, expected)) { res.writeHead(401).end(); return; }\n if (!seen.has(id)) { seen.add(id); console.log(JSON.parse(rawBody)); }\n res.writeHead(204).end();\n}).listen(8080, \'127.0.0.1\');\n```\n'
731
+ content: '# Webhooks\n\nUse `questlog` to configure opt-in outbound automation. No server or scheduler is added.\nNo targets means no network calls. `manage_hooks` supports list, remove, tick with\n`dry_run`, and deliveries; add/test are CLI-only. `review_questlog` view `deliveries`\nand `questlog show <id>` expose pending/failed deliveries.\n\n```sh\nquestlog hook add https://example.org/wake --min-gap 60\nquestlog hook add https://example.org/team --tag work --events quest.due,quest.overdue --secret-stdin\nquestlog hook label work\nquestlog hook list\nquestlog tick --dry-run\nquestlog tick\nquestlog watch --interval 60\nquestlog hook test 1\nquestlog review deliveries\nquestlog show 12\nquestlog hook remove 1\n```\n\n`hook add` prints its random 32-byte signing secret once, also as `secret` in `--json`.\nSave it securely. `--secret-stdin` supplies one via stdin; there is no secret flag.\nSecrets and custom header values are stored in the DB. New POSIX DB/WAL/SHM files use 0600 and created directories use 0700; existing DB modes are preserved. Windows uses existing access controls.\nUse repeatable `--header "Name: value"` or `--header-stdin` (one line per header, avoiding shell history). Headers are sent on every delivery and test; output shows names with `***` values. HTTP token names are required; host, content-length, content-type, and x-questlog-* are reserved. Change headers by removing and adding the hook again. Use --header-stdin and --secret-stdin separately. URL userinfo is rejected; use a custom header or query key.\n`questlog show d_\u2026` displays delivery status, attempts, HTTP status, next attempt, redacted target URL and event ids. Text is the default; --json prints JSON. Missing hook remove/test targets exit 3; missing quests/deliveries exit 1. hook label names the database.\nList output redacts userinfo/query strings. Removal deletes the target and deliveries.\nDefault scope is the database; `--tag` matches quest tags, never rumors.\n`--events` defaults to all types. The DB label defaults to the filename without its\npath/extension (`questlog` in memory); set it to a name suitable for disclosure.\n\n```cron\n* * * * * questlog tick\n```\n\nCron\'s PATH may need `$HOME/.local/bin/questlog tick --db /path/to/quests.db`.\nWatch defaults to 60 seconds (minimum 5); SIGINT/SIGTERM finishes the current tick.\nTick exits 0 even when deliveries need retries. Dry-run records nothing and sends nothing.\n\nEvents are detected from the model at tick time:\n\n- `quest.due`: an unresolved, undeleted quest reaches its effective due time (own `due_at`, otherwise its undeleted questline\'s due time).\n- `quest.overdue`: that effective deadline is strictly earlier than now and the quest remains unresolved.\n- `quest.unblocked`: an unresolved quest is available, all active prerequisites have succeeded or been deleted, and at least one prerequisite succeeded after its unlock was installed. Occurrence time is the latest active prerequisite success or not-before time, whichever is later.\n- `repeatable.spawned`: a concrete repeatable instance was created; occurrence time is its creation time. Tick first materializes due anchors through the existing spawn operation.\n- `rumor.captured`: an undeleted rumor was created; occurrence time is its creation time, even if subsequently settled or dismissed.\n\nPersistent event keys contain type, entity ids, and occurrence time. Restarts and repeated\nticks do not repeat occurrences. Removed transitions between ticks cannot be reconstructed.\nOnly occurrences at/after target creation are assigned; tags are evaluated at assignment.\nTick materializes due repeatables using the existing spawn path, including during preview\nwith rollback. Batches contain only ids, event types, timestamps, and the DB label:\n\n```json\n{"type":"questlog.events","db":"work","deliveryId":"d_\u2026","sentAt":"2026-10-28T10:00:00.000Z","events":[{"id":"e_\u2026","type":"quest.due","questId":12,"occurredAt":"2026-10-28T09:59:00.000Z"}]}\n```\n\nHeaders are `content-type: application/json`, `user-agent: questlog/<version>`,\n`x-questlog-delivery: <deliveryId>`, `x-questlog-timestamp: <unix seconds>`, and\n`x-questlog-signature: sha256=<hex HMAC-SHA256(secret, timestamp + "." + rawBody)>`.\nNo titles, objectives, details, or tag text are sent. `hook test` sends a logged `hook.test`\nevent immediately, ignoring the min gap. Add and test are CLI-only in the tool facade;\n`manage_hooks` supports list, remove, tick (with `dry_run`), and deliveries.\n\n2xx delivers. Timeout is 5 seconds. Failures retry after 30 s, 1 m, 2 m, 4 m, doubling\nto 1 h plus <1 s jitter, subject to the target\'s min gap. Eight failures mark failed.\nRetries keep the delivery id/events; new events queue behind them. Keep the last 500\ndelivered log entries per target and all pending/failed entries. Transactional claims\nlease work for two minutes. A crash after receipt may retry: dedupe at the receiver.\n\n### Verifying a delivery\n\nCompute HMAC-SHA256 over `timestamp + "." + rawBody` with the secret and compare in constant\ntime. Reject timestamps older than 5 minutes (and timestamps too far in the future), then\ndedupe on `x-questlog-delivery`. Use durable deduplication storage in a production receiver.\nThis 15-line Node example uses an in-memory set:\n\n```js\nimport { createHmac, timingSafeEqual } from \'node:crypto\';\nimport { createServer } from \'node:http\';\nconst secret = process.env.WEBHOOK_SECRET, seen = new Set();\ncreateServer(async (req, res) => {\n const chunks = []; for await (const chunk of req) chunks.push(chunk);\n const rawBody = Buffer.concat(chunks), h = req.headers;\n const timestamp = h[\'x-questlog-timestamp\'], id = h[\'x-questlog-delivery\'];\n const age = Date.now() / 1000 - Number(timestamp);\n const expected = createHmac(\'sha256\', secret).update(timestamp + \'.\').update(rawBody).digest();\n const signature = String(h[\'x-questlog-signature\'] || \'\');\n const actual = Buffer.from(signature.slice(7), \'hex\');\n if (!id || !/^\\d+$/.test(timestamp) || age > 300 || age < -300 || !/^sha256=[a-f0-9]{64}$/.test(signature) || actual.length !== expected.length || !timingSafeEqual(actual, expected)) { res.writeHead(401).end(); return; }\n if (!seen.has(id)) { seen.add(id); console.log(JSON.parse(rawBody)); }\n res.writeHead(204).end();\n}).listen(8080, \'127.0.0.1\');\n```\n'
732
732
  });
733
733
 
734
734
  // src/skills/skill_registry.ts
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { mkdirSync } from 'node:fs';
2
+ import { mkdirSync, closeSync, openSync } from 'node:fs';
3
3
  import { dirname } from 'node:path';
4
4
 
5
5
  // src/hooks/init_hook_tables.ts
@@ -23,6 +23,8 @@ function initHookTables(db) {
23
23
  CREATE INDEX IF NOT EXISTS idx_hook_deliveries_target ON hook_deliveries(target_id, status, next_attempt_at);
24
24
  CREATE TABLE IF NOT EXISTS hook_settings (id INTEGER PRIMARY KEY CHECK(id = 1), label TEXT NOT NULL);
25
25
  `);
26
+ if (!db.prepare("PRAGMA table_info(hook_targets)").all().some((row) => row.name === "headers"))
27
+ db.exec("ALTER TABLE hook_targets ADD COLUMN headers TEXT NOT NULL DEFAULT '{}'");
26
28
  }
27
29
 
28
30
  // src/questlines/init_questline_tables.ts
@@ -392,7 +394,14 @@ async function openQuestlogDb(path) {
392
394
  } finally {
393
395
  process.emitWarning = original;
394
396
  }
395
- if (path !== ":memory:") mkdirSync(dirname(path), { recursive: true });
397
+ if (path !== ":memory:") {
398
+ mkdirSync(dirname(path), { recursive: true, mode: 448 });
399
+ try {
400
+ closeSync(openSync(path, "wx", 384));
401
+ } catch (error) {
402
+ if (error.code !== "EEXIST") throw error;
403
+ }
404
+ }
396
405
  const db = new sqlite.DatabaseSync(path);
397
406
  try {
398
407
  db.exec("PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000;");