@devinnn/docdrift 0.1.0 → 0.1.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
@@ -3,7 +3,8 @@
3
3
  Docs that never lie: detect drift between merged code and docs, then open low-noise, evidence-grounded remediation via Devin sessions.
4
4
 
5
5
  ## Deliverables
6
- - TypeScript CLI package (`docdrift`)
6
+
7
+ - **npm package**: [@devinnn/docdrift](https://www.npmjs.com/package/@devinnn/docdrift) — TypeScript CLI (`docdrift`)
7
8
  - `validate`
8
9
  - `detect --base <sha> --head <sha>`
9
10
  - `run --base <sha> --head <sha>`
@@ -14,6 +15,7 @@ Docs that never lie: detect drift between merged code and docs, then open low-no
14
15
  - PR template + Loom script
15
16
 
16
17
  ## Why this is low-noise
18
+
17
19
  - One PR per doc area per day (bundling rule).
18
20
  - Global PR/day cap.
19
21
  - Confidence gating and allowlist enforcement.
@@ -21,17 +23,20 @@ Docs that never lie: detect drift between merged code and docs, then open low-no
21
23
  - Idempotency key prevents duplicate actions for same repo/SHAs/action.
22
24
 
23
25
  ## Detection tiers
24
- - Tier 0: docs checks (`npm run docs:check`)
25
- - Tier 1: OpenAPI drift (`openapi/generated.json` vs `docs/reference/openapi.json`)
26
- - Tier 2: heuristic path impacts (e.g. `apps/api/src/auth/**` -> `docs/guides/auth.md`)
26
+
27
+ - Tier 0: docsite verification (`npm run docs:gen` then `npm run docs:build`)
28
+ - Tier 1: OpenAPI drift (`openapi/generated.json` vs `apps/docs-site/openapi/openapi.json`)
29
+ - Tier 2: heuristic path impacts (e.g. `apps/api/src/auth/**` -> `apps/docs-site/docs/guides/auth.md`)
27
30
 
28
31
  Output artifacts (under `.docdrift/`):
32
+
29
33
  - `drift_report.json`
30
34
  - `metrics.json` (after `run`)
31
35
 
32
36
  When you run docdrift as a package (e.g. `npx docdrift` or from another repo), all of this is written to **that repo’s** `.docdrift/` — i.e. the current working directory where the CLI is invoked, not inside the package. Add `.docdrift/` to the consuming repo’s `.gitignore` if you don’t want to commit run artifacts.
33
37
 
34
38
  ## Core flow (`docdrift run`)
39
+
35
40
  1. Validate config and command availability.
36
41
  2. Build drift report.
37
42
  3. Policy decision (`OPEN_PR | UPDATE_EXISTING_PR | OPEN_ISSUE | NOOP`).
@@ -41,7 +46,26 @@ When you run docdrift as a package (e.g. `npx docdrift` or from another repo), a
41
46
  7. Surface result via GitHub commit comment; open issue on blocked/low-confidence paths.
42
47
  8. Persist state in `.docdrift/state.json` and write `.docdrift/metrics.json`.
43
48
 
49
+ ## Where the docs are (this repo)
50
+
51
+ | Path | Purpose |
52
+ | ------------------------------------------ | ----------------------------------------------------------------------- |
53
+ | `apps/docs-site/openapi/openapi.json` | Published OpenAPI spec (docdrift updates this when drift is detected). |
54
+ | `apps/docs-site/docs/api/` | API reference MDX generated from the spec (`npm run docs:gen`). |
55
+ | `apps/docs-site/docs/guides/auth.md` | Conceptual auth guide (updated only for conceptual drift). |
56
+
57
+ The docsite is a Docusaurus app with `docusaurus-plugin-openapi-docs`. The **generated** spec from code lives at `openapi/generated.json` (from `npm run openapi:export`). Drift = generated vs published differ. Verification runs `docs:gen` and `docs:build` so the docsite actually builds.
58
+
59
+ ## How Devin updates them
60
+
61
+ 1. **Evidence bundle** — Docdrift builds a tarball with the drift report, OpenAPI diff, and impacted doc snippets, and uploads it to the Devin API as session attachments.
62
+ 2. **Devin session** — Devin is prompted (see `src/devin/prompts.ts`) to update only files under the allowlist (`openapi/**`, `apps/docs-site/**`), make minimal correct edits, run verification (`npm run docs:gen`, `npm run docs:build`), and open **one PR** per doc area with a clear description.
63
+ 3. **PR** — Devin updates `apps/docs-site/openapi/openapi.json` to match the current API, runs `docs:gen` to regenerate API reference MDX, and opens a pull request. You review and merge; the docsite builds and the docs are updated.
64
+
65
+ So the “fix” is a **PR opened by Devin** that you merge; the repo’s docs don’t change until that PR is merged.
66
+
44
67
  ## Local usage
68
+
45
69
  ```bash
46
70
  npm install
47
71
  npx tsx src/cli.ts validate
@@ -54,25 +78,29 @@ DEVIN_API_KEY=... GITHUB_TOKEN=... GITHUB_REPOSITORY=owner/repo GITHUB_SHA=<sha>
54
78
 
55
79
  You can run a full end-to-end demo locally with no remote repo. Ensure `.env` has `DEVIN_API_KEY` (and optionally `GITHUB_TOKEN` only when you have a real repo).
56
80
 
57
- 1. **One-time setup (already done if you have two commits with drift)**
58
- - Git is inited; baseline commit has docs in sync with API.
81
+ 1. **One-time setup (already done if you have two commits with drift)**
82
+ - Git is inited; baseline commit has docs in sync with API.
59
83
  - A later commit changes `apps/api/src/model.ts` (e.g. `name` → `fullName`) and runs `npm run openapi:export`, so `openapi/generated.json` drifts from `docs/reference/openapi.json`.
60
84
 
61
85
  2. **Run the pipeline**
86
+
62
87
  ```bash
63
88
  npm install
64
89
  npx tsx src/cli.ts validate
65
90
  npx tsx src/cli.ts detect --base b0f624f --head 6030902
66
91
  ```
92
+
67
93
  - Use your own `git log --oneline -3` to get `base` (older) and `head` (newer) SHAs if you recreated the demo.
68
94
 
69
95
  3. **Run with Devin (no GitHub calls)**
70
96
  Omit `GITHUB_TOKEN` so the CLI does not post comments or create issues. Devin session still runs; results are printed to stdout and written to `.docdrift/state.json` and `metrics.json`.
97
+
71
98
  ```bash
72
99
  export $(grep -v '^#' .env | xargs)
73
100
  unset GITHUB_TOKEN GITHUB_REPOSITORY GITHUB_SHA
74
101
  npx tsx src/cli.ts run --base b0f624f --head 6030902
75
102
  ```
103
+
76
104
  - `run` can take 1–3 minutes while the Devin session runs.
77
105
 
78
106
  4. **What you’ll see**
@@ -93,14 +121,15 @@ You can run a full end-to-end demo locally with no remote repo. Ensure `.env` ha
93
121
  ## Run on GitHub
94
122
 
95
123
  1. **Create a repo** on GitHub (e.g. `your-org/docdrift`), then add the remote and push:
124
+
96
125
  ```bash
97
126
  git remote add origin https://github.com/your-org/docdrift.git
98
127
  git push -u origin main
99
128
  ```
100
129
 
101
130
  2. **Add secret**
102
- Repo → **Settings** → **Secrets and variables** → **Actions** → **New repository secret**
103
- - Name: `DEVIN_API_KEY`
131
+ Repo → **Settings** → **Secrets and variables** → **Actions** → **New repository secret**
132
+ - Name: `DEVIN_API_KEY`
104
133
  - Value: your Devin API key (same as in `.env` locally)
105
134
 
106
135
  `GITHUB_TOKEN` is provided automatically; the workflow uses it for commit comments and issues.
@@ -109,6 +138,25 @@ You can run a full end-to-end demo locally with no remote repo. Ensure `.env` ha
109
138
  - **Push to `main`** — runs on every push (compares previous commit vs current).
110
139
  - **Manual run** — **Actions** tab → **devin-doc-drift** → **Run workflow** (uses `HEAD` and `HEAD^` as head/base).
111
140
 
141
+ ## See it work (demo on GitHub)
142
+
143
+ This repo has **intentional drift**: the API has been expanded (new fields `fullName`, `avatarUrl`, `createdAt`, `role` and new endpoint `GET /v1/users` with pagination), but **docs are unchanged** (`docs/reference/openapi.json` and `docs/reference/api.md` still describe the old single-endpoint, `id`/`name`/`email` only). Running docdrift will detect that and hand a large diff to Devin to fix via a PR. To see it:
144
+
145
+ 1. **Create a new GitHub repo** (e.g. `docdrift-demo`) so you have a clean place to run the workflow.
146
+ 2. **Push this project with full history** (so both commits are on `main`):
147
+ ```bash
148
+ git remote add origin https://github.com/YOUR_ORG/docdrift-demo.git
149
+ git push -u origin main
150
+ ```
151
+ 3. **Add secret** in that repo: **Settings** → **Secrets and variables** → **Actions** → `DEVIN_API_KEY` = your Devin API key.
152
+ 4. **Trigger the workflow**
153
+ - Either push another small commit (e.g. README tweak), or
154
+ - **Actions** → **devin-doc-drift** → **Run workflow**.
155
+ 5. **Where to look**
156
+ - **Actions** → open the run → **Run Doc Drift** step: the step logs print JSON with `sessionUrl`, `prUrl`, and `outcome` per doc area. Open any `sessionUrl` in your browser to see the Devin session.
157
+ - **Artifacts**: download **docdrift-artifacts** for `.docdrift/drift_report.json`, `.docdrift/metrics.json`, and evidence.
158
+ - **Devin dashboard**: sessions are tagged `docdrift`; you’ll see the run there once the step completes (often 1–3 minutes).
159
+
112
160
  ## Using in another repo (published package)
113
161
 
114
162
  Once published to npm, any repo can use the CLI locally or in GitHub Actions.
@@ -116,14 +164,14 @@ Once published to npm, any repo can use the CLI locally or in GitHub Actions.
116
164
  1. **In the consuming repo** add a `docdrift.yaml` at the root (see this repo’s `docdrift.yaml` and `docdrift-yml.md`).
117
165
  2. **CLI**
118
166
  ```bash
119
- npx docdrift@latest validate
120
- npx docdrift@latest detect --base <base-sha> --head <head-sha>
167
+ npx @devinnn/docdrift validate
168
+ npx @devinnn/docdrift detect --base <base-sha> --head <head-sha>
121
169
  # With env for run:
122
- DEVIN_API_KEY=... GITHUB_TOKEN=... GITHUB_REPOSITORY=owner/repo GITHUB_SHA=<sha> npx docdrift@latest run --base <base-sha> --head <head-sha>
170
+ DEVIN_API_KEY=... GITHUB_TOKEN=... GITHUB_REPOSITORY=owner/repo GITHUB_SHA=<sha> npx @devinnn/docdrift run --base <base-sha> --head <head-sha>
123
171
  ```
124
172
  3. **GitHub Actions** — add a step that runs the CLI (e.g. after checkout and setting base/head):
125
173
  ```yaml
126
- - run: npx docdrift@latest run --base ${{ steps.shas.outputs.base }} --head ${{ steps.shas.outputs.head }}
174
+ - run: npx @devinnn/docdrift run --base ${{ steps.shas.outputs.base }} --head ${{ steps.shas.outputs.head }}
127
175
  env:
128
176
  DEVIN_API_KEY: ${{ secrets.DEVIN_API_KEY }}
129
177
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -140,8 +188,10 @@ Once published to npm, any repo can use the CLI locally or in GitHub Actions.
140
188
  - Only the `dist/` directory is included (`files` in `package.json`). Consumers get the built CLI; they provide their own `docdrift.yaml` in their repo.
141
189
 
142
190
  ## Demo scenario
191
+
143
192
  - Autogen drift: rename a field in `apps/api/src/model.ts`, merge to `main`, observe docs PR path.
144
193
  - Conceptual drift: change auth behavior under `apps/api/src/auth/**`, merge to `main`, observe single escalation issue.
145
194
 
146
195
  ## Loom
196
+
147
197
  See `/Users/cameronking/Desktop/sideproject/docdrift/loom.md` for the minute-by-minute recording script.
package/dist/src/cli.js CHANGED
@@ -1,6 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
3
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
+ const node_fs_1 = __importDefault(require("node:fs"));
8
+ const node_path_1 = __importDefault(require("node:path"));
4
9
  const index_1 = require("./index");
5
10
  function getArg(args, flag) {
6
11
  const index = args.indexOf(flag);
@@ -32,6 +37,9 @@ async function main() {
32
37
  const headSha = (0, index_1.requireSha)(getArg(args, "--head"), "--head");
33
38
  const trigger = (0, index_1.resolveTrigger)(process.env.GITHUB_EVENT_NAME);
34
39
  const results = await (0, index_1.runDocDrift)({ baseSha, headSha, trigger });
40
+ const outPath = node_path_1.default.resolve(".docdrift", "run-output.json");
41
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(outPath), { recursive: true });
42
+ node_fs_1.default.writeFileSync(outPath, JSON.stringify(results, null, 2), "utf-8");
35
43
  console.log(JSON.stringify(results, null, 2));
36
44
  return;
37
45
  }
@@ -21,5 +21,18 @@ function loadConfig(configPath = "docdrift.yaml") {
21
21
  .join("\n");
22
22
  throw new Error(`Invalid config:\n${message}`);
23
23
  }
24
- return result.data;
24
+ const data = result.data;
25
+ if (data.devin.customInstructions?.length) {
26
+ const configDir = node_path_1.default.dirname(resolved);
27
+ const contents = [];
28
+ for (const p of data.devin.customInstructions) {
29
+ const fullPath = node_path_1.default.resolve(configDir, p);
30
+ if (!node_fs_1.default.existsSync(fullPath)) {
31
+ throw new Error(`Custom instructions file not found: ${fullPath}`);
32
+ }
33
+ contents.push(node_fs_1.default.readFileSync(fullPath, "utf8"));
34
+ }
35
+ data.devin.customInstructionContent = contents.join("\n\n");
36
+ }
37
+ return data;
25
38
  }
