supipowers 1.5.3 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +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 +131 -42
- 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 +18 -8
- 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 +135 -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 +268 -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 +221 -109
- package/src/review/fixer.ts +10 -6
- package/src/review/multi-agent-runner.ts +114 -13
- 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 +1 -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 +1399 -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,108 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { fetchPrComments, parsePrCommentsJsonl } from "../fetch-comments.js";
|
|
5
|
+
import type { PrComment } from "../types.js";
|
|
6
|
+
import { createCliPlatformExec } from "./exec.js";
|
|
7
|
+
|
|
8
|
+
export interface WaitAndCheckSummary {
|
|
9
|
+
hasNewComments: boolean;
|
|
10
|
+
count: number;
|
|
11
|
+
iteration: number;
|
|
12
|
+
error?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function fingerprint(comment: PrComment): string {
|
|
16
|
+
return `${comment.id}\t${comment.updatedAt ?? ""}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function diffComments(previous: readonly PrComment[], current: readonly PrComment[]): PrComment[] {
|
|
20
|
+
if (previous.length === 0) {
|
|
21
|
+
return [...current];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const previousFingerprints = new Set(previous.map(fingerprint));
|
|
25
|
+
return current.filter((comment) => !previousFingerprints.has(fingerprint(comment)));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function sleep(ms: number): Promise<void> {
|
|
29
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function waitAndCheck(
|
|
33
|
+
sessionDir: string,
|
|
34
|
+
delaySeconds: number,
|
|
35
|
+
iteration: number,
|
|
36
|
+
repo: string,
|
|
37
|
+
prNumber: number,
|
|
38
|
+
): Promise<{ exitCode: number; output: string }> {
|
|
39
|
+
const snapshotsDir = path.join(sessionDir, "snapshots");
|
|
40
|
+
const previousSnapshotPath = path.join(snapshotsDir, `comments-${iteration - 1}.jsonl`);
|
|
41
|
+
const newSnapshotPath = path.join(snapshotsDir, `comments-${iteration}.jsonl`);
|
|
42
|
+
|
|
43
|
+
await sleep(delaySeconds * 1_000);
|
|
44
|
+
|
|
45
|
+
const fetchError = await fetchPrComments(
|
|
46
|
+
createCliPlatformExec() as any,
|
|
47
|
+
repo,
|
|
48
|
+
prNumber,
|
|
49
|
+
newSnapshotPath,
|
|
50
|
+
process.cwd(),
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
if (fetchError) {
|
|
54
|
+
const summary: WaitAndCheckSummary = {
|
|
55
|
+
hasNewComments: false,
|
|
56
|
+
count: 0,
|
|
57
|
+
iteration,
|
|
58
|
+
error: fetchError,
|
|
59
|
+
};
|
|
60
|
+
return { exitCode: 1, output: JSON.stringify(summary) };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const previousComments = fs.existsSync(previousSnapshotPath)
|
|
64
|
+
? parsePrCommentsJsonl(fs.readFileSync(previousSnapshotPath, "utf8"))
|
|
65
|
+
: [];
|
|
66
|
+
const currentComments = fs.existsSync(newSnapshotPath)
|
|
67
|
+
? parsePrCommentsJsonl(fs.readFileSync(newSnapshotPath, "utf8"))
|
|
68
|
+
: [];
|
|
69
|
+
const changedComments = diffComments(previousComments, currentComments);
|
|
70
|
+
const summary: WaitAndCheckSummary = {
|
|
71
|
+
hasNewComments: changedComments.length > 0,
|
|
72
|
+
count: changedComments.length,
|
|
73
|
+
iteration,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const lines = changedComments.map((comment) => JSON.stringify(comment));
|
|
77
|
+
lines.push(JSON.stringify(summary));
|
|
78
|
+
return {
|
|
79
|
+
exitCode: 0,
|
|
80
|
+
output: lines.join("\n"),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function main(): Promise<void> {
|
|
85
|
+
const [sessionDir, delayArg, iterationArg, repo, prNumberArg] = process.argv.slice(2);
|
|
86
|
+
const delaySeconds = Number.parseInt(delayArg ?? "", 10);
|
|
87
|
+
const iteration = Number.parseInt(iterationArg ?? "", 10);
|
|
88
|
+
const prNumber = Number.parseInt(prNumberArg ?? "", 10);
|
|
89
|
+
|
|
90
|
+
if (!sessionDir || !Number.isInteger(delaySeconds) || !Number.isInteger(iteration) || !repo || !Number.isInteger(prNumber)) {
|
|
91
|
+
console.log(JSON.stringify({
|
|
92
|
+
hasNewComments: false,
|
|
93
|
+
count: 0,
|
|
94
|
+
iteration: Number.isInteger(iteration) ? iteration : 0,
|
|
95
|
+
error: "Usage: wait-and-check.ts <session_dir> <delay_seconds> <iteration> <owner/repo> <pr_number>",
|
|
96
|
+
}));
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const result = await waitAndCheck(sessionDir, delaySeconds, iteration, repo, prNumber);
|
|
101
|
+
console.log(result.output);
|
|
102
|
+
process.exit(result.exitCode);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
106
|
+
if (isMain) {
|
|
107
|
+
void main();
|
|
108
|
+
}
|
package/src/fix-pr/types.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { FixPrAssessmentBatch } from "./contracts.js";
|
|
2
|
+
|
|
1
3
|
/** Supported automated PR reviewers */
|
|
2
4
|
export type ReviewerType = "coderabbit" | "copilot" | "gemini" | "none";
|
|
3
5
|
|
|
@@ -58,4 +60,6 @@ export interface FixPrSessionLedger {
|
|
|
58
60
|
iteration: number;
|
|
59
61
|
config: FixPrConfig;
|
|
60
62
|
commentsProcessed: number[];
|
|
63
|
+
/** Validated per-comment assessment artifact. Absent in older sessions. */
|
|
64
|
+
assessment?: FixPrAssessmentBatch;
|
|
61
65
|
}
|
package/src/git/branch-finish.ts
CHANGED
|
@@ -71,6 +71,11 @@ export function buildBranchFinishPrompt(
|
|
|
71
71
|
"",
|
|
72
72
|
"```bash",
|
|
73
73
|
`git push -u origin ${branchName}`,
|
|
74
|
+
"```",
|
|
75
|
+
"",
|
|
76
|
+
"Then open the PR. **Preferred:** call the `github` tool with `op: \"pr_create\"` — supply `title` and `body` (or `fill: true` to auto-fill from commits); it returns the new PR URL. **Fallback** if the tool is unavailable in your runtime:",
|
|
77
|
+
"",
|
|
78
|
+
"```bash",
|
|
74
79
|
`gh pr create --title "<title>" --body "<summary>"`,
|
|
75
80
|
"```",
|
|
76
81
|
"",
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// src/git/commit-contract.ts
|
|
2
|
+
//
|
|
3
|
+
// Schema-backed contract for AI-generated commit plans. The AI must return a
|
|
4
|
+
// CommitPlan matching this schema; parseStructuredOutput enforces the structure
|
|
5
|
+
// and runWithOutputValidation retries with schema feedback on drift. Coverage
|
|
6
|
+
// (every staged file appears in exactly one commit) is a runtime rule that
|
|
7
|
+
// can't live in the schema — see validateCommitPlanCoverage.
|
|
8
|
+
|
|
9
|
+
import { type Static, Type } from "@sinclair/typebox";
|
|
10
|
+
import { VALID_COMMIT_TYPES } from "../release/commit-types.js";
|
|
11
|
+
import type { ValidationError } from "../types.js";
|
|
12
|
+
|
|
13
|
+
export const CommitGroupSchema = Type.Object(
|
|
14
|
+
{
|
|
15
|
+
type: Type.Union(VALID_COMMIT_TYPES.map((value) => Type.Literal(value))),
|
|
16
|
+
scope: Type.Union([Type.String(), Type.Null()]),
|
|
17
|
+
summary: Type.String({ minLength: 1 }),
|
|
18
|
+
details: Type.Array(Type.String()),
|
|
19
|
+
files: Type.Array(Type.String({ minLength: 1 }), { minItems: 1 }),
|
|
20
|
+
},
|
|
21
|
+
{ additionalProperties: false },
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
export const CommitPlanSchema = Type.Object(
|
|
25
|
+
{
|
|
26
|
+
commits: Type.Array(CommitGroupSchema, { minItems: 1 }),
|
|
27
|
+
},
|
|
28
|
+
{ additionalProperties: false },
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
export type CommitGroup = Static<typeof CommitGroupSchema>;
|
|
32
|
+
export type CommitPlan = Static<typeof CommitPlanSchema>;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Verify every staged file appears in exactly one commit and that no commit
|
|
36
|
+
* references a file outside the staged set. Returns an empty array on success;
|
|
37
|
+
* a non-empty array indicates the plan is unusable and the caller must block.
|
|
38
|
+
*/
|
|
39
|
+
export function validateCommitPlanCoverage(
|
|
40
|
+
plan: CommitPlan,
|
|
41
|
+
stagedFiles: string[],
|
|
42
|
+
): ValidationError[] {
|
|
43
|
+
const errors: ValidationError[] = [];
|
|
44
|
+
const stagedSet = new Set(stagedFiles);
|
|
45
|
+
const occurrences = new Map<string, number[]>();
|
|
46
|
+
|
|
47
|
+
plan.commits.forEach((commit, commitIdx) => {
|
|
48
|
+
commit.files.forEach((file, fileIdx) => {
|
|
49
|
+
if (!stagedSet.has(file)) {
|
|
50
|
+
errors.push({
|
|
51
|
+
path: `commits[${commitIdx}].files[${fileIdx}]`,
|
|
52
|
+
message: `File is not in the staged set: ${file}`,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
const existing = occurrences.get(file);
|
|
56
|
+
if (existing) {
|
|
57
|
+
existing.push(commitIdx);
|
|
58
|
+
} else {
|
|
59
|
+
occurrences.set(file, [commitIdx]);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
for (const [file, commitIdxs] of occurrences) {
|
|
65
|
+
if (commitIdxs.length > 1) {
|
|
66
|
+
errors.push({
|
|
67
|
+
path: "commits",
|
|
68
|
+
message: `File appears in multiple commits (indices ${commitIdxs.join(", ")}): ${file}`,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
for (const file of stagedFiles) {
|
|
74
|
+
if (!occurrences.has(file)) {
|
|
75
|
+
errors.push({
|
|
76
|
+
path: "commits",
|
|
77
|
+
message: `Staged file not covered by any commit: ${file}`,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return errors;
|
|
83
|
+
}
|
package/src/git/commit.ts
CHANGED
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
// a conventional-commit plan (optionally split by file), presents
|
|
5
5
|
// the plan for user approval, then executes file-level staging + commit.
|
|
6
6
|
|
|
7
|
-
import type { Platform } from "../platform/types.js";
|
|
7
|
+
import type { Platform, PlatformPaths } from "../platform/types.js";
|
|
8
|
+
import { appendReliabilityRecord } from "../storage/reliability-metrics.js";
|
|
8
9
|
import { createWorkflowProgress } from "../platform/progress.js";
|
|
9
10
|
import { VALID_COMMIT_TYPES } from "../release/commit-types.js";
|
|
11
|
+
import { resolveRepoRoot } from "../workspace/repo-root.js";
|
|
10
12
|
import { validateCommitMessage } from "./commit-msg.js";
|
|
11
13
|
import { getWorkingTreeStatus } from "./status.js";
|
|
12
14
|
import { discoverCommitConventions } from "./conventions.js";
|
|
@@ -18,17 +20,13 @@ import { loadModelConfig } from "../config/model-config.js";
|
|
|
18
20
|
|
|
19
21
|
// ── Public types ───────────────────────────────────────────
|
|
20
22
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
summary: string;
|
|
25
|
-
details: string[];
|
|
26
|
-
files: string[];
|
|
27
|
-
}
|
|
23
|
+
import { CommitPlanSchema, validateCommitPlanCoverage, type CommitGroup, type CommitPlan } from "./commit-contract.js";
|
|
24
|
+
import { parseStructuredOutput, runWithOutputValidation, formatValidationErrors } from "../ai/structured-output.js";
|
|
25
|
+
import { renderSchemaText } from "../ai/schema-text.js";
|
|
28
26
|
|
|
29
|
-
export
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
export type { CommitGroup, CommitPlan };
|
|
28
|
+
|
|
29
|
+
const COMMIT_PLAN_SCHEMA_TEXT = renderSchemaText(CommitPlanSchema);
|
|
32
30
|
|
|
33
31
|
export interface CommitResult {
|
|
34
32
|
committed: number;
|
|
@@ -135,6 +133,49 @@ function createProgress(ctx: any) {
|
|
|
135
133
|
};
|
|
136
134
|
}
|
|
137
135
|
|
|
136
|
+
// ── Staging context ───────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
interface CommitStagingContext {
|
|
139
|
+
stagedFiles: string[];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function ensureStagedChanges(
|
|
143
|
+
platform: Platform,
|
|
144
|
+
ctx: any,
|
|
145
|
+
cwd: string,
|
|
146
|
+
status: Awaited<ReturnType<typeof getWorkingTreeStatus>>,
|
|
147
|
+
progress: ReturnType<typeof createProgress>,
|
|
148
|
+
): Promise<CommitStagingContext | null> {
|
|
149
|
+
const exec = platform.exec.bind(platform);
|
|
150
|
+
|
|
151
|
+
if (status.stagedFiles.length === 0) {
|
|
152
|
+
progress.activate(1, `${status.files.length} file(s)`);
|
|
153
|
+
const addResult = await exec("git", ["add", "-A"], { cwd });
|
|
154
|
+
if (addResult.code !== 0) {
|
|
155
|
+
notifyError(ctx, "git add failed", addResult.stderr || "Non-zero exit");
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
progress.complete(1, `${status.files.length} file(s)`);
|
|
159
|
+
} else {
|
|
160
|
+
progress.activate(1, `${status.stagedFiles.length} staged`);
|
|
161
|
+
progress.complete(1, `${status.stagedFiles.length} staged`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const stagedFilesResult = await exec("git", ["diff", "--cached", "--name-only"], { cwd });
|
|
165
|
+
if (stagedFilesResult.code !== 0) {
|
|
166
|
+
notifyError(ctx, "git diff failed", stagedFilesResult.stderr || "Could not read staged files");
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const stagedFiles = normalizeLineEndings(stagedFilesResult.stdout).trim().split("\n").filter(Boolean);
|
|
171
|
+
if (stagedFiles.length === 0) {
|
|
172
|
+
notifyInfo(ctx, "Nothing to commit", "No changes after staging");
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return { stagedFiles };
|
|
177
|
+
}
|
|
178
|
+
|
|
138
179
|
// ── Main entry point ───────────────────────────────────────
|
|
139
180
|
|
|
140
181
|
/**
|
|
@@ -148,7 +189,7 @@ export async function analyzeAndCommit(
|
|
|
148
189
|
options: CommitOptions = {},
|
|
149
190
|
): Promise<CommitResult | null> {
|
|
150
191
|
const exec = platform.exec.bind(platform);
|
|
151
|
-
const cwd
|
|
192
|
+
const cwd = await resolveRepoRoot(platform, ctx.cwd);
|
|
152
193
|
const progress = createProgress(ctx);
|
|
153
194
|
|
|
154
195
|
try {
|
|
@@ -163,32 +204,24 @@ export async function analyzeAndCommit(
|
|
|
163
204
|
}
|
|
164
205
|
progress.complete(0, `${status.files.length} file(s)`);
|
|
165
206
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
207
|
+
const stagingContext = await ensureStagedChanges(
|
|
208
|
+
platform,
|
|
209
|
+
ctx,
|
|
210
|
+
cwd,
|
|
211
|
+
status,
|
|
212
|
+
progress,
|
|
213
|
+
);
|
|
214
|
+
if (!stagingContext) {
|
|
172
215
|
return null;
|
|
173
216
|
}
|
|
174
|
-
progress.complete(1, `${status.files.length} file(s)`);
|
|
175
217
|
|
|
176
218
|
// 3. Gather diff information
|
|
177
|
-
|
|
178
|
-
|
|
219
|
+
const fileList = stagingContext.stagedFiles;
|
|
220
|
+
progress.activate(2, `${fileList.length} file(s)`);
|
|
221
|
+
const [diffResult, statResult] = await Promise.all([
|
|
179
222
|
exec("git", ["diff", "--cached"], { cwd }),
|
|
180
223
|
exec("git", ["diff", "--cached", "--stat"], { cwd }),
|
|
181
|
-
exec("git", ["diff", "--cached", "--name-only"], { cwd }),
|
|
182
224
|
]);
|
|
183
|
-
|
|
184
|
-
const fileList = normalizeLineEndings(filesResult.stdout).trim().split("\n").filter(Boolean);
|
|
185
|
-
if (fileList.length === 0) {
|
|
186
|
-
progress.complete(2, "empty");
|
|
187
|
-
progress.dispose();
|
|
188
|
-
notifyInfo(ctx, "Nothing to commit", "No changes after staging");
|
|
189
|
-
await exec("git", ["reset", "HEAD"], { cwd });
|
|
190
|
-
return null;
|
|
191
|
-
}
|
|
192
225
|
progress.complete(2, `${fileList.length} file(s)`);
|
|
193
226
|
|
|
194
227
|
// 4. Discover conventions
|
|
@@ -207,6 +240,7 @@ export async function analyzeAndCommit(
|
|
|
207
240
|
|
|
208
241
|
let plan: CommitPlan | null = null;
|
|
209
242
|
let agentReason: string | undefined;
|
|
243
|
+
let agentAttempts = 0;
|
|
210
244
|
|
|
211
245
|
if (platform.capabilities.agentSessions) {
|
|
212
246
|
progress.activate(4, `${fileList.length} file(s)`);
|
|
@@ -223,20 +257,21 @@ export async function analyzeAndCommit(
|
|
|
223
257
|
"harness role";
|
|
224
258
|
let detail = sourceLabel;
|
|
225
259
|
if (candidate.thinkingLevel) {
|
|
226
|
-
detail += `
|
|
260
|
+
detail += ` · ${candidate.thinkingLevel} thinking`;
|
|
227
261
|
}
|
|
228
262
|
ctx.ui?.setStatus?.("supi-model", `Model: ${candidate.model} (${detail})`);
|
|
229
263
|
}
|
|
230
264
|
|
|
231
|
-
const agentResult = await tryAgentPlan(platform, cwd, prompt, candidate.model);
|
|
265
|
+
const agentResult = await tryAgentPlan(platform, cwd, prompt, fileList, candidate.model);
|
|
232
266
|
if (agentResult.plan) {
|
|
233
|
-
plan =
|
|
267
|
+
plan = agentResult.plan;
|
|
234
268
|
progress.complete(4, `${plan.commits.length} commit(s)`);
|
|
235
269
|
break;
|
|
236
270
|
}
|
|
237
271
|
|
|
238
272
|
// Store last failure reason; try next candidate
|
|
239
273
|
agentReason = agentResult.reason;
|
|
274
|
+
agentAttempts = agentResult.attempts;
|
|
240
275
|
}
|
|
241
276
|
|
|
242
277
|
if (!plan) {
|
|
@@ -254,7 +289,7 @@ export async function analyzeAndCommit(
|
|
|
254
289
|
const reason = !platform.capabilities.agentSessions
|
|
255
290
|
? "no agent sessions"
|
|
256
291
|
: agentReason;
|
|
257
|
-
return manualFallback(platform, ctx, cwd, fileList, reason);
|
|
292
|
+
return manualFallback(platform, ctx, cwd, fileList, platform.paths, agentAttempts, reason);
|
|
258
293
|
}
|
|
259
294
|
|
|
260
295
|
// 6. Present plan for approval
|
|
@@ -262,11 +297,11 @@ export async function analyzeAndCommit(
|
|
|
262
297
|
const planDisplay = formatPlanForDisplay(plan);
|
|
263
298
|
notifyInfo(ctx, "Commit plan ready", "\n" + planDisplay);
|
|
264
299
|
const commitLabel = plan.commits.length === 1
|
|
265
|
-
? `commit
|
|
266
|
-
: `commit
|
|
300
|
+
? `commit — ${formatCommitHeader(plan.commits[0])}`
|
|
301
|
+
: `commit — apply ${plan.commits.length} commits`;
|
|
267
302
|
const action = await ctx.ui.select("Proceed?", [
|
|
268
303
|
commitLabel,
|
|
269
|
-
"abort
|
|
304
|
+
"abort — cancel",
|
|
270
305
|
]);
|
|
271
306
|
|
|
272
307
|
if (!action || action.startsWith("abort")) {
|
|
@@ -292,62 +327,53 @@ interface AgentPlanResult {
|
|
|
292
327
|
plan: CommitPlan | null;
|
|
293
328
|
/** Human-readable reason when plan is null */
|
|
294
329
|
reason?: string;
|
|
330
|
+
/** Number of attempts made by runWithOutputValidation (0 if never reached). */
|
|
331
|
+
attempts: number;
|
|
295
332
|
}
|
|
296
333
|
|
|
297
334
|
async function tryAgentPlan(
|
|
298
335
|
platform: Platform,
|
|
299
336
|
cwd: string,
|
|
300
337
|
prompt: string,
|
|
338
|
+
stagedFiles: string[],
|
|
301
339
|
model?: string,
|
|
302
340
|
): Promise<AgentPlanResult> {
|
|
303
|
-
let session: Awaited<ReturnType<Platform["createAgentSession"]>> | null = null;
|
|
304
341
|
try {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
if (!lastAssistant) return { plan: null, reason: "no assistant response" };
|
|
323
|
-
|
|
324
|
-
const text =
|
|
325
|
-
typeof lastAssistant.content === "string"
|
|
326
|
-
? lastAssistant.content
|
|
327
|
-
: Array.isArray(lastAssistant.content)
|
|
328
|
-
? lastAssistant.content
|
|
329
|
-
.filter((b: any) => b.type === "text")
|
|
330
|
-
.map((b: any) => b.text)
|
|
331
|
-
.join("\n")
|
|
332
|
-
: "";
|
|
342
|
+
const result = await runWithOutputValidation<CommitPlan>(
|
|
343
|
+
platform.createAgentSession.bind(platform),
|
|
344
|
+
{
|
|
345
|
+
cwd,
|
|
346
|
+
prompt,
|
|
347
|
+
schema: COMMIT_PLAN_SCHEMA_TEXT,
|
|
348
|
+
parse: (raw) => parseStructuredOutput<CommitPlan>(raw, CommitPlanSchema),
|
|
349
|
+
model,
|
|
350
|
+
maxAttempts: 3,
|
|
351
|
+
reliability: {
|
|
352
|
+
paths: platform.paths,
|
|
353
|
+
cwd,
|
|
354
|
+
command: "commit",
|
|
355
|
+
operation: "commit-plan",
|
|
356
|
+
},
|
|
357
|
+
},
|
|
358
|
+
);
|
|
333
359
|
|
|
334
|
-
if (
|
|
360
|
+
if (result.status === "blocked") {
|
|
361
|
+
return { plan: null, reason: result.error, attempts: result.attempts };
|
|
362
|
+
}
|
|
335
363
|
|
|
336
|
-
const
|
|
337
|
-
if (
|
|
364
|
+
const coverageErrors = validateCommitPlanCoverage(result.output, stagedFiles);
|
|
365
|
+
if (coverageErrors.length > 0) {
|
|
366
|
+
return {
|
|
367
|
+
plan: null,
|
|
368
|
+
reason: `Commit plan coverage check failed: ${formatValidationErrors(coverageErrors).join("; ")}`,
|
|
369
|
+
attempts: result.attempts,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
338
372
|
|
|
339
|
-
return { plan };
|
|
373
|
+
return { plan: result.output, attempts: result.attempts };
|
|
340
374
|
} catch (err) {
|
|
341
375
|
const message = err instanceof Error ? err.message : String(err);
|
|
342
|
-
return { plan: null, reason: message };
|
|
343
|
-
} finally {
|
|
344
|
-
if (session) {
|
|
345
|
-
try {
|
|
346
|
-
await session.dispose();
|
|
347
|
-
} catch {
|
|
348
|
-
// Swallow disposal errors
|
|
349
|
-
}
|
|
350
|
-
}
|
|
376
|
+
return { plan: null, reason: message, attempts: 0 };
|
|
351
377
|
}
|
|
352
378
|
}
|
|
353
379
|
|
|
@@ -358,10 +384,24 @@ async function manualFallback(
|
|
|
358
384
|
ctx: any,
|
|
359
385
|
cwd: string,
|
|
360
386
|
fileList: string[],
|
|
387
|
+
paths: PlatformPaths,
|
|
388
|
+
attempts: number,
|
|
361
389
|
reason?: string,
|
|
362
390
|
): Promise<CommitResult | null> {
|
|
363
391
|
const exec = platform.exec.bind(platform);
|
|
364
392
|
|
|
393
|
+
try {
|
|
394
|
+
appendReliabilityRecord(paths, cwd, {
|
|
395
|
+
ts: new Date().toISOString(),
|
|
396
|
+
command: "commit",
|
|
397
|
+
operation: "commit-plan",
|
|
398
|
+
outcome: "fallback",
|
|
399
|
+
attempts,
|
|
400
|
+
reason,
|
|
401
|
+
cwd,
|
|
402
|
+
});
|
|
403
|
+
} catch {}
|
|
404
|
+
|
|
365
405
|
notifyInfo(
|
|
366
406
|
ctx,
|
|
367
407
|
"AI commit unavailable",
|
|
@@ -389,24 +429,6 @@ async function manualFallback(
|
|
|
389
429
|
return { committed: 1, messages: [message] };
|
|
390
430
|
}
|
|
391
431
|
|
|
392
|
-
// ── Plan validation ────────────────────────────────────────
|
|
393
|
-
|
|
394
|
-
/**
|
|
395
|
-
* Filter an AI-generated commit plan against the actual staged file list.
|
|
396
|
-
* Removes hallucinated paths that aren't staged, and drops empty groups.
|
|
397
|
-
* Falls back to the original plan if filtering would leave nothing.
|
|
398
|
-
*/
|
|
399
|
-
export function validatePlanFiles(plan: CommitPlan, stagedFiles: string[]): CommitPlan {
|
|
400
|
-
const stagedSet = new Set(stagedFiles);
|
|
401
|
-
const validCommits = plan.commits
|
|
402
|
-
.map((group) => ({
|
|
403
|
-
...group,
|
|
404
|
-
files: group.files.filter((f) => stagedSet.has(f)),
|
|
405
|
-
}))
|
|
406
|
-
.filter((group) => group.files.length > 0);
|
|
407
|
-
|
|
408
|
-
return validCommits.length > 0 ? { commits: validCommits } : plan;
|
|
409
|
-
}
|
|
410
432
|
|
|
411
433
|
|
|
412
434
|
// ── Commit execution ───────────────────────────────────────
|
|
@@ -542,6 +564,7 @@ export function buildAnalysisPrompt(input: PromptInput): string {
|
|
|
542
564
|
parts.push(`**Developer context:** ${userContext}`, "");
|
|
543
565
|
}
|
|
544
566
|
|
|
567
|
+
|
|
545
568
|
// Diff content — truncate for large diffs
|
|
546
569
|
const diffBytes = Buffer.byteLength(normalizedDiff, "utf8");
|
|
547
570
|
|
|
@@ -589,93 +612,7 @@ export function buildAnalysisPrompt(input: PromptInput): string {
|
|
|
589
612
|
return parts.join("\n");
|
|
590
613
|
}
|
|
591
614
|
|
|
592
|
-
// ── Plan parsing ───────────────────────────────────────────
|
|
593
|
-
|
|
594
|
-
/**
|
|
595
|
-
* Produce a human-readable reason why parseCommitPlan returned null.
|
|
596
|
-
* Used for diagnostics — never shown raw to the user.
|
|
597
|
-
*/
|
|
598
|
-
function diagnoseParseFailure(text: string): string {
|
|
599
|
-
const fenceRe = /```json\s*\n([\s\S]*?)```/;
|
|
600
|
-
const match = fenceRe.exec(text);
|
|
601
|
-
if (!match) return "no JSON code block in response";
|
|
602
|
-
|
|
603
|
-
let parsed: any;
|
|
604
|
-
try {
|
|
605
|
-
parsed = JSON.parse(match[1]);
|
|
606
|
-
} catch {
|
|
607
|
-
return "JSON parse error in response";
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
if (!parsed.commits || !Array.isArray(parsed.commits)) return "missing commits array";
|
|
611
|
-
if (parsed.commits.length === 0) return "empty commits array";
|
|
612
615
|
|
|
613
|
-
for (const c of parsed.commits) {
|
|
614
|
-
if (!c.type) return "commit missing type";
|
|
615
|
-
if (!c.summary) return "commit missing summary";
|
|
616
|
-
if (!Array.isArray(c.files) || c.files.length === 0) return "commit missing files";
|
|
617
|
-
if (!(VALID_COMMIT_TYPES as readonly string[]).includes(c.type)) {
|
|
618
|
-
return `invalid commit type: ${c.type}`;
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
// Check for duplicate files across groups
|
|
623
|
-
const seen = new Set<string>();
|
|
624
|
-
for (const group of parsed.commits) {
|
|
625
|
-
for (const file of group.files) {
|
|
626
|
-
if (seen.has(file)) return `duplicate file across commits: ${file}`;
|
|
627
|
-
seen.add(file);
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
return "unknown parse failure";
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
/** Exported for testing */
|
|
636
|
-
export function parseCommitPlan(text: string): CommitPlan | null {
|
|
637
|
-
// Look for ```json ... ``` fenced block
|
|
638
|
-
const fenceRe = /```json\s*\n([\s\S]*?)```/;
|
|
639
|
-
const match = fenceRe.exec(text);
|
|
640
|
-
if (!match) return null;
|
|
641
|
-
|
|
642
|
-
try {
|
|
643
|
-
const parsed = JSON.parse(match[1]);
|
|
644
|
-
if (!parsed.commits || !Array.isArray(parsed.commits)) return null;
|
|
645
|
-
|
|
646
|
-
const commits: CommitGroup[] = [];
|
|
647
|
-
for (const c of parsed.commits) {
|
|
648
|
-
if (!c.type || !c.summary || !Array.isArray(c.files) || c.files.length === 0) {
|
|
649
|
-
return null;
|
|
650
|
-
}
|
|
651
|
-
if (!(VALID_COMMIT_TYPES as readonly string[]).includes(c.type)) {
|
|
652
|
-
return null;
|
|
653
|
-
}
|
|
654
|
-
commits.push({
|
|
655
|
-
type: c.type,
|
|
656
|
-
scope: c.scope ?? null,
|
|
657
|
-
summary: String(c.summary),
|
|
658
|
-
details: Array.isArray(c.details) ? c.details.map(String) : [],
|
|
659
|
-
files: c.files.map(String),
|
|
660
|
-
});
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
if (commits.length === 0) return null;
|
|
664
|
-
|
|
665
|
-
// Validate: no duplicate files across groups
|
|
666
|
-
const seen = new Set<string>();
|
|
667
|
-
for (const group of commits) {
|
|
668
|
-
for (const file of group.files) {
|
|
669
|
-
if (seen.has(file)) return null;
|
|
670
|
-
seen.add(file);
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
return { commits };
|
|
675
|
-
} catch {
|
|
676
|
-
return null;
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
616
|
|
|
680
617
|
// ── Formatting ─────────────────────────────────────────────
|
|
681
618
|
|