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,426 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Harness pipeline tool registrations.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors `src/ultraplan/authoring/authoring-tools.ts`:
|
|
5
|
+
* - one tool per stage artifact + queue mutation,
|
|
6
|
+
* - thin JSON-schema validation in the harness layer,
|
|
7
|
+
* - thin JSON-shape sanity check before the storage layer's atomic write,
|
|
8
|
+
* - structured `{ok, message?, path?, details?}` returns instead of thrown errors.
|
|
9
|
+
*
|
|
10
|
+
* Tools registered:
|
|
11
|
+
* - harness_discover_record
|
|
12
|
+
* - harness_research_record
|
|
13
|
+
* - harness_decision_record
|
|
14
|
+
* - harness_design_spec_persist
|
|
15
|
+
* - harness_validate_finding
|
|
16
|
+
* - harness_slop_queue_append
|
|
17
|
+
* - harness_slop_queue_resolve
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type { Platform } from "../platform/types.js";
|
|
21
|
+
import type {
|
|
22
|
+
HarnessSlopQueueEntry,
|
|
23
|
+
HarnessValidateFinding,
|
|
24
|
+
UltraPlanStorageResult,
|
|
25
|
+
} from "../types.js";
|
|
26
|
+
import {
|
|
27
|
+
appendHarnessDecision,
|
|
28
|
+
appendImplementLog,
|
|
29
|
+
saveHarnessDesignSpec,
|
|
30
|
+
saveHarnessDiscover,
|
|
31
|
+
saveHarnessResearchTopic,
|
|
32
|
+
} from "./storage.js";
|
|
33
|
+
import {
|
|
34
|
+
appendOpen as appendQueueEntry,
|
|
35
|
+
computeQueueEntryId,
|
|
36
|
+
resolve as resolveQueueEntry,
|
|
37
|
+
markWontfix as markQueueWontfix,
|
|
38
|
+
} from "./anti_slop/queue.js";
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Helpers
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
45
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface ToolReturn {
|
|
49
|
+
ok: boolean;
|
|
50
|
+
message?: string;
|
|
51
|
+
path?: string;
|
|
52
|
+
id?: string;
|
|
53
|
+
details?: unknown;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function toolResult(payload: ToolReturn): ToolReturn {
|
|
57
|
+
return payload;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const SESSION_ID_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9._-]{0,126}[A-Za-z0-9_-])?$/;
|
|
61
|
+
|
|
62
|
+
function isSafeSessionId(value: string): boolean {
|
|
63
|
+
if (!SESSION_ID_PATTERN.test(value)) return false;
|
|
64
|
+
if (value.includes("..")) return false;
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function isSafeTopicSlug(value: string): boolean {
|
|
69
|
+
// Stricter than session id: slugs cannot contain dots so they don't escape via
|
|
70
|
+
// ".." or hidden files.
|
|
71
|
+
return /^[A-Za-z0-9](?:[A-Za-z0-9_-]{0,126}[A-Za-z0-9_-])?$/.test(value);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function readSessionId(
|
|
75
|
+
params: unknown,
|
|
76
|
+
toolName: string,
|
|
77
|
+
): { ok: true; sessionId: string } | { ok: false; message: string } {
|
|
78
|
+
if (!isRecord(params)) return { ok: false, message: `${toolName} requires an object payload` };
|
|
79
|
+
const raw = params.sessionId;
|
|
80
|
+
if (typeof raw !== "string" || raw.trim().length === 0) {
|
|
81
|
+
return { ok: false, message: `${toolName} requires a sessionId string` };
|
|
82
|
+
}
|
|
83
|
+
if (!isSafeSessionId(raw)) {
|
|
84
|
+
return {
|
|
85
|
+
ok: false,
|
|
86
|
+
message: `${toolName} rejected sessionId: must match ${SESSION_ID_PATTERN} and contain no '..' segments`,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
return { ok: true, sessionId: raw };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function readCwd(toolCtx: unknown, toolName: string): { ok: true; cwd: string } | { ok: false; message: string } {
|
|
93
|
+
if (isRecord(toolCtx) && typeof toolCtx.cwd === "string" && toolCtx.cwd.trim().length > 0) {
|
|
94
|
+
return { ok: true, cwd: toolCtx.cwd };
|
|
95
|
+
}
|
|
96
|
+
return { ok: false, message: `${toolName} requires a tool context cwd` };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function unwrap<T>(result: UltraPlanStorageResult<T>, toolName: string): { ok: true; value: T } | { ok: false; message: string } {
|
|
100
|
+
if (result.ok) return { ok: true, value: result.value };
|
|
101
|
+
const detail = result.error.details && result.error.details.length > 0 ? `: ${result.error.details.join(", ")}` : "";
|
|
102
|
+
return {
|
|
103
|
+
ok: false,
|
|
104
|
+
message: `${toolName} storage error (${result.error.kind}) at ${result.error.path}${detail}: ${result.error.message}`,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// Schema fragments
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
const SESSION_ID_PROP = {
|
|
113
|
+
type: "string",
|
|
114
|
+
description: "Harness session id; assigned by the pipeline runner and passed through the agent prompt.",
|
|
115
|
+
} as const;
|
|
116
|
+
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
// Public registration entry point
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Register every harness pipeline tool. Idempotent at the harness boundary: when
|
|
123
|
+
* `platform.registerTool` is missing (legacy harness), we silently no-op so existing
|
|
124
|
+
* deploys keep booting.
|
|
125
|
+
*/
|
|
126
|
+
export function registerHarnessPipelineTools(platform: Platform): void {
|
|
127
|
+
if (!platform.registerTool) return;
|
|
128
|
+
|
|
129
|
+
// -------- harness_discover_record ----------
|
|
130
|
+
platform.registerTool({
|
|
131
|
+
name: "harness_discover_record",
|
|
132
|
+
label: "Harness Discover Record",
|
|
133
|
+
description: "Record the discover stage artifact for a harness session.",
|
|
134
|
+
parameters: {
|
|
135
|
+
type: "object",
|
|
136
|
+
properties: {
|
|
137
|
+
sessionId: SESSION_ID_PROP,
|
|
138
|
+
artifact: {
|
|
139
|
+
type: "object",
|
|
140
|
+
description: "HarnessDiscoverArtifact JSON. Schema: docs/supipowers/harness.md.",
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
required: ["sessionId", "artifact"],
|
|
144
|
+
},
|
|
145
|
+
async execute(_id: string, params: unknown, _signal: AbortSignal, _onUpdate: unknown, toolCtx: unknown) {
|
|
146
|
+
const cwdR = readCwd(toolCtx, "harness_discover_record");
|
|
147
|
+
if (!cwdR.ok) return toolResult(cwdR);
|
|
148
|
+
const sidR = readSessionId(params, "harness_discover_record");
|
|
149
|
+
if (!sidR.ok) return toolResult(sidR);
|
|
150
|
+
const p = params as Record<string, unknown>;
|
|
151
|
+
if (!isRecord(p.artifact)) {
|
|
152
|
+
return toolResult({ ok: false, message: "harness_discover_record requires an artifact object" });
|
|
153
|
+
}
|
|
154
|
+
const persisted = saveHarnessDiscover(platform.paths, cwdR.cwd, sidR.sessionId, p.artifact as never);
|
|
155
|
+
const result = unwrap(persisted, "harness_discover_record");
|
|
156
|
+
if (!result.ok) return toolResult(result);
|
|
157
|
+
return toolResult({ ok: true, path: result.value });
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// -------- harness_research_record ----------
|
|
162
|
+
platform.registerTool({
|
|
163
|
+
name: "harness_research_record",
|
|
164
|
+
label: "Harness Research Record",
|
|
165
|
+
description: "Record a research topic writeup for a harness session.",
|
|
166
|
+
parameters: {
|
|
167
|
+
type: "object",
|
|
168
|
+
properties: {
|
|
169
|
+
sessionId: SESSION_ID_PROP,
|
|
170
|
+
topicSlug: {
|
|
171
|
+
type: "string",
|
|
172
|
+
description: "Slug for this topic (lowercase letters, digits, hyphen, underscore only).",
|
|
173
|
+
},
|
|
174
|
+
markdown: {
|
|
175
|
+
type: "string",
|
|
176
|
+
description: "Full markdown body. Must include `## Options` and `## Recommendation` headings and ≥2 source URLs.",
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
required: ["sessionId", "topicSlug", "markdown"],
|
|
180
|
+
},
|
|
181
|
+
async execute(_id: string, params: unknown, _signal: AbortSignal, _onUpdate: unknown, toolCtx: unknown) {
|
|
182
|
+
const cwdR = readCwd(toolCtx, "harness_research_record");
|
|
183
|
+
if (!cwdR.ok) return toolResult(cwdR);
|
|
184
|
+
const sidR = readSessionId(params, "harness_research_record");
|
|
185
|
+
if (!sidR.ok) return toolResult(sidR);
|
|
186
|
+
const p = params as Record<string, unknown>;
|
|
187
|
+
const slug = typeof p.topicSlug === "string" ? p.topicSlug : "";
|
|
188
|
+
if (!isSafeTopicSlug(slug)) {
|
|
189
|
+
return toolResult({ ok: false, message: "harness_research_record rejected topicSlug: must be alphanumeric with hyphen/underscore only" });
|
|
190
|
+
}
|
|
191
|
+
const markdown = typeof p.markdown === "string" ? p.markdown : "";
|
|
192
|
+
if (markdown.length === 0) {
|
|
193
|
+
return toolResult({ ok: false, message: "harness_research_record requires a non-empty markdown body" });
|
|
194
|
+
}
|
|
195
|
+
const persisted = saveHarnessResearchTopic(platform.paths, cwdR.cwd, sidR.sessionId, slug, markdown);
|
|
196
|
+
const result = unwrap(persisted, "harness_research_record");
|
|
197
|
+
if (!result.ok) return toolResult(result);
|
|
198
|
+
return toolResult({ ok: true, path: result.value });
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// -------- harness_decision_record ----------
|
|
203
|
+
platform.registerTool({
|
|
204
|
+
name: "harness_decision_record",
|
|
205
|
+
label: "Harness Decision Record",
|
|
206
|
+
description: "Append a single Design-stage decision to decisions.jsonl.",
|
|
207
|
+
parameters: {
|
|
208
|
+
type: "object",
|
|
209
|
+
properties: {
|
|
210
|
+
sessionId: SESSION_ID_PROP,
|
|
211
|
+
area: { type: "string", description: "Short label for the decision area, e.g. 'anti-slop-backend'." },
|
|
212
|
+
question: { type: "string", description: "The exact question that was answered." },
|
|
213
|
+
decision: { type: "string", description: "The locked answer." },
|
|
214
|
+
rationale: { type: "string", description: "Why this decision was chosen." },
|
|
215
|
+
impact: { type: "array", items: { type: "string" }, description: "Modules / files this decision affects." },
|
|
216
|
+
},
|
|
217
|
+
required: ["sessionId", "area", "question", "decision"],
|
|
218
|
+
},
|
|
219
|
+
async execute(_id: string, params: unknown, _signal: AbortSignal, _onUpdate: unknown, toolCtx: unknown) {
|
|
220
|
+
const cwdR = readCwd(toolCtx, "harness_decision_record");
|
|
221
|
+
if (!cwdR.ok) return toolResult(cwdR);
|
|
222
|
+
const sidR = readSessionId(params, "harness_decision_record");
|
|
223
|
+
if (!sidR.ok) return toolResult(sidR);
|
|
224
|
+
const p = params as Record<string, unknown>;
|
|
225
|
+
const area = typeof p.area === "string" ? p.area : "";
|
|
226
|
+
const question = typeof p.question === "string" ? p.question : "";
|
|
227
|
+
const decision = typeof p.decision === "string" ? p.decision : "";
|
|
228
|
+
if (!area || !question || !decision) {
|
|
229
|
+
return toolResult({ ok: false, message: "harness_decision_record requires non-empty area, question, decision" });
|
|
230
|
+
}
|
|
231
|
+
const record: Record<string, unknown> = {
|
|
232
|
+
recordedAt: new Date().toISOString(),
|
|
233
|
+
area,
|
|
234
|
+
question,
|
|
235
|
+
decision,
|
|
236
|
+
...(typeof p.rationale === "string" ? { rationale: p.rationale } : {}),
|
|
237
|
+
...(Array.isArray(p.impact) ? { impact: p.impact } : {}),
|
|
238
|
+
};
|
|
239
|
+
const persisted = appendHarnessDecision(platform.paths, cwdR.cwd, sidR.sessionId, record);
|
|
240
|
+
const r = unwrap(persisted, "harness_decision_record");
|
|
241
|
+
if (!r.ok) return toolResult(r);
|
|
242
|
+
return toolResult({ ok: true, path: r.value });
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// -------- harness_design_spec_persist ----------
|
|
247
|
+
platform.registerTool({
|
|
248
|
+
name: "harness_design_spec_persist",
|
|
249
|
+
label: "Harness Design Spec Persist",
|
|
250
|
+
description: "Persist the rendered design-spec.md for a harness session.",
|
|
251
|
+
parameters: {
|
|
252
|
+
type: "object",
|
|
253
|
+
properties: {
|
|
254
|
+
sessionId: SESSION_ID_PROP,
|
|
255
|
+
markdown: { type: "string", description: "Full markdown body of the design spec." },
|
|
256
|
+
},
|
|
257
|
+
required: ["sessionId", "markdown"],
|
|
258
|
+
},
|
|
259
|
+
async execute(_id: string, params: unknown, _signal: AbortSignal, _onUpdate: unknown, toolCtx: unknown) {
|
|
260
|
+
const cwdR = readCwd(toolCtx, "harness_design_spec_persist");
|
|
261
|
+
if (!cwdR.ok) return toolResult(cwdR);
|
|
262
|
+
const sidR = readSessionId(params, "harness_design_spec_persist");
|
|
263
|
+
if (!sidR.ok) return toolResult(sidR);
|
|
264
|
+
const p = params as Record<string, unknown>;
|
|
265
|
+
const markdown = typeof p.markdown === "string" ? p.markdown : "";
|
|
266
|
+
if (markdown.length === 0) {
|
|
267
|
+
return toolResult({ ok: false, message: "harness_design_spec_persist requires a non-empty markdown body" });
|
|
268
|
+
}
|
|
269
|
+
const persisted = saveHarnessDesignSpec(platform.paths, cwdR.cwd, sidR.sessionId, markdown);
|
|
270
|
+
const r = unwrap(persisted, "harness_design_spec_persist");
|
|
271
|
+
if (!r.ok) return toolResult(r);
|
|
272
|
+
return toolResult({ ok: true, path: r.value });
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// -------- harness_validate_finding ----------
|
|
277
|
+
platform.registerTool({
|
|
278
|
+
name: "harness_validate_finding",
|
|
279
|
+
label: "Harness Validate Finding",
|
|
280
|
+
description: "Append a single Validate-stage finding to the implement log (forensics).",
|
|
281
|
+
parameters: {
|
|
282
|
+
type: "object",
|
|
283
|
+
properties: {
|
|
284
|
+
sessionId: SESSION_ID_PROP,
|
|
285
|
+
finding: {
|
|
286
|
+
type: "object",
|
|
287
|
+
properties: {
|
|
288
|
+
severity: { type: "string", enum: ["error", "warning", "info"] },
|
|
289
|
+
file: { type: "string" },
|
|
290
|
+
line: { type: "integer", minimum: 1 },
|
|
291
|
+
message: { type: "string" },
|
|
292
|
+
remediation: { type: "string" },
|
|
293
|
+
source: { type: "string" },
|
|
294
|
+
},
|
|
295
|
+
required: ["severity", "file", "message", "remediation", "source"],
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
required: ["sessionId", "finding"],
|
|
299
|
+
},
|
|
300
|
+
async execute(_id: string, params: unknown, _signal: AbortSignal, _onUpdate: unknown, toolCtx: unknown) {
|
|
301
|
+
const cwdR = readCwd(toolCtx, "harness_validate_finding");
|
|
302
|
+
if (!cwdR.ok) return toolResult(cwdR);
|
|
303
|
+
const sidR = readSessionId(params, "harness_validate_finding");
|
|
304
|
+
if (!sidR.ok) return toolResult(sidR);
|
|
305
|
+
const p = params as Record<string, unknown>;
|
|
306
|
+
if (!isRecord(p.finding)) {
|
|
307
|
+
return toolResult({ ok: false, message: "harness_validate_finding requires a finding object" });
|
|
308
|
+
}
|
|
309
|
+
const finding = p.finding as unknown as HarnessValidateFinding;
|
|
310
|
+
const persisted = appendImplementLog(platform.paths, cwdR.cwd, sidR.sessionId, {
|
|
311
|
+
kind: "validate-finding",
|
|
312
|
+
recordedAt: new Date().toISOString(),
|
|
313
|
+
finding,
|
|
314
|
+
});
|
|
315
|
+
const r = unwrap(persisted, "harness_validate_finding");
|
|
316
|
+
if (!r.ok) return toolResult(r);
|
|
317
|
+
return toolResult({ ok: true, path: r.value });
|
|
318
|
+
},
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// -------- harness_slop_queue_append ----------
|
|
322
|
+
platform.registerTool({
|
|
323
|
+
name: "harness_slop_queue_append",
|
|
324
|
+
label: "Harness Slop Queue Append",
|
|
325
|
+
description: "Append a slop violation to the project's persistent queue.",
|
|
326
|
+
parameters: {
|
|
327
|
+
type: "object",
|
|
328
|
+
properties: {
|
|
329
|
+
kind: { type: "string", enum: ["duplicate", "dead-code", "layer-violation", "naming", "file-too-large", "complexity", "circular-dependency", "other"] },
|
|
330
|
+
file: { type: "string" },
|
|
331
|
+
startLine: { type: "integer", minimum: 1 },
|
|
332
|
+
endLine: { type: "integer", minimum: 1 },
|
|
333
|
+
severity: { type: "string", enum: ["blocker", "warning", "info"] },
|
|
334
|
+
source: { type: "string", enum: ["fallow", "desloppify", "checks", "review", "supi-native"] },
|
|
335
|
+
message: { type: "string" },
|
|
336
|
+
remediation: { type: "string" },
|
|
337
|
+
ruleHint: { type: "string", description: "Optional rule slug from the source backend; combined into the queue id hash." },
|
|
338
|
+
},
|
|
339
|
+
required: ["kind", "file", "severity", "source", "message"],
|
|
340
|
+
},
|
|
341
|
+
async execute(_id: string, params: unknown, _signal: AbortSignal, _onUpdate: unknown, toolCtx: unknown) {
|
|
342
|
+
const cwdR = readCwd(toolCtx, "harness_slop_queue_append");
|
|
343
|
+
if (!cwdR.ok) return toolResult(cwdR);
|
|
344
|
+
if (!isRecord(params)) {
|
|
345
|
+
return toolResult({ ok: false, message: "harness_slop_queue_append requires an object payload" });
|
|
346
|
+
}
|
|
347
|
+
const p = params as Record<string, unknown>;
|
|
348
|
+
const range =
|
|
349
|
+
typeof p.startLine === "number"
|
|
350
|
+
? { startLine: p.startLine, endLine: typeof p.endLine === "number" ? p.endLine : p.startLine }
|
|
351
|
+
: null;
|
|
352
|
+
const id = computeQueueEntryId({
|
|
353
|
+
kind: p.kind as HarnessSlopQueueEntry["kind"],
|
|
354
|
+
file: typeof p.file === "string" ? p.file : "",
|
|
355
|
+
range,
|
|
356
|
+
ruleHint: typeof p.ruleHint === "string" ? p.ruleHint : undefined,
|
|
357
|
+
});
|
|
358
|
+
const entry: HarnessSlopQueueEntry = {
|
|
359
|
+
id,
|
|
360
|
+
kind: p.kind as HarnessSlopQueueEntry["kind"],
|
|
361
|
+
file: typeof p.file === "string" ? p.file : "",
|
|
362
|
+
range,
|
|
363
|
+
severity: p.severity as HarnessSlopQueueEntry["severity"],
|
|
364
|
+
source: p.source as HarnessSlopQueueEntry["source"],
|
|
365
|
+
state: "open",
|
|
366
|
+
message: typeof p.message === "string" ? p.message : "",
|
|
367
|
+
remediation: typeof p.remediation === "string" ? p.remediation : undefined,
|
|
368
|
+
ts: new Date().toISOString(),
|
|
369
|
+
};
|
|
370
|
+
const persisted = appendQueueEntry(platform.paths, cwdR.cwd, entry);
|
|
371
|
+
const r = unwrap(persisted, "harness_slop_queue_append");
|
|
372
|
+
if (!r.ok) return toolResult(r);
|
|
373
|
+
return toolResult({ ok: true, id, path: r.value });
|
|
374
|
+
},
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
// -------- harness_slop_queue_resolve ----------
|
|
378
|
+
platform.registerTool({
|
|
379
|
+
name: "harness_slop_queue_resolve",
|
|
380
|
+
label: "Harness Slop Queue Resolve",
|
|
381
|
+
description: "Mark a queue entry as resolved or wontfix.",
|
|
382
|
+
parameters: {
|
|
383
|
+
type: "object",
|
|
384
|
+
properties: {
|
|
385
|
+
id: { type: "string", description: "The queue entry id." },
|
|
386
|
+
state: { type: "string", enum: ["resolved", "wontfix"] },
|
|
387
|
+
},
|
|
388
|
+
required: ["id", "state"],
|
|
389
|
+
},
|
|
390
|
+
async execute(_id: string, params: unknown, _signal: AbortSignal, _onUpdate: unknown, toolCtx: unknown) {
|
|
391
|
+
const cwdR = readCwd(toolCtx, "harness_slop_queue_resolve");
|
|
392
|
+
if (!cwdR.ok) return toolResult(cwdR);
|
|
393
|
+
if (!isRecord(params)) {
|
|
394
|
+
return toolResult({ ok: false, message: "harness_slop_queue_resolve requires an object payload" });
|
|
395
|
+
}
|
|
396
|
+
const p = params as Record<string, unknown>;
|
|
397
|
+
const id = typeof p.id === "string" ? p.id : "";
|
|
398
|
+
const state = typeof p.state === "string" ? p.state : "";
|
|
399
|
+
if (!id || (state !== "resolved" && state !== "wontfix")) {
|
|
400
|
+
return toolResult({ ok: false, message: "harness_slop_queue_resolve requires id and state ∈ {resolved, wontfix}" });
|
|
401
|
+
}
|
|
402
|
+
const result = state === "resolved"
|
|
403
|
+
? resolveQueueEntry(platform.paths, cwdR.cwd, id)
|
|
404
|
+
: markQueueWontfix(platform.paths, cwdR.cwd, id);
|
|
405
|
+
const r = unwrap(result, "harness_slop_queue_resolve");
|
|
406
|
+
if (!r.ok) return toolResult(r);
|
|
407
|
+
if (r.value === null) {
|
|
408
|
+
return toolResult({ ok: false, message: `harness_slop_queue_resolve: id ${id} not found` });
|
|
409
|
+
}
|
|
410
|
+
return toolResult({ ok: true, id, details: { state } });
|
|
411
|
+
},
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** Names exposed for hook-bridge correlation. */
|
|
416
|
+
export const HARNESS_PIPELINE_TOOL_NAMES = [
|
|
417
|
+
"harness_discover_record",
|
|
418
|
+
"harness_research_record",
|
|
419
|
+
"harness_decision_record",
|
|
420
|
+
"harness_design_spec_persist",
|
|
421
|
+
"harness_validate_finding",
|
|
422
|
+
"harness_slop_queue_append",
|
|
423
|
+
"harness_slop_queue_resolve",
|
|
424
|
+
] as const;
|
|
425
|
+
|
|
426
|
+
export type HarnessPipelineToolName = (typeof HARNESS_PIPELINE_TOOL_NAMES)[number];
|
package/src/lsp/bridge.ts
CHANGED
|
@@ -1,93 +1,25 @@
|
|
|
1
1
|
// src/lsp/bridge.ts
|
|
2
2
|
import type { GateExecutionContext, GateIssue } from "../types.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function isDiagnostic(value: unknown): value is Diagnostic {
|
|
19
|
-
return (
|
|
20
|
-
typeof value === "object" &&
|
|
21
|
-
value !== null &&
|
|
22
|
-
["error", "warning", "info", "hint"].includes((value as Diagnostic).severity) &&
|
|
23
|
-
typeof (value as Diagnostic).message === "string" &&
|
|
24
|
-
typeof (value as Diagnostic).line === "number" &&
|
|
25
|
-
typeof (value as Diagnostic).column === "number"
|
|
26
|
-
);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function isDiagnosticsResult(value: unknown): value is DiagnosticsResult {
|
|
30
|
-
return (
|
|
31
|
-
typeof value === "object" &&
|
|
32
|
-
value !== null &&
|
|
33
|
-
typeof (value as DiagnosticsResult).file === "string" &&
|
|
34
|
-
Array.isArray((value as DiagnosticsResult).diagnostics) &&
|
|
35
|
-
(value as DiagnosticsResult).diagnostics.every((diagnostic) => isDiagnostic(diagnostic))
|
|
36
|
-
);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function normalizeDiagnosticSeverity(severity: Diagnostic["severity"]): GateIssue["severity"] {
|
|
3
|
+
import { parseStructuredOutput, runWithOutputValidation, type ReliabilityReporter } from "../ai/structured-output.js";
|
|
4
|
+
import { renderSchemaText } from "../ai/schema-text.js";
|
|
5
|
+
import { appendReliabilityRecord } from "../storage/reliability-metrics.js";
|
|
6
|
+
import { probeLspCapabilities } from "./capabilities.js";
|
|
7
|
+
import {
|
|
8
|
+
LspDiagnosticsResultsSchema,
|
|
9
|
+
type LspDiagnostic,
|
|
10
|
+
type LspDiagnosticsResults,
|
|
11
|
+
} from "./contracts.js";
|
|
12
|
+
|
|
13
|
+
const LSP_DIAGNOSTICS_SCHEMA_TEXT = renderSchemaText(LspDiagnosticsResultsSchema);
|
|
14
|
+
|
|
15
|
+
function normalizeDiagnosticSeverity(severity: LspDiagnostic["severity"]): GateIssue["severity"] {
|
|
40
16
|
return severity === "hint" ? "info" : severity;
|
|
41
17
|
}
|
|
42
18
|
|
|
43
|
-
function formatDiagnosticDetail(diagnostic:
|
|
19
|
+
function formatDiagnosticDetail(diagnostic: LspDiagnostic): string | undefined {
|
|
44
20
|
return diagnostic.column > 0 ? `column ${diagnostic.column}` : undefined;
|
|
45
21
|
}
|
|
46
22
|
|
|
47
|
-
function extractJsonArray(raw: string): unknown | null {
|
|
48
|
-
const trimmed = raw.trim();
|
|
49
|
-
|
|
50
|
-
// 1. Try direct parse (no fence)
|
|
51
|
-
try {
|
|
52
|
-
const parsed = JSON.parse(trimmed);
|
|
53
|
-
if (Array.isArray(parsed)) return parsed;
|
|
54
|
-
} catch {}
|
|
55
|
-
|
|
56
|
-
// 2. Strip markdown code fence and retry
|
|
57
|
-
const stripped = stripMarkdownCodeFence(trimmed);
|
|
58
|
-
if (stripped !== trimmed) {
|
|
59
|
-
try {
|
|
60
|
-
const parsed = JSON.parse(stripped);
|
|
61
|
-
if (Array.isArray(parsed)) return parsed;
|
|
62
|
-
} catch {}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// 3. Extract first JSON array from prose via bracket matching
|
|
66
|
-
const start = trimmed.indexOf("[");
|
|
67
|
-
if (start === -1) return null;
|
|
68
|
-
|
|
69
|
-
let depth = 0;
|
|
70
|
-
for (let i = start; i < trimmed.length; i++) {
|
|
71
|
-
if (trimmed[i] === "[") depth++;
|
|
72
|
-
else if (trimmed[i] === "]") depth--;
|
|
73
|
-
if (depth === 0) {
|
|
74
|
-
try {
|
|
75
|
-
const parsed = JSON.parse(trimmed.slice(start, i + 1));
|
|
76
|
-
if (Array.isArray(parsed)) return parsed;
|
|
77
|
-
} catch {}
|
|
78
|
-
break;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return null;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function parseLspDiagnosticsResults(raw: string): DiagnosticsResult[] | null {
|
|
86
|
-
const parsed = extractJsonArray(raw);
|
|
87
|
-
if (!Array.isArray(parsed)) return null;
|
|
88
|
-
return parsed.every((entry) => isDiagnosticsResult(entry)) ? parsed : null;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
23
|
/**
|
|
92
24
|
* Request LSP diagnostics via a headless agent session that can call the lsp tool.
|
|
93
25
|
*/
|
|
@@ -112,8 +44,8 @@ export function buildLspDiagnosticsPrompt(
|
|
|
112
44
|
"You are collecting structured LSP diagnostics for a review quality gate.",
|
|
113
45
|
...scopeInstruction,
|
|
114
46
|
"",
|
|
115
|
-
"Return JSON only
|
|
116
|
-
|
|
47
|
+
"Return JSON only matching this schema:",
|
|
48
|
+
LSP_DIAGNOSTICS_SCHEMA_TEXT,
|
|
117
49
|
"",
|
|
118
50
|
"Rules:",
|
|
119
51
|
"- Do not wrap the JSON in markdown fences.",
|
|
@@ -128,29 +60,58 @@ export async function collectLspDiagnostics(options: {
|
|
|
128
60
|
fileScope: GateExecutionContext["fileScope"];
|
|
129
61
|
createAgentSession: GateExecutionContext["createAgentSession"];
|
|
130
62
|
reviewModel?: GateExecutionContext["reviewModel"];
|
|
63
|
+
reliability?: ReliabilityReporter;
|
|
131
64
|
}): Promise<GateIssue[]> {
|
|
132
|
-
const
|
|
65
|
+
const caps = await probeLspCapabilities({
|
|
66
|
+
cwd: options.cwd,
|
|
67
|
+
createAgentSession: options.createAgentSession,
|
|
68
|
+
reviewModel: options.reviewModel,
|
|
69
|
+
reliability: options.reliability,
|
|
70
|
+
});
|
|
71
|
+
if (!caps.diagnostics) {
|
|
72
|
+
// Active LSP server is registered but advertises no
|
|
73
|
+
// textDocument/diagnostic support — fail the gate cleanly with an
|
|
74
|
+
// empty issue list rather than throwing on a vacuous probe error.
|
|
75
|
+
// Emit a single reliability record so /supi:doctor surfaces the skip.
|
|
76
|
+
if (options.reliability) {
|
|
77
|
+
try {
|
|
78
|
+
appendReliabilityRecord(options.reliability.paths, options.reliability.cwd, {
|
|
79
|
+
ts: new Date().toISOString(),
|
|
80
|
+
command: options.reliability.command,
|
|
81
|
+
operation: options.reliability.operation,
|
|
82
|
+
outcome: "fallback",
|
|
83
|
+
attempts: 0,
|
|
84
|
+
reason: "lsp-server-lacks-diagnostics",
|
|
85
|
+
cwd: options.reliability.cwd,
|
|
86
|
+
});
|
|
87
|
+
} catch {
|
|
88
|
+
// appendReliabilityRecord already swallows its own errors; this
|
|
89
|
+
// try/catch is belt-and-braces in case the helper itself throws.
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const result = await runWithOutputValidation<LspDiagnosticsResults>(options.createAgentSession, {
|
|
133
96
|
cwd: options.cwd,
|
|
134
97
|
prompt: buildLspDiagnosticsPrompt(options.scopeFiles, options.fileScope),
|
|
98
|
+
schema: LSP_DIAGNOSTICS_SCHEMA_TEXT,
|
|
99
|
+
parse: (raw) => parseStructuredOutput<LspDiagnosticsResults>(raw, LspDiagnosticsResultsSchema),
|
|
135
100
|
model: options.reviewModel?.model,
|
|
136
101
|
thinkingLevel: options.reviewModel?.thinkingLevel ?? null,
|
|
137
102
|
timeoutMs: 120_000,
|
|
103
|
+
reliability: options.reliability,
|
|
138
104
|
});
|
|
139
105
|
|
|
140
|
-
if (
|
|
141
|
-
throw new Error(
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const parsed = parseLspDiagnosticsResults(sessionResult.finalText);
|
|
145
|
-
if (!parsed) {
|
|
146
|
-
throw new Error("LSP diagnostics integration returned invalid JSON.");
|
|
106
|
+
if (result.status === "blocked") {
|
|
107
|
+
throw new Error(`LSP diagnostics integration failed: ${result.error}`);
|
|
147
108
|
}
|
|
148
109
|
|
|
149
|
-
return
|
|
150
|
-
|
|
110
|
+
return result.output.flatMap((entry) =>
|
|
111
|
+
entry.diagnostics.map<GateIssue>((diagnostic) => ({
|
|
151
112
|
severity: normalizeDiagnosticSeverity(diagnostic.severity),
|
|
152
113
|
message: diagnostic.message,
|
|
153
|
-
file:
|
|
114
|
+
file: entry.file,
|
|
154
115
|
line: diagnostic.line,
|
|
155
116
|
detail: formatDiagnosticDetail(diagnostic),
|
|
156
117
|
})),
|