@@ -4,31 +4,31 @@ exports.docDriftConfigSchema = void 0;
4
4
  const zod_1 = require("zod");
5
5
  const pathRuleSchema = zod_1.z.object({
6
6
  match: zod_1.z.string().min(1),
7
- impacts: zod_1.z.array(zod_1.z.string().min(1)).min(1)
7
+ impacts: zod_1.z.array(zod_1.z.string().min(1)).min(1),
8
8
  });
9
9
  const openApiDetectSchema = zod_1.z.object({
10
10
  exportCmd: zod_1.z.string().min(1),
11
11
  generatedPath: zod_1.z.string().min(1),
12
- publishedPath: zod_1.z.string().min(1)
12
+ publishedPath: zod_1.z.string().min(1),
13
13
  });
14
14
  const docAreaSchema = zod_1.z.object({
15
15
  name: zod_1.z.string().min(1),
16
16
  mode: zod_1.z.enum(["autogen", "conceptual"]),
17
17
  owners: zod_1.z.object({
18
- reviewers: zod_1.z.array(zod_1.z.string().min(1)).min(1)
18
+ reviewers: zod_1.z.array(zod_1.z.string().min(1)).min(1),
19
19
  }),
20
20
  detect: zod_1.z
21
21
  .object({
22
22
  openapi: openApiDetectSchema.optional(),
23
- paths: zod_1.z.array(pathRuleSchema).optional()
23
+ paths: zod_1.z.array(pathRuleSchema).optional(),
24
24
  })
25
25
  .refine((v) => Boolean(v.openapi) || Boolean(v.paths?.length), {
26
- message: "docArea.detect must include openapi or paths"
26
+ message: "docArea.detect must include openapi or paths",
27
27
  }),
28
28
  patch: zod_1.z.object({
29
29
  targets: zod_1.z.array(zod_1.z.string().min(1)).optional(),
30
- requireHumanConfirmation: zod_1.z.boolean().optional().default(false)
31
- })
30
+ requireHumanConfirmation: zod_1.z.boolean().optional().default(false),
31
+ }),
32
32
  });
