supipowers 1.5.2 → 2.0.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/README.md +14 -8
- package/bin/install.mjs +20 -5
- package/bin/install.ts +95 -0
- package/package.json +8 -4
- package/skills/context-mode/SKILL.md +17 -10
- package/skills/harness/SKILL.md +94 -0
- package/skills/ui-design/SKILL.md +63 -0
- package/skills/ui-design/sub-agent-templates/component-builder.md +29 -0
- package/skills/ui-design/sub-agent-templates/design-critic.md +46 -0
- package/skills/ui-design/sub-agent-templates/pencil/component-builder.md +29 -0
- package/skills/ui-design/sub-agent-templates/pencil/design-critic.md +42 -0
- package/skills/ui-design/sub-agent-templates/pencil/section-assembler.md +27 -0
- package/skills/ui-design/sub-agent-templates/section-assembler.md +27 -0
- package/skills/ultraplan-discover/SKILL.md +96 -0
- package/skills/ultraplan-intake/SKILL.md +89 -0
- package/skills/ultraplan-research/SKILL.md +129 -0
- package/skills/ultraplan-review/SKILL.md +86 -0
- package/skills/ultraplan-review-scope/SKILL.md +111 -0
- package/skills/ultraplan-review-structure/SKILL.md +120 -0
- package/skills/ultraplan-review-tdd/SKILL.md +142 -0
- package/skills/ultraplan-scout/SKILL.md +110 -0
- package/skills/ultraplan-synthesize/SKILL.md +124 -0
- package/src/{quality/ai-session.ts → ai/final-message.ts} +27 -0
- package/src/ai/schema-text.ts +129 -0
- package/src/ai/structured-output.ts +274 -0
- package/src/ai/template.ts +27 -0
- package/src/bootstrap.ts +63 -28
- package/src/commands/agents.ts +149 -45
- package/src/commands/ai-review.ts +251 -30
- package/src/commands/clear.ts +434 -0
- package/src/commands/commit.ts +1 -0
- package/src/commands/config.ts +242 -44
- package/src/commands/context.ts +55 -28
- package/src/commands/doctor.ts +234 -6
- package/src/commands/fix-pr.ts +306 -131
- package/src/commands/generate.ts +111 -21
- package/src/commands/memory.ts +192 -0
- package/src/commands/model-picker.ts +28 -21
- package/src/commands/model.ts +19 -9
- package/src/commands/optimize-context.ts +408 -29
- package/src/commands/plan.ts +2 -0
- package/src/commands/qa.ts +312 -137
- package/src/commands/release.ts +259 -76
- package/src/commands/review.ts +293 -59
- package/src/commands/status.ts +200 -13
- package/src/commands/supi.ts +3 -35
- package/src/commands/ui-design.ts +394 -0
- package/src/commands/ultraplan.ts +1518 -0
- package/src/commands/update.ts +86 -0
- package/src/config/defaults.ts +62 -0
- package/src/config/loader.ts +448 -60
- package/src/config/schema.ts +108 -2
- package/src/context/optimizer.ts +25 -33
- package/src/context/rule-renderer.ts +223 -0
- package/src/context/savings.ts +258 -0
- package/src/context/startup-check.ts +380 -0
- package/src/context/startup-optimizer.ts +355 -0
- package/src/context/tokenignore.ts +146 -0
- package/src/context-mode/cache-handle.ts +49 -0
- package/src/context-mode/cache-preview.ts +71 -0
- package/src/context-mode/cache-store.ts +738 -0
- package/src/context-mode/compressor.ts +131 -26
- package/src/context-mode/dedup.ts +108 -0
- package/src/context-mode/detector.ts +35 -4
- package/src/context-mode/event-extractor.ts +14 -12
- package/src/context-mode/event-store.ts +91 -36
- package/src/context-mode/hooks.ts +798 -56
- package/src/context-mode/knowledge/store.ts +255 -11
- package/src/context-mode/memory-store.ts +325 -0
- package/src/context-mode/metrics-recorder.ts +158 -0
- package/src/context-mode/metrics-store.ts +765 -0
- package/src/context-mode/model.ts +24 -0
- package/src/context-mode/processor-keys.ts +29 -0
- package/src/context-mode/processors/build.ts +66 -0
- package/src/context-mode/processors/docker.ts +57 -0
- package/src/context-mode/processors/git.ts +111 -0
- package/src/context-mode/processors/json.ts +112 -0
- package/src/context-mode/processors/k8s.ts +67 -0
- package/src/context-mode/processors/lint.ts +67 -0
- package/src/context-mode/processors/log.ts +86 -0
- package/src/context-mode/processors/registry.ts +116 -0
- package/src/context-mode/processors/test-runner.ts +102 -0
- package/src/context-mode/processors/types.ts +20 -0
- package/src/context-mode/repomap.ts +400 -0
- package/src/context-mode/routing.ts +97 -24
- package/src/context-mode/sandbox/runners.ts +5 -1
- package/src/context-mode/snapshot-builder.ts +106 -11
- package/src/context-mode/source-hash.ts +173 -0
- package/src/context-mode/tool-name.ts +11 -0
- package/src/context-mode/tools.ts +654 -22
- package/src/context-mode/web/fetcher.ts +31 -12
- package/src/debug/logger.ts +2 -1
- package/src/deps/registry.ts +1 -1
- package/src/discipline/failure-summarizer.ts +170 -0
- package/src/discipline/failure-taxonomy.ts +131 -0
- package/src/discipline/workflow-invariants.ts +125 -0
- package/src/discovery/index.ts +31 -0
- package/src/discovery/lsp.ts +87 -0
- package/src/discovery/rank.ts +144 -0
- package/src/discovery/sources.ts +89 -0
- package/src/discovery/workflow.ts +87 -0
- package/src/docs/contracts.ts +39 -0
- package/src/docs/drift.ts +117 -87
- package/src/fix-pr/assessment.ts +200 -0
- package/src/fix-pr/contracts.ts +47 -0
- package/src/fix-pr/fetch-comments.ts +80 -0
- package/src/fix-pr/prompt-builder.ts +58 -40
- package/src/fix-pr/scripts/exec.ts +34 -0
- package/src/fix-pr/scripts/trigger-review.ts +106 -0
- package/src/fix-pr/scripts/wait-and-check.ts +108 -0
- package/src/fix-pr/types.ts +4 -0
- package/src/git/branch-finish.ts +5 -0
- package/src/git/commit-contract.ts +83 -0
- package/src/git/commit.ts +121 -184
- package/src/git/status.ts +62 -8
- package/src/harness/anti_slop/architecture-parser.ts +210 -0
- package/src/harness/anti_slop/backend-factory.ts +30 -0
- package/src/harness/anti_slop/backend.ts +140 -0
- package/src/harness/anti_slop/desloppify-adapter.ts +319 -0
- package/src/harness/anti_slop/fallow-adapter.ts +305 -0
- package/src/harness/anti_slop/installer.ts +227 -0
- package/src/harness/anti_slop/queue.ts +216 -0
- package/src/harness/anti_slop/recommend.ts +84 -0
- package/src/harness/anti_slop/score.ts +180 -0
- package/src/harness/anti_slop/synthetic-edit-test.ts +128 -0
- package/src/harness/artifacts/agents-md.ts +88 -0
- package/src/harness/artifacts/checks-wiring.ts +57 -0
- package/src/harness/artifacts/docs-tree.ts +79 -0
- package/src/harness/artifacts/lint-configs.ts +136 -0
- package/src/harness/artifacts/review-agents.ts +67 -0
- package/src/harness/bare-entry.ts +108 -0
- package/src/harness/command.ts +1010 -0
- package/src/harness/default-agents/design.md +23 -0
- package/src/harness/default-agents/discover.md +18 -0
- package/src/harness/default-agents/implement.md +24 -0
- package/src/harness/default-agents/plan.md +19 -0
- package/src/harness/default-agents/research.md +21 -0
- package/src/harness/default-agents/validate.md +22 -0
- package/src/harness/gc/reporter.ts +28 -0
- package/src/harness/gc/runner.ts +136 -0
- package/src/harness/hooks/layer-context-inject.ts +155 -0
- package/src/harness/hooks/post-session-sweep.ts +130 -0
- package/src/harness/hooks/pre-edit-dupe-probe.ts +224 -0
- package/src/harness/hooks/register.ts +118 -0
- package/src/harness/model.ts +117 -0
- package/src/harness/pipeline.ts +348 -0
- package/src/harness/project-paths.ts +235 -0
- package/src/harness/stage-runner.ts +107 -0
- package/src/harness/stages/design.ts +386 -0
- package/src/harness/stages/discover.ts +454 -0
- package/src/harness/stages/implement.ts +162 -0
- package/src/harness/stages/plan.ts +335 -0
- package/src/harness/stages/research.ts +263 -0
- package/src/harness/stages/validate.ts +684 -0
- package/src/harness/storage.ts +467 -0
- package/src/harness/tools.ts +426 -0
- package/src/lsp/bridge.ts +56 -95
- package/src/lsp/capabilities.ts +108 -0
- package/src/lsp/contracts.ts +35 -0
- package/src/lsp/detector.ts +8 -12
- package/src/markdown-frontmatter.ts +68 -0
- package/src/mempalace/bridge.ts +129 -0
- package/src/mempalace/config.ts +75 -0
- package/src/mempalace/format.ts +163 -0
- package/src/mempalace/hooks.ts +370 -0
- package/src/mempalace/installer-helper.ts +194 -0
- package/src/mempalace/python/mempalace_bridge.py +440 -0
- package/src/mempalace/runtime.ts +565 -0
- package/src/mempalace/schema.ts +264 -0
- package/src/mempalace/session-summary.ts +198 -0
- package/src/mempalace/tool.ts +186 -0
- package/src/mempalace/uv.ts +256 -0
- package/src/migrate/runner.ts +354 -0
- package/src/planning/approval-flow.ts +206 -9
- package/src/planning/plan-writer-prompt.ts +4 -3
- package/src/planning/planning-ask-tool.ts +39 -0
- package/src/planning/render-markdown.ts +74 -0
- package/src/planning/spec.ts +42 -0
- package/src/planning/system-prompt.ts +11 -8
- package/src/planning/validate.ts +84 -0
- package/src/platform/omp.ts +15 -2
- package/src/platform/system-prompt.ts +37 -0
- package/src/platform/test-utils.ts +3 -0
- package/src/platform/types.ts +6 -1
- package/src/qa/config.ts +12 -6
- package/src/qa/detect-app-type.ts +13 -6
- package/src/qa/matrix.ts +12 -6
- package/src/qa/prompt-builder.ts +28 -30
- package/src/qa/scripts/dev-server-utils.ts +72 -0
- package/src/qa/scripts/run-e2e-tests.ts +226 -0
- package/src/qa/scripts/start-dev-server.ts +138 -0
- package/src/qa/scripts/stop-dev-server.ts +77 -0
- package/src/qa/session.ts +13 -7
- package/src/quality/ai-setup.ts +27 -25
- package/src/quality/contracts.ts +34 -0
- package/src/quality/gates/ai-review.ts +20 -58
- package/src/quality/gates/command.ts +249 -46
- package/src/quality/review-gates.ts +18 -2
- package/src/quality/runner.ts +63 -22
- package/src/quality/schemas.ts +37 -2
- package/src/quality/setup.ts +96 -16
- package/src/release/changelog.ts +1 -1
- package/src/release/channels/custom.ts +13 -3
- package/src/release/channels/types.ts +5 -0
- package/src/release/contracts.ts +90 -0
- package/src/release/executor.ts +122 -45
- package/src/release/prompt.ts +18 -2
- package/src/release/targets.ts +86 -0
- package/src/release/version.ts +96 -71
- package/src/review/agent-loader.ts +298 -127
- package/src/review/fixer.ts +10 -6
- package/src/review/multi-agent-runner.ts +115 -14
- package/src/review/output.ts +12 -139
- package/src/review/runner.ts +12 -6
- package/src/review/scope.ts +144 -24
- package/src/review/types.ts +11 -20
- package/src/review/validator.ts +12 -6
- package/src/storage/fix-pr-sessions.ts +21 -14
- package/src/storage/plans.ts +14 -5
- package/src/storage/qa-sessions.ts +25 -19
- package/src/storage/reliability-metrics.ts +180 -0
- package/src/storage/reports.ts +8 -7
- package/src/storage/review-sessions.ts +55 -20
- package/src/tool-catalog/active-tool-controller.ts +164 -0
- package/src/tool-catalog/active-tool-planner.ts +212 -0
- package/src/tool-catalog/tool-groups.ts +102 -0
- package/src/types.ts +1401 -5
- package/src/ui-design/backend-adapter.ts +78 -0
- package/src/ui-design/backends/local-html.ts +82 -0
- package/src/ui-design/backends/pencil-mcp.ts +111 -0
- package/src/ui-design/components-scanner.ts +124 -0
- package/src/ui-design/config.ts +55 -0
- package/src/ui-design/pen-scanner.ts +95 -0
- package/src/ui-design/pen-selector.ts +72 -0
- package/src/ui-design/prompt-builder.ts +73 -0
- package/src/ui-design/scanner.ts +136 -0
- package/src/ui-design/session.ts +974 -0
- package/src/ui-design/system-prompt.ts +312 -0
- package/src/ui-design/tokens-scanner.ts +181 -0
- package/src/ui-design/types.ts +96 -0
- package/src/ultraplan/agent-catalog.ts +522 -0
- package/src/ultraplan/authoring/agent-catalog.ts +310 -0
- package/src/ultraplan/authoring/authoring-tools.ts +552 -0
- package/src/ultraplan/authoring/command-handlers.ts +339 -0
- package/src/ultraplan/authoring/markdown.ts +510 -0
- package/src/ultraplan/authoring/model.ts +162 -0
- package/src/ultraplan/authoring/pipeline.ts +319 -0
- package/src/ultraplan/authoring/stage-runner.ts +141 -0
- package/src/ultraplan/authoring/stages/approve.ts +249 -0
- package/src/ultraplan/authoring/stages/discover.ts +289 -0
- package/src/ultraplan/authoring/stages/intake.ts +203 -0
- package/src/ultraplan/authoring/stages/research.ts +399 -0
- package/src/ultraplan/authoring/stages/review.ts +333 -0
- package/src/ultraplan/authoring/stages/scout.ts +188 -0
- package/src/ultraplan/authoring/stages/synthesize.ts +348 -0
- package/src/ultraplan/authoring/storage.ts +594 -0
- package/src/ultraplan/authoring/synth-gate.ts +165 -0
- package/src/ultraplan/authoring-draft.ts +653 -0
- package/src/ultraplan/authoring-persist.ts +180 -0
- package/src/ultraplan/authoring-tool.ts +608 -0
- package/src/ultraplan/authoring-wizard.ts +587 -0
- package/src/ultraplan/batch/merge.ts +98 -0
- package/src/ultraplan/batch/planner.ts +150 -0
- package/src/ultraplan/batch/presenter.ts +97 -0
- package/src/ultraplan/batch/storage.ts +420 -0
- package/src/ultraplan/batch/supervisor.ts +317 -0
- package/src/ultraplan/batch/worker.ts +26 -0
- package/src/ultraplan/batch/worktree.ts +110 -0
- package/src/ultraplan/contracts.ts +1593 -0
- package/src/ultraplan/default-agents/authoring/discoverer.md +12 -0
- package/src/ultraplan/default-agents/authoring/intake.md +12 -0
- package/src/ultraplan/default-agents/authoring/planner.md +12 -0
- package/src/ultraplan/default-agents/authoring/researcher.md +12 -0
- package/src/ultraplan/default-agents/authoring/scope-checker.md +12 -0
- package/src/ultraplan/default-agents/authoring/scout.md +12 -0
- package/src/ultraplan/default-agents/authoring/structure-checker.md +12 -0
- package/src/ultraplan/default-agents/authoring/tdd-checker.md +12 -0
- package/src/ultraplan/default-agents/backend-domain-reviewer.md +10 -0
- package/src/ultraplan/default-agents/backend-executor.md +10 -0
- package/src/ultraplan/default-agents/backend-stack-reviewer.md +10 -0
- package/src/ultraplan/default-agents/backend-tester.md +10 -0
- package/src/ultraplan/default-agents/frontend-domain-reviewer.md +10 -0
- package/src/ultraplan/default-agents/frontend-executor.md +10 -0
- package/src/ultraplan/default-agents/frontend-stack-reviewer.md +10 -0
- package/src/ultraplan/default-agents/frontend-tester.md +10 -0
- package/src/ultraplan/default-agents/infrastructure-domain-reviewer.md +10 -0
- package/src/ultraplan/default-agents/infrastructure-executor.md +10 -0
- package/src/ultraplan/default-agents/infrastructure-stack-reviewer.md +10 -0
- package/src/ultraplan/default-agents/infrastructure-tester.md +10 -0
- package/src/ultraplan/execution/contract.ts +71 -0
- package/src/ultraplan/execution/policy.ts +217 -0
- package/src/ultraplan/execution/runtime-tools.ts +107 -0
- package/src/ultraplan/execution/session-runner.ts +281 -0
- package/src/ultraplan/next-router.ts +85 -0
- package/src/ultraplan/presenter.ts +359 -0
- package/src/ultraplan/project-paths.ts +342 -0
- package/src/ultraplan/runtime/active-execution.ts +72 -0
- package/src/ultraplan/runtime/apply-mutation.ts +416 -0
- package/src/ultraplan/runtime/blockers.ts +243 -0
- package/src/ultraplan/runtime/hook-bridge.ts +486 -0
- package/src/ultraplan/runtime/launch-context.ts +207 -0
- package/src/ultraplan/runtime/migration.ts +524 -0
- package/src/ultraplan/runtime/normalize.ts +281 -0
- package/src/ultraplan/runtime/proof.ts +260 -0
- package/src/ultraplan/runtime/reducer.ts +416 -0
- package/src/ultraplan/runtime/repair.ts +251 -0
- package/src/ultraplan/runtime/tracker-storage.ts +368 -0
- package/src/ultraplan/session-selection.ts +291 -0
- package/src/ultraplan/storage.ts +374 -0
- package/src/utils/editor.ts +38 -0
- package/src/utils/executable.ts +80 -0
- package/src/utils/paths.ts +1 -20
- package/src/utils/shell.ts +31 -0
- package/src/visual/companion.ts +2 -1
- package/src/visual/scripts/frame-template.html +60 -0
- package/src/visual/scripts/index.js +59 -13
- package/src/visual/scripts/package.json +3 -0
- package/src/visual/start-server.ts +2 -1
- package/src/workspace/git-scope.ts +64 -0
- package/src/workspace/locks.ts +23 -0
- package/src/workspace/package-manager.ts +117 -0
- package/src/workspace/path-mapping.ts +75 -0
- package/src/workspace/project-slug.ts +92 -0
- package/src/workspace/repo-root.ts +137 -0
- package/src/workspace/selector.ts +115 -0
- package/src/workspace/state-paths.ts +118 -0
- package/src/workspace/targets.ts +313 -0
- package/src/fix-pr/scripts/diff-comments.sh +0 -33
- package/src/fix-pr/scripts/fetch-pr-comments.sh +0 -25
- package/src/fix-pr/scripts/trigger-review.sh +0 -36
- package/src/fix-pr/scripts/wait-and-check.sh +0 -37
- package/src/qa/scripts/detect-app-type.sh +0 -68
- package/src/qa/scripts/discover-routes.sh +0 -143
- package/src/qa/scripts/run-e2e-tests.sh +0 -131
- package/src/qa/scripts/start-dev-server.sh +0 -46
- package/src/qa/scripts/stop-dev-server.sh +0 -36
- package/src/review/prompts/fix-output-schema.md +0 -18
- package/src/review/prompts/review-output-schema.md +0 -38
- package/src/review/template.ts +0 -15
- /package/src/{review → ai}/prompts/invalid-output-retry.md +0 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// src/discovery/rank.ts
|
|
2
|
+
//
|
|
3
|
+
// Deterministic ranking over a set of candidate paths. Each scoring source
|
|
4
|
+
// contributes a weighted score and a rationale line; the final ranking is
|
|
5
|
+
// the sum of contributions, sorted desc and then lex for stability.
|
|
6
|
+
|
|
7
|
+
export interface DiscoveryInput {
|
|
8
|
+
cwd: string;
|
|
9
|
+
/** Absolute or cwd-relative repo root. */
|
|
10
|
+
repoRoot: string;
|
|
11
|
+
/** Free-text workflow query, e.g. "fix the login bug". Used for path-token scoring. */
|
|
12
|
+
query?: string;
|
|
13
|
+
/** Files changed in the current context (git diff, uncommitted, PR scope). */
|
|
14
|
+
changedFiles?: string[];
|
|
15
|
+
/**
|
|
16
|
+
* All discoverable files to consider. If omitted, only `changedFiles` are
|
|
17
|
+
* scored. Callers should keep this list pre-filtered to reasonable size.
|
|
18
|
+
*/
|
|
19
|
+
candidatePool?: string[];
|
|
20
|
+
/**
|
|
21
|
+
* Additional per-source boost map: path → { score, rationale }. Used by
|
|
22
|
+
* workflow-specific callers (e.g. fix-pr injecting files mentioned in PR
|
|
23
|
+
* comments).
|
|
24
|
+
*/
|
|
25
|
+
externalSignals?: Record<string, { score: number; rationale: string }>;
|
|
26
|
+
/** Cap the returned ranked list. Default: 20. */
|
|
27
|
+
limit?: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type DiscoverySource =
|
|
31
|
+
| "changed"
|
|
32
|
+
| "query-path-match"
|
|
33
|
+
| "external-signal"
|
|
34
|
+
| "lsp";
|
|
35
|
+
|
|
36
|
+
export interface DiscoveryCandidate {
|
|
37
|
+
path: string;
|
|
38
|
+
score: number;
|
|
39
|
+
/** Each source that contributed to the score. */
|
|
40
|
+
sources: DiscoverySource[];
|
|
41
|
+
/** Human-readable reasons explaining the score. */
|
|
42
|
+
rationale: string[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface DiscoveryResult {
|
|
46
|
+
candidates: DiscoveryCandidate[];
|
|
47
|
+
/** Every source that touched at least one candidate, for observability. */
|
|
48
|
+
sourcesUsed: DiscoverySource[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// Source weights. Small and explicit — avoid hidden tuning.
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
const WEIGHT_CHANGED = 10;
|
|
56
|
+
const WEIGHT_QUERY_TOKEN = 2;
|
|
57
|
+
const MIN_QUERY_TOKEN_LENGTH = 4;
|
|
58
|
+
|
|
59
|
+
function normalizePath(p: string): string {
|
|
60
|
+
return p.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function tokenize(query: string): string[] {
|
|
64
|
+
return [
|
|
65
|
+
...new Set(
|
|
66
|
+
query
|
|
67
|
+
.toLowerCase()
|
|
68
|
+
.split(/[^a-z0-9_-]+/)
|
|
69
|
+
.filter((t) => t.length >= MIN_QUERY_TOKEN_LENGTH),
|
|
70
|
+
),
|
|
71
|
+
];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function countTokenHitsInPath(path: string, tokens: string[]): number {
|
|
75
|
+
if (tokens.length === 0) return 0;
|
|
76
|
+
const lower = path.toLowerCase();
|
|
77
|
+
let hits = 0;
|
|
78
|
+
for (const t of tokens) {
|
|
79
|
+
if (lower.includes(t)) hits += 1;
|
|
80
|
+
}
|
|
81
|
+
return hits;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Rank candidate files. Deterministic given identical input: sort is by
|
|
86
|
+
* score desc, then path asc, so ties resolve stably.
|
|
87
|
+
*/
|
|
88
|
+
export function rankDiscoveryCandidates(input: DiscoveryInput): DiscoveryResult {
|
|
89
|
+
const changedFiles = new Set((input.changedFiles ?? []).map(normalizePath));
|
|
90
|
+
const pool = new Set<string>([
|
|
91
|
+
...changedFiles,
|
|
92
|
+
...(input.candidatePool ?? []).map(normalizePath),
|
|
93
|
+
...Object.keys(input.externalSignals ?? {}).map(normalizePath),
|
|
94
|
+
]);
|
|
95
|
+
|
|
96
|
+
const queryTokens = input.query ? tokenize(input.query) : [];
|
|
97
|
+
const sourcesUsed = new Set<DiscoverySource>();
|
|
98
|
+
|
|
99
|
+
const candidates: DiscoveryCandidate[] = [];
|
|
100
|
+
for (const path of pool) {
|
|
101
|
+
const rationale: string[] = [];
|
|
102
|
+
const sources: DiscoverySource[] = [];
|
|
103
|
+
let score = 0;
|
|
104
|
+
|
|
105
|
+
if (changedFiles.has(path)) {
|
|
106
|
+
score += WEIGHT_CHANGED;
|
|
107
|
+
sources.push("changed");
|
|
108
|
+
sourcesUsed.add("changed");
|
|
109
|
+
rationale.push("changed in current context");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const tokenHits = countTokenHitsInPath(path, queryTokens);
|
|
113
|
+
if (tokenHits > 0) {
|
|
114
|
+
const contribution = tokenHits * WEIGHT_QUERY_TOKEN;
|
|
115
|
+
score += contribution;
|
|
116
|
+
sources.push("query-path-match");
|
|
117
|
+
sourcesUsed.add("query-path-match");
|
|
118
|
+
rationale.push(`path matches ${tokenHits} query token${tokenHits === 1 ? "" : "s"}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const external = input.externalSignals?.[path];
|
|
122
|
+
if (external) {
|
|
123
|
+
score += external.score;
|
|
124
|
+
sources.push("external-signal");
|
|
125
|
+
sourcesUsed.add("external-signal");
|
|
126
|
+
rationale.push(external.rationale);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (score > 0) {
|
|
130
|
+
candidates.push({ path, score, sources, rationale });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
candidates.sort((a, b) => {
|
|
135
|
+
if (b.score !== a.score) return b.score - a.score;
|
|
136
|
+
return a.path.localeCompare(b.path);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const limit = input.limit ?? 20;
|
|
140
|
+
return {
|
|
141
|
+
candidates: candidates.slice(0, limit),
|
|
142
|
+
sourcesUsed: [...sourcesUsed].sort(),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// src/discovery/sources.ts
|
|
2
|
+
//
|
|
3
|
+
// Side-effectful discovery helpers that read from the filesystem / git /
|
|
4
|
+
// workspace. Kept in a dedicated module so `rank.ts` stays pure and unit-
|
|
5
|
+
// testable without mocks.
|
|
6
|
+
|
|
7
|
+
import * as fs from "node:fs";
|
|
8
|
+
import * as path from "node:path";
|
|
9
|
+
import type { DiscoveryInput, DiscoveryResult } from "./rank.js";
|
|
10
|
+
import { rankDiscoveryCandidates } from "./rank.js";
|
|
11
|
+
|
|
12
|
+
export interface SourcesDiscoveryOptions extends Omit<DiscoveryInput, "candidatePool"> {
|
|
13
|
+
/**
|
|
14
|
+
* Glob-lite allow-list of path suffixes to include in the candidate pool
|
|
15
|
+
* (e.g. [".ts", ".tsx"]). Empty means no suffix filter.
|
|
16
|
+
*/
|
|
17
|
+
extensions?: string[];
|
|
18
|
+
/**
|
|
19
|
+
* Max pool size after filesystem walk. Prevents huge repos from dominating
|
|
20
|
+
* candidate evaluation. Default: 5000.
|
|
21
|
+
*/
|
|
22
|
+
maxPoolSize?: number;
|
|
23
|
+
/**
|
|
24
|
+
* Directory names to skip while walking. Defaults cover `.git`, `node_modules`,
|
|
25
|
+
* `dist`, `.omp`, `.cache`.
|
|
26
|
+
*/
|
|
27
|
+
excludeDirs?: string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const DEFAULT_EXCLUDES = new Set([".git", "node_modules", "dist", "build", ".omp", ".cache", ".next", ".turbo"]);
|
|
31
|
+
|
|
32
|
+
function walkFiles(
|
|
33
|
+
root: string,
|
|
34
|
+
options: { extensions?: string[]; maxPoolSize: number; excludeDirs: Set<string> },
|
|
35
|
+
): string[] {
|
|
36
|
+
const results: string[] = [];
|
|
37
|
+
const stack: string[] = [root];
|
|
38
|
+
const extFilter = options.extensions && options.extensions.length > 0 ? options.extensions : null;
|
|
39
|
+
|
|
40
|
+
while (stack.length > 0 && results.length < options.maxPoolSize) {
|
|
41
|
+
const current = stack.pop()!;
|
|
42
|
+
let entries: fs.Dirent[];
|
|
43
|
+
try {
|
|
44
|
+
entries = fs.readdirSync(current, { withFileTypes: true });
|
|
45
|
+
} catch {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
for (const entry of entries) {
|
|
50
|
+
if (options.excludeDirs.has(entry.name)) continue;
|
|
51
|
+
const full = path.join(current, entry.name);
|
|
52
|
+
if (entry.isDirectory()) {
|
|
53
|
+
stack.push(full);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (!entry.isFile()) continue;
|
|
57
|
+
if (extFilter && !extFilter.some((ext) => entry.name.endsWith(ext))) continue;
|
|
58
|
+
results.push(path.relative(root, full));
|
|
59
|
+
if (results.length >= options.maxPoolSize) break;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return results;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Walk `repoRoot`, build a candidate pool, then rank. Deterministic given
|
|
68
|
+
* the same filesystem state.
|
|
69
|
+
*/
|
|
70
|
+
export function discoverFromSources(options: SourcesDiscoveryOptions): DiscoveryResult {
|
|
71
|
+
const excludeDirs = new Set([...DEFAULT_EXCLUDES, ...(options.excludeDirs ?? [])]);
|
|
72
|
+
const maxPoolSize = options.maxPoolSize ?? 5000;
|
|
73
|
+
|
|
74
|
+
const candidatePool = walkFiles(options.repoRoot, {
|
|
75
|
+
extensions: options.extensions,
|
|
76
|
+
maxPoolSize,
|
|
77
|
+
excludeDirs,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
return rankDiscoveryCandidates({
|
|
81
|
+
cwd: options.cwd,
|
|
82
|
+
repoRoot: options.repoRoot,
|
|
83
|
+
query: options.query,
|
|
84
|
+
changedFiles: options.changedFiles,
|
|
85
|
+
candidatePool,
|
|
86
|
+
externalSignals: options.externalSignals,
|
|
87
|
+
limit: options.limit,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// src/discovery/workflow.ts
|
|
2
|
+
//
|
|
3
|
+
// High-level integration helper for workflows. Commands (/supi:review,
|
|
4
|
+
// /supi:plan, /supi:qa, /supi:fix-pr) adopt discovery by calling
|
|
5
|
+
// `suggestCandidatesForWorkflow` with the context they already have. The
|
|
6
|
+
// helper composes the deterministic ranker, optional LSP augmentation, and
|
|
7
|
+
// a concise rationale formatter so workflows don't wire the pieces
|
|
8
|
+
// themselves.
|
|
9
|
+
//
|
|
10
|
+
// The helper is safe-by-default: when no inputs are provided, it returns
|
|
11
|
+
// an empty result rather than scanning the whole repo. Workflows that want
|
|
12
|
+
// a full filesystem pool should call `discoverFromSources` directly.
|
|
13
|
+
|
|
14
|
+
import { rankDiscoveryCandidates, type DiscoveryCandidate, type DiscoveryInput } from "./rank.js";
|
|
15
|
+
import { rankWithLspAugmentation, type LspSymbolLocation } from "./lsp.js";
|
|
16
|
+
|
|
17
|
+
export interface WorkflowDiscoveryInput extends Omit<DiscoveryInput, "candidatePool"> {
|
|
18
|
+
/**
|
|
19
|
+
* Optional pre-filtered candidate pool (e.g. tracked files for a workspace
|
|
20
|
+
* target). When omitted, only changedFiles + externalSignals seed the pool.
|
|
21
|
+
*/
|
|
22
|
+
candidatePool?: string[];
|
|
23
|
+
/**
|
|
24
|
+
* Optional LSP symbol lookup. When provided, LSP hits are folded in as
|
|
25
|
+
* external signals; failures degrade to the non-LSP ranking.
|
|
26
|
+
*/
|
|
27
|
+
querySymbols?: (query: string) => LspSymbolLocation[] | Promise<LspSymbolLocation[]>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface WorkflowDiscoveryResult {
|
|
31
|
+
candidates: DiscoveryCandidate[];
|
|
32
|
+
/** True when LSP augmentation ran successfully. False when disabled or it threw. */
|
|
33
|
+
lspUsed: boolean;
|
|
34
|
+
/** Short formatted summary lines suitable for notify / log / prompt injection. */
|
|
35
|
+
summaryLines: string[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function formatSummary(candidates: DiscoveryCandidate[], maxLines = 5): string[] {
|
|
39
|
+
return candidates.slice(0, maxLines).map((c) => {
|
|
40
|
+
const why = c.rationale.join("; ");
|
|
41
|
+
return `${c.path} (score ${c.score}) — ${why}`;
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Produce ranked candidates for a workflow. Always safe to call — returns
|
|
47
|
+
* empty candidates when no signals are provided. Prefer this over calling
|
|
48
|
+
* `rankDiscoveryCandidates` / `rankWithLspAugmentation` directly so every
|
|
49
|
+
* workflow uses the same composition and rationale format.
|
|
50
|
+
*/
|
|
51
|
+
export async function suggestCandidatesForWorkflow(
|
|
52
|
+
input: WorkflowDiscoveryInput,
|
|
53
|
+
): Promise<WorkflowDiscoveryResult> {
|
|
54
|
+
if (input.querySymbols) {
|
|
55
|
+
const result = await rankWithLspAugmentation({
|
|
56
|
+
cwd: input.cwd,
|
|
57
|
+
repoRoot: input.repoRoot,
|
|
58
|
+
query: input.query,
|
|
59
|
+
changedFiles: input.changedFiles,
|
|
60
|
+
candidatePool: input.candidatePool,
|
|
61
|
+
externalSignals: input.externalSignals,
|
|
62
|
+
limit: input.limit,
|
|
63
|
+
querySymbols: input.querySymbols,
|
|
64
|
+
});
|
|
65
|
+
return {
|
|
66
|
+
candidates: result.candidates,
|
|
67
|
+
lspUsed: result.lspAvailable && result.lspHitCount > 0,
|
|
68
|
+
summaryLines: formatSummary(result.candidates),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const ranked = rankDiscoveryCandidates({
|
|
73
|
+
cwd: input.cwd,
|
|
74
|
+
repoRoot: input.repoRoot,
|
|
75
|
+
query: input.query,
|
|
76
|
+
changedFiles: input.changedFiles,
|
|
77
|
+
candidatePool: input.candidatePool,
|
|
78
|
+
externalSignals: input.externalSignals,
|
|
79
|
+
limit: input.limit,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
candidates: ranked.candidates,
|
|
84
|
+
lspUsed: false,
|
|
85
|
+
summaryLines: formatSummary(ranked.candidates),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// src/docs/contracts.ts
|
|
2
|
+
//
|
|
3
|
+
// Schema-backed contract for doc-drift sub-agent output. Every doc-drift
|
|
4
|
+
// sub-agent must emit JSON that parses against DocDriftOutputSchema. The
|
|
5
|
+
// retry loop (runWithOutputValidation) will hand validation errors back to
|
|
6
|
+
// the model rather than letting a silent regex heuristic invent findings.
|
|
7
|
+
|
|
8
|
+
import { Type, type Static } from "@sinclair/typebox";
|
|
9
|
+
|
|
10
|
+
export const DOC_DRIFT_SEVERITIES = ["info", "warning", "error"] as const;
|
|
11
|
+
export const DOC_DRIFT_STATUSES = ["ok", "drifted"] as const;
|
|
12
|
+
|
|
13
|
+
export type DocDriftSeverity = (typeof DOC_DRIFT_SEVERITIES)[number];
|
|
14
|
+
export type DocDriftStatus = (typeof DOC_DRIFT_STATUSES)[number];
|
|
15
|
+
|
|
16
|
+
export const DocDriftFindingSchema = Type.Object(
|
|
17
|
+
{
|
|
18
|
+
file: Type.String({ minLength: 1 }),
|
|
19
|
+
description: Type.String({ minLength: 1 }),
|
|
20
|
+
severity: Type.Union(
|
|
21
|
+
DOC_DRIFT_SEVERITIES.map((value) => Type.Literal(value)),
|
|
22
|
+
),
|
|
23
|
+
relatedFiles: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
|
|
24
|
+
},
|
|
25
|
+
{ additionalProperties: false },
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
export const DocDriftOutputSchema = Type.Object(
|
|
29
|
+
{
|
|
30
|
+
findings: Type.Array(DocDriftFindingSchema),
|
|
31
|
+
status: Type.Union(
|
|
32
|
+
DOC_DRIFT_STATUSES.map((value) => Type.Literal(value)),
|
|
33
|
+
),
|
|
34
|
+
},
|
|
35
|
+
{ additionalProperties: false },
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
export type DocDriftFinding = Static<typeof DocDriftFindingSchema>;
|
|
39
|
+
export type DocDriftOutput = Static<typeof DocDriftOutputSchema>;
|
package/src/docs/drift.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import type { Platform, PlatformPaths } from "../platform/types.js";
|
|
4
|
-
import type { DocDriftState, DriftCheckResult, DriftFinding } from "../types.js";
|
|
5
|
-
import {
|
|
4
|
+
import type { DocDriftState, DriftCheckResult, DriftFinding, WorkspaceTarget } from "../types.js";
|
|
5
|
+
import { runWithOutputValidation, parseStructuredOutput } from "../ai/structured-output.js";
|
|
6
|
+
import { renderSchemaText } from "../ai/schema-text.js";
|
|
7
|
+
import { DocDriftOutputSchema, type DocDriftOutput } from "./contracts.js";
|
|
8
|
+
import { ReleaseDocFixOutputSchema } from "../release/contracts.js";
|
|
9
|
+
import { filterPathsForWorkspaceTarget } from "../workspace/path-mapping.js";
|
|
10
|
+
import { getProjectStatePath, getProjectTargetStatePath } from "../workspace/state-paths.js";
|
|
6
11
|
|
|
7
12
|
// ── State persistence ─────────────────────────────────────────
|
|
8
13
|
|
|
@@ -14,21 +19,41 @@ const EMPTY_STATE: DocDriftState = {
|
|
|
14
19
|
lastRunAt: null,
|
|
15
20
|
};
|
|
16
21
|
|
|
17
|
-
export
|
|
18
|
-
|
|
22
|
+
export interface DocDriftScope<TTarget extends WorkspaceTarget = WorkspaceTarget> {
|
|
23
|
+
target: TTarget;
|
|
24
|
+
allTargets: TTarget[];
|
|
19
25
|
}
|
|
20
26
|
|
|
21
|
-
|
|
22
|
-
|
|
27
|
+
function filterTrackedFilesToScope(
|
|
28
|
+
trackedFiles: string[],
|
|
29
|
+
scope?: DocDriftScope,
|
|
30
|
+
): string[] {
|
|
31
|
+
if (!scope) {
|
|
32
|
+
return trackedFiles;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return filterPathsForWorkspaceTarget(scope.allTargets, scope.target, trackedFiles);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function statePath(paths: PlatformPaths, cwd: string, target?: WorkspaceTarget): string {
|
|
39
|
+
return target ? getProjectTargetStatePath(paths, target, STATE_FILENAME) : getProjectStatePath(paths, cwd, STATE_FILENAME);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function loadState(paths: PlatformPaths, cwd: string, scope?: DocDriftScope): DocDriftState {
|
|
43
|
+
const file = statePath(paths, cwd, scope?.target);
|
|
23
44
|
try {
|
|
24
|
-
|
|
45
|
+
const state = JSON.parse(fs.readFileSync(file, "utf-8")) as DocDriftState;
|
|
46
|
+
return {
|
|
47
|
+
...state,
|
|
48
|
+
trackedFiles: filterTrackedFilesToScope(state.trackedFiles, scope),
|
|
49
|
+
};
|
|
25
50
|
} catch {
|
|
26
51
|
return { ...EMPTY_STATE, trackedFiles: [] };
|
|
27
52
|
}
|
|
28
53
|
}
|
|
29
54
|
|
|
30
|
-
export function saveState(paths: PlatformPaths, cwd: string, state: DocDriftState): void {
|
|
31
|
-
const file = statePath(paths, cwd);
|
|
55
|
+
export function saveState(paths: PlatformPaths, cwd: string, state: DocDriftState, target?: WorkspaceTarget): void {
|
|
56
|
+
const file = statePath(paths, cwd, target);
|
|
32
57
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
33
58
|
fs.writeFileSync(file, JSON.stringify(state, null, 2) + "\n");
|
|
34
59
|
}
|
|
@@ -77,6 +102,7 @@ export function isProjectDoc(filePath: string): boolean {
|
|
|
77
102
|
export async function discoverDocFiles(
|
|
78
103
|
platform: Platform,
|
|
79
104
|
cwd: string,
|
|
105
|
+
scope?: DocDriftScope,
|
|
80
106
|
): Promise<string[]> {
|
|
81
107
|
const result = await platform.exec(
|
|
82
108
|
"git",
|
|
@@ -85,13 +111,16 @@ export async function discoverDocFiles(
|
|
|
85
111
|
);
|
|
86
112
|
if (result.code !== 0) return [];
|
|
87
113
|
|
|
88
|
-
const files =
|
|
89
|
-
.
|
|
90
|
-
|
|
91
|
-
|
|
114
|
+
const files = [...new Set(
|
|
115
|
+
result.stdout
|
|
116
|
+
.split("\n")
|
|
117
|
+
.map((f) => f.trim())
|
|
118
|
+
.filter((f) => f.length > 0 && isProjectDoc(f)),
|
|
119
|
+
)].sort();
|
|
92
120
|
|
|
93
|
-
|
|
94
|
-
|
|
121
|
+
return scope
|
|
122
|
+
? filterPathsForWorkspaceTarget(scope.allTargets, scope.target, files).sort()
|
|
123
|
+
: files;
|
|
95
124
|
}
|
|
96
125
|
|
|
97
126
|
// ── Git helpers ───────────────────────────────────────────────
|
|
@@ -279,85 +308,48 @@ export function buildSubAgentPrompt(group: DocDriftGroup, isFirstRun: boolean):
|
|
|
279
308
|
`- Only flag things that are factually wrong or missing`,
|
|
280
309
|
`- If documentation is accurate, say so`,
|
|
281
310
|
``,
|
|
282
|
-
`Respond with a JSON object:`,
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
` "description": "What is wrong or missing",`,
|
|
288
|
-
` "severity": "info" | "warning" | "error",`,
|
|
289
|
-
` "relatedFiles": ["path/to/source.ts"]`,
|
|
290
|
-
` }`,
|
|
291
|
-
` ],`,
|
|
292
|
-
` "status": "ok" | "drifted"`,
|
|
293
|
-
`}`,
|
|
311
|
+
`Respond with a JSON object that matches this TypeScript shape exactly:`,
|
|
312
|
+
``,
|
|
313
|
+
`\`\`\`ts`,
|
|
314
|
+
renderSchemaText(DocDriftOutputSchema),
|
|
315
|
+
`\`\`\``,
|
|
294
316
|
``,
|
|
295
|
-
`Set status to "drifted" if ANY findings exist, "ok" if all docs are accurate.`,
|
|
296
|
-
`Respond
|
|
317
|
+
`Set "status" to "drifted" if ANY findings exist, "ok" if all docs are accurate.`,
|
|
318
|
+
`Respond with only the JSON object. You may wrap it in a \`\`\`json fence.`,
|
|
297
319
|
);
|
|
298
320
|
|
|
299
321
|
return parts.join("\n");
|
|
300
322
|
}
|
|
301
323
|
|
|
302
|
-
// ── Response parsing ──────────────────────────────────────────
|
|
303
|
-
|
|
304
|
-
export function parseDriftFindings(text: string): { findings: DriftFinding[]; status: string } {
|
|
305
|
-
try {
|
|
306
|
-
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
307
|
-
if (jsonMatch) {
|
|
308
|
-
const parsed = JSON.parse(jsonMatch[0]);
|
|
309
|
-
if (Array.isArray(parsed.findings)) {
|
|
310
|
-
const findings: DriftFinding[] = parsed.findings
|
|
311
|
-
.filter(
|
|
312
|
-
(f: any) =>
|
|
313
|
-
typeof f.file === "string" &&
|
|
314
|
-
typeof f.description === "string",
|
|
315
|
-
)
|
|
316
|
-
.map((f: any) => ({
|
|
317
|
-
file: f.file,
|
|
318
|
-
description: f.description,
|
|
319
|
-
severity: f.severity === "warning" || f.severity === "error" ? f.severity : "info",
|
|
320
|
-
...(Array.isArray(f.relatedFiles) ? { relatedFiles: f.relatedFiles } : {}),
|
|
321
|
-
}));
|
|
322
|
-
return { findings, status: parsed.status === "ok" ? "ok" : "drifted" };
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
} catch {
|
|
326
|
-
// Fall through
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// Fallback: treat unparseable response as potential drift
|
|
330
|
-
const lower = text.toLowerCase();
|
|
331
|
-
const likelyDrifted =
|
|
332
|
-
lower.includes("inaccura") ||
|
|
333
|
-
lower.includes("outdated") ||
|
|
334
|
-
lower.includes("missing") ||
|
|
335
|
-
lower.includes("drift");
|
|
336
|
-
return {
|
|
337
|
-
findings: likelyDrifted
|
|
338
|
-
? [{ file: "unknown", description: text.slice(0, 200), severity: "warning" }]
|
|
339
|
-
: [],
|
|
340
|
-
status: likelyDrifted ? "drifted" : "ok",
|
|
341
|
-
};
|
|
342
|
-
}
|
|
343
|
-
|
|
344
324
|
// ── Sub-agent runner ──────────────────────────────────────────
|
|
345
325
|
|
|
326
|
+
export type DriftSubAgentResult =
|
|
327
|
+
| { status: "ok"; output: DocDriftOutput }
|
|
328
|
+
| { status: "blocked"; error: string };
|
|
329
|
+
|
|
346
330
|
async function runDriftSubAgent(
|
|
347
331
|
createAgentSession: Platform["createAgentSession"],
|
|
332
|
+
paths: PlatformPaths,
|
|
348
333
|
group: DocDriftGroup,
|
|
349
334
|
isFirstRun: boolean,
|
|
350
335
|
cwd: string,
|
|
351
|
-
): Promise<
|
|
336
|
+
): Promise<DriftSubAgentResult> {
|
|
352
337
|
const prompt = buildSubAgentPrompt(group, isFirstRun);
|
|
353
|
-
const result = await
|
|
354
|
-
createAgentSession
|
|
355
|
-
{
|
|
338
|
+
const result = await runWithOutputValidation<DocDriftOutput>(
|
|
339
|
+
createAgentSession as any,
|
|
340
|
+
{
|
|
341
|
+
cwd,
|
|
342
|
+
prompt,
|
|
343
|
+
schema: renderSchemaText(DocDriftOutputSchema),
|
|
344
|
+
parse: (raw) => parseStructuredOutput<DocDriftOutput>(raw, DocDriftOutputSchema),
|
|
345
|
+
reliability: { paths, cwd, command: "docs", operation: "drift-analyze" },
|
|
346
|
+
},
|
|
356
347
|
);
|
|
357
|
-
|
|
358
|
-
|
|
348
|
+
|
|
349
|
+
if (result.status === "ok") {
|
|
350
|
+
return { status: "ok", output: result.output };
|
|
359
351
|
}
|
|
360
|
-
return
|
|
352
|
+
return { status: "blocked", error: result.error };
|
|
361
353
|
}
|
|
362
354
|
|
|
363
355
|
// ── Orchestrator ──────────────────────────────────────────────
|
|
@@ -365,19 +357,25 @@ async function runDriftSubAgent(
|
|
|
365
357
|
/**
|
|
366
358
|
* Checks tracked docs for drift using parallel sub-agents.
|
|
367
359
|
* Returns null to skip silently, or a DriftCheckResult with per-doc findings.
|
|
360
|
+
* Sub-agents whose output fails schema validation surface their error via
|
|
361
|
+
* `result.errors` — the orchestrator never fabricates findings on parse failure.
|
|
368
362
|
*/
|
|
369
363
|
export async function checkDocDrift(
|
|
370
364
|
platform: Platform,
|
|
371
365
|
cwd: string,
|
|
366
|
+
scope?: DocDriftScope,
|
|
372
367
|
): Promise<DriftCheckResult | null> {
|
|
373
|
-
const state = loadState(platform.paths, cwd);
|
|
368
|
+
const state = loadState(platform.paths, cwd, scope);
|
|
374
369
|
if (state.trackedFiles.length === 0) return null;
|
|
375
370
|
|
|
376
371
|
let changedFiles: string[] = [];
|
|
377
372
|
const isFirstRun = !state.lastCommit;
|
|
378
373
|
|
|
379
374
|
if (!isFirstRun) {
|
|
380
|
-
|
|
375
|
+
const diffFiles = await getDiffFilesSince(platform, cwd, state.lastCommit!);
|
|
376
|
+
changedFiles = scope
|
|
377
|
+
? filterPathsForWorkspaceTarget(scope.allTargets, scope.target, diffFiles)
|
|
378
|
+
: diffFiles;
|
|
381
379
|
if (changedFiles.length === 0) return null;
|
|
382
380
|
}
|
|
383
381
|
|
|
@@ -390,6 +388,7 @@ export async function checkDocDrift(
|
|
|
390
388
|
groups.map((group) =>
|
|
391
389
|
runDriftSubAgent(
|
|
392
390
|
platform.createAgentSession.bind(platform),
|
|
391
|
+
platform.paths,
|
|
393
392
|
group,
|
|
394
393
|
isFirstRun,
|
|
395
394
|
cwd,
|
|
@@ -397,18 +396,32 @@ export async function checkDocDrift(
|
|
|
397
396
|
),
|
|
398
397
|
);
|
|
399
398
|
|
|
400
|
-
// Aggregate findings
|
|
399
|
+
// Aggregate findings and errors
|
|
401
400
|
const allFindings: DriftFinding[] = [];
|
|
401
|
+
const errors: string[] = [];
|
|
402
402
|
for (const result of results) {
|
|
403
|
-
|
|
403
|
+
if (result.status === "ok") {
|
|
404
|
+
allFindings.push(...result.output.findings);
|
|
405
|
+
} else {
|
|
406
|
+
errors.push(result.error);
|
|
407
|
+
}
|
|
404
408
|
}
|
|
405
409
|
|
|
406
410
|
const drifted = allFindings.length > 0;
|
|
407
|
-
const
|
|
408
|
-
|
|
409
|
-
|
|
411
|
+
const parts: string[] = [];
|
|
412
|
+
if (drifted) {
|
|
413
|
+
parts.push(`${allFindings.length} finding(s) across ${new Set(allFindings.map((f) => f.file)).size} doc(s)`);
|
|
414
|
+
} else if (errors.length === 0) {
|
|
415
|
+
parts.push("All documentation is up to date.");
|
|
416
|
+
}
|
|
417
|
+
if (errors.length > 0) {
|
|
418
|
+
parts.push(`${errors.length} sub-agent(s) failed validation`);
|
|
419
|
+
}
|
|
420
|
+
const summary = parts.join(" · ");
|
|
410
421
|
|
|
411
|
-
return
|
|
422
|
+
return errors.length > 0
|
|
423
|
+
? { drifted, summary, findings: allFindings, errors }
|
|
424
|
+
: { drifted, summary, findings: allFindings };
|
|
412
425
|
}
|
|
413
426
|
|
|
414
427
|
// ── Doc fix prompt ────────────────────────────────────────────
|
|
@@ -449,6 +462,23 @@ export function buildFixPrompt(findings: DriftFinding[]): string {
|
|
|
449
462
|
parts.push(``);
|
|
450
463
|
}
|
|
451
464
|
|
|
452
|
-
parts.push(
|
|
465
|
+
parts.push(
|
|
466
|
+
`Read each file listed above, apply the fixes, and write the corrected files.`,
|
|
467
|
+
``,
|
|
468
|
+
`## Output`,
|
|
469
|
+
``,
|
|
470
|
+
`After applying the edits, respond with a JSON object that matches this TypeScript shape exactly:`,
|
|
471
|
+
``,
|
|
472
|
+
"```ts",
|
|
473
|
+
renderSchemaText(ReleaseDocFixOutputSchema),
|
|
474
|
+
"```",
|
|
475
|
+
``,
|
|
476
|
+
`Field guide:`,
|
|
477
|
+
`- \`edits\`: one entry per file you modified, with \`file\` as the relative path and \`instructions\` as a short description of what you changed.`,
|
|
478
|
+
`- \`summary\`: one-sentence summary of the overall fix.`,
|
|
479
|
+
`- \`status\`: \`"ok"\` when every finding was addressed; \`"blocked"\` when you could not complete the fixes — put the reason in \`summary\`.`,
|
|
480
|
+
``,
|
|
481
|
+
"Respond with only the JSON object. You may wrap it in a ```json fence.",
|
|
482
|
+
);
|
|
453
483
|
return parts.join("\n");
|
|
454
484
|
}
|