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.
- package/CHANGELOG.md +44 -0
- package/LICENSE +21 -0
- package/README.md +98 -2
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +504 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/bilanz.d.ts +23 -0
- package/dist/commands/bilanz.js +106 -0
- package/dist/commands/bilanz.js.map +1 -0
- package/dist/commands/digest.d.ts +36 -0
- package/dist/commands/digest.js +288 -0
- package/dist/commands/digest.js.map +1 -0
- package/dist/commands/export.d.ts +36 -0
- package/dist/commands/export.js +118 -0
- package/dist/commands/export.js.map +1 -0
- package/dist/commands/file.d.ts +18 -0
- package/dist/commands/file.js +51 -0
- package/dist/commands/file.js.map +1 -0
- package/dist/commands/import.d.ts +31 -0
- package/dist/commands/import.js +283 -0
- package/dist/commands/import.js.map +1 -0
- package/dist/commands/init.d.ts +44 -0
- package/dist/commands/init.js +256 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/list.d.ts +16 -0
- package/dist/commands/list.js +57 -0
- package/dist/commands/list.js.map +1 -0
- package/dist/commands/log.d.ts +18 -0
- package/dist/commands/log.js +52 -0
- package/dist/commands/log.js.map +1 -0
- package/dist/commands/rm.d.ts +9 -0
- package/dist/commands/rm.js +22 -0
- package/dist/commands/rm.js.map +1 -0
- package/dist/commands/scan.d.ts +25 -0
- package/dist/commands/scan.js +73 -0
- package/dist/commands/scan.js.map +1 -0
- package/dist/commands/search.d.ts +15 -0
- package/dist/commands/search.js +37 -0
- package/dist/commands/search.js.map +1 -0
- package/dist/commands/sync-export.d.ts +46 -0
- package/dist/commands/sync-export.js +80 -0
- package/dist/commands/sync-export.js.map +1 -0
- package/dist/commands/update.d.ts +12 -0
- package/dist/commands/update.js +24 -0
- package/dist/commands/update.js.map +1 -0
- package/dist/config.d.ts +31 -0
- package/dist/config.js +145 -0
- package/dist/config.js.map +1 -0
- package/dist/db.d.ts +81 -0
- package/dist/db.js +548 -0
- package/dist/db.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/paths.d.ts +12 -0
- package/dist/paths.js +34 -0
- package/dist/paths.js.map +1 -0
- package/dist/scanners/claude-code.d.ts +6 -0
- package/dist/scanners/claude-code.js +202 -0
- package/dist/scanners/claude-code.js.map +1 -0
- package/dist/scanners/index.d.ts +4 -0
- package/dist/scanners/index.js +12 -0
- package/dist/scanners/index.js.map +1 -0
- package/dist/sinks/agent-tasks.d.ts +36 -0
- package/dist/sinks/agent-tasks.js +153 -0
- package/dist/sinks/agent-tasks.js.map +1 -0
- package/dist/sinks/github-issues.d.ts +20 -0
- package/dist/sinks/github-issues.js +98 -0
- package/dist/sinks/github-issues.js.map +1 -0
- package/dist/sinks/index.d.ts +11 -0
- package/dist/sinks/index.js +48 -0
- package/dist/sinks/index.js.map +1 -0
- package/dist/sinks/linear.d.ts +25 -0
- package/dist/sinks/linear.js +157 -0
- package/dist/sinks/linear.js.map +1 -0
- package/dist/sinks/markdown-file.d.ts +7 -0
- package/dist/sinks/markdown-file.js +54 -0
- package/dist/sinks/markdown-file.js.map +1 -0
- package/dist/sinks/stdout-json.d.ts +15 -0
- package/dist/sinks/stdout-json.js +49 -0
- package/dist/sinks/stdout-json.js.map +1 -0
- package/dist/templates/auth-expiry.yml +30 -0
- package/dist/templates/doc-gap.yml +30 -0
- package/dist/templates/output-overflow.yml +25 -0
- package/dist/templates/schema-drift.yml +26 -0
- package/dist/templates/tool-error.yml +34 -0
- package/dist/templates/tool-missing-capability.yml +26 -0
- package/dist/templates/workflow-friction.yml +26 -0
- package/dist/templates.d.ts +5 -0
- package/dist/templates.js +88 -0
- package/dist/templates.js.map +1 -0
- package/dist/types.d.ts +79 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/docs/commands.md +74 -0
- package/docs/design.md +25 -0
- package/docs/sinks.md +79 -0
- package/docs/storage.md +20 -0
- package/docs/sync-export.md +23 -0
- package/package.json +64 -4
- package/templates/auth-expiry.yml +30 -0
- package/templates/doc-gap.yml +30 -0
- package/templates/output-overflow.yml +25 -0
- package/templates/schema-drift.yml +26 -0
- package/templates/tool-error.yml +34 -0
- package/templates/tool-missing-capability.yml +26 -0
- package/templates/workflow-friction.yml +26 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
const ISSUE_CREATE_MUTATION = `
|
|
2
|
+
mutation IssueCreate($input: IssueCreateInput!) {
|
|
3
|
+
issueCreate(input: $input) {
|
|
4
|
+
success
|
|
5
|
+
issue { id identifier url }
|
|
6
|
+
}
|
|
7
|
+
}`.trim();
|
|
8
|
+
/**
|
|
9
|
+
* Create a Linear issue via GraphQL. LINEAR_API_KEY env var supplies the
|
|
10
|
+
* personal access token by default; teamId is required.
|
|
11
|
+
*
|
|
12
|
+
* The `state` option is matched case-insensitively against the team's
|
|
13
|
+
* workflow-state names so callers can write "Backlog" instead of a UUID.
|
|
14
|
+
* Looking up the workflow state requires one extra query, kept minimal.
|
|
15
|
+
*/
|
|
16
|
+
export class LinearSink {
|
|
17
|
+
fetchImpl;
|
|
18
|
+
name = "linear";
|
|
19
|
+
constructor(fetchImpl = (url, init) => fetch(url, init)) {
|
|
20
|
+
this.fetchImpl = fetchImpl;
|
|
21
|
+
}
|
|
22
|
+
async file(friction, rendered, opts) {
|
|
23
|
+
const parsed = parseOpts(opts);
|
|
24
|
+
const apiKey = parsed.apiKey ?? process.env.LINEAR_API_KEY;
|
|
25
|
+
if (!apiKey) {
|
|
26
|
+
throw new Error(`friction-log: linear sink requires an API key. Set LINEAR_API_KEY env var or sinks.linear.apiKey.`);
|
|
27
|
+
}
|
|
28
|
+
const endpoint = parsed.endpoint ?? "https://api.linear.app/graphql";
|
|
29
|
+
const stateId = parsed.state
|
|
30
|
+
? isUuid(parsed.state)
|
|
31
|
+
? parsed.state
|
|
32
|
+
: await resolveStateId(this.fetchImpl, endpoint, apiKey, parsed.teamId, parsed.state)
|
|
33
|
+
: undefined;
|
|
34
|
+
const input = {
|
|
35
|
+
teamId: parsed.teamId,
|
|
36
|
+
title: rendered.title,
|
|
37
|
+
description: renderDescription(friction, rendered),
|
|
38
|
+
labelIds: undefined,
|
|
39
|
+
};
|
|
40
|
+
if (stateId)
|
|
41
|
+
input.stateId = stateId;
|
|
42
|
+
if (parsed.assignee)
|
|
43
|
+
input.assigneeId = parsed.assignee;
|
|
44
|
+
const variables = { input };
|
|
45
|
+
const response = await this.fetchImpl(endpoint, {
|
|
46
|
+
method: "POST",
|
|
47
|
+
headers: {
|
|
48
|
+
"content-type": "application/json",
|
|
49
|
+
authorization: apiKey,
|
|
50
|
+
},
|
|
51
|
+
body: JSON.stringify({ query: ISSUE_CREATE_MUTATION, variables }),
|
|
52
|
+
});
|
|
53
|
+
if (!response.ok) {
|
|
54
|
+
const body = await safeText(response);
|
|
55
|
+
throw new Error(`friction-log: linear API returned ${response.status}: ${body.slice(0, 400)}`);
|
|
56
|
+
}
|
|
57
|
+
const json = (await response.json());
|
|
58
|
+
if (json.errors?.length) {
|
|
59
|
+
throw new Error(`friction-log: linear API errors: ${json.errors.map((e) => e.message ?? "(no message)").join("; ")}`);
|
|
60
|
+
}
|
|
61
|
+
const issue = json.data?.issueCreate?.issue;
|
|
62
|
+
if (!json.data?.issueCreate?.success ||
|
|
63
|
+
!issue?.id ||
|
|
64
|
+
!issue.identifier ||
|
|
65
|
+
!issue.url) {
|
|
66
|
+
throw new Error(`friction-log: linear issueCreate did not return a complete issue: ${JSON.stringify(json).slice(0, 400)}`);
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
ok: true,
|
|
70
|
+
sinkTarget: `linear:${parsed.teamId}`,
|
|
71
|
+
externalRef: issue.url,
|
|
72
|
+
message: `created ${issue.identifier} (${issue.url})`,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const WORKFLOW_STATES_QUERY = `
|
|
77
|
+
query WorkflowStates($teamId: String!) {
|
|
78
|
+
team(id: $teamId) {
|
|
79
|
+
states { nodes { id name } }
|
|
80
|
+
}
|
|
81
|
+
}`.trim();
|
|
82
|
+
async function resolveStateId(fetchImpl, endpoint, apiKey, teamId, stateName) {
|
|
83
|
+
const response = await fetchImpl(endpoint, {
|
|
84
|
+
method: "POST",
|
|
85
|
+
headers: {
|
|
86
|
+
"content-type": "application/json",
|
|
87
|
+
authorization: apiKey,
|
|
88
|
+
},
|
|
89
|
+
body: JSON.stringify({
|
|
90
|
+
query: WORKFLOW_STATES_QUERY,
|
|
91
|
+
variables: { teamId },
|
|
92
|
+
}),
|
|
93
|
+
});
|
|
94
|
+
if (!response.ok) {
|
|
95
|
+
const body = await safeText(response);
|
|
96
|
+
throw new Error(`friction-log: linear state lookup ${response.status}: ${body.slice(0, 400)}`);
|
|
97
|
+
}
|
|
98
|
+
const json = (await response.json());
|
|
99
|
+
if (json.errors?.length) {
|
|
100
|
+
throw new Error(`friction-log: linear state lookup errors: ${json.errors.map((e) => e.message ?? "(no message)").join("; ")}`);
|
|
101
|
+
}
|
|
102
|
+
const nodes = json.data?.team?.states?.nodes ?? [];
|
|
103
|
+
const wanted = stateName.toLowerCase();
|
|
104
|
+
const match = nodes.find((n) => n.name?.toLowerCase() === wanted);
|
|
105
|
+
if (!match?.id) {
|
|
106
|
+
const known = nodes
|
|
107
|
+
.map((n) => n.name)
|
|
108
|
+
.filter(Boolean)
|
|
109
|
+
.join(", ");
|
|
110
|
+
throw new Error(`friction-log: linear team ${teamId} has no state matching "${stateName}". Known: ${known || "(none)"}`);
|
|
111
|
+
}
|
|
112
|
+
return match.id;
|
|
113
|
+
}
|
|
114
|
+
function parseOpts(opts) {
|
|
115
|
+
const raw = opts.sinkOpts ?? {};
|
|
116
|
+
const teamId = raw.teamId;
|
|
117
|
+
if (typeof teamId !== "string" || !teamId) {
|
|
118
|
+
throw new Error(`friction-log: linear sink requires "teamId". Set sinks.linear.teamId in config.yml or pass --sink-opt teamId=...`);
|
|
119
|
+
}
|
|
120
|
+
const state = optionalString(raw.state, "state");
|
|
121
|
+
const assignee = optionalString(raw.assignee, "assignee");
|
|
122
|
+
const apiKey = optionalString(raw.apiKey, "apiKey");
|
|
123
|
+
const endpoint = optionalString(raw.endpoint, "endpoint");
|
|
124
|
+
return { teamId, state, assignee, apiKey, endpoint };
|
|
125
|
+
}
|
|
126
|
+
function isUuid(s) {
|
|
127
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(s);
|
|
128
|
+
}
|
|
129
|
+
function optionalString(value, name) {
|
|
130
|
+
if (value == null)
|
|
131
|
+
return undefined;
|
|
132
|
+
if (typeof value === "string")
|
|
133
|
+
return value;
|
|
134
|
+
throw new Error(`friction-log: linear "${name}" must be a string`);
|
|
135
|
+
}
|
|
136
|
+
function renderDescription(friction, rendered) {
|
|
137
|
+
const meta = [];
|
|
138
|
+
meta.push(`friction-log id ${friction.id} captured ${friction.capturedAt}`);
|
|
139
|
+
if (friction.toolSurface)
|
|
140
|
+
meta.push(`tool: \`${friction.toolSurface}\``);
|
|
141
|
+
if (friction.category)
|
|
142
|
+
meta.push(`category: ${friction.category}`);
|
|
143
|
+
if (friction.severity)
|
|
144
|
+
meta.push(`severity: ${friction.severity}`);
|
|
145
|
+
if (friction.recurrenceOfId != null)
|
|
146
|
+
meta.push(`recurrence of friction #${friction.recurrenceOfId}`);
|
|
147
|
+
return `${rendered.body}\n\n---\n${meta.join(" \n")}\n`;
|
|
148
|
+
}
|
|
149
|
+
async function safeText(response) {
|
|
150
|
+
try {
|
|
151
|
+
return await response.text();
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return "(no body)";
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
//# sourceMappingURL=linear.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"linear.js","sourceRoot":"","sources":["../../src/sinks/linear.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { FileOptions, FileResult, Friction, RenderedTemplate, Sink } from "../types.js";
|
|
2
|
+
export declare class MarkdownFileSink implements Sink {
|
|
3
|
+
private readonly baseDir;
|
|
4
|
+
readonly name = "markdown-file";
|
|
5
|
+
constructor(baseDir?: string);
|
|
6
|
+
file(friction: Friction, rendered: RenderedTemplate, opts: FileOptions): Promise<FileResult>;
|
|
7
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { defaultMarkdownSinkDir } from "../paths.js";
|
|
4
|
+
export class MarkdownFileSink {
|
|
5
|
+
baseDir;
|
|
6
|
+
name = "markdown-file";
|
|
7
|
+
constructor(baseDir = defaultMarkdownSinkDir()) {
|
|
8
|
+
this.baseDir = baseDir;
|
|
9
|
+
}
|
|
10
|
+
async file(friction, rendered, opts) {
|
|
11
|
+
const targetDir = opts.sinkTarget ? resolve(opts.sinkTarget) : this.baseDir;
|
|
12
|
+
mkdirSync(targetDir, { recursive: true });
|
|
13
|
+
const filename = buildFilename(friction);
|
|
14
|
+
const fullPath = join(targetDir, filename);
|
|
15
|
+
const content = renderMarkdown(friction, rendered);
|
|
16
|
+
mkdirSync(dirname(fullPath), { recursive: true });
|
|
17
|
+
writeFileSync(fullPath, content, "utf8");
|
|
18
|
+
return {
|
|
19
|
+
ok: true,
|
|
20
|
+
sinkTarget: fullPath,
|
|
21
|
+
message: `wrote ${fullPath}`,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function buildFilename(friction) {
|
|
26
|
+
const date = friction.capturedAt.slice(0, 10);
|
|
27
|
+
const slug = friction.title
|
|
28
|
+
.toLowerCase()
|
|
29
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
30
|
+
.replace(/^-+|-+$/g, "")
|
|
31
|
+
.slice(0, 60) || `friction-${friction.id}`;
|
|
32
|
+
return `${date}-${friction.id}-${slug}.md`;
|
|
33
|
+
}
|
|
34
|
+
function renderMarkdown(friction, rendered) {
|
|
35
|
+
const frontmatter = [
|
|
36
|
+
"---",
|
|
37
|
+
`friction_id: ${friction.id}`,
|
|
38
|
+
`captured_at: ${friction.capturedAt}`,
|
|
39
|
+
`priority: ${rendered.priority}`,
|
|
40
|
+
`labels: [${rendered.labels.map((l) => JSON.stringify(l)).join(", ")}]`,
|
|
41
|
+
];
|
|
42
|
+
if (friction.toolSurface)
|
|
43
|
+
frontmatter.push(`tool_surface: ${friction.toolSurface}`);
|
|
44
|
+
if (friction.category)
|
|
45
|
+
frontmatter.push(`category: ${friction.category}`);
|
|
46
|
+
if (friction.severity)
|
|
47
|
+
frontmatter.push(`severity: ${friction.severity}`);
|
|
48
|
+
if (friction.sessionId)
|
|
49
|
+
frontmatter.push(`session_id: ${friction.sessionId}`);
|
|
50
|
+
frontmatter.push(`source: ${friction.source}`);
|
|
51
|
+
frontmatter.push("---");
|
|
52
|
+
return `${frontmatter.join("\n")}\n\n# ${rendered.title}\n\n${rendered.body}\n`;
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=markdown-file.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"markdown-file.js","sourceRoot":"","sources":["../../src/sinks/markdown-file.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { FileOptions, FileResult, Friction, RenderedTemplate, Sink } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Emit a structured JSON record to stdout. No side effects beyond stdout.
|
|
4
|
+
* Useful for piping into custom workflows:
|
|
5
|
+
*
|
|
6
|
+
* friction-log file 7 --sink stdout-json | jq '.rendered.body'
|
|
7
|
+
*
|
|
8
|
+
* The record shape is stable: any future field is additive.
|
|
9
|
+
*/
|
|
10
|
+
export declare class StdoutJsonSink implements Sink {
|
|
11
|
+
private readonly write;
|
|
12
|
+
readonly name = "stdout-json";
|
|
13
|
+
constructor(write?: (line: string) => void);
|
|
14
|
+
file(friction: Friction, rendered: RenderedTemplate, _opts: FileOptions): Promise<FileResult>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Emit a structured JSON record to stdout. No side effects beyond stdout.
|
|
3
|
+
* Useful for piping into custom workflows:
|
|
4
|
+
*
|
|
5
|
+
* friction-log file 7 --sink stdout-json | jq '.rendered.body'
|
|
6
|
+
*
|
|
7
|
+
* The record shape is stable: any future field is additive.
|
|
8
|
+
*/
|
|
9
|
+
export class StdoutJsonSink {
|
|
10
|
+
write;
|
|
11
|
+
name = "stdout-json";
|
|
12
|
+
// Tests inject a fake writer so we don't pollute the test runner's stdout.
|
|
13
|
+
constructor(write = (line) => process.stdout.write(line)) {
|
|
14
|
+
this.write = write;
|
|
15
|
+
}
|
|
16
|
+
async file(friction, rendered, _opts) {
|
|
17
|
+
const record = {
|
|
18
|
+
sink: this.name,
|
|
19
|
+
emittedAt: new Date().toISOString(),
|
|
20
|
+
friction: {
|
|
21
|
+
id: friction.id,
|
|
22
|
+
sessionId: friction.sessionId,
|
|
23
|
+
toolSurface: friction.toolSurface,
|
|
24
|
+
title: friction.title,
|
|
25
|
+
description: friction.description,
|
|
26
|
+
capturedAt: friction.capturedAt,
|
|
27
|
+
severity: friction.severity,
|
|
28
|
+
category: friction.category,
|
|
29
|
+
status: friction.status,
|
|
30
|
+
recurrenceOfId: friction.recurrenceOfId,
|
|
31
|
+
source: friction.source,
|
|
32
|
+
},
|
|
33
|
+
rendered: {
|
|
34
|
+
title: rendered.title,
|
|
35
|
+
body: rendered.body,
|
|
36
|
+
labels: rendered.labels,
|
|
37
|
+
priority: rendered.priority,
|
|
38
|
+
metadata: rendered.metadata,
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
this.write(JSON.stringify(record) + "\n");
|
|
42
|
+
return {
|
|
43
|
+
ok: true,
|
|
44
|
+
sinkTarget: "stdout",
|
|
45
|
+
message: "emitted JSON record to stdout",
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=stdout-json.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stdout-json.js","sourceRoot":"","sources":["../../src/sinks/stdout-json.ts"],"names":[],"mappings":"AAQA;;;;;;;GAOG;AACH,MAAM,OAAO,cAAc;IAKN;IAJV,IAAI,GAAG,aAAa,CAAC;IAE9B,2EAA2E;IAC3E,YACmB,QAAgC,CAAC,IAAI,EAAE,EAAE,CACxD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC;QADX,UAAK,GAAL,KAAK,CACM;IAC3B,CAAC;IAEJ,KAAK,CAAC,IAAI,CACR,QAAkB,EAClB,QAA0B,EAC1B,KAAkB;QAElB,MAAM,MAAM,GAAG;YACb,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;YACnC,QAAQ,EAAE;gBACR,EAAE,EAAE,QAAQ,CAAC,EAAE;gBACf,SAAS,EAAE,QAAQ,CAAC,SAAS;gBAC7B,WAAW,EAAE,QAAQ,CAAC,WAAW;gBACjC,KAAK,EAAE,QAAQ,CAAC,KAAK;gBACrB,WAAW,EAAE,QAAQ,CAAC,WAAW;gBACjC,UAAU,EAAE,QAAQ,CAAC,UAAU;gBAC/B,QAAQ,EAAE,QAAQ,CAAC,QAAQ;gBAC3B,QAAQ,EAAE,QAAQ,CAAC,QAAQ;gBAC3B,MAAM,EAAE,QAAQ,CAAC,MAAM;gBACvB,cAAc,EAAE,QAAQ,CAAC,cAAc;gBACvC,MAAM,EAAE,QAAQ,CAAC,MAAM;aACxB;YACD,QAAQ,EAAE;gBACR,KAAK,EAAE,QAAQ,CAAC,KAAK;gBACrB,IAAI,EAAE,QAAQ,CAAC,IAAI;gBACnB,MAAM,EAAE,QAAQ,CAAC,MAAM;gBACvB,QAAQ,EAAE,QAAQ,CAAC,QAAQ;gBAC3B,QAAQ,EAAE,QAAQ,CAAC,QAAQ;aAC5B;SACF,CAAC;QACF,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC;QAC1C,OAAO;YACL,EAAE,EAAE,IAAI;YACR,UAAU,EAAE,QAAQ;YACpB,OAAO,EAAE,+BAA+B;SACzC,CAAC;IACJ,CAAC;CACF"}
|
|
@@ -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._
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Friction, RenderedTemplate, Template } from "./types.js";
|
|
2
|
+
export declare function loadTemplate(name: string): Template;
|
|
3
|
+
export declare function listTemplates(): string[];
|
|
4
|
+
export declare function pickTemplateForCategory(category: string | null | undefined): string;
|
|
5
|
+
export declare function render(template: Template, friction: Friction, extra?: Record<string, string>): RenderedTemplate;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { readFileSync, readdirSync, existsSync } from "node:fs";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { parse as parseYaml } from "yaml";
|
|
5
|
+
const DEFAULT_TEMPLATE_NAME = "workflow-friction";
|
|
6
|
+
function templatesDir() {
|
|
7
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const candidates = [
|
|
9
|
+
resolve(here, "..", "templates"),
|
|
10
|
+
resolve(here, "..", "..", "templates"),
|
|
11
|
+
];
|
|
12
|
+
for (const c of candidates) {
|
|
13
|
+
if (existsSync(c))
|
|
14
|
+
return c;
|
|
15
|
+
}
|
|
16
|
+
throw new Error(`friction-log: cannot locate bundled templates (looked in ${candidates.join(", ")})`);
|
|
17
|
+
}
|
|
18
|
+
export function loadTemplate(name) {
|
|
19
|
+
const path = join(templatesDir(), `${name}.yml`);
|
|
20
|
+
if (!existsSync(path)) {
|
|
21
|
+
throw new Error(`friction-log: unknown template "${name}" (no file at ${path})`);
|
|
22
|
+
}
|
|
23
|
+
const raw = readFileSync(path, "utf8");
|
|
24
|
+
const parsed = parseYaml(raw);
|
|
25
|
+
if (!parsed || typeof parsed !== "object") {
|
|
26
|
+
throw new Error(`friction-log: template "${name}" is not a valid YAML object`);
|
|
27
|
+
}
|
|
28
|
+
const title = typeof parsed.title === "string" ? parsed.title : "";
|
|
29
|
+
const body = typeof parsed.body === "string" ? parsed.body : "";
|
|
30
|
+
if (!title || !body) {
|
|
31
|
+
throw new Error(`friction-log: template "${name}" must define both title and body`);
|
|
32
|
+
}
|
|
33
|
+
const labels = Array.isArray(parsed.labels)
|
|
34
|
+
? parsed.labels.filter((l) => typeof l === "string")
|
|
35
|
+
: [];
|
|
36
|
+
const priority = parsed.priority === "LOW" ||
|
|
37
|
+
parsed.priority === "MEDIUM" ||
|
|
38
|
+
parsed.priority === "HIGH" ||
|
|
39
|
+
parsed.priority === "CRITICAL"
|
|
40
|
+
? parsed.priority
|
|
41
|
+
: "MEDIUM";
|
|
42
|
+
return {
|
|
43
|
+
name,
|
|
44
|
+
title,
|
|
45
|
+
body,
|
|
46
|
+
labels,
|
|
47
|
+
priority,
|
|
48
|
+
metadata: parsed.metadata,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export function listTemplates() {
|
|
52
|
+
return readdirSync(templatesDir())
|
|
53
|
+
.filter((f) => f.endsWith(".yml"))
|
|
54
|
+
.map((f) => f.replace(/\.yml$/, ""))
|
|
55
|
+
.sort();
|
|
56
|
+
}
|
|
57
|
+
export function pickTemplateForCategory(category) {
|
|
58
|
+
if (!category)
|
|
59
|
+
return DEFAULT_TEMPLATE_NAME;
|
|
60
|
+
return listTemplates().includes(category) ? category : DEFAULT_TEMPLATE_NAME;
|
|
61
|
+
}
|
|
62
|
+
export function render(template, friction, extra = {}) {
|
|
63
|
+
const vars = {
|
|
64
|
+
id: String(friction.id),
|
|
65
|
+
title: friction.title,
|
|
66
|
+
description: friction.description ?? "",
|
|
67
|
+
tool: friction.toolSurface ?? "",
|
|
68
|
+
category: friction.category ?? "",
|
|
69
|
+
severity: friction.severity ?? "",
|
|
70
|
+
capturedAt: friction.capturedAt,
|
|
71
|
+
sessionId: friction.sessionId ?? "",
|
|
72
|
+
source: friction.source,
|
|
73
|
+
...extra,
|
|
74
|
+
};
|
|
75
|
+
return {
|
|
76
|
+
title: substitute(template.title, vars),
|
|
77
|
+
body: substitute(template.body, vars),
|
|
78
|
+
labels: template.labels,
|
|
79
|
+
priority: template.priority,
|
|
80
|
+
metadata: template.metadata,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function substitute(input, vars) {
|
|
84
|
+
return input.replace(/\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}/g, (_match, key) => {
|
|
85
|
+
return vars[key] ?? "";
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=templates.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"templates.js","sourceRoot":"","sources":["../src/templates.ts"],"names":[],"mappings":"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"}
|