33
33
  exports.docDriftConfigSchema = zod_1.z.object({
34
34
  version: zod_1.z.literal(1),
@@ -36,20 +36,22 @@ exports.docDriftConfigSchema = zod_1.z.object({
36
36
  apiVersion: zod_1.z.literal("v1"),
37
37
  unlisted: zod_1.z.boolean().default(true),
38
38
  maxAcuLimit: zod_1.z.number().int().positive().default(2),
39
- tags: zod_1.z.array(zod_1.z.string().min(1)).default(["docdrift"])
39
+ tags: zod_1.z.array(zod_1.z.string().min(1)).default(["docdrift"]),
40
+ customInstructions: zod_1.z.array(zod_1.z.string().min(1)).optional(),
41
+ customInstructionContent: zod_1.z.string().optional(),
40
42
  }),
41
43
  policy: zod_1.z.object({
42
44
  prCaps: zod_1.z.object({
43
45
  maxPrsPerDay: zod_1.z.number().int().positive().default(1),
44
- maxFilesTouched: zod_1.z.number().int().positive().default(12)
46
+ maxFilesTouched: zod_1.z.number().int().positive().default(12),
45
47
  }),
46
48
  confidence: zod_1.z.object({
47
- autopatchThreshold: zod_1.z.number().min(0).max(1).default(0.8)
49
+ autopatchThreshold: zod_1.z.number().min(0).max(1).default(0.8),
48
50
  }),
49
51
  allowlist: zod_1.z.array(zod_1.z.string().min(1)).min(1),
50
52
  verification: zod_1.z.object({
51
- commands: zod_1.z.array(zod_1.z.string().min(1)).min(1)
52
- })
53
+ commands: zod_1.z.array(zod_1.z.string().min(1)).min(1),
54
+ }),
53
55
  }),
54
- docAreas: zod_1.z.array(docAreaSchema).min(1)
56
+ docAreas: zod_1.z.array(docAreaSchema).min(1),
55
57
  });
