friction-log 0.0.0-stage → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/LICENSE +21 -0
  3. package/README.md +98 -2
  4. package/dist/cli.d.ts +2 -0
  5. package/dist/cli.js +504 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/commands/bilanz.d.ts +23 -0
  8. package/dist/commands/bilanz.js +106 -0
  9. package/dist/commands/bilanz.js.map +1 -0
  10. package/dist/commands/digest.d.ts +36 -0
  11. package/dist/commands/digest.js +288 -0
  12. package/dist/commands/digest.js.map +1 -0
  13. package/dist/commands/export.d.ts +36 -0
  14. package/dist/commands/export.js +118 -0
  15. package/dist/commands/export.js.map +1 -0
  16. package/dist/commands/file.d.ts +18 -0
  17. package/dist/commands/file.js +51 -0
  18. package/dist/commands/file.js.map +1 -0
  19. package/dist/commands/import.d.ts +31 -0
  20. package/dist/commands/import.js +283 -0
  21. package/dist/commands/import.js.map +1 -0
  22. package/dist/commands/init.d.ts +44 -0
  23. package/dist/commands/init.js +256 -0
  24. package/dist/commands/init.js.map +1 -0
  25. package/dist/commands/list.d.ts +16 -0
  26. package/dist/commands/list.js +57 -0
  27. package/dist/commands/list.js.map +1 -0
  28. package/dist/commands/log.d.ts +18 -0
  29. package/dist/commands/log.js +52 -0
  30. package/dist/commands/log.js.map +1 -0
  31. package/dist/commands/rm.d.ts +9 -0
  32. package/dist/commands/rm.js +22 -0
  33. package/dist/commands/rm.js.map +1 -0
  34. package/dist/commands/scan.d.ts +25 -0
  35. package/dist/commands/scan.js +73 -0
  36. package/dist/commands/scan.js.map +1 -0
  37. package/dist/commands/search.d.ts +15 -0
  38. package/dist/commands/search.js +37 -0
  39. package/dist/commands/search.js.map +1 -0
  40. package/dist/commands/sync-export.d.ts +46 -0
  41. package/dist/commands/sync-export.js +80 -0
  42. package/dist/commands/sync-export.js.map +1 -0
  43. package/dist/commands/update.d.ts +12 -0
  44. package/dist/commands/update.js +24 -0
  45. package/dist/commands/update.js.map +1 -0
  46. package/dist/config.d.ts +31 -0
  47. package/dist/config.js +145 -0
  48. package/dist/config.js.map +1 -0
  49. package/dist/db.d.ts +81 -0
  50. package/dist/db.js +548 -0
  51. package/dist/db.js.map +1 -0
  52. package/dist/index.d.ts +15 -0
  53. package/dist/index.js +16 -0
  54. package/dist/index.js.map +1 -0
  55. package/dist/paths.d.ts +12 -0
  56. package/dist/paths.js +34 -0
  57. package/dist/paths.js.map +1 -0
  58. package/dist/scanners/claude-code.d.ts +6 -0
  59. package/dist/scanners/claude-code.js +202 -0
  60. package/dist/scanners/claude-code.js.map +1 -0
  61. package/dist/scanners/index.d.ts +4 -0
  62. package/dist/scanners/index.js +12 -0
  63. package/dist/scanners/index.js.map +1 -0
  64. package/dist/sinks/agent-tasks.d.ts +36 -0
  65. package/dist/sinks/agent-tasks.js +153 -0
  66. package/dist/sinks/agent-tasks.js.map +1 -0
  67. package/dist/sinks/github-issues.d.ts +20 -0
  68. package/dist/sinks/github-issues.js +98 -0
  69. package/dist/sinks/github-issues.js.map +1 -0
  70. package/dist/sinks/index.d.ts +11 -0
  71. package/dist/sinks/index.js +48 -0
  72. package/dist/sinks/index.js.map +1 -0
  73. package/dist/sinks/linear.d.ts +25 -0
  74. package/dist/sinks/linear.js +157 -0
  75. package/dist/sinks/linear.js.map +1 -0
  76. package/dist/sinks/markdown-file.d.ts +7 -0
  77. package/dist/sinks/markdown-file.js +54 -0
  78. package/dist/sinks/markdown-file.js.map +1 -0
  79. package/dist/sinks/stdout-json.d.ts +15 -0
  80. package/dist/sinks/stdout-json.js +49 -0
  81. package/dist/sinks/stdout-json.js.map +1 -0
  82. package/dist/templates/auth-expiry.yml +30 -0
  83. package/dist/templates/doc-gap.yml +30 -0
  84. package/dist/templates/output-overflow.yml +25 -0
  85. package/dist/templates/schema-drift.yml +26 -0
  86. package/dist/templates/tool-error.yml +34 -0
  87. package/dist/templates/tool-missing-capability.yml +26 -0
  88. package/dist/templates/workflow-friction.yml +26 -0
  89. package/dist/templates.d.ts +5 -0
  90. package/dist/templates.js +88 -0
  91. package/dist/templates.js.map +1 -0
  92. package/dist/types.d.ts +79 -0
  93. package/dist/types.js +2 -0
  94. package/dist/types.js.map +1 -0
  95. package/docs/commands.md +74 -0
  96. package/docs/design.md +25 -0
  97. package/docs/sinks.md +79 -0
  98. package/docs/storage.md +20 -0
  99. package/docs/sync-export.md +23 -0
  100. package/package.json +64 -4
  101. package/templates/auth-expiry.yml +30 -0
  102. package/templates/doc-gap.yml +30 -0
  103. package/templates/output-overflow.yml +25 -0
  104. package/templates/schema-drift.yml +26 -0
  105. package/templates/tool-error.yml +34 -0
  106. package/templates/tool-missing-capability.yml +26 -0
  107. package/templates/workflow-friction.yml +26 -0
