@doxbrix/doxloop 0.1.5 → 0.2.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 +1145 -0
- package/README.md +422 -227
- package/assets/doxbrix-preview.css +77 -15
- package/contracts/agent-events-v1.schema.json +1 -0
- package/contracts/coverage-v1.schema.json +59 -0
- package/contracts/drift-v1.schema.json +1 -0
- package/contracts/evaluation-v1.schema.json +62 -0
- package/contracts/quality-codes-v1.json +1 -0
- package/contracts/quality-report-v1.schema.json +1 -0
- package/contracts/validation-v1.schema.json +1 -0
- package/dist/agent-failure.d.ts +36 -0
- package/dist/agent-failure.js +98 -0
- package/dist/agent-isolation.d.ts +17 -0
- package/dist/agent-isolation.js +94 -0
- package/dist/agent-log.d.ts +197 -0
- package/dist/agent-log.js +1094 -0
- package/dist/agent-process.d.ts +63 -0
- package/dist/agent-process.js +128 -0
- package/dist/agent-reply.d.ts +47 -0
- package/dist/agent-reply.js +315 -0
- package/dist/agents.d.ts +24 -5
- package/dist/agents.js +60 -9
- package/dist/api-coverage.d.ts +11 -0
- package/dist/api-coverage.js +72 -0
- package/dist/application-probe.d.ts +28 -0
- package/dist/application-probe.js +93 -0
- package/dist/application-url.d.ts +14 -0
- package/dist/application-url.js +26 -0
- package/dist/args.js +15 -0
- package/dist/artifact-deploy.d.ts +9 -0
- package/dist/artifact-deploy.js +28 -8
- package/dist/assets.d.ts +72 -0
- package/dist/assets.js +382 -0
- package/dist/auth.d.ts +10 -0
- package/dist/auth.js +38 -9
- package/dist/author.d.ts +206 -4
- package/dist/author.js +1650 -52
- package/dist/authoring-batches.d.ts +190 -0
- package/dist/authoring-batches.js +489 -0
- package/dist/authoring-postpass.d.ts +79 -0
- package/dist/authoring-postpass.js +1289 -0
- package/dist/authoring-progress.d.ts +95 -0
- package/dist/authoring-progress.js +269 -0
- package/dist/autosync.d.ts +56 -0
- package/dist/autosync.js +442 -0
- package/dist/batch-artifacts.d.ts +81 -0
- package/dist/batch-artifacts.js +412 -0
- package/dist/batch-limits.d.ts +37 -0
- package/dist/batch-limits.js +80 -0
- package/dist/branding.d.ts +41 -0
- package/dist/branding.js +229 -0
- package/dist/bundle-upload.d.ts +26 -0
- package/dist/bundle-upload.js +99 -0
- package/dist/capture-auth.d.ts +95 -0
- package/dist/capture-auth.js +204 -0
- package/dist/capture.d.ts +48 -0
- package/dist/capture.js +20 -1
- package/dist/cli.js +839 -32
- package/dist/content-links.d.ts +10 -0
- package/dist/content-links.js +49 -0
- package/dist/contract-validation.d.ts +4 -0
- package/dist/contract-validation.js +25 -0
- package/dist/coverage-actions.d.ts +8 -0
- package/dist/coverage-actions.js +71 -0
- package/dist/coverage-resolutions.d.ts +16 -0
- package/dist/coverage-resolutions.js +58 -0
- package/dist/db.d.ts +37 -0
- package/dist/db.js +288 -0
- package/dist/demo.d.ts +12 -0
- package/dist/demo.js +122 -0
- package/dist/deploy-credentials.d.ts +7 -0
- package/dist/deploy-credentials.js +76 -0
- package/dist/deploy-targets/github-pages.d.ts +3 -0
- package/dist/deploy-targets/github-pages.js +100 -0
- package/dist/deploy-targets/index.d.ts +8 -0
- package/dist/deploy-targets/index.js +78 -0
- package/dist/deploy-targets/netlify.d.ts +3 -0
- package/dist/deploy-targets/netlify.js +49 -0
- package/dist/deploy-targets/types.d.ts +32 -0
- package/dist/deploy-targets/types.js +2 -0
- package/dist/deploy-targets/vercel.d.ts +3 -0
- package/dist/deploy-targets/vercel.js +95 -0
- package/dist/deploy.d.ts +9 -0
- package/dist/deploy.js +87 -13
- package/dist/deterministic-capture.d.ts +90 -0
- package/dist/deterministic-capture.js +435 -0
- package/dist/direct-edit.d.ts +40 -0
- package/dist/direct-edit.js +159 -0
- package/dist/docs-crawl.d.ts +86 -0
- package/dist/docs-crawl.js +536 -0
- package/dist/docs-site.d.ts +49 -0
- package/dist/docs-site.js +233 -0
- package/dist/doctor.js +8 -0
- package/dist/documentation-collections.d.ts +23 -0
- package/dist/documentation-collections.js +191 -0
- package/dist/documentation-plan.d.ts +253 -0
- package/dist/documentation-plan.js +2563 -0
- package/dist/doxbrix-build.d.ts +19 -0
- package/dist/doxbrix-build.js +167 -0
- package/dist/doxbrix-markdown.d.ts +9 -0
- package/dist/doxbrix-markdown.js +115 -14
- package/dist/drift.d.ts +10 -0
- package/dist/drift.js +164 -0
- package/dist/evaluation.d.ts +46 -0
- package/dist/evaluation.js +113 -0
- package/dist/evidence-pack.d.ts +47 -0
- package/dist/evidence-pack.js +358 -0
- package/dist/evidence.d.ts +25 -0
- package/dist/evidence.js +175 -0
- package/dist/fs.d.ts +8 -2
- package/dist/fs.js +41 -11
- package/dist/generator-api.d.ts +89 -0
- package/dist/generator-preflight.d.ts +27 -0
- package/dist/generator-preflight.js +105 -0
- package/dist/generator-runtime.d.ts +7 -0
- package/dist/generator-runtime.js +17 -1
- package/dist/generators.d.ts +24 -2
- package/dist/generators.js +54 -1
- package/dist/git-delivery.d.ts +17 -0
- package/dist/git-delivery.js +123 -0
- package/dist/globs.d.ts +16 -0
- package/dist/globs.js +65 -0
- package/dist/glossary.d.ts +26 -0
- package/dist/glossary.js +179 -0
- package/dist/history.d.ts +106 -0
- package/dist/history.js +600 -0
- package/dist/html-markdown.d.ts +46 -0
- package/dist/html-markdown.js +423 -0
- package/dist/interactive.js +3 -0
- package/dist/job-events.d.ts +74 -0
- package/dist/job-events.js +377 -0
- package/dist/keep-awake.d.ts +50 -0
- package/dist/keep-awake.js +123 -0
- package/dist/local-source-snapshot.d.ts +20 -0
- package/dist/local-source-snapshot.js +61 -0
- package/dist/mintlify-detect.d.ts +3 -0
- package/dist/mintlify-detect.js +18 -0
- package/dist/mintlify-import.d.ts +75 -0
- package/dist/mintlify-import.js +190 -0
- package/dist/navigation.d.ts +98 -0
- package/dist/navigation.js +310 -0
- package/dist/openapi.d.ts +60 -0
- package/dist/openapi.js +439 -0
- package/dist/page-editor-bridge.d.ts +3 -0
- package/dist/page-editor-bridge.js +109 -0
- package/dist/page-editor-preview.d.ts +10 -0
- package/dist/page-editor-preview.js +55 -0
- package/dist/page-extension.d.ts +9 -0
- package/dist/page-extension.js +15 -0
- package/dist/page-metadata.d.ts +28 -0
- package/dist/page-metadata.js +166 -0
- package/dist/page-operations.d.ts +34 -0
- package/dist/page-operations.js +215 -0
- package/dist/page-routes.d.ts +4 -0
- package/dist/page-routes.js +61 -0
- package/dist/pages.d.ts +20 -0
- package/dist/pages.js +184 -0
- package/dist/plan-generator.d.ts +3 -0
- package/dist/plan-generator.js +21 -0
- package/dist/plan-navigation.d.ts +11 -0
- package/dist/plan-navigation.js +30 -0
- package/dist/planning-captures.d.ts +20 -0
- package/dist/planning-captures.js +143 -0
- package/dist/planning-research.d.ts +135 -0
- package/dist/planning-research.js +472 -0
- package/dist/planning-triage.d.ts +23 -0
- package/dist/planning-triage.js +131 -0
- package/dist/preview.d.ts +24 -0
- package/dist/preview.js +280 -29
- package/dist/project-detect.d.ts +36 -0
- package/dist/project-detect.js +251 -0
- package/dist/project-import.d.ts +54 -0
- package/dist/project-import.js +157 -0
- package/dist/project-lock.d.ts +6 -0
- package/dist/project-lock.js +96 -0
- package/dist/project-registry.d.ts +25 -0
- package/dist/project-registry.js +79 -0
- package/dist/project.d.ts +35 -3
- package/dist/project.js +388 -38
- package/dist/prompts.d.ts +9 -0
- package/dist/prompts.js +25 -0
- package/dist/proposal-replay.d.ts +32 -0
- package/dist/proposal-replay.js +99 -0
- package/dist/quality-claims.d.ts +8 -0
- package/dist/quality-claims.js +168 -0
- package/dist/quality-config.d.ts +5 -0
- package/dist/quality-config.js +84 -0
- package/dist/quality-contract.d.ts +37 -0
- package/dist/quality-contract.js +46 -0
- package/dist/quality-examples.d.ts +4 -0
- package/dist/quality-examples.js +233 -0
- package/dist/quality-gates.d.ts +16 -0
- package/dist/quality-gates.js +192 -0
- package/dist/quality-links.d.ts +7 -0
- package/dist/quality-links.js +149 -0
- package/dist/quality-lint.d.ts +6 -0
- package/dist/quality-lint.js +124 -0
- package/dist/quality-rendered.d.ts +11 -0
- package/dist/quality-rendered.js +222 -0
- package/dist/quality-schema.d.ts +3 -0
- package/dist/quality-schema.js +71 -0
- package/dist/release-notes.d.ts +44 -0
- package/dist/release-notes.js +183 -0
- package/dist/remote-monitor.d.ts +16 -0
- package/dist/remote-monitor.js +74 -0
- package/dist/remote-source.d.ts +34 -0
- package/dist/remote-source.js +426 -0
- package/dist/review-diff.d.ts +82 -0
- package/dist/review-diff.js +400 -0
- package/dist/review-learning.d.ts +11 -0
- package/dist/review-learning.js +60 -0
- package/dist/review-render.d.ts +38 -0
- package/dist/review-render.js +224 -0
- package/dist/review-report.d.ts +9 -0
- package/dist/review-report.js +89 -0
- package/dist/review-ui.d.ts +14 -0
- package/dist/review-ui.js +1248 -0
- package/dist/schedule.d.ts +78 -0
- package/dist/schedule.js +480 -0
- package/dist/screen-capture-provider.d.ts +52 -0
- package/dist/screen-capture-provider.js +218 -0
- package/dist/screenshot-workflow.d.ts +167 -0
- package/dist/screenshot-workflow.js +1237 -0
- package/dist/settings.d.ts +1 -1
- package/dist/settings.js +95 -7
- package/dist/site-export.d.ts +18 -0
- package/dist/site-export.js +87 -0
- package/dist/source-connectors.d.ts +33 -0
- package/dist/source-connectors.js +268 -0
- package/dist/source-discovery.d.ts +132 -0
- package/dist/source-discovery.js +823 -0
- package/dist/source-intelligence.d.ts +9 -0
- package/dist/source-intelligence.js +306 -0
- package/dist/sync-review.d.ts +28 -0
- package/dist/sync-review.js +264 -0
- package/dist/sync-runs.d.ts +192 -0
- package/dist/sync-runs.js +2244 -0
- package/dist/sync.d.ts +35 -0
- package/dist/sync.js +298 -32
- package/dist/text-diff.d.ts +9 -0
- package/dist/text-diff.js +59 -0
- package/dist/types.d.ts +946 -1
- package/dist/ui/assets/doxloop-logo-light-De7Nx7j7.png +0 -0
- package/dist/ui/assets/index-BHBYU2aG.css +1 -0
- package/dist/ui/assets/index-Cq3RPQiC.js +33 -0
- package/dist/ui/index.html +18 -0
- package/dist/ui-server.d.ts +83 -0
- package/dist/ui-server.js +3532 -0
- package/dist/usage-budget.d.ts +28 -0
- package/dist/usage-budget.js +90 -0
- package/dist/validation.d.ts +25 -1
- package/dist/validation.js +312 -26
- package/dist/workspace-tools.d.ts +54 -0
- package/dist/workspace-tools.js +123 -0
- package/docs/agent-compatibility.md +49 -32
- package/docs/ci-and-automation.md +105 -66
- package/docs/doxbrix-http-api.md +8 -1
- package/docs/existing-documentation.md +80 -0
- package/docs/generation-performance.md +108 -0
- package/docs/generator-authoring.md +68 -5
- package/docs/generator-selection.md +50 -13
- package/docs/mintlify-import.md +71 -0
- package/docs/openapi-security.md +25 -0
- package/docs/project-format.md +331 -36
- package/docs/release-quality.md +158 -0
- package/docs/releasing.md +72 -0
- package/docs/review-workflows.md +51 -0
- package/docs/security-model.md +102 -39
- package/docs/troubleshooting.md +226 -93
- package/package.json +46 -15
- package/scripts/test-auto-screenshot.mjs +172 -0
- package/skills/doxloop-authoring/SKILL.md +242 -361
- package/skills/doxloop-authoring/references/existing-documentation.md +94 -0
- package/skills/doxloop-authoring/references/navigation-architecture.md +18 -7
- package/skills/doxloop-authoring/references/page-depth.md +169 -0
- package/skills/doxloop-authoring/references/project-format.md +106 -7
- package/skills/doxloop-authoring/references/quality.md +10 -0
- package/skills/doxloop-authoring/references/screenshot-manifest.md +113 -0
- package/skills/doxloop-authoring/references/screenshots.md +149 -235
- package/skills/doxloop-authoring/references/workflows.md +76 -0
- package/skills/doxloop-doxbrix/SKILL.md +38 -22
- package/skills/doxloop-doxbrix/references/api-endpoints.md +15 -14
- package/skills/doxloop-doxbrix/references/components.md +30 -3
- package/skills/doxloop-doxbrix/references/manifest.md +3 -2
- package/vendor/doxbrix-import/LICENSE +202 -0
- package/vendor/doxbrix-import/README.md +17 -0
- package/vendor/doxbrix-import/UPSTREAM.json +47 -0
- package/vendor/doxbrix-import/dist/docs/frontmatter.d.ts +13 -0
- package/vendor/doxbrix-import/dist/docs/frontmatter.js +83 -0
- package/vendor/doxbrix-import/dist/docs/import.d.ts +21 -0
- package/vendor/doxbrix-import/dist/docs/import.js +147 -0
- package/vendor/doxbrix-import/dist/docs/manifest.d.ts +163 -0
- package/vendor/doxbrix-import/dist/docs/manifest.js +64 -0
- package/vendor/doxbrix-import/dist/docs/project.d.ts +25 -0
- package/vendor/doxbrix-import/dist/docs/project.js +77 -0
- package/vendor/doxbrix-import/dist/docs/starter.d.ts +4 -0
- package/vendor/doxbrix-import/dist/docs/starter.js +11 -0
- package/vendor/doxbrix-import/dist/importer.d.ts +227 -0
- package/vendor/doxbrix-import/dist/importer.js +1567 -0
- package/vendor/doxbrix-import/dist/mintlify-openapi.d.ts +37 -0
- package/vendor/doxbrix-import/dist/mintlify-openapi.js +305 -0
- package/vendor/doxbrix-import/dist/safe-path.d.ts +9 -0
- package/vendor/doxbrix-import/dist/safe-path.js +47 -0
- package/dist/agents.d.ts.map +0 -1
- package/dist/agents.js.map +0 -1
- package/dist/args.d.ts.map +0 -1
- package/dist/args.js.map +0 -1
- package/dist/artifact-deploy.d.ts.map +0 -1
- package/dist/artifact-deploy.js.map +0 -1
- package/dist/auth.d.ts.map +0 -1
- package/dist/auth.js.map +0 -1
- package/dist/author.d.ts.map +0 -1
- package/dist/author.js.map +0 -1
- package/dist/capture.d.ts.map +0 -1
- package/dist/capture.js.map +0 -1
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/deploy.d.ts.map +0 -1
- package/dist/deploy.js.map +0 -1
- package/dist/deployment-visibility.d.ts.map +0 -1
- package/dist/deployment-visibility.js.map +0 -1
- package/dist/doctor.d.ts.map +0 -1
- package/dist/doctor.js.map +0 -1
- package/dist/doxbrix-markdown.d.ts.map +0 -1
- package/dist/doxbrix-markdown.js.map +0 -1
- package/dist/errors.d.ts.map +0 -1
- package/dist/errors.js.map +0 -1
- package/dist/fs.d.ts.map +0 -1
- package/dist/fs.js.map +0 -1
- package/dist/generator-api.d.ts.map +0 -1
- package/dist/generator-api.js.map +0 -1
- package/dist/generator-manager.d.ts.map +0 -1
- package/dist/generator-manager.js.map +0 -1
- package/dist/generator-runtime.d.ts.map +0 -1
- package/dist/generator-runtime.js.map +0 -1
- package/dist/generators.d.ts.map +0 -1
- package/dist/generators.js.map +0 -1
- package/dist/interactive.d.ts.map +0 -1
- package/dist/interactive.js.map +0 -1
- package/dist/preview.d.ts.map +0 -1
- package/dist/preview.js.map +0 -1
- package/dist/progress.d.ts.map +0 -1
- package/dist/progress.js.map +0 -1
- package/dist/project.d.ts.map +0 -1
- package/dist/project.js.map +0 -1
- package/dist/prompts.d.ts.map +0 -1
- package/dist/prompts.js.map +0 -1
- package/dist/settings.d.ts.map +0 -1
- package/dist/settings.js.map +0 -1
- package/dist/sync.d.ts.map +0 -1
- package/dist/sync.js.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js.map +0 -1
- package/dist/validation.d.ts.map +0 -1
- package/dist/validation.js.map +0 -1
- package/dist/version.d.ts.map +0 -1
- package/dist/version.js.map +0 -1
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
import { budgetContext, isAccountLimit } from './usage-budget.js';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
import { readTaggedJson } from './agent-reply.js';
|
|
6
|
+
import { captureAuthPrompt } from './author.js';
|
|
7
|
+
import { runPool } from './batch-artifacts.js';
|
|
8
|
+
import { readDocsSiteManifest } from './docs-site.js';
|
|
9
|
+
import { DoxloopError } from './errors.js';
|
|
10
|
+
import { normalizeScreenshotIntent } from './screenshot-workflow.js';
|
|
11
|
+
import { discoveryGuidance, formatDiscoveryInventory } from './source-discovery.js';
|
|
12
|
+
/**
|
|
13
|
+
* Planning research: the reading, browsing, and auditing that used to happen
|
|
14
|
+
* inside one long planning session now runs as short sessions, several at a
|
|
15
|
+
* time, each returning a compact brief. A separate synthesis session writes
|
|
16
|
+
* the plan from the briefs. Every brief is checkpointed under the plan, so a
|
|
17
|
+
* failed or retried plan repeats only the work that is missing.
|
|
18
|
+
*/
|
|
19
|
+
export const DEFAULT_PLANNING_PARALLEL = 2;
|
|
20
|
+
/** Crawled pages one existing-documentation research session reads. */
|
|
21
|
+
export const EXISTING_DOCS_SHARD_PAGES = 20;
|
|
22
|
+
const RESEARCH_DIRECTORY = 'research';
|
|
23
|
+
/** Staged planning is on unless `DOXLOOP_PLANNING_STAGED` turns it off. */
|
|
24
|
+
export function stagedPlanningEnabled(env = process.env) {
|
|
25
|
+
const raw = env.DOXLOOP_PLANNING_STAGED?.trim().toLowerCase();
|
|
26
|
+
return !(raw === '0' || raw === 'false' || raw === 'off' || raw === 'no');
|
|
27
|
+
}
|
|
28
|
+
/** How many research sessions run at once (`DOXLOOP_PLANNING_PARALLEL`, default 3). */
|
|
29
|
+
export function planningParallelism(env = process.env) {
|
|
30
|
+
const raw = env.DOXLOOP_PLANNING_PARALLEL?.trim();
|
|
31
|
+
if (raw) {
|
|
32
|
+
const parsed = Number(raw);
|
|
33
|
+
if (Number.isFinite(parsed) && parsed >= 1)
|
|
34
|
+
return Math.floor(parsed);
|
|
35
|
+
}
|
|
36
|
+
return DEFAULT_PLANNING_PARALLEL;
|
|
37
|
+
}
|
|
38
|
+
function docsSiteSources(sources) {
|
|
39
|
+
return sources.filter((source) => (source.kind ?? 'directory') === 'docs-site');
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Which research sessions this plan needs: one product-surface audit when
|
|
43
|
+
* code or specification sources are configured, one application exploration
|
|
44
|
+
* when screenshots are wanted and an application is configured, and one
|
|
45
|
+
* existing-documentation audit per shard of crawled pages. The plan's
|
|
46
|
+
* research scope (see planning-triage) narrows that: a navigation-only
|
|
47
|
+
* update researches nothing, and a page-scoped update audits only the
|
|
48
|
+
* product surface behind the named pages, exploring the application only
|
|
49
|
+
* when screenshots are required.
|
|
50
|
+
*/
|
|
51
|
+
export async function researchTasks(root, project, current) {
|
|
52
|
+
const tasks = [];
|
|
53
|
+
const scope = current.research?.scope ?? 'product';
|
|
54
|
+
if (scope === 'navigation')
|
|
55
|
+
return tasks;
|
|
56
|
+
const productSources = project.sources.filter((source) => (source.kind ?? 'directory') !== 'docs-site');
|
|
57
|
+
if (productSources.length > 0) {
|
|
58
|
+
const focus = scope === 'pages' ? [...(current.research?.pages ?? [])].sort() : undefined;
|
|
59
|
+
tasks.push({
|
|
60
|
+
id: 'product',
|
|
61
|
+
kind: 'product',
|
|
62
|
+
label: focus ? `Auditing the product surface behind ${focus.length} page${focus.length === 1 ? '' : 's'}` : 'Auditing the product surface',
|
|
63
|
+
browser: false,
|
|
64
|
+
...(focus ? { focus } : {}),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
const screenshots = normalizeScreenshotIntent(current.execution.screenshots);
|
|
68
|
+
if (project.application && (scope === 'pages' ? screenshots === 'enabled' : screenshots !== 'disabled')) {
|
|
69
|
+
tasks.push({ id: 'application', kind: 'application', label: 'Exploring the live application', browser: true });
|
|
70
|
+
}
|
|
71
|
+
if (scope === 'pages')
|
|
72
|
+
return tasks;
|
|
73
|
+
for (const source of docsSiteSources(project.sources)) {
|
|
74
|
+
const manifest = await readDocsSiteManifest(root, source);
|
|
75
|
+
const pages = manifest.pages.map((page) => ({ file: page.file, title: page.title, words: page.words }));
|
|
76
|
+
const total = Math.max(1, Math.ceil(pages.length / EXISTING_DOCS_SHARD_PAGES));
|
|
77
|
+
for (let index = 0; index < total; index += 1) {
|
|
78
|
+
const slice = pages.slice(index * EXISTING_DOCS_SHARD_PAGES, (index + 1) * EXISTING_DOCS_SHARD_PAGES);
|
|
79
|
+
if (slice.length === 0)
|
|
80
|
+
continue;
|
|
81
|
+
const suffix = total === 1 ? '' : ` (${index + 1} of ${total})`;
|
|
82
|
+
tasks.push({
|
|
83
|
+
id: total === 1 ? `existing-docs-${slug(source.name)}` : `existing-docs-${slug(source.name)}-${index + 1}`,
|
|
84
|
+
kind: 'existing-docs',
|
|
85
|
+
label: `Auditing the existing documentation "${source.name}"${suffix}`,
|
|
86
|
+
browser: false,
|
|
87
|
+
shard: { source: source.name, snapshotPath: source.path, pages: slice, index: index + 1, total },
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return tasks;
|
|
92
|
+
}
|
|
93
|
+
function slug(value) {
|
|
94
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'source';
|
|
95
|
+
}
|
|
96
|
+
/** Browser calls one application research session may spend; a session without a budget made a hundred. */
|
|
97
|
+
export const APPLICATION_BROWSER_CALLS = 30;
|
|
98
|
+
const EVIDENCE_SHAPE = '{ "source": "configured source name", "path": "source-relative path", "kind": "discovery kind", "label": "symbol, route, operation, or file", "line": 1 }';
|
|
99
|
+
function preamble(task, context) {
|
|
100
|
+
return `You are one research session of Doxloop's planning stage. Doxloop runs several research sessions at once, and a separate session writes the documentation plan from their briefs; you do not write the plan, decide pages, or author documentation. Do not edit source or documentation files; the application task may save screenshot assets. Do not read the skill files under .agents/skills or .claude/skills: they guide authoring, and this brief is complete for your task.
|
|
101
|
+
|
|
102
|
+
Research task "${task.id}": ${task.label}.
|
|
103
|
+
${task.kind !== 'application' && context.project.application ? `\nDo not open, curl, or otherwise probe the application URL: a separate research session explores the live application through Doxloop's capture browser. Your sandbox may have no network access, so a failed request here says nothing about whether the application is running; never report the application as unavailable or its screens as unconfirmed.\n` : ''}
|
|
104
|
+
Project configuration (compact JSON):
|
|
105
|
+
${JSON.stringify(context.project)}
|
|
106
|
+
|
|
107
|
+
Planning request this research serves (${context.current.mode}, ${context.current.scope} scope):
|
|
108
|
+
${context.current.request || 'Use the configured evidence and documentation brief to recommend the right documentation.'}
|
|
109
|
+
`;
|
|
110
|
+
}
|
|
111
|
+
function ending(task) {
|
|
112
|
+
return `
|
|
113
|
+
End your reply with exactly one machine-readable block and put nothing after it. Do not use Markdown fences inside the block, and never emit this block around an example or a file you read:
|
|
114
|
+
<doxloop-brief>
|
|
115
|
+
{ the single JSON object described above }
|
|
116
|
+
</doxloop-brief>
|
|
117
|
+
The block holds one complete top-level object for research task "${task.id}": close every bracket you open, and make the object's own closing brace the last character before </doxloop-brief>. Keep string values short and factual; the brief is read by another session, not by readers.`;
|
|
118
|
+
}
|
|
119
|
+
export function researchPrompt(task, context) {
|
|
120
|
+
if (task.kind === 'product')
|
|
121
|
+
return productPrompt(task, context);
|
|
122
|
+
if (task.kind === 'application')
|
|
123
|
+
return applicationPrompt(task, context);
|
|
124
|
+
return existingDocsPrompt(task, context);
|
|
125
|
+
}
|
|
126
|
+
function productPrompt(task, context) {
|
|
127
|
+
if (task.focus && task.focus.length > 0)
|
|
128
|
+
return focusedProductPrompt(task, task.focus, context);
|
|
129
|
+
return `${preamble(task, context)}
|
|
130
|
+
Audit the complete public product surface from the configured sources: package metadata and entry points; exported APIs, commands, routes, and configuration; installation and prerequisites; authentication and permissions; primary and advanced workflows; examples, tests, and integrations; errors, limits, recovery paths, and operational concerns. Use the deterministic inventory below as your map and read the files it points to; read a file once and grep before reading. Quote displayed UI strings from the label catalogs, never translation keys. Report only evidence-supported behavior and list everything you could not confirm under "unknowns". Every capability that deserves its own page must appear; a mid-sized product usually yields 15–60 capabilities.
|
|
131
|
+
Evidence is what the writers start from, so make it precise: cite two to four entries per capability, each with the "line" of the symbol, route, handler, or option you read (grep -n gives it), and lead with the file that implements the behaviour (component, view, route, handler, command, config schema). Cite the UI label catalog only as a supplement, with the dotted key path as the label (for example "task.repeat.everyDay"), never a quoted fragment of the file. Put the concrete behaviour you confirmed — steps, options, defaults, limits, error texts — in "summary" and "notes"; a writer who never opens the source should be able to write the page from them.
|
|
132
|
+
|
|
133
|
+
Deterministic source discovery (trusted inventory produced by Doxloop):
|
|
134
|
+
${formatDiscoveryInventory(context.discovery)}
|
|
135
|
+
${discoveryGuidance(context.discovery)}
|
|
136
|
+
Deterministic source-change summary:
|
|
137
|
+
${context.changes}
|
|
138
|
+
|
|
139
|
+
The JSON object must use this shape:
|
|
140
|
+
{
|
|
141
|
+
"productProfile": "short evidence-grounded product classification",
|
|
142
|
+
"audiences": ["specific reader groups the evidence supports"],
|
|
143
|
+
"entryPoints": [{ "how": "how readers install, run, or access the product", "evidence": [${EVIDENCE_SHAPE}] }],
|
|
144
|
+
"capabilities": [{
|
|
145
|
+
"id": "stable-kebab-id",
|
|
146
|
+
"title": "public capability or reader workflow",
|
|
147
|
+
"kind": "workflow | api | command | configuration | concept | operation",
|
|
148
|
+
"summary": "one or two sentences: what a reader does with it and what happens",
|
|
149
|
+
"audience": "who needs it",
|
|
150
|
+
"evidence": [${EVIDENCE_SHAPE}],
|
|
151
|
+
"notes": "prerequisites, limits, errors, and related capabilities worth documenting"
|
|
152
|
+
}],
|
|
153
|
+
"configuration": [{ "name": "setting, flag, or variable", "purpose": "what it controls", "evidence": [${EVIDENCE_SHAPE}] }],
|
|
154
|
+
"authentication": { "summary": "how users and API clients authenticate and what permissions exist", "evidence": [${EVIDENCE_SHAPE}] },
|
|
155
|
+
"integrations": [{ "name": "external system or protocol", "summary": "what it does", "evidence": [${EVIDENCE_SHAPE}] }],
|
|
156
|
+
"operations": ["deployment, backup, upgrade, monitoring, or maintenance facts with evidence paths"],
|
|
157
|
+
"errors": ["user-facing error conditions and recovery paths with evidence paths"],
|
|
158
|
+
"terminology": { "product term": "meaning or preferred usage" },
|
|
159
|
+
"unknowns": ["what the sources do not show or you could not confirm"]
|
|
160
|
+
}
|
|
161
|
+
${ending(task)}`;
|
|
162
|
+
}
|
|
163
|
+
function applicationPrompt(task, context) {
|
|
164
|
+
const application = context.project.application;
|
|
165
|
+
return `${preamble(task, context)}
|
|
166
|
+
Explore the live application read-only so the plan can promise only screenshots of states that actually exist. Doxloop supplies the purpose-built doxloop_capture MCP browser for this session. It is the capture connector required by this task and must take precedence over any generic in-app Browser plugin, Chrome extension, or node_repl browser mechanism. Call the doxloop_capture navigation and snapshot tools directly; do not use failure of another browser mechanism as evidence that Doxloop capture is unavailable. Save a PNG immediately for every distinct useful state you actually reach, using the screenshot tool, before leaving it. Save every image with the screenshot tool's filename parameter set to the absolute path ${context.captureDirectory ?? '.doxloop/capture-output/planning'}/<stable-kebab-id>.png (a bare filename is written somewhere else and is lost to the plan); record that same <stable-kebab-id>.png as the capture's "file". Reuse a capture if the screen is unchanged. Review privacy, legibility, and the expected state before marking its checks true; never claim an unvisited state was captured. Never create, change, or delete data; open dialogs, tabs, drawers, and forms, fill forms only with safe example values, and cancel instead of submitting anything that would persist.
|
|
167
|
+
|
|
168
|
+
- Application capture surface: ${application.baseUrl}
|
|
169
|
+
- User-provided default starting route: ${application.screenshots?.startPath ?? 'not provided'}
|
|
170
|
+
- User-provided capture workflow: ${application.screenshots?.workflow ?? 'not provided'}
|
|
171
|
+
- Sign-in handling: ${captureAuthPrompt(context.captureAuth)}
|
|
172
|
+
|
|
173
|
+
Explore with a fixed budget: at most ${APPLICATION_BROWSER_CALLS} navigation/interaction/snapshot calls and ${APPLICATION_BROWSER_CALLS} screenshot calls in total, at most one snapshot per distinct screen, and never a second snapshot of a screen you already recorded. Starting from the default route, visit each top-level navigation destination once, then open only the dialogs, tabs, drawers, and forms a reader would use on the most important screens; a snapshot's accessibility tree already lists the controls a screen offers, so record those as states instead of opening each one. For each screen, record its route, what is visible (quote the displayed labels), the states a reader can open from it without changing data, safe example values for its forms, and the reader workflows it would illustrate. Record routes you tried that were unreachable and why. Stop when the budget is spent or every navigation entry is recorded, whichever comes first.
|
|
174
|
+
|
|
175
|
+
The JSON object must use this shape:
|
|
176
|
+
{
|
|
177
|
+
"baseUrl": "${application.baseUrl}",
|
|
178
|
+
"signIn": { "required": true, "reached": true, "route": "/login", "notes": "how the signed-in state was obtained, or why it was not" },
|
|
179
|
+
"fixtures": "what sample data exists and is safe to show",
|
|
180
|
+
"screens": [{
|
|
181
|
+
"route": "/application-relative/route",
|
|
182
|
+
"name": "screen name as displayed",
|
|
183
|
+
"reachedFrom": "navigation label or action that opens it",
|
|
184
|
+
"visible": "what is on screen, with displayed labels quoted",
|
|
185
|
+
"captures": [{ "id": "stable-kebab-id", "file": "filename.png", "action": "action that reached this screen", "state": "exact visible state", "alt": "reader-facing image description", "checks": { "expectedStateConfirmed": true, "privacyReviewed": true, "legibilityReviewed": true, "meaningful": true } }],
|
|
186
|
+
"states": ["dialogs, drawers, tabs, expanded sections, and forms reachable here without changing data, one per entry"],
|
|
187
|
+
"safeValues": ["safe example values for forms on this screen"],
|
|
188
|
+
"workflows": ["reader workflows this screen illustrates"]
|
|
189
|
+
}],
|
|
190
|
+
"unreachable": [{ "route": "/route", "why": "what happened" }],
|
|
191
|
+
"notes": "anything the plan must know to capture reliably (timing, permissions, layout quirks)"
|
|
192
|
+
}
|
|
193
|
+
${ending(task)}`;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* The product audit for a page-scoped update: only the capabilities behind
|
|
197
|
+
* the pages the request names, with the same evidence contract, so the
|
|
198
|
+
* writers still start from line-numbered citations.
|
|
199
|
+
*/
|
|
200
|
+
function focusedProductPrompt(task, focus, context) {
|
|
201
|
+
return `${preamble(task, context)}
|
|
202
|
+
This update concerns only these existing documentation pages:
|
|
203
|
+
${focus.map((page) => `- ${page}`).join('\n')}
|
|
204
|
+
Read those pages first to learn what they cover, then audit only the product surface behind them in the configured sources: the commands, routes, options, screens, permissions, errors, and limits those pages describe or should describe. Do not audit the rest of the product; a capability that no listed page covers belongs in the brief only when the request cannot be satisfied without it, and then say so under "notes". Use the deterministic inventory below as your map and read the files it points to; read a file once and grep before reading. Quote displayed UI strings from the label catalogs, never translation keys. Report only evidence-supported behavior and list everything you could not confirm under "unknowns". Expect one to eight capabilities.
|
|
205
|
+
Evidence is what the writers start from, so make it precise: cite two to four entries per capability, each with the "line" of the symbol, route, handler, or option you read (grep -n gives it), and lead with the file that implements the behaviour; a label catalog entry only supplements it.
|
|
206
|
+
|
|
207
|
+
Deterministic source discovery (trusted inventory produced by Doxloop):
|
|
208
|
+
${formatDiscoveryInventory(context.discovery)}
|
|
209
|
+
${discoveryGuidance(context.discovery)}
|
|
210
|
+
Deterministic source-change summary:
|
|
211
|
+
${context.changes}
|
|
212
|
+
|
|
213
|
+
The JSON object must use this shape:
|
|
214
|
+
{
|
|
215
|
+
"productProfile": "one sentence",
|
|
216
|
+
"capabilities": [{
|
|
217
|
+
"id": "kebab-case id",
|
|
218
|
+
"title": "capability title",
|
|
219
|
+
"kind": "workflow | reference | concept | setup | integration | operations",
|
|
220
|
+
"audience": ["who needs it"],
|
|
221
|
+
"summary": "what it does and why a reader would use it",
|
|
222
|
+
"pages": ["the listed pages it concerns"],
|
|
223
|
+
"evidence": [${EVIDENCE_SHAPE}],
|
|
224
|
+
"labels": ["exact UI strings from the label catalog, when a screen is involved"],
|
|
225
|
+
"limits": ["limits, defaults, and irreversible actions"]
|
|
226
|
+
}],
|
|
227
|
+
"errors": [{ "message": "exact error text", "cause": "why it happens", "recovery": "what the reader does", "evidence": [${EVIDENCE_SHAPE}] }],
|
|
228
|
+
"terminology": { "term": "definition" },
|
|
229
|
+
"notes": "what the listed pages need that the sources do not show, or a capability outside them the request requires",
|
|
230
|
+
"unknowns": ["what the sources do not show or you could not confirm"]
|
|
231
|
+
}
|
|
232
|
+
${ending(task)}`;
|
|
233
|
+
}
|
|
234
|
+
function existingDocsPrompt(task, context) {
|
|
235
|
+
const shard = task.shard;
|
|
236
|
+
const productSources = context.project.sources.filter((source) => (source.kind ?? 'directory') !== 'docs-site');
|
|
237
|
+
const pageLines = shard.pages.map((page) => `- ${page.file} — "${page.title}" (${page.words} words)`).join('\n');
|
|
238
|
+
const shardNote = shard.total > 1 ? ` This is shard ${shard.index} of ${shard.total}; other sessions audit the other pages, so judge only the pages listed here and name duplicates outside the list only when you notice them.` : '';
|
|
239
|
+
const factRule = productSources.length > 0
|
|
240
|
+
? `Product sources (${productSources.map((source) => `"${source.name}"`).join(', ')}) are the truth for facts, but this session does not read them: list every claim that must be verified against them under "suspectClaims", and every piece of knowledge the code is unlikely to show under "uniqueKnowledge".`
|
|
241
|
+
: 'No product code or API specification is configured, so these pages are the only product evidence: record what each teaches so the plan can restructure and rewrite it.';
|
|
242
|
+
return `${preamble(task, context)}
|
|
243
|
+
Audit the existing documentation being rewritten. Read every page listed below fully; the read-only Markdown snapshot lives at ${shard.snapshotPath} (page files under its pages/ directory, each with its original URL in frontmatter).${shardNote} Judge accuracy, structure, depth, duplication, terminology, and reader journeys. ${factRule}
|
|
244
|
+
|
|
245
|
+
Pages to audit (source "${shard.source}"):
|
|
246
|
+
${pageLines}
|
|
247
|
+
|
|
248
|
+
The JSON object must use this shape:
|
|
249
|
+
{
|
|
250
|
+
"source": "${shard.source}",
|
|
251
|
+
"pages": [{
|
|
252
|
+
"path": "pages/existing-page.md",
|
|
253
|
+
"title": "page title",
|
|
254
|
+
"topic": "what the page is about, in a few words",
|
|
255
|
+
"summary": "one or two sentences: what the page teaches and who it serves",
|
|
256
|
+
"quality": "keep | rewrite | thin | obsolete | duplicate",
|
|
257
|
+
"duplicateOf": "pages/other-page.md or omit",
|
|
258
|
+
"issues": ["specific problems: outdated claims, gaps, confusing structure"],
|
|
259
|
+
"uniqueKnowledge": ["facts, procedures, or rationale the product sources are unlikely to show; worth carrying over"],
|
|
260
|
+
"suspectClaims": ["claims to verify against the product sources before reuse"]
|
|
261
|
+
}],
|
|
262
|
+
"strengths": ["what these pages do well and the rewrite should keep"],
|
|
263
|
+
"findings": [{ "severity": "blocker | major | minor", "title": "short problem title", "description": "evidence-based problem across these pages", "pages": ["pages/existing-page.md"] }]
|
|
264
|
+
}
|
|
265
|
+
${ending(task)}`;
|
|
266
|
+
}
|
|
267
|
+
const BRIEF_KEYS = { product: 'capabilities', application: 'screens', 'existing-docs': 'pages' };
|
|
268
|
+
function briefContract(task) {
|
|
269
|
+
const key = BRIEF_KEYS[task.kind];
|
|
270
|
+
return {
|
|
271
|
+
tag: 'doxloop-brief',
|
|
272
|
+
accept: (value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value) && Array.isArray(value[key]),
|
|
273
|
+
attempted: (text) => new RegExp(`"${key}"\\s*:`).test(text),
|
|
274
|
+
noun: `research brief`,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
/** The brief a research session returned, or the reason it could not be read. */
|
|
278
|
+
export function readBriefOutput(raw, agent, task, prompt) {
|
|
279
|
+
let reply;
|
|
280
|
+
try {
|
|
281
|
+
reply = readTaggedJson(raw, agent, briefContract(task), prompt);
|
|
282
|
+
}
|
|
283
|
+
catch (error) {
|
|
284
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
285
|
+
throw new DoxloopError(`Research session "${task.id}" returned an unreadable brief: ${message}`);
|
|
286
|
+
}
|
|
287
|
+
if (!reply)
|
|
288
|
+
throw new DoxloopError(`Research session "${task.id}" did not return a research-brief JSON object inside <doxloop-brief>. Open the full log, then retry the plan.`);
|
|
289
|
+
return { content: reply.value, repairs: reply.repairs.map((note) => note.replace(/^The research brief reply/, `The "${task.id}" research reply`)) };
|
|
290
|
+
}
|
|
291
|
+
/** What the synthesis session reads instead of the sources, application, and pages. */
|
|
292
|
+
export function formatResearchBriefs(briefs) {
|
|
293
|
+
const sections = briefs.map((brief) => `### Brief "${brief.task}": ${brief.label}\n${JSON.stringify(brief.content)}`);
|
|
294
|
+
return `Research briefs (produced for this run by Doxloop's research sessions from the same sources, application, and crawled pages; treat them as your own findings and plan from them):\n\n${sections.join('\n\n')}`;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Briefs depend on the sources, the capture intent, the agent, and the
|
|
298
|
+
* research focus, not on the plan's wording or identity: a revision reuses
|
|
299
|
+
* them, and so does a later plan on unchanged sources (see
|
|
300
|
+
* `readResearchCheckpoint`). A page-scoped audit is keyed by its pages.
|
|
301
|
+
*/
|
|
302
|
+
export function researchCheckpointKey(current, sourceSnapshot, focus) {
|
|
303
|
+
const pages = focus === undefined ? (current.research?.scope === 'pages' ? [...current.research.pages].sort() : null) : focus;
|
|
304
|
+
return createHash('sha256')
|
|
305
|
+
.update(JSON.stringify({
|
|
306
|
+
captureSchema: 2,
|
|
307
|
+
sourceSnapshot,
|
|
308
|
+
screenshots: normalizeScreenshotIntent(current.execution.screenshots),
|
|
309
|
+
agent: current.execution.agent ?? null,
|
|
310
|
+
model: current.execution.model ?? null,
|
|
311
|
+
focus: pages && pages.length > 0 ? pages : null,
|
|
312
|
+
}))
|
|
313
|
+
.digest('hex');
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Keys a saved brief may carry to serve this plan: its own key first, then
|
|
317
|
+
* the key of a full product audit on the same sources, which covers
|
|
318
|
+
* anything a page-scoped audit would find.
|
|
319
|
+
*/
|
|
320
|
+
export function researchCheckpointKeys(current, sourceSnapshot) {
|
|
321
|
+
const own = researchCheckpointKey(current, sourceSnapshot);
|
|
322
|
+
const full = researchCheckpointKey(current, sourceSnapshot, null);
|
|
323
|
+
return own === full ? [own] : [own, full];
|
|
324
|
+
}
|
|
325
|
+
function researchCheckpointPath(root, planId, taskId) {
|
|
326
|
+
return join(root, '.doxloop', 'plans', planId, RESEARCH_DIRECTORY, `${taskId}.json`);
|
|
327
|
+
}
|
|
328
|
+
async function readResearchCheckpointFile(path, task, keys) {
|
|
329
|
+
let text;
|
|
330
|
+
try {
|
|
331
|
+
text = await readFile(path, 'utf8');
|
|
332
|
+
}
|
|
333
|
+
catch {
|
|
334
|
+
return undefined;
|
|
335
|
+
}
|
|
336
|
+
try {
|
|
337
|
+
const parsed = JSON.parse(text);
|
|
338
|
+
if (typeof parsed.key !== 'string' || !keys.includes(parsed.key) || parsed.task !== task.id || parsed.content === undefined)
|
|
339
|
+
return undefined;
|
|
340
|
+
return {
|
|
341
|
+
key: parsed.key,
|
|
342
|
+
task: task.id,
|
|
343
|
+
kind: task.kind,
|
|
344
|
+
label: task.label,
|
|
345
|
+
content: parsed.content,
|
|
346
|
+
...(Array.isArray(parsed.repairs) ? { repairs: parsed.repairs.filter((item) => typeof item === 'string') } : {}),
|
|
347
|
+
savedAt: typeof parsed.savedAt === 'string' ? parsed.savedAt : '',
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
catch {
|
|
351
|
+
return undefined;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* The brief saved for this task under one of `keys`: this plan's own first,
|
|
356
|
+
* then the newest other plan's. Sources that have not changed since another
|
|
357
|
+
* plan audited them do not need auditing again, and a second update on the
|
|
358
|
+
* same snapshot used to redo every session. The application brief is the
|
|
359
|
+
* exception: it names captures saved under its own plan, which this plan
|
|
360
|
+
* cannot embed, so it is only ever reused from this plan.
|
|
361
|
+
*/
|
|
362
|
+
export async function readResearchCheckpoint(root, planId, task, keys) {
|
|
363
|
+
const accepted = typeof keys === 'string' ? [keys] : keys;
|
|
364
|
+
const own = await readResearchCheckpointFile(researchCheckpointPath(root, planId, task.id), task, accepted);
|
|
365
|
+
if (own)
|
|
366
|
+
return { ...own, planId };
|
|
367
|
+
if (task.kind === 'application')
|
|
368
|
+
return undefined;
|
|
369
|
+
let plans;
|
|
370
|
+
try {
|
|
371
|
+
plans = (await readdir(join(root, '.doxloop', 'plans'), { withFileTypes: true }))
|
|
372
|
+
.filter((entry) => entry.isDirectory() && /^[a-z0-9-]+$/.test(entry.name) && entry.name !== planId)
|
|
373
|
+
.map((entry) => entry.name)
|
|
374
|
+
.sort()
|
|
375
|
+
.reverse();
|
|
376
|
+
}
|
|
377
|
+
catch {
|
|
378
|
+
return undefined;
|
|
379
|
+
}
|
|
380
|
+
for (const other of plans) {
|
|
381
|
+
const saved = await readResearchCheckpointFile(researchCheckpointPath(root, other, task.id), task, accepted);
|
|
382
|
+
if (saved)
|
|
383
|
+
return { ...saved, planId: other };
|
|
384
|
+
}
|
|
385
|
+
return undefined;
|
|
386
|
+
}
|
|
387
|
+
/** Briefs saved for these tasks under one of the keys; empty unless every task has one. */
|
|
388
|
+
export async function savedResearchBriefs(root, planId, tasks, keys) {
|
|
389
|
+
const briefs = [];
|
|
390
|
+
for (const task of tasks) {
|
|
391
|
+
const saved = await readResearchCheckpoint(root, planId, task, keys);
|
|
392
|
+
if (!saved)
|
|
393
|
+
return [];
|
|
394
|
+
briefs.push({ task: task.id, kind: task.kind, label: task.label, content: saved.content });
|
|
395
|
+
}
|
|
396
|
+
return briefs;
|
|
397
|
+
}
|
|
398
|
+
export async function writeResearchCheckpoint(root, planId, checkpoint) {
|
|
399
|
+
const path = researchCheckpointPath(root, planId, checkpoint.task);
|
|
400
|
+
await mkdir(dirname(path), { recursive: true });
|
|
401
|
+
await writeFile(path, `${JSON.stringify({ ...checkpoint, savedAt: new Date().toISOString() }, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Run every research task that has no valid checkpoint, at most
|
|
405
|
+
* `concurrency` at a time. A session that fails or returns an unreadable
|
|
406
|
+
* brief is tried once more in a fresh session; the pool keeps going so the
|
|
407
|
+
* other briefs are saved, then the first failure is reported.
|
|
408
|
+
*/
|
|
409
|
+
export async function runResearch(tasks, options) {
|
|
410
|
+
const results = new Map();
|
|
411
|
+
const failures = [];
|
|
412
|
+
const pending = [];
|
|
413
|
+
let done = 0;
|
|
414
|
+
for (const task of tasks) {
|
|
415
|
+
const cached = await readResearchCheckpoint(options.root, options.planId, task, options.acceptKeys ?? [options.key]);
|
|
416
|
+
if (cached) {
|
|
417
|
+
if (cached.planId !== options.planId) {
|
|
418
|
+
// Keep a copy under this plan so a retry, a revision, and the plan
|
|
419
|
+
// review all find it where the plan's own briefs live.
|
|
420
|
+
await writeResearchCheckpoint(options.root, options.planId, { key: cached.key, task: task.id, kind: task.kind, label: task.label, content: cached.content, ...(cached.repairs?.length ? { repairs: cached.repairs } : {}) });
|
|
421
|
+
}
|
|
422
|
+
results.set(task.id, { brief: { task: task.id, kind: task.kind, label: task.label, content: cached.content }, repairs: cached.repairs ?? [] });
|
|
423
|
+
done += 1;
|
|
424
|
+
options.onProgress?.(done, tasks.length, task, 'cached');
|
|
425
|
+
options.log?.(cached.planId === options.planId
|
|
426
|
+
? `Reusing the "${task.id}" research brief saved at ${cached.savedAt}.`
|
|
427
|
+
: `Reusing the "${task.id}" research brief plan ${cached.planId} saved at ${cached.savedAt}: the sources have not changed since.`);
|
|
428
|
+
}
|
|
429
|
+
else {
|
|
430
|
+
pending.push(task);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
await runPool(pending, options.concurrency, async (task) => {
|
|
434
|
+
const prompt = researchPrompt(task, options.context);
|
|
435
|
+
let lastError;
|
|
436
|
+
for (const attempt of [0, 1]) {
|
|
437
|
+
try {
|
|
438
|
+
const text = attempt === 0 ? prompt : `${prompt}
|
|
439
|
+
|
|
440
|
+
Your previous reply could not be used: ${lastError instanceof Error ? lastError.message : String(lastError)}
|
|
441
|
+
|
|
442
|
+
Send the brief again as one complete, strictly valid JSON object inside the <doxloop-brief> block.`;
|
|
443
|
+
const output = await options.runSession(task, text);
|
|
444
|
+
const read = readBriefOutput(output, options.agent, task, text);
|
|
445
|
+
await writeResearchCheckpoint(options.root, options.planId, { key: options.key, task: task.id, kind: task.kind, label: task.label, content: read.content, ...(read.repairs.length > 0 ? { repairs: read.repairs } : {}) });
|
|
446
|
+
results.set(task.id, { brief: { task: task.id, kind: task.kind, label: task.label, content: read.content }, repairs: read.repairs });
|
|
447
|
+
done += 1;
|
|
448
|
+
options.onProgress?.(done, tasks.length, task, 'completed');
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
catch (error) {
|
|
452
|
+
lastError = error;
|
|
453
|
+
if (budgetContext.getStore()?.stoppedReason || isAccountLimit(String(error)))
|
|
454
|
+
break;
|
|
455
|
+
options.log?.(`Research session "${task.id}" ${attempt === 0 ? 'failed; starting a fresh session' : 'failed again'}: ${error instanceof Error ? error.message : String(error)}`);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
failures.push({ task, error: lastError });
|
|
459
|
+
done += 1;
|
|
460
|
+
options.onProgress?.(done, tasks.length, task, 'failed');
|
|
461
|
+
}, () => Boolean(budgetContext.getStore()?.stoppedReason));
|
|
462
|
+
budgetContext.getStore()?.assertAvailable();
|
|
463
|
+
if (failures.length > 0) {
|
|
464
|
+
const detail = failures.map(({ task, error }) => `"${task.id}": ${error instanceof Error ? error.message : String(error)}`).join('; ');
|
|
465
|
+
throw new DoxloopError(`${failures.length === 1 ? 'A research session' : `${failures.length} research sessions`} did not finish (${detail}). The briefs that finished are saved; retry the plan to run only the missing ${failures.length === 1 ? 'one' : 'ones'}.`);
|
|
466
|
+
}
|
|
467
|
+
return {
|
|
468
|
+
briefs: tasks.flatMap((task) => (results.has(task.id) ? [results.get(task.id).brief] : [])),
|
|
469
|
+
repairs: tasks.flatMap((task) => results.get(task.id)?.repairs ?? []),
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
//# sourceMappingURL=planning-research.js.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { AgentName, DocumentationPlan, DocumentationPlanResearch } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Existing pages the request names, matched by their file name ("reverse
|
|
4
|
+
* proxy" names self-hosting/reverse-proxy.mdx) or by their path. Generic
|
|
5
|
+
* file names (index, overview) never count: "the overview" is not a page
|
|
6
|
+
* reference in a request about the overview of the product.
|
|
7
|
+
*/
|
|
8
|
+
export declare function citedPages(request: string, existingPages: readonly string[]): string[];
|
|
9
|
+
/**
|
|
10
|
+
* The deterministic decision, or undefined when the request is ambiguous:
|
|
11
|
+
* navigation vocabulary with no content change is a navigation request;
|
|
12
|
+
* named pages with nothing product-wide is a page request; product-wide
|
|
13
|
+
* vocabulary alone is a product request.
|
|
14
|
+
*/
|
|
15
|
+
export declare function triageByRules(request: string, existingPages: readonly string[]): DocumentationPlanResearch | undefined;
|
|
16
|
+
/** The research a create run always does, and the fallback for an ambiguous update the agent could not settle. */
|
|
17
|
+
export declare function fullResearch(reason: string, decidedBy?: DocumentationPlanResearch['decidedBy']): DocumentationPlanResearch;
|
|
18
|
+
export declare function triagePrompt(current: Pick<DocumentationPlan, 'request'>, existingPages: readonly string[]): string;
|
|
19
|
+
/** The agent's decision, restricted to pages that exist; undefined when the reply carries none. */
|
|
20
|
+
export declare function readTriageOutput(raw: string, agent: AgentName, existingPages: readonly string[], prompt?: string): DocumentationPlanResearch | undefined;
|
|
21
|
+
/** One line for the log and the plan review. */
|
|
22
|
+
export declare function describeResearchScope(research: DocumentationPlanResearch): string;
|
|
23
|
+
//# sourceMappingURL=planning-triage.d.ts.map
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { readTaggedJson } from './agent-reply.js';
|
|
2
|
+
/**
|
|
3
|
+
* Request triage: how much research an update request needs, decided before
|
|
4
|
+
* a single research session starts. A one-line request to add icons to the
|
|
5
|
+
* sidebar used to pay the same research bill as a product-wide create run
|
|
6
|
+
* (a product audit, every crawled page of the existing site, an application
|
|
7
|
+
* exploration: five minutes and half a million tokens before planning even
|
|
8
|
+
* began), because the research tasks were chosen from the project
|
|
9
|
+
* configuration alone. Deterministic rules decide the clear cases; a short
|
|
10
|
+
* agent session settles an ambiguous request; and the safe default when
|
|
11
|
+
* nothing is sure is the full research a product-wide update needs.
|
|
12
|
+
*/
|
|
13
|
+
const NAVIGATION_WORDS = /\b(?:nav|navigation|sidebar|side[- ]?bar|left[- ]nav|menus?|icons?|favicon|logo|brand(?:ing)?|theme|colou?rs?|fonts?|re-?order(?:ing)?|order of|rename|group(?:s|ing)?|top[- ]?nav|header|footer|tabs?|collapse|expand(?:ed)? by default)\b/gi;
|
|
14
|
+
const CONTENT_WORDS = /\b(?:write|rewrite|explain|describe|content|wording|text|paragraph|examples?|steps?|procedure|tutorial|outdated|incorrect|wrong|inaccurate|typo|clarify|shorten|add (?:a |an |new )?(?:page|section|guide|tutorial|chapter)|new pages?|missing (?:pages?|sections?|docs|documentation|coverage)|coverage|api|endpoints?|reference|feature|screenshots?|troubleshooting|prerequisites?)\b/gi;
|
|
15
|
+
const PRODUCT_WORDS = /\b(?:all (?:the )?(?:pages|docs|documentation)|every page|whole|entire|everything|refresh|re-?sync|new (?:release|version)|changed since|latest (?:release|version|changes)|comprehensive|audit)\b/gi;
|
|
16
|
+
const GENERIC_PAGE_NAMES = new Set(['index', 'readme', 'overview', 'introduction', 'intro', 'home', 'docs', 'guide', 'guides', 'reference', 'api', 'page']);
|
|
17
|
+
function count(text, pattern) {
|
|
18
|
+
return [...text.matchAll(pattern)].length;
|
|
19
|
+
}
|
|
20
|
+
function words(value) {
|
|
21
|
+
return value.toLowerCase().replace(/\.[a-z0-9]+$/i, '').replace(/[^a-z0-9]+/g, ' ').trim();
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Existing pages the request names, matched by their file name ("reverse
|
|
25
|
+
* proxy" names self-hosting/reverse-proxy.mdx) or by their path. Generic
|
|
26
|
+
* file names (index, overview) never count: "the overview" is not a page
|
|
27
|
+
* reference in a request about the overview of the product.
|
|
28
|
+
*/
|
|
29
|
+
export function citedPages(request, existingPages) {
|
|
30
|
+
const text = ` ${words(request)} `;
|
|
31
|
+
if (!text.trim())
|
|
32
|
+
return [];
|
|
33
|
+
const cited = [];
|
|
34
|
+
for (const page of existingPages) {
|
|
35
|
+
const segments = page.split('/');
|
|
36
|
+
const name = words(segments.at(-1) ?? '');
|
|
37
|
+
const parent = segments.length > 1 ? words(segments.at(-2) ?? '') : '';
|
|
38
|
+
const candidates = [name, parent && name ? `${parent} ${name}` : '', words(page)].filter((candidate) => candidate.length >= 4 && !GENERIC_PAGE_NAMES.has(candidate));
|
|
39
|
+
if (candidates.some((candidate) => text.includes(` ${candidate} `)))
|
|
40
|
+
cited.push(page);
|
|
41
|
+
}
|
|
42
|
+
return cited;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The deterministic decision, or undefined when the request is ambiguous:
|
|
46
|
+
* navigation vocabulary with no content change is a navigation request;
|
|
47
|
+
* named pages with nothing product-wide is a page request; product-wide
|
|
48
|
+
* vocabulary alone is a product request.
|
|
49
|
+
*/
|
|
50
|
+
export function triageByRules(request, existingPages) {
|
|
51
|
+
const text = request.trim();
|
|
52
|
+
if (!text)
|
|
53
|
+
return { scope: 'product', reason: 'The request is empty, so the update is planned from the source changes across the whole product.', pages: [], decidedBy: 'rules' };
|
|
54
|
+
const navigation = count(text, NAVIGATION_WORDS);
|
|
55
|
+
const content = count(text, CONTENT_WORDS);
|
|
56
|
+
const product = count(text, PRODUCT_WORDS);
|
|
57
|
+
const pages = citedPages(text, existingPages);
|
|
58
|
+
if (navigation > 0 && content === 0) {
|
|
59
|
+
return { scope: 'navigation', reason: 'The request is about navigation, icons, branding, or page metadata and asks for no content change, so no source, page, or application research is needed.', pages, decidedBy: 'rules' };
|
|
60
|
+
}
|
|
61
|
+
if (pages.length > 0 && navigation === 0 && product === 0) {
|
|
62
|
+
return { scope: 'pages', reason: `The request names ${pages.length === 1 ? 'one existing page' : `${pages.length} existing pages`} (${pages.slice(0, 4).join(', ')}${pages.length > 4 ? ', …' : ''}), so research covers only the product surface behind ${pages.length === 1 ? 'it' : 'them'}.`, pages, decidedBy: 'rules' };
|
|
63
|
+
}
|
|
64
|
+
if (product > 0 && navigation === 0 && pages.length === 0) {
|
|
65
|
+
return { scope: 'product', reason: 'The request is product-wide, so every research session runs.', pages: [], decidedBy: 'rules' };
|
|
66
|
+
}
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
/** The research a create run always does, and the fallback for an ambiguous update the agent could not settle. */
|
|
70
|
+
export function fullResearch(reason, decidedBy = 'mode') {
|
|
71
|
+
return { scope: 'product', reason, pages: [], decidedBy };
|
|
72
|
+
}
|
|
73
|
+
export function triagePrompt(current, existingPages) {
|
|
74
|
+
const pageList = existingPages.length > 0 ? existingPages.slice(0, 400).map((page) => `- ${page}`).join('\n') : '- (no existing pages)';
|
|
75
|
+
return `You are the triage step of Doxloop's planning stage. Decide how much research one documentation update request needs. Do not read any file, do not browse, do not plan pages, and do not ask questions: answer from the request and the page list below only.
|
|
76
|
+
|
|
77
|
+
Update request:
|
|
78
|
+
${current.request}
|
|
79
|
+
|
|
80
|
+
Existing documentation pages (project-relative paths):
|
|
81
|
+
${pageList}
|
|
82
|
+
|
|
83
|
+
Choose exactly one scope:
|
|
84
|
+
- "navigation": the request changes only navigation, icons, ordering, group names, branding, theme, or page metadata; no page content changes. Doxloop then reads nothing and works from the current navigation.
|
|
85
|
+
- "pages": the request changes the content of specific existing pages you can name from the list (fix, expand, correct, add a section to them). Doxloop then audits only the product surface behind those pages.
|
|
86
|
+
- "product": the request is product-wide, adds pages whose subject is not on the list, or depends on what changed in the product. Doxloop then runs the full research (product surface, application, existing documentation).
|
|
87
|
+
When in doubt between "pages" and "product", choose "product".
|
|
88
|
+
|
|
89
|
+
End your reply with exactly one machine-readable block and put nothing after it:
|
|
90
|
+
<doxloop-triage>
|
|
91
|
+
{ "scope": "navigation | pages | product", "pages": ["paths from the list, only for scope pages"], "reason": "one sentence naming what in the request decided the scope" }
|
|
92
|
+
</doxloop-triage>`;
|
|
93
|
+
}
|
|
94
|
+
const SCOPES = ['navigation', 'pages', 'product'];
|
|
95
|
+
const TRIAGE_CONTRACT = {
|
|
96
|
+
tag: 'doxloop-triage',
|
|
97
|
+
accept: (value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value) && SCOPES.includes(value.scope),
|
|
98
|
+
attempted: (text) => /"scope"\s*:/.test(text),
|
|
99
|
+
noun: 'triage',
|
|
100
|
+
};
|
|
101
|
+
/** The agent's decision, restricted to pages that exist; undefined when the reply carries none. */
|
|
102
|
+
export function readTriageOutput(raw, agent, existingPages, prompt) {
|
|
103
|
+
let reply;
|
|
104
|
+
try {
|
|
105
|
+
reply = readTaggedJson(raw, agent, TRIAGE_CONTRACT, prompt);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
if (!reply)
|
|
111
|
+
return undefined;
|
|
112
|
+
const scope = reply.value.scope;
|
|
113
|
+
const known = new Set(existingPages);
|
|
114
|
+
const pages = Array.isArray(reply.value.pages) ? reply.value.pages.filter((page) => typeof page === 'string' && known.has(page)) : [];
|
|
115
|
+
const reason = typeof reply.value.reason === 'string' && reply.value.reason.trim() ? reply.value.reason.trim().slice(0, 300) : `The triage session chose the ${scope} scope.`;
|
|
116
|
+
// A page scope that names no existing page has nothing to focus on.
|
|
117
|
+
if (scope === 'pages' && pages.length === 0)
|
|
118
|
+
return { scope: 'product', reason: `${reason} No page it named exists, so the full research runs.`, pages: [], decidedBy: 'agent' };
|
|
119
|
+
return { scope, reason, pages: scope === 'pages' ? pages : [], decidedBy: 'agent' };
|
|
120
|
+
}
|
|
121
|
+
/** One line for the log and the plan review. */
|
|
122
|
+
export function describeResearchScope(research) {
|
|
123
|
+
const what = research.scope === 'navigation'
|
|
124
|
+
? 'no research: navigation, icons, branding, or metadata only'
|
|
125
|
+
: research.scope === 'pages'
|
|
126
|
+
? `product research focused on ${research.pages.length} page${research.pages.length === 1 ? '' : 's'}`
|
|
127
|
+
: 'full research';
|
|
128
|
+
const who = research.decidedBy === 'agent' ? 'decided by a triage session' : research.decidedBy === 'rules' ? 'decided from the request' : 'as every create run does';
|
|
129
|
+
return `Research scope: ${what} (${who}). ${research.reason}`;
|
|
130
|
+
}
|
|
131
|
+
//# sourceMappingURL=planning-triage.js.map
|