@@ -15,7 +15,7 @@ async function validateRuntimeConfig(config) {
15
15
  ...config.policy.verification.commands,
16
16
  ...config.docAreas
17
17
  .map((area) => area.detect.openapi?.exportCmd)
18
- .filter((value) => Boolean(value))
18
+ .filter((value) => Boolean(value)),
19
19
  ]);
20
20
  for (const command of commandSet) {
21
21
  const binary = commandBinary(command);
@@ -21,7 +21,7 @@ async function runDocsChecks(commands, evidenceDir) {
21
21
  "\n--- stdout ---",
22
22
  result.stdout,
23
23
  "\n--- stderr ---",
24
- result.stderr
24
+ result.stderr,
25
25
  ].join("\n"), "utf8");
26
26
  logs.push(logPath);
27
27
  commandResults.push({ command, exitCode: result.exitCode, logPath });
@@ -31,7 +31,7 @@ async function runDocsChecks(commands, evidenceDir) {
31
31
  return {
32
32
  logs,
33
33
  commandResults,
34
- summary: "Docs checks passed"
34
+ summary: "Docs checks passed",
35
35
  };
36
36
  }
37
37
  return {
@@ -42,7 +42,7 @@ async function runDocsChecks(commands, evidenceDir) {
42
42
  kind: "docs_check_failed",
43
43
  tier: 0,
44
44
  confidence: 0.99,
45
- evidence: failed.map((result) => result.logPath)
46
- }
45
+ evidence: failed.map((result) => result.logPath),
46
+ },
47
47
  };
48
48
  }
@@ -38,7 +38,7 @@ function detectHeuristicImpacts(docArea, changedPaths, evidenceDir) {
38
38
  kind: "heuristic_path_impact",
39
39
  tier: 2,
40
40
  confidence: 0.67,
41
- evidence: [evidencePath]
42
- }
41
+ evidence: [evidencePath],
42
+ },
43
43
  };
44
44
  }
@@ -26,7 +26,7 @@ async function buildDriftReport(input) {
26
26
  baseSha: input.baseSha,
27
27
  headSha: input.headSha,
28
28
  trigger: input.trigger,
29
- timestamp: new Date().toISOString()
29
+ timestamp: new Date().toISOString(),
30
30
  };
31
31
  const evidenceRoot = node_path_1.default.resolve(".docdrift", "evidence", runInfo.runId);
32
32
  (0, fs_1.ensureDir)(evidenceRoot);
@@ -69,7 +69,7 @@ async function buildDriftReport(input) {
69
69
  signals,
70
70
  impactedDocs: [...impactedDocs],
71
71
  recommendedAction: defaultRecommendation(docArea.mode, signals),
72
- summary: summaries.filter(Boolean).join(" | ")
72
+ summary: summaries.filter(Boolean).join(" | "),
73
73
  });
74
74
  }
75
75
  const report = {
@@ -78,15 +78,15 @@ async function buildDriftReport(input) {
78
78
  baseSha: input.baseSha,
79
79
  headSha: input.headSha,
80
80
  trigger: input.trigger,
81
- timestamp: runInfo.timestamp
81
+ timestamp: runInfo.timestamp,
82
82
  },
83
- items
83
+ items,
84
84
  };
85
85
  (0, fs_1.writeJsonFile)(node_path_1.default.resolve(".docdrift", "drift_report.json"), report);
86
86
  (0, fs_1.writeJsonFile)(node_path_1.default.join(evidenceRoot, "changeset.json"), {
87
87
  changedPaths,
88
88
  diffSummary,
89
- commits
89
+ commits,
90
90
  });
91
91
  return { report, changedPaths, evidenceRoot, runInfo, checkSummaries };
92
92
  }
@@ -56,7 +56,7 @@ async function detectOpenApiDrift(docArea, evidenceDir) {
56
56
  "\n--- stdout ---",
57
57
  exportResult.stdout,
58
58
  "\n--- stderr ---",
59
- exportResult.stderr
59
+ exportResult.stderr,
60
60
  ].join("\n"), "utf8");
61
61
  if (exportResult.exitCode !== 0) {
62
62
  return {
@@ -67,8 +67,8 @@ async function detectOpenApiDrift(docArea, evidenceDir) {
67
67
  kind: "weak_evidence",
68
68
  tier: 2,
69
69
  confidence: 0.35,
70
- evidence: [exportLogPath]
71
- }
70
+ evidence: [exportLogPath],
71
+ },
72
72
  };
73
73
  }
74
74
  if (!node_fs_1.default.existsSync(openapi.generatedPath) || !node_fs_1.default.existsSync(openapi.publishedPath)) {
@@ -80,8 +80,8 @@ async function detectOpenApiDrift(docArea, evidenceDir) {
80
80
  kind: "weak_evidence",
81
81
  tier: 2,
82
82
  confidence: 0.35,
83
- evidence: [exportLogPath]
84
- }
83
+ evidence: [exportLogPath],
84
+ },
85
85
  };
86
86
  }
87
87
  const generatedRaw = node_fs_1.default.readFileSync(openapi.generatedPath, "utf8");
@@ -94,7 +94,7 @@ async function detectOpenApiDrift(docArea, evidenceDir) {
94
94
  return {
95
95
  impactedDocs: [openapi.publishedPath],
96
96
  evidenceFiles: [exportLogPath],
97
- summary: "No OpenAPI drift detected"
97
+ summary: "No OpenAPI drift detected",
98
98
  };
99
99
  }
100
100
  const summary = summarizeSpecDelta(publishedJson, generatedJson);
@@ -107,7 +107,7 @@ async function detectOpenApiDrift(docArea, evidenceDir) {
107
107
  normalizedPublished,
108
108
  "",
109
109
  "# Generated (normalized)",
110
- normalizedGenerated
110
+ normalizedGenerated,
111
111
  ].join("\n"), "utf8");
112
112
  return {
113
113
  impactedDocs: [...new Set([openapi.publishedPath, ...(docArea.patch.targets ?? [])])],
@@ -117,7 +117,7 @@ async function detectOpenApiDrift(docArea, evidenceDir) {
117
117
  kind: "openapi_diff",
118
118
  tier: 1,
119
119
  confidence: 0.95,
120
- evidence: [diffPath]
121
- }
120
+ evidence: [diffPath],
121
+ },
122
122
  };