@@ -0,0 +1,79 @@
1
+ export type FrictionSource = "scan" | "manual" | "import";
2
+ export type FrictionStatus = "open" | "filed" | "resolved" | "wontfix";
3
+ export type Severity = "low" | "medium" | "high" | "critical";
4
+ export type Priority = "LOW" | "MEDIUM" | "HIGH" | "CRITICAL";
5
+ export interface Session {
6
+ id: string;
7
+ startedAt: string;
8
+ endedAt: string | null;
9
+ projectPaths: string[] | null;
10
+ transcriptPath: string | null;
11
+ adapter: string;
12
+ }
13
+ export interface Friction {
14
+ id: number;
15
+ sessionId: string | null;
16
+ toolSurface: string | null;
17
+ title: string;
18
+ description: string | null;
19
+ capturedAt: string;
20
+ severity: Severity | null;
21
+ category: string | null;
22
+ status: FrictionStatus;
23
+ recurrenceOfId: number | null;
24
+ source: FrictionSource;
25
+ }
26
+ export interface Task {
27
+ id: number;
28
+ frictionId: number;
29
+ sinkName: string;
30
+ sinkTarget: string | null;
31
+ externalRef: string | null;
32
+ createdAt: string;
33
+ prUrl: string | null;
34
+ resolutionStatus: string | null;
35
+ }
36
+ export interface Template {
37
+ name: string;
38
+ title: string;
39
+ body: string;
40
+ labels: string[];
41
+ priority: Priority;
42
+ metadata?: Record<string, unknown>;
43
+ }
44
+ export interface RenderedTemplate {
45
+ title: string;
46
+ body: string;
47
+ labels: string[];
48
+ priority: Priority;
49
+ metadata?: Record<string, unknown>;
50
+ }
51
+ export interface FileOptions {
52
+ sinkTarget?: string;
53
+ sinkOpts?: Record<string, unknown>;
54
+ }
55
+ export interface FileResult {
56
+ ok: boolean;
57
+ sinkTarget: string;
58
+ externalRef?: string;
59
+ prUrl?: string;
60
+ message?: string;
61
+ }
62
+ export interface Sink {
63
+ readonly name: string;
64
+ file(friction: Friction, rendered: RenderedTemplate, opts: FileOptions): Promise<FileResult>;
65
+ }
66
+ export interface ScannerInput {
67
+ sessionId?: string;
68
+ transcriptPath?: string;
69
+ }
70
+ export interface ScannerOutput {
71
+ session: Omit<Session, "adapter"> & {
72
+ adapter?: string;
73
+ };
74
+ frictionCandidates: Array<Pick<Friction, "toolSurface" | "title" | "description" | "severity" | "category">>;
75
+ }
76
+ export interface Scanner {
77
+ readonly name: string;
78
+ scan(input: ScannerInput): Promise<ScannerOutput>;
79
+ }
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,74 @@
1
+ # Command reference
2
+
3
+ Reference for every `friction-log` subcommand and its main flags. Run any command with `--help` for the complete flag list.
4
+
5
+ | Command | What it does |
6
+ |---------|--------------|
7
+ | `init` | One-command setup: detects the local environment (Claude Code dir, `gh` CLI, Linear key, agent-tasks token), suggests a default sink, writes `~/.config/friction-log/config.yml`, and (when Claude Code is present) offers to install the Stop-hook. Non-interactive: `init --sink <name> --yes`. Idempotent. |
8
+ | `import <path>` | Bulk-ingest frictions from a directory of markdown files. `--format markdown-frontmatter` (the only format in M5) parses YAML frontmatter, falls back to the first `# H1` for the title, preserves unknown frontmatter keys as `key:value` tags, and dedupes on a content hash so re-running the same import is a no-op. |
9
+ | `log` | Manually record a friction with title, tool, category, severity. Returns the new id. Optional `--recurrence-of <id>` to explicitly mark a duplicate; otherwise auto-links on matching (tool, title) against an open root, see [recurrence semantics](./storage.md#recurrence_of_id-semantics). |
10
+ | `list` | List frictions with filters: `--status`, `--tool`, `--category`, `--source`, `--age 14d`, `--limit`. Use `--json` for piping. |
11
+ | `search <query>` | FTS5 MATCH over title and description, plus the same structured filters as `list`. Use `--json` for piping. Accepts the full [FTS5 query syntax](https://sqlite.org/fts5.html#full_text_query_syntax). |
12
+ | `digest --group-by <field>` | Aggregations over frictions: total, open / filed / resolved / wontfix counts, open percentage, recurrence count, and average hours from `captured_at` to the first `tasks.created_at` (time-to-triage proxy). `--group-by tool\|category\|severity\|source`. Optional `--last <span>` window. `--include-peers` additionally renders one read-only section per `sync_export.peer_paths` entry; see [Sync-export](./sync-export.md). |
13
+ | `export --format <json\|csv\|md>` | Render frictions for offline analysis or hand-off. Same filter combinators as `list`, plus `--query <text>` for an FTS pre-filter. `--out <path>` writes to a file, otherwise stdout. |
14
+ | `sync-export` | Write every friction as deterministic, origin-tagged JSON to the configured `sync_export.path`. No-op error unless `sync_export` is configured; see [Sync-export](./sync-export.md). |
15
+ | `file <id>` | Push a friction through a sink. Default sink is `markdown-file`, default template matches the friction's category and falls back to `workflow-friction`. See [Sinks](./sinks.md). |
16
+ | `scan` | Parse a transcript and extract candidate frictions (tool-call errors, non-zero Bash exits, friction phrases). Flags: `--transcript <path>`, `--session <id>`, `--adapter claude-code`, `--silent`, `--stdin-payload`. Idempotent on re-run. |
17
+ | `bilanz` | Print a session-boundary summary: tools exercised, frictions noticed, tasks filed, plus a highlighted list of open frictions that have not been filed yet. `--session <id>` defaults to the most recent session. |
18
+ | `rm <id>` | Delete a friction and any task rows pointing at it from the local store. |
19
+ | `update <id> --status <state>` | Change a friction's status. |
20
+
21
+ ## Auto-capture via Claude Code Stop-hook
22
+
23
+ To run `friction-log scan` automatically at the end of every Claude Code session, add this entry to your `~/.claude/settings.json`:
24
+
25
+ ```jsonc
26
+ {
27
+ "hooks": {
28
+ "Stop": [
29
+ {
30
+ "matcher": "",
31
+ "hooks": [
32
+ {
33
+ "type": "command",
34
+ "command": "friction-log scan --silent --stdin-payload"
35
+ }
36
+ ]
37
+ }
38
+ ]
39
+ }
40
+ }
41
+ ```
42
+
43
+ The hook passes a JSON payload to stdin with `session_id` and `transcript_path`. `--stdin-payload` reads it and feeds the scan. `--silent` keeps the hook non-blocking: if anything fails, the error goes to stderr and exit is always 0 so the session shutdown is never delayed.
44
+
45
+ After the hook is wired, run `friction-log bilanz` whenever you want a summary of the most recent session.
46
+
47
+ ## Manually scanning a past session
48
+
49
+ ```bash
50
+ friction-log scan \
51
+ --transcript ~/.claude/projects/<encoded-cwd>/<sessionId>.jsonl
52
+
53
+ friction-log bilanz --session <sessionId>
54
+ ```
55
+
56
+ Re-running `scan` against the same transcript is idempotent, so it is safe to run on every project sync.
57
+
58
+ ## Templates
59
+
60
+ Seven categories ship in v1:
61
+
62
+ | Template | When to use |
63
+ |----------|-------------|
64
+ | `tool-error` | Tool, CLI, or MCP verb behaves differently than its docs claim. |
65
+ | `output-overflow` | Tool output overflows the agent's context window or significantly degrades performance. |
66
+ | `workflow-friction` | Generic catch-all. Used as the fallback when no category matches. |
67
+ | `tool-missing-capability` | Tool lacks a capability the workflow needs; not a defect, a gap. |
68
+ | `auth-expiry` | Token, JWT, session, or OAuth refresh lifecycle issue. |
69
+ | `schema-drift` | Tool schema contradicts the workflow's expected contract. |
70
+ | `doc-gap` | Tool behavior contradicts its documentation; usually a one-line doc PR upstream. |
71
+
72
+ Each template is a YAML file under `packages/friction-log/templates/`. Mustache-style `{{var}}` substitution: `id`, `title`, `description`, `tool`, `category`, `severity`, `capturedAt`, `sessionId`, `source`.
73
+
74
+ The `--template <name>` flag on `file` overrides the auto-selection.
package/docs/design.md ADDED
@@ -0,0 +1,25 @@
1
+ # Design notes
2
+
3
+ Public-tool framing: zero org-specific-stack assumptions in the core. The default sink is plain markdown files so the tool works without any external infrastructure, and integrations are configurable adapters that load only when used.
4
+
5
+ Local SQLite, single-user, single-machine at the core. Still no live sync, no server, no cloud: the database itself is never shared or written to remotely. The one opt-in exception is [sync-export](./sync-export.md): a deterministic, config-gated file dump of the local db, plus a read-only merge of other machines' dumps into `digest`. Both are exact no-ops until configured, and transport between machines is left to something else (Dropbox, iCloud, `agent-memory-sync`, ...); this package produces and consumes a file, it does not move it. Friction records are personal observation data, the smallest store that lets queries answer questions is the right one.
6
+
7
+ Deterministic detection only: regex on tool-call errors, non-zero exits, friction phrases. No LLM API calls in the default Stop-hook so it stays free and fast. An opt-in `--with-llm` flag for deeper end-of-week reviews is on the M5+ roadmap.
8
+
9
+ ## ADR: FileOptions widening
10
+
11
+ The pre-M4 `FileOptions { sinkTarget?: string }` was too narrow for sinks that need a project id, an api base, a team id, etc. Three options were considered (per the M4 task description):
12
+
13
+ - **A. Discriminated union per sink.** Strictest, but every sink change becomes a type bump in `types.ts`; doesn't compose with config files cleanly.
14
+ - **B. Open `Record<string, unknown>` bag validated per sink.** Flexible, forward-compatible CLI, but loses the type-level guarantee that a given key exists.
15
+ - **C. Drop CLI options entirely, read everything from `config.yml`.** Simplest CLI, but blocks one-off `--sink-opt repo=other/repo` overrides that are convenient when scripting.
16
+
17
+ **Shipped: B with a config-file layer.** Each sink reads `opts.sinkOpts`, which is the merge of the per-sink section in `config.yml` and any `--sink-opt key=value` CLI overrides (CLI wins on collision). Each sink validates the keys it cares about and surfaces missing-required-key with a single-line error pointing at both the config path and the equivalent `--sink-opt` flag. Unknown keys pass through untouched so a forward-compatible CLI does not fail against an older sink build. The C option (drop CLI options entirely) was rejected because `--sink-opt repo=other/repo` overrides are convenient when scripting one-off file calls.
18
+
19
+ ## What's next
20
+
21
+ The v1 surface is complete. Future ideas (no scheduled milestone): web dashboard, vector-based recurrence detection, additional import formats (github-issues, agent-tasks backfill), `digest --with-llm` for end-of-week reviews.
22
+
23
+ ## Where this fits
24
+
25
+ `friction-log` and `slop-detector` are sibling hygiene tools in the agent-dx workshop. `slop-detector` catches prose tells at PR time. `friction-log` catches workflow tells at session time. Both run cheaply, both produce a structured artifact, both compose with whatever issue tracker and review process the team already uses.
package/docs/sinks.md ADDED
@@ -0,0 +1,79 @@
1
+ # Sinks
2
+
3
+ A sink is the thing that receives a rendered friction. Five sinks ship, all behind a lazy-loaded registry (the Linear API client and the agent-tasks REST helper only get imported when those sinks are actually picked):
4
+
5
+ ```ts
6
+ interface Sink {
7
+ readonly name: string;
8
+ file(friction: Friction, rendered: RenderedTemplate, opts: FileOptions): Promise<FileResult>;
9
+ }
10
+
11
+ interface FileOptions {
12
+ sinkTarget?: string; // markdown-file legacy shortcut
13
+ sinkOpts?: Record<string, unknown>; // merged config-file defaults + CLI overrides
14
+ }
15
+ ```
16
+
17
+ Sink-specific configuration lives in `~/.config/friction-log/config.yml` (override with `FRICTION_LOG_CONFIG=/path` or `--config /path`). CLI overrides via `--sink-opt key=value` (repeatable) win on key collision. Heuristic value coercion: commas split into arrays, `true`/`false`/`null` are literal, integers parse as numbers, prefix `s:` for a literal that would otherwise coerce.
18
+
19
+ ## `markdown-file` (default)
20
+
21
+ Writes a markdown file under `~/.local/share/friction-log/frictions/` (override with `FRICTION_LOG_MARKDOWN_DIR` or `--sink-target /path/to/dir`). YAML frontmatter with `friction_id`, `captured_at`, `priority`, `labels`, plus tool surface, category, and severity when set. No external dependencies.
22
+
23
+ ## `stdout-json`
24
+
25
+ Emits a single-line JSON record to stdout and returns. Useful for piping into custom workflows:
26
+
27
+ ```bash
28
+ friction-log file 7 --sink stdout-json | jq '.rendered.body'
29
+ ```
30
+
31
+ The schema is stable: any future field is additive.
32
+
33
+ ## `github-issues`
34
+
35
+ Spawns `gh issue create` under the hood, so authentication, retries, and proxy config stay with the `gh` CLI. Required: `repo` (`owner/name`). Optional: `labels`, `assignee`, `milestone`.
36
+
37
+ ```yaml
38
+ # config.yml
39
+ sinks:
40
+ github-issues:
41
+ repo: your-org/your-repo
42
+ labels: [bug, friction]
43
+ assignee: octocat
44
+ ```
45
+
46
+ ```bash
47
+ friction-log file 7 --sink github-issues
48
+ # or override per-call:
49
+ friction-log file 7 --sink github-issues --sink-opt repo=other/repo --sink-opt labels=quick-fix
50
+ ```
51
+
52
+ ## `agent-tasks`
53
+
54
+ Two modes, both honest about what they do:
55
+
56
+ - **`mode: rest` (default)**: POSTs to `<apiBase>/api/projects/<id>/tasks` with bearer auth. Requires `apiBase`, `projectId`, and a token from `AGENT_TASKS_TOKEN` env or `token:` in config.
57
+ - **`mode: mcp-emit`**: prints the equivalent `mcp__agent-tasks__task_create` invocation JSON to stdout and returns; no network call is made. This is the version of "the MCP path" that an honest standalone Node CLI can actually offer. An agent-harness wrapper can pick the line up and execute it under its own MCP scope.
58
+
59
+ ```yaml
60
+ sinks:
61
+ agent-tasks:
62
+ mode: rest
63
+ apiBase: https://agent-tasks.example.com
64
+ projectId: 00000000-0000-0000-0000-000000000000
65
+ # token: # set AGENT_TASKS_TOKEN env var instead in production
66
+ ```
67
+
68
+ ## `linear`
69
+
70
+ GraphQL `issueCreate` against `api.linear.app/graphql`. Required: `teamId` and an API key (`LINEAR_API_KEY` env or `apiKey:` in config). Optional: `state` (matched case-insensitively against the team's workflow-state names; one extra query resolves it to a state id) and `assignee`.
71
+
72
+ ```yaml
73
+ sinks:
74
+ linear:
75
+ teamId: TEAM-UUID
76
+ state: Backlog
77
+ ```
78
+
79
+ > Note: Linear allows duplicate state names across a team's workflow. If two states share a name, the sink picks the first match in the API response and emits no warning. Pass the state's UUID directly via `--sink-opt state=<uuid>` when the name is not unique.
@@ -0,0 +1,20 @@
1
+ # Storage
2
+
3
+ SQLite under `~/.local/share/friction-log/db.sqlite` (XDG-compliant). Override with `FRICTION_LOG_DB=/path/to/db.sqlite` or `--db /path/to/db.sqlite` per command.
4
+
5
+ Schema (M1): `sessions`, `frictions` (plus FTS5 virtual table), `tasks` (records what was filed to which sink), `tags`, `schema_version`.
6
+
7
+ Schema v2 (M3): adds `CHECK(severity IN ('low','medium','high','critical') OR severity IS NULL)` to `frictions`. On upgrade the migration normalizes any rogue severity values to `NULL` before swapping the table in, so existing rows survive. The FTS5 virtual table and triggers are recreated against the new physical table.
8
+
9
+ ## `recurrence_of_id` semantics
10
+
11
+ When `log` (or `scan`) inserts a new friction without an explicit `--recurrence-of` flag, the database looks for an existing friction that is:
12
+
13
+ 1. `status = 'open'`,
14
+ 2. itself a root (`recurrence_of_id IS NULL`),
15
+ 3. with the same `tool_surface`,
16
+ 4. and the same `title` (exact match).
17
+
18
+ The oldest such match becomes the new friction's `recurrence_of_id`. The chain therefore always points one level deep, at the root; downstream queries can `GROUP BY coalesce(recurrence_of_id, id)` to fold recurrences into their root for free.
19
+
20
+ This is the cheap, deterministic rule, deliberately small enough to predict by eye. A future milestone may add fuzzier matching (template-aware, vector-based) behind an opt-in flag, but the cheap rule is the default so the database remains explainable from a glance at the source.
@@ -0,0 +1,23 @@
1
+ # Sync-export (optional, multi-machine file merge)
2
+
3
+ `friction-log` is still local SQLite, single-machine, no server (see [Design notes](./design.md)). Sync-export is an opt-in, config-gated file export that makes it possible to *look at* frictions from more than one machine without turning the tool into a client-server system: each machine writes its own deterministic JSON dump to a path of your choosing, and any machine can read the others' dumps read-only. Moving that file between machines (Dropbox, iCloud, a git-tracked dotfiles repo, `agent-memory-sync`, `rsync`, ...) is entirely up to you; this package only produces and consumes the file.
4
+
5
+ **Without a `sync_export` block, every piece of this is an exact no-op.** No new file is ever written, and `list`/`search`/`export`/`digest` render exactly as before.
6
+
7
+ ```yaml
8
+ # config.yml
9
+ sync_export:
10
+ path: /Users/<name>/Sync/friction-log/macbook.json # where THIS machine writes
11
+ origin: macbook # this machine's label
12
+ peer_paths: # OTHER machines' files, read-only
13
+ - /Users/<name>/Sync/friction-log/mac-mini.json
14
+ ```
15
+
16
+ `path` and `origin` can also come from `FRICTION_LOG_SYNC_EXPORT_PATH` / `FRICTION_LOG_SYNC_EXPORT_ORIGIN` (env wins over the YAML value when both are set), so the same `config.yml` can be checked into dotfiles and shared across machines while each machine supplies its own path/origin via its shell profile or `.env`. `friction-log init --sync-export-path <path> --sync-export-origin <name> [--sync-export-peer <path> ...]` scaffolds the block non-interactively.
17
+
18
+ - **Write-through.** Once configured, every mutating command (`log`, `scan`, `import`, `update`, `rm`, `file`) rewrites the full export file after it commits its own change, so the file always reflects the current local state. It skips the rewrite entirely when the mutation itself was a no-op (a `scan`/`import` that found nothing new, or an `update` to the friction's existing status) and again whenever the freshly rendered JSON is byte-identical to what is already on disk, so an idle machine never bumps the file's mtime. The write itself is atomic: write to a same-directory `.tmp` file, then rename over the real path, so nothing ever observes a half-written file. Write (or config-load) failures, e.g. an unmounted sync folder or a broken `config.yml`, log a warning to stderr; they never fail the command whose data is already safely committed to the local db.
19
+ - **`friction-log sync-export`** does the same write on demand, useful for a cron job or a manual "sync now", and, unlike the write-through above, errors loudly if `sync_export` is not configured, since the whole point of calling it is to produce the file.
20
+ - **Determinism.** The file has no timestamp field and lists every friction (no 100-row cap) sorted by `captured_at` ascending with an `id` tiebreaker, so two exports with no mutation in between are byte-identical: safe to diff or to feed into a dumb "did anything change" check.
21
+ - **Shape:** a top-level object `{ "origin": "<label>", "records": [ ... ] }`; `origin` is a sibling of `records`, not a field on each record. Each entry in `records` carries the same fields as `export --format json` (`id`, `sessionId`, `toolSurface`, `title`, `description`, `capturedAt`, `severity`, `category`, `status`, `recurrenceOfId`, `source`, `tags`). One file = one machine.
22
+ - **This file is exactly as sensitive as your local db; treat it the same way.** `description` on `scan`-sourced frictions is verbatim tool output (error text, stdout/stderr) truncated to 2000 characters, and can contain paths, hostnames, or anything else that showed up in a failing command. Point `sync_export.path` (and any peer file someone hands you) at a destination with the same trust level as your local machine, e.g. a private synced folder, never a public repo or a shared drive.
23
+ - **`digest --group-by <field> --include-peers`** reads every `sync_export.peer_paths` file read-only (never writes to them) and renders one additional section per peer, labeled by that file's own `origin` field (falling back to the filename if it's missing or blank). Peer totals/open/filed/resolved/wontfix/avg-hours numbers are computed with the exact same aggregation as the local digest, replayed against a throwaway in-memory database; `avg-h-triage` is structurally always empty for peer sections, since the export payload carries no task history to replay. `recurrences` is the one number NOT taken from that replay: it is counted directly from each record's own `recurrenceOfId !== null` flag, because a peer's friction ids (and `recurrenceOfId`) belong to a different machine's AUTOINCREMENT sequence and are never dereferenced against local or replayed ids, only counted as a boolean. Individual malformed records (missing/non-string title, unknown status, an unparseable `capturedAt`) are dropped rather than silently miscounted, and the drop count is surfaced as `skipped` in the section header and a stderr warning. Local digest numbers are completely unaffected by `--include-peers` either way, and a missing or corrupt peer file degrades the whole section to a warning instead of failing the command.
package/package.json CHANGED
@@ -1,6 +1,66 @@
1
1
  {
2
2
  "name": "friction-log",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.6.0",
4
+ "description": "Capture, query, and infer agent-workflow frictions. SQLite-backed, sink-pluggable, zero-config default.",
5
+ "main": "dist/index.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "friction-log": "./dist/cli.js"
9
+ },
10
+ "exports": {
11
+ ".": "./dist/index.js",
12
+ "./db": "./dist/db.js",
13
+ "./types": "./dist/types.js",
14
+ "./sinks/markdown-file": "./dist/sinks/markdown-file.js"
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "docs",
19
+ "templates",
20
+ "LICENSE",
21
+ "README.md",
22
+ "CHANGELOG.md"
23
+ ],
24
+ "scripts": {
25
+ "build": "tsc && node scripts/copy-templates.mjs && chmod +x dist/cli.js",
26
+ "typecheck": "tsc --noEmit",
27
+ "typecheck:test": "tsc --noEmit -p tsconfig.test.json",
28
+ "dev": "node --import tsx src/cli.ts",
29
+ "test": "vitest run",
30
+ "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
31
+ "format:check": "prettier --check \"src/**/*.ts\" \"test/**/*.ts\""
32
+ },
33
+ "keywords": [
34
+ "ai",
35
+ "agent",
36
+ "friction",
37
+ "dogfood",
38
+ "dx",
39
+ "sqlite",
40
+ "claude",
41
+ "workflow"
42
+ ],
43
+ "author": "Lava (@lavaclawdbot) & Lan Nguyen Si",
44
+ "license": "MIT",
45
+ "repository": {
46
+ "type": "git",
47
+ "url": "https://github.com/LanNguyenSi/agent-dx.git",
48
+ "directory": "packages/friction-log"
49
+ },
50
+ "dependencies": {
51
+ "better-sqlite3": "^13.0.0",
52
+ "commander": "^12.0.0",
53
+ "yaml": "^2.8.3"
54
+ },
55
+ "devDependencies": {
56
+ "@types/better-sqlite3": "^7.6.11",
57
+ "@types/node": "^20.11.0",
58
+ "prettier": "^3.8.1",
59
+ "tsx": "^4.22.4",
60
+ "typescript": "^5.3.3",
61
+ "vitest": "^4.1.6"
62
+ },
63
+ "engines": {
64
+ "node": ">=22"
65
+ }
66
+ }
@@ -0,0 +1,30 @@
1
+ name: auth-expiry
2
+ title: "{{tool}}: auth / token lifecycle issue"
3
+ priority: HIGH
4
+ labels:
5
+ - tooling
6
+ - friction
7
+ - auth-expiry
8
+ body: |
9
+ ## Symptom
10
+
11
+ {{description}}
12
+
13
+ ## Context
14
+
15
+ - Tool surface: `{{tool}}`
16
+ - Captured: {{capturedAt}}
17
+ - Session: `{{sessionId}}`
18
+ - Source: `{{source}}`
19
+
20
+ ## Suspected cause
21
+
22
+ Token, JWT, session, or OAuth refresh lifecycle. Common shapes: skew at the issuer, expiry window too tight, refresh not wired, scope drift.
23
+
24
+ ## Reproduction
25
+
26
+ _Capture the failing request, the credential's iat / exp, and the wall-clock at failure time. Without those three the next debug pass restarts from scratch._
27
+
28
+ ## Suggested next step
29
+
30
+ Check the credential's exp vs `date -u +%s` at failure, then widen the issuance window or wire a refresh. If skew is in play, document the deployment environment's clock source.
@@ -0,0 +1,30 @@
1
+ name: doc-gap
2
+ title: "{{tool}}: behavior contradicts documentation"
3
+ priority: LOW
4
+ labels:
5
+ - tooling
6
+ - friction
7
+ - doc-gap
8
+ body: |
9
+ ## Symptom
10
+
11
+ {{description}}
12
+
13
+ ## Context
14
+
15
+ - Tool surface: `{{tool}}`
16
+ - Captured: {{capturedAt}}
17
+ - Session: `{{sessionId}}`
18
+ - Source: `{{source}}`
19
+
20
+ ## What the docs claim
21
+
22
+ _Quote the exact passage that misled the workflow. Link to the URL or path._
23
+
24
+ ## What actually happened
25
+
26
+ See symptom. The gap is small enough to fix in docs alone, no tool change needed.
27
+
28
+ ## Suggested next step
29
+
30
+ Send a one-line doc PR upstream, or pin the corrected note in the team's internal playbook so future runs do not pay this cost again.
@@ -0,0 +1,25 @@
1
+ name: output-overflow
2
+ title: "{{tool}}: output overflows agent context"
3
+ priority: HIGH
4
+ labels:
5
+ - tooling
6
+ - friction
7
+ - output-overflow
8
+ body: |
9
+ ## Symptom
10
+
11
+ `{{tool}}` returned output large enough to overflow the agent's context window or significantly degrade performance.
12
+
13
+ {{description}}
14
+
15
+ ## Context
16
+
17
+ - Tool surface: `{{tool}}`
18
+ - Captured: {{capturedAt}}
19
+ - Session: `{{sessionId}}`
20
+
21
+ ## Suggested fixes
22
+
23
+ - Add server-side pagination or filtering defaults.
24
+ - Add a `--summary` or `--ids-only` mode that returns a compact representation.
25
+ - Document a recommended page size in the tool's README.
@@ -0,0 +1,26 @@
1
+ name: schema-drift
2
+ title: "{{tool}}: schema contradicts workflow docs"
3
+ priority: MEDIUM
4
+ labels:
5
+ - tooling
6
+ - friction
7
+ - schema-drift
8
+ body: |
9
+ ## Symptom
10
+
11
+ {{description}}
12
+
13
+ ## Context
14
+
15
+ - Tool surface: `{{tool}}`
16
+ - Captured: {{capturedAt}}
17
+ - Session: `{{sessionId}}`
18
+ - Source: `{{source}}`
19
+
20
+ ## Drift detected
21
+
22
+ The schema returned (or accepted) by `{{tool}}` does not match what the workflow docs / call-site expects. This is the class of bug that quietly corrupts downstream code paths until something explodes far from the cause.
23
+
24
+ ## Suggested next step
25
+
26
+ Pin both sides: snapshot the actual schema (`--help`, sample response, OpenAPI dump) and the expected schema (workflow contract / type declaration). File the diff so the next contributor finds it before they hit the same wall.
@@ -0,0 +1,34 @@
1
+ name: tool-error
2
+ title: "{{tool}}: {{title}}"
3
+ priority: MEDIUM
4
+ labels:
5
+ - tooling
6
+ - friction
7
+ - tool-error
8
+ body: |
9
+ ## Symptom
10
+
11
+ {{description}}
12
+
13
+ ## Context
14
+
15
+ - Tool surface: `{{tool}}`
16
+ - Captured: {{capturedAt}}
17
+ - Session: `{{sessionId}}`
18
+ - Source: `{{source}}`
19
+
20
+ ## Expected
21
+
22
+ Behaviour documented for `{{tool}}`.
23
+
24
+ ## Actual
25
+
26
+ See symptom above.
27
+
28
+ ## Reproduction
29
+
30
+ _Fill in the exact invocation that produced this. Friction-log capture is the receipt, not the repro._
31
+
32
+ ## Suggested next step
33
+
34
+ Triage whether this is a documentation gap (update docs), a real defect (fix tool), or expected behaviour the workflow needs to adapt to.
@@ -0,0 +1,26 @@
1
+ name: tool-missing-capability
2
+ title: "{{tool}}: missing capability for workflow"
3
+ priority: MEDIUM
4
+ labels:
5
+ - tooling
6
+ - friction
7
+ - tool-missing-capability
8
+ body: |
9
+ ## Symptom
10
+
11
+ {{description}}
12
+
13
+ ## What the workflow needs
14
+
15
+ `{{tool}}` is the natural place for this capability, but the surface does not expose it. The workflow currently has to either route around the gap, file a manual ticket, or stop early.
16
+
17
+ ## Context
18
+
19
+ - Tool surface: `{{tool}}`
20
+ - Captured: {{capturedAt}}
21
+ - Session: `{{sessionId}}`
22
+ - Source: `{{source}}`
23
+
24
+ ## Suggested next step
25
+
26
+ Decide whether to: (a) request the capability upstream, (b) ship a sibling tool that fills the gap, or (c) document the workaround so future runs do not pay this cost again.
@@ -0,0 +1,26 @@
1
+ name: workflow-friction
2
+ title: "{{title}}"
3
+ priority: MEDIUM
4
+ labels:
5
+ - tooling
6
+ - friction
7
+ body: |
8
+ ## What happened
9
+
10
+ {{description}}
11
+
12
+ ## Context
13
+
14
+ - Tool surface: `{{tool}}`
15
+ - Category: `{{category}}`
16
+ - Captured: {{capturedAt}}
17
+ - Session: `{{sessionId}}`
18
+ - Source: `{{source}}`
19
+
20
+ ## Why this matters
21
+
22
+ _Describe how this friction slows down or breaks the workflow._
23
+
24
+ ## Possible next steps
25
+
26
+ _List the smallest change that would remove the friction._