123
123
  }
@@ -6,7 +6,7 @@ function attachmentBlock(attachmentUrls) {
6
6
  return attachmentUrls.map((url, index) => `- ATTACHMENT ${index + 1}: ${url}`).join("\n");
7
7
  }
8
8
  function buildAutogenPrompt(input) {
9
- return [
9
+ const base = [
10
10
  "You are Devin. Task: update API reference docs to match actual code/spec changes.",
11
11
  "",
12
12
  "EVIDENCE (attachments):",
@@ -30,12 +30,16 @@ function buildAutogenPrompt(input) {
30
30
  " e) get blocked (status=BLOCKED + questions),",
31
31
  " f) complete (status=DONE).",
32
32
  "",
33
- `Goal: Produce a PR for doc area ${input.item.docArea} using only the evidence.`
33
+ `Goal: Produce a PR for doc area ${input.item.docArea} using only the evidence.`,
34
34
  ].join("\n");
35
+ if (input.customAppend) {
36
+ return base + "\n\n---\n\nCustom instructions:\n\n" + input.customAppend;
37
+ }
38
+ return base;
35
39
  }
36
40
  function buildConceptualPrompt(input) {
37
- return [
38
- "You are Devin. Task: propose minimal edits to conceptual docs potentially impacted by code changes.",
41
+ const base = [
42
+ "You are Devin. Task: propose minimal edits to conceptual docs potentially impacted by code changes. i..e GUIDES",
39
43
  "",
40
44
  "EVIDENCE (attachments):",
41
45
  attachmentBlock(input.attachmentUrls),
@@ -50,6 +54,10 @@ function buildConceptualPrompt(input) {
50
54
  "- Update at planning, editing, verifying, open-pr, blocked, done milestones.",
51
55
  "- If blocked, fill blocked.questions with concrete, reviewer-actionable questions.",
52
56
  "",
53
- "Goal: either open a very small PR with confidence or open an issue/comment with crisp questions and suggested patch text."
57
+ "Goal: either open a very small PR with confidence or open an issue/comment with crisp questions and suggested patch text.",
54
58
  ].join("\n");
59
+ if (input.customAppend) {
60
+ return base + "\n\n---\n\nCustom instructions:\n\n" + input.customAppend;
61
+ }
62
+ return base;
55
63
  }
@@ -13,7 +13,7 @@ exports.PatchPlanSchema = {
13
13
  "evidence",
14
14
  "filesToEdit",
15
15
  "verification",
16
- "nextAction"
16
+ "nextAction",
17
17
  ],
18
18
  properties: {
19
19
  status: { enum: ["PLANNING", "EDITING", "VERIFYING", "OPENED_PR", "BLOCKED", "DONE"] },
@@ -27,8 +27,8 @@ exports.PatchPlanSchema = {
27
27
  required: ["attachments", "diffSummary"],
28
28
  properties: {
29
29
  attachments: { type: "array", items: { type: "string" } },
30
- diffSummary: { type: "string" }
31
- }
30
+ diffSummary: { type: "string" },
31
+ },
32
32
  },
33
33
  filesToEdit: { type: "array", items: { type: "string" } },
34
34
  verification: {
@@ -37,8 +37,8 @@ exports.PatchPlanSchema = {
37
37
  required: ["commands"],
38
38
  properties: {
39
39
  commands: { type: "array", items: { type: "string" } },
40
- results: { type: "array", items: { type: "string" } }
41
- }
40
+ results: { type: "array", items: { type: "string" } },
41
+ },
42
42
  },
43
43
  nextAction: { enum: ["OPEN_PR", "OPEN_ISSUE", "NOOP"] },
44
44
  pr: {
@@ -46,18 +46,18 @@ exports.PatchPlanSchema = {
46
46
  additionalProperties: false,
47
47
  properties: {
48
48
  title: { type: "string" },
49
- url: { type: "string" }
50
- }
49
+ url: { type: "string" },
50
+ },
51
51
  },
52
52
  blocked: {
53
53
  type: "object",
54
54
  additionalProperties: false,
55
55
  properties: {
56
56
  reason: { type: "string" },
57
- questions: { type: "array", items: { type: "string" } }
58
- }
59
- }
60
- }
57
+ questions: { type: "array", items: { type: "string" } },
58
+ },
59
+ },
60
+ },
61
61
  };
62
62
  exports.PatchResultSchema = {
63
63
  type: "object",
@@ -74,8 +74,8 @@ exports.PatchResultSchema = {
74
74
  required: ["commands", "results"],
75
75
  properties: {
76
76
  commands: { type: "array", items: { type: "string" } },
77
- results: { type: "array", items: { type: "string" } }
78
- }
77
+ results: { type: "array", items: { type: "string" } },
78
+ },
79
79
  },
80
80
  links: {
81
81
  type: "object",
@@ -84,16 +84,16 @@ exports.PatchResultSchema = {
84
84
  properties: {
85
85
  sessionUrl: { type: "string" },
86
86
  prUrl: { type: "string" },
87
- issueUrl: { type: "string" }
88
- }
87
+ issueUrl: { type: "string" },
88
+ },
89
89
  },
90
90
  blocked: {
91
91
  type: "object",
92
92
  additionalProperties: false,
93
93
  properties: {
94
94
  reason: { type: "string" },
95
- questions: { type: "array", items: { type: "string" } }
96
- }
97
- }
98
- }
95
+ questions: { type: "array", items: { type: "string" } },
96
+ },
97
+ },
98
+ },
99
99
  };
@@ -24,9 +24,9 @@ async function devinUploadAttachment(apiKey, filePath) {
24
24
  const response = await fetch("https://api.devin.ai/v1/attachments", {
25
25
  method: "POST",
26
26
  headers: {
27
- Authorization: `Bearer ${apiKey}`
27
+ Authorization: `Bearer ${apiKey}`,
28
28
  },
29
- body: form
29
+ body: form,
30
30
  });
31
31
  const text = await response.text();
32
32
  ensureOk(response, text, "Upload attachment");
@@ -49,9 +49,9 @@ async function devinCreateSession(apiKey, body) {
49
49
  method: "POST",
50
50
  headers: {
51
51
  Authorization: `Bearer ${apiKey}`,
52
- "Content-Type": "application/json"
52
+ "Content-Type": "application/json",
53
53
  },
54
- body: JSON.stringify(body)
54
+ body: JSON.stringify(body),
55
55
  });
56
56
  const text = await response.text();
57
57
  ensureOk(response, text, "Create session");
@@ -60,8 +60,8 @@ async function devinCreateSession(apiKey, body) {
60
60
  async function devinGetSession(apiKey, sessionId) {
61
61
  const response = await fetch(`https://api.devin.ai/v1/sessions/${sessionId}`, {
62
62
  headers: {
63
- Authorization: `Bearer ${apiKey}`
64
- }
63
+ Authorization: `Bearer ${apiKey}`,
64
+ },
65
65
  });
66
66
  const text = await response.text();
67
67
  ensureOk(response, text, "Get session");
@@ -77,8 +77,8 @@ async function devinListSessions(apiKey, params = {}) {
77
77
  }
78
78
  const response = await fetch(url, {
79
79
  headers: {
80
- Authorization: `Bearer ${apiKey}`
81
- }
80
+ Authorization: `Bearer ${apiKey}`,
81
+ },
82
82
  });
83
83
  const text = await response.text();
84
84
  ensureOk(response, text, "List sessions");
@@ -52,7 +52,7 @@ async function buildEvidenceBundle(input) {
52
52
  baseSha: input.runInfo.baseSha,
53
53
  headSha: input.runInfo.headSha,
54
54
  trigger: input.runInfo.trigger,
55
- timestamp: input.runInfo.timestamp
55
+ timestamp: input.runInfo.timestamp,
56
56
  },
57
57
  docArea: input.item.docArea,
58
58
  mode: input.item.mode,
@@ -60,7 +60,7 @@ async function buildEvidenceBundle(input) {
60
60
  signals: input.item.signals,
61
61
  impactedDocs: input.item.impactedDocs,
62
62
  copiedEvidence,
63
- copiedDocs
63
+ copiedDocs,
64
64
  });
65
65
  const archivePath = `${bundleDir}.tar.gz`;
66
66
  const parent = node_path_1.default.dirname(bundleDir);
@@ -73,7 +73,7 @@ async function buildEvidenceBundle(input) {
73
73
  bundleDir,
74
74
  archivePath,
75
75
  manifestPath,
76
- attachmentPaths: [archivePath, manifestPath]
76
+ attachmentPaths: [archivePath, manifestPath],
77
77
  };
78
78
  }
79
79
  function writeMetrics(metrics) {
@@ -19,7 +19,7 @@ async function postCommitComment(input) {
19
19
  owner,
20
20
  repo,
21
21
  commit_sha: input.commitSha,
22
- body: input.body
22
+ body: input.body,
23
23
  });
24
24
  return response.data.html_url;
25
25
  }
@@ -31,7 +31,7 @@ async function createIssue(input) {
31
31
  repo,
32
32
  title: input.issue.title,
33
33
  body: input.issue.body,
34
- labels: input.issue.labels
34
+ labels: input.issue.labels,
35
35
  });
36
36
  return response.data.html_url;
37
37
  }
package/dist/src/index.js CHANGED
@@ -45,7 +45,7 @@ function inferQuestions(structured) {
45
45
  }
46
46
  return [
47
47
  "Which conceptual docs should be updated for this behavior change?",
48
- "What are the exact user-visible semantics after this merge?"
48
+ "What are the exact user-visible semantics after this merge?",
49
49
  ];
50
50
  }
51
51
  async function executeSession(input) {
@@ -60,14 +60,16 @@ async function executeSession(input) {
60
60
  attachmentUrls,
61
61
  verificationCommands: input.config.policy.verification.commands,
62
62
  allowlist: input.config.policy.allowlist,
63
- confidenceThreshold: input.config.policy.confidence.autopatchThreshold
63
+ confidenceThreshold: input.config.policy.confidence.autopatchThreshold,
64
+ customAppend: input.config.devin.customInstructionContent ?? undefined,
64
65
  })
65
66
  : (0, prompts_1.buildConceptualPrompt)({
66
67
  item: input.item,
67
68
  attachmentUrls,
68
69
  verificationCommands: input.config.policy.verification.commands,
69
70
  allowlist: input.config.policy.allowlist,
70
- confidenceThreshold: input.config.policy.confidence.autopatchThreshold
71
+ confidenceThreshold: input.config.policy.confidence.autopatchThreshold,
72
+ customAppend: input.config.devin.customInstructionContent ?? undefined,
71
73
  });
72
74
  const session = await (0, v1_1.devinCreateSession)(input.apiKey, {
73
75
  prompt,
@@ -76,13 +78,13 @@ async function executeSession(input) {
76
78
  tags: [...new Set([...(input.config.devin.tags ?? []), "docdrift", input.item.docArea])],
77
79
  attachments: attachmentUrls,
78
80
  structured_output: {
79
- schema: schemas_1.PatchPlanSchema
81
+ schema: schemas_1.PatchPlanSchema,
80
82
  },
81
83
  metadata: {
82
84
  repository: input.repository,
83
85
  docArea: input.item.docArea,
84
- mode: input.item.mode
85
- }
86
+ mode: input.item.mode,
87
+ },
86
88
  });
87
89
  const finalSession = await (0, v1_1.pollUntilTerminal)(input.apiKey, session.session_id);
88
90
  const structured = parseStructured(finalSession);
@@ -96,7 +98,7 @@ async function executeSession(input) {
96
98
  : verificationCommands.map(() => "not reported");
97
99
  const verification = verificationCommands.map((command, idx) => ({
98
100
  command,
99
- result: verificationResults[idx] ?? "not reported"
101
+ result: verificationResults[idx] ?? "not reported",
100
102
  }));
101
103
  if (prUrl) {
102
104
  return {
@@ -104,7 +106,7 @@ async function executeSession(input) {
104
106
  summary: String(structured?.summary ?? "PR opened by Devin"),
105
107
  sessionUrl: session.url,
106
108
  prUrl,
107
- verification
109
+ verification,
108
110
  };
109
111
  }
110
112
  if (status === "blocked" || structured?.status === "BLOCKED") {
@@ -113,14 +115,14 @@ async function executeSession(input) {
113
115
  summary: String(structured?.blocked?.reason ?? structured?.summary ?? "Session blocked"),
114
116
  sessionUrl: session.url,
115
117
  questions: inferQuestions(structured),
116
- verification
118
+ verification,
117
119
  };
118
120
  }
119
121
  return {
120
122
  outcome: "NO_CHANGE",
121
123
  summary: String(structured?.summary ?? "Session completed without PR"),
122
124
  sessionUrl: session.url,
123
- verification
125
+ verification,
124
126
  };
125
127
  }
126
128
  async function runDetect(options) {
@@ -135,7 +137,7 @@ async function runDetect(options) {
135
137
  repo,
136
138
  baseSha: options.baseSha,
137
139
  headSha: options.headSha,
138
- trigger: options.trigger ?? "manual"
140
+ trigger: options.trigger ?? "manual",
139
141
  });
140
142
  (0, log_1.logInfo)(`Drift items detected: ${report.items.length}`);
141
143
  return { hasDrift: report.items.length > 0 };
@@ -155,7 +157,7 @@ async function runDocDrift(options) {
155
157
  repo,
156
158
  baseSha: options.baseSha,
157
159
  headSha: options.headSha,
158
- trigger: options.trigger ?? "manual"
160
+ trigger: options.trigger ?? "manual",
159
161
  });
160
162
  const docAreaByName = new Map(config.docAreas.map((area) => [area.name, area]));
161
163
  let state = (0, state_1.loadState)();
@@ -168,7 +170,7 @@ async function runDocDrift(options) {
168
170
  blockedCount: 0,
169
171
  timeToSessionTerminalMs: [],
170
172
  docAreaCounts: {},
171
- noiseRateProxy: 0
173
+ noiseRateProxy: 0,
172
174
  };
173
175
  for (const item of report.items) {
174
176
  metrics.docAreaCounts[item.docArea] = (metrics.docAreaCounts[item.docArea] ?? 0) + 1;
@@ -183,14 +185,14 @@ async function runDocDrift(options) {
183
185
  state,
184
186
  repo,
185
187
  baseSha: options.baseSha,
186
- headSha: options.headSha
188
+ headSha: options.headSha,
187
189
  });
188
190
  if (decision.action === "NOOP") {
189
191
  results.push({
190
192
  docArea: item.docArea,
191
193
  decision,
192
194
  outcome: "NO_CHANGE",
193
- summary: decision.reason
195
+ summary: decision.reason,
194
196
  });
195
197
  continue;
196
198
  }
@@ -205,14 +207,14 @@ async function runDocDrift(options) {
205
207
  decision,
206
208
  outcome,
207
209
  summary,
208
- prUrl: existingPr
210
+ prUrl: existingPr,
209
211
  });
210
212
  state = (0, engine_1.applyDecisionToState)({
211
213
  state,
212
214
  decision,
213
215
  docArea: item.docArea,
214
216
  outcome,
215
- link: existingPr
217
+ link: existingPr,
216
218
  });
217
219
  continue;
218
220
  }
@@ -221,7 +223,10 @@ async function runDocDrift(options) {
221
223
  let sessionOutcome = {
222
224
  outcome: "NO_CHANGE",
223
225
  summary: "Skipped Devin session",
224
- verification: config.policy.verification.commands.map((command) => ({ command, result: "not run" }))
226
+ verification: config.policy.verification.commands.map((command) => ({
227
+ command,
228
+ result: "not run",
229
+ })),
225
230
  };
226
231
  if (devinApiKey) {
227
232
  const sessionStart = Date.now();
@@ -230,7 +235,7 @@ async function runDocDrift(options) {
230
235
  repository: repo,
231
236
  item,
232
237
  attachmentPaths,
233
- config
238
+ config,
234
239
  });
235
240
  metrics.timeToSessionTerminalMs.push(Date.now() - sessionStart);
236
241
  }
@@ -240,12 +245,17 @@ async function runDocDrift(options) {
240
245
  outcome: "BLOCKED",
241
246
  summary: "DEVIN_API_KEY missing; cannot start Devin session",
242
247
  questions: ["Set DEVIN_API_KEY in environment or GitHub Actions secrets"],
243
- verification: config.policy.verification.commands.map((command) => ({ command, result: "not run" }))
248
+ verification: config.policy.verification.commands.map((command) => ({
249
+ command,
250
+ result: "not run",
251
+ })),
244
252
  };
245
253
  }
246
254
  let issueUrl;
247
255
  if (githubToken &&
248
- (decision.action === "OPEN_ISSUE" || sessionOutcome.outcome === "BLOCKED" || sessionOutcome.outcome === "NO_CHANGE")) {
256
+ (decision.action === "OPEN_ISSUE" ||
257
+ sessionOutcome.outcome === "BLOCKED" ||
258
+ sessionOutcome.outcome === "NO_CHANGE")) {
249
259
  issueUrl = await (0, client_1.createIssue)({
250
260
  token: githubToken,
251
261
  repository: repo,
@@ -254,11 +264,13 @@ async function runDocDrift(options) {
254
264
  body: (0, client_1.renderBlockedIssueBody)({
255
265
  docArea: item.docArea,
256
266
  evidenceSummary: item.summary,
257
- questions: sessionOutcome.questions ?? ["Please confirm intended behavior and doc wording."],
258
- sessionUrl: sessionOutcome.sessionUrl
267
+ questions: sessionOutcome.questions ?? [
268
+ "Please confirm intended behavior and doc wording.",
269
+ ],
270
+ sessionUrl: sessionOutcome.sessionUrl,
259
271
  }),
260
- labels: ["docdrift"]
261
- }
272
+ labels: ["docdrift"],
273
+ },
262
274
  });
263
275
  metrics.issuesOpened += 1;
264
276
  sessionOutcome.outcome = "ISSUE_OPENED";
@@ -276,7 +288,7 @@ async function runDocDrift(options) {
276
288
  summary: sessionOutcome.summary,
277
289
  sessionUrl: sessionOutcome.sessionUrl,
278
290
  prUrl: sessionOutcome.prUrl,
279
- issueUrl
291
+ issueUrl,
280
292
  };
281
293
  results.push(result);
282
294
  if (githubToken) {
@@ -288,13 +300,13 @@ async function runDocDrift(options) {
288
300
  sessionUrl: sessionOutcome.sessionUrl,
289
301
  prUrl: sessionOutcome.prUrl,
290
302
  issueUrl,
291
- validation: sessionOutcome.verification
303
+ validation: sessionOutcome.verification,
292
304
  });
293
305
  await (0, client_1.postCommitComment)({
294
306
  token: githubToken,
295
307
  repository: repo,
296
308
  commitSha,
297
- body
309
+ body,
298
310
  });
299
311
  }
300
312
  state = (0, engine_1.applyDecisionToState)({
@@ -302,16 +314,18 @@ async function runDocDrift(options) {
302
314
  decision,
303
315
  docArea: item.docArea,
304
316
  outcome: sessionOutcome.outcome,
305
- link: sessionOutcome.prUrl ?? issueUrl
317
+ link: sessionOutcome.prUrl ?? issueUrl,
306
318
  });
307
319
  }
308
320
  (0, state_1.saveState)(state);
309
321
  metrics.noiseRateProxy =
310
- metrics.driftItemsDetected === 0 ? 0 : Number((metrics.prsOpened / metrics.driftItemsDetected).toFixed(4));
322
+ metrics.driftItemsDetected === 0
323
+ ? 0
324
+ : Number((metrics.prsOpened / metrics.driftItemsDetected).toFixed(4));
311
325
  (0, bundle_1.writeMetrics)(metrics);
312
326
  (0, log_1.logInfo)("Run complete", {
313
327
  items: report.items.length,
314
- elapsedMs: Date.now() - startedAt
328
+ elapsedMs: Date.now() - startedAt,
315
329
  });
316
330
  return results;
317
331
  }
@@ -5,6 +5,6 @@ const emptyState = () => ({
5
5
  idempotency: {},
6
6
  dailyPrCount: {},
7
7
  areaDailyPrOpened: {},
8
- areaLatestPr: {}
8
+ areaLatestPr: {},
9
9
  });
10
10
  exports.emptyState = emptyState;
@@ -6,7 +6,7 @@ const tierWeight = {
6
6
  0: 1,
7
7
  1: 0.9,
8
8
  2: 0.6,
9
- 3: 0.35
9
+ 3: 0.35,
10
10
  };
11
11
  function clamp01(value) {
12
12
  return Math.max(0, Math.min(1, value));
@@ -70,21 +70,21 @@ function decidePolicy(input) {
70
70
  docArea: item.docArea,
71
71
  baseSha: input.baseSha,
72
72
  headSha: input.headSha,
73
- action
73
+ action,
74
74
  });
75
75
  if (state.idempotency[idempotencyKey]) {
76
76
  return {
77
77
  action: "NOOP",
78
78
  confidence,
79
79
  reason: "Idempotency key already processed",
80
- idempotencyKey
80
+ idempotencyKey,
81
81
  };
82
82
  }
83
83
  return {
84
84
  action,
85
85
  confidence,
86
86
  reason,
87
- idempotencyKey
87
+ idempotencyKey,
88
88
  };
89
89
  }
90
90
  function applyDecisionToState(input) {
@@ -94,7 +94,7 @@ function applyDecisionToState(input) {
94
94
  createdAt: new Date().toISOString(),
95
95
  action: input.decision.action,
96
96
  outcome: input.outcome,
97
- link: input.link
97
+ link: input.link,
98
98
  };
99
99
  next.idempotency[input.decision.idempotencyKey] = record;
100
100
  if (input.outcome === "PR_OPENED") {
@@ -8,7 +8,7 @@ async function execCommand(command, cwd = process.cwd()) {
8
8
  try {
9
9
  const { stdout, stderr } = await exec(command, {
10
10
  cwd,
11
- maxBuffer: 10 * 1024 * 1024
11
+ maxBuffer: 10 * 1024 * 1024,
12
12
  });
13
13
  return { command, stdout, stderr, exitCode: 0 };
14
14
  }
@@ -18,7 +18,7 @@ async function execCommand(command, cwd = process.cwd()) {
18
18
  command,
19
19
  stdout: e.stdout ?? "",
20
20
  stderr: e.stderr ?? String(error),
21
- exitCode: typeof e.code === "number" ? e.code : 1
21
+ exitCode: typeof e.code === "number" ? e.code : 1,
22
22
  };
23
23
  }
24
24
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devinnn/docdrift",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "private": false,
5
5
  "description": "Detect and remediate documentation drift with Devin sessions",
6
6
  "main": "dist/src/index.js",
@@ -11,9 +11,20 @@
11
11
  "engines": {
12
12
  "node": ">=20"
13
13
  },
14
- "files": ["dist/src"],
15
- "repository": { "type": "git", "url": "" },
16
- "keywords": ["docs", "drift", "openapi", "devin", "github-actions"],
14
+ "files": [
15
+ "dist/src"
16
+ ],
17
+ "repository": {
18
+ "type": "git",
19
+ "url": ""
20
+ },
21
+ "keywords": [
22
+ "docs",
23
+ "drift",
24
+ "openapi",
25
+ "devin",
26
+ "github-actions"
27
+ ],
17
28
  "license": "MIT",
18
29
  "scripts": {
19
30
  "build": "tsc -p tsconfig.json",
@@ -21,7 +32,9 @@
21
32
  "lint": "eslint .",
22
33
  "format": "prettier -w .",
23
34
  "openapi:export": "tsx apps/api/scripts/export-openapi.ts",
24
- "docs:check": "node scripts/docs-check.js",
35
+ "docs:gen": "npm run --prefix apps/docs-site docusaurus -- gen-api-docs api",
36
+ "docs:build": "npm run --prefix apps/docs-site build",
37
+ "docs:serve": "npm run --prefix apps/docs-site start",
25
38
  "docdrift": "tsx src/cli.ts"
26
39
  },
27
40
  "dependencies": {