supipowers 2.2.0 → 2.2.2
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 +71 -12
- package/package.json +11 -15
- package/skills/ui-design/SKILL.md +2 -2
- package/src/ai/final-message.ts +15 -1
- package/src/ai/schema-text.ts +60 -40
- package/src/ai/schema-validation.ts +88 -0
- package/src/ai/structured-output.ts +19 -19
- package/src/bootstrap.ts +2 -1
- package/src/commands/doctor.ts +3 -2
- package/src/commands/fix-pr.ts +166 -26
- package/src/commands/plan.ts +2 -1
- package/src/commands/update.ts +7 -5
- package/src/config/schema.ts +102 -139
- package/src/docs/contracts.ts +13 -23
- package/src/fix-pr/assessment.ts +63 -24
- package/src/fix-pr/contracts.ts +15 -23
- package/src/fix-pr/fetch-comments.ts +119 -0
- package/src/fix-pr/prompt-builder.ts +19 -8
- package/src/git/commit-contract.ts +13 -19
- package/src/git/commit.ts +168 -6
- package/src/harness/anti_slop/fallow-adapter.ts +4 -3
- package/src/harness/command.ts +12 -7
- package/src/harness/pipeline.ts +2 -8
- package/src/harness/stage-runner.ts +3 -0
- package/src/harness/stages/docs.ts +82 -0
- package/src/lsp/capabilities.ts +9 -12
- package/src/lsp/contracts.ts +15 -23
- package/src/mempalace/uv.ts +15 -7
- package/src/planning/approval-flow.ts +15 -17
- package/src/planning/planning-ask-tool.ts +13 -2
- package/src/planning/spec.ts +21 -27
- package/src/planning/system-prompt.ts +1 -1
- package/src/planning/validate.ts +4 -7
- package/src/platform/progress.ts +11 -0
- package/src/quality/contracts.ts +15 -23
- package/src/quality/schemas.ts +40 -67
- package/src/release/contracts.ts +19 -28
- package/src/review/types.ts +142 -186
- package/src/types.ts +15 -2
- package/src/ui-design/session.ts +13 -2
- package/src/ui-design/system-prompt.ts +2 -2
- package/src/ultraplan/contracts.ts +458 -524
- package/src/utils/exec-cli.ts +106 -0
- package/src/visual/scripts/npm-shrinkwrap.json +878 -0
- package/src/visual/scripts/package-lock.json +878 -0
|
@@ -177,26 +177,37 @@ export function buildFixPrOrchestratorPrompt(options: FixPrPromptOptions): strin
|
|
|
177
177
|
sections.push(buildReplyInstructions(config), "");
|
|
178
178
|
|
|
179
179
|
sections.push(
|
|
180
|
-
"## Step 6: Push and Check for New Comments",
|
|
180
|
+
"## Step 6: Push, Monitor CI, and Check for New Comments",
|
|
181
181
|
"",
|
|
182
|
-
`1. Stage and commit: \`git add -A && git commit -m
|
|
182
|
+
`1. Stage and commit: \`git add -A && git commit -m "fix: address PR review comments (iteration ${iteration})"\``,
|
|
183
183
|
"2. Push: `git push`",
|
|
184
184
|
);
|
|
185
185
|
|
|
186
186
|
if (reviewer.type !== "none" && reviewer.triggerMethod) {
|
|
187
187
|
sections.push(
|
|
188
|
-
`3. Trigger re-review: \`bun
|
|
188
|
+
`3. Trigger re-review: \`bun "${scriptsDir}/trigger-review.ts" "${repo}" ${prNumber} "${reviewer.type}" "${reviewer.triggerMethod}"\``,
|
|
189
|
+
`4. While the reviewer runs, start the green pipeline: invoke OMP \`/green\` if available; otherwise run \`gh pr checks ${prNumber} --repo ${repo} --watch\`.`,
|
|
190
|
+
" - If CI turns red, stop waiting for review comments and focus on CI.",
|
|
191
|
+
" - Diagnose the failed check from its logs, fix the root cause, push again, then restart green monitoring.",
|
|
192
|
+
"5. Run the wait-and-check runner:",
|
|
193
|
+
);
|
|
194
|
+
} else {
|
|
195
|
+
sections.push(
|
|
196
|
+
`3. Start the green pipeline: invoke OMP \`/green\` if available; otherwise run \`gh pr checks ${prNumber} --repo ${repo} --watch\`.`,
|
|
197
|
+
" - If CI turns red, focus on CI before considering the PR complete.",
|
|
198
|
+
" - Diagnose the failed check from its logs, fix the root cause, push again, then restart green monitoring.",
|
|
199
|
+
"4. Run the wait-and-check runner:",
|
|
189
200
|
);
|
|
190
201
|
}
|
|
191
202
|
|
|
192
203
|
sections.push(
|
|
193
|
-
`${reviewer.type !== "none" ? "4" : "3"}. Run the wait-and-check runner:`,
|
|
194
204
|
"```text",
|
|
195
|
-
`bun
|
|
205
|
+
`bun "${scriptsDir}/wait-and-check.ts" "${sessionDir}" ${delay} ${iteration + 1} "${repo}" ${prNumber}`,
|
|
196
206
|
"```",
|
|
197
|
-
`${reviewer.type !== "none" ? "
|
|
198
|
-
` - If \`hasNewComments: true\` and iteration < ${maxIter}: process the new comments (go back to Step 1)`,
|
|
199
|
-
` - If \`hasNewComments: false\` or iteration >= ${maxIter}:
|
|
207
|
+
`${reviewer.type !== "none" ? "6" : "5"}. Read the last line of output:`,
|
|
208
|
+
` - If \`hasNewComments: true\` and iteration < ${maxIter}: process the new comments (go back to Step 1), then repeat push, green monitoring, and final validation`,
|
|
209
|
+
` - If \`hasNewComments: false\` or iteration >= ${maxIter}: continue only after CI is green`,
|
|
210
|
+
`${reviewer.type !== "none" ? "7" : "6"}. Full validation is mandatory at the end: run \`bun ci\` locally after comments are handled and CI is green. Do not report done until both remote CI and local full validation are green.`,
|
|
200
211
|
"",
|
|
201
212
|
);
|
|
202
213
|
|
|
@@ -6,30 +6,24 @@
|
|
|
6
6
|
// (every staged file appears in exactly one commit) is a runtime rule that
|
|
7
7
|
// can't live in the schema — see validateCommitPlanCoverage.
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import { z } from "zod/v4"
|
|
10
10
|
import { VALID_COMMIT_TYPES } from "../release/commit-types.js";
|
|
11
11
|
import type { ValidationError } from "../types.js";
|
|
12
12
|
|
|
13
|
-
export const CommitGroupSchema =
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
},
|
|
21
|
-
{ additionalProperties: false },
|
|
22
|
-
);
|
|
13
|
+
export const CommitGroupSchema = z.object({
|
|
14
|
+
type: z.enum(VALID_COMMIT_TYPES),
|
|
15
|
+
scope: z.string().nullable(),
|
|
16
|
+
summary: z.string().min(1),
|
|
17
|
+
details: z.array(z.string()),
|
|
18
|
+
files: z.array(z.string().min(1)).min(1),
|
|
19
|
+
}).strict();
|
|
23
20
|
|
|
24
|
-
export const CommitPlanSchema =
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
},
|
|
28
|
-
{ additionalProperties: false },
|
|
29
|
-
);
|
|
21
|
+
export const CommitPlanSchema = z.object({
|
|
22
|
+
commits: z.array(CommitGroupSchema).min(1),
|
|
23
|
+
}).strict();
|
|
30
24
|
|
|
31
|
-
export type CommitGroup =
|
|
32
|
-
export type CommitPlan =
|
|
25
|
+
export type CommitGroup = z.infer<typeof CommitGroupSchema>;
|
|
26
|
+
export type CommitPlan = z.infer<typeof CommitPlanSchema>;
|
|
33
27
|
|
|
34
28
|
/**
|
|
35
29
|
* Verify every staged file appears in exactly one commit and that no commit
|
package/src/git/commit.ts
CHANGED
|
@@ -103,6 +103,8 @@ const COMMIT_STEPS = [
|
|
|
103
103
|
{ key: "ai-analysis", label: "AI analysis" },
|
|
104
104
|
{ key: "review-plan", label: "Review plan" },
|
|
105
105
|
{ key: "execute-commits", label: "Execute commits" },
|
|
106
|
+
{ key: "push-commits", label: "Push commits" },
|
|
107
|
+
{ key: "open-pr", label: "Open pull request" },
|
|
106
108
|
] as const;
|
|
107
109
|
|
|
108
110
|
function createProgress(ctx: any) {
|
|
@@ -127,6 +129,9 @@ function createProgress(ctx: any) {
|
|
|
127
129
|
skip(stepIndex: number, detail?: string) {
|
|
128
130
|
progress.skip(COMMIT_STEPS[stepIndex]!.key, detail);
|
|
129
131
|
},
|
|
132
|
+
fail(stepIndex: number, detail?: string) {
|
|
133
|
+
progress.fail(COMMIT_STEPS[stepIndex]!.key, detail);
|
|
134
|
+
},
|
|
130
135
|
dispose() {
|
|
131
136
|
progress.dispose();
|
|
132
137
|
},
|
|
@@ -340,14 +345,11 @@ export async function analyzeAndCommit(
|
|
|
340
345
|
}
|
|
341
346
|
|
|
342
347
|
if (!plan) {
|
|
343
|
-
// Skip remaining tracked steps for the manual path
|
|
344
348
|
progress.skip(5, "manual");
|
|
345
|
-
progress.skip(6, "manual");
|
|
346
|
-
progress.dispose();
|
|
347
349
|
const reason = !platform.capabilities.agentSessions
|
|
348
350
|
? "no agent sessions"
|
|
349
351
|
: agentReason;
|
|
350
|
-
return manualFallback(platform, ctx, cwd, fileList, platform.paths, agentAttempts, reason);
|
|
352
|
+
return manualFallback(platform, ctx, cwd, fileList, platform.paths, agentAttempts, progress, reason);
|
|
351
353
|
}
|
|
352
354
|
|
|
353
355
|
// 6. Present plan for approval
|
|
@@ -444,6 +446,7 @@ async function manualFallback(
|
|
|
444
446
|
fileList: string[],
|
|
445
447
|
paths: PlatformPaths,
|
|
446
448
|
attempts: number,
|
|
449
|
+
progress: ReturnType<typeof createProgress>,
|
|
447
450
|
reason?: string,
|
|
448
451
|
): Promise<CommitResult | null> {
|
|
449
452
|
const exec = platform.exec.bind(platform);
|
|
@@ -467,27 +470,186 @@ async function manualFallback(
|
|
|
467
470
|
? `${reason} \u2014 enter a commit message manually`
|
|
468
471
|
: "Enter a commit message manually",
|
|
469
472
|
);
|
|
473
|
+
progress.activate(6, "manual");
|
|
470
474
|
|
|
471
475
|
const message = await ctx.ui.input("Commit message (empty to abort)", {
|
|
472
476
|
helpText: `${fileList.length} file(s) staged`,
|
|
473
477
|
});
|
|
474
|
-
|
|
475
478
|
if (!message?.trim()) {
|
|
476
479
|
notifyInfo(ctx, "Commit cancelled", "No message provided");
|
|
480
|
+
progress.skip(6, "aborted");
|
|
477
481
|
return null;
|
|
478
482
|
}
|
|
479
483
|
|
|
480
484
|
const commitResult = await commitStaged(exec, cwd, message);
|
|
481
485
|
if (!commitResult.success) {
|
|
482
486
|
notifyError(ctx, "Commit failed", commitResult.error);
|
|
487
|
+
progress.fail(6, "failed");
|
|
483
488
|
return null;
|
|
484
489
|
}
|
|
485
490
|
|
|
486
491
|
notifySuccess(ctx, "Committed", message.split("\n")[0]);
|
|
492
|
+
progress.complete(6, "1 done");
|
|
493
|
+
await offerPostCommitActions(platform, ctx, cwd, progress);
|
|
487
494
|
return { committed: 1, messages: [message] };
|
|
488
495
|
}
|
|
489
496
|
|
|
490
497
|
|
|
498
|
+
const PUSH_NO_OPTION = "No — keep commits local";
|
|
499
|
+
const PR_NO_OPTION = "No — leave branch without a PR";
|
|
500
|
+
const PR_YES_OPTION = "Yes — open a Pull Request";
|
|
501
|
+
|
|
502
|
+
function pushYesOption(branch: string): string {
|
|
503
|
+
return `Yes — push to origin/${branch}`;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function isDefaultBranchName(branch: string): boolean {
|
|
507
|
+
return branch === "main" || branch === "master";
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function formatCommandFailure(result: { stdout: string; stderr: string; code: number }): string {
|
|
511
|
+
return result.stderr.trim() || result.stdout.trim() || `exit code ${result.code}`;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
async function readCurrentBranch(
|
|
515
|
+
exec: ExecFn,
|
|
516
|
+
ctx: any,
|
|
517
|
+
cwd: string,
|
|
518
|
+
): Promise<string | null> {
|
|
519
|
+
const result = await exec("git", ["branch", "--show-current"], { cwd });
|
|
520
|
+
if (result.code !== 0) {
|
|
521
|
+
notifyError(ctx, "Could not determine current branch", formatCommandFailure(result));
|
|
522
|
+
return null;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return result.stdout.trim() || null;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
async function pushCurrentBranch(
|
|
529
|
+
exec: ExecFn,
|
|
530
|
+
ctx: any,
|
|
531
|
+
cwd: string,
|
|
532
|
+
branch: string,
|
|
533
|
+
): Promise<boolean> {
|
|
534
|
+
const result = await exec("git", ["push", "-u", "origin", branch], { cwd });
|
|
535
|
+
if (result.code !== 0) {
|
|
536
|
+
notifyError(ctx, "Push failed", formatCommandFailure(result));
|
|
537
|
+
return false;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
notifySuccess(ctx, "Pushed", `origin/${branch}`);
|
|
541
|
+
return true;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
async function createPullRequest(
|
|
545
|
+
exec: ExecFn,
|
|
546
|
+
ctx: any,
|
|
547
|
+
cwd: string,
|
|
548
|
+
branch: string,
|
|
549
|
+
): Promise<boolean> {
|
|
550
|
+
const result = await exec("gh", ["pr", "create", "--fill", "--head", branch], { cwd });
|
|
551
|
+
if (result.code !== 0) {
|
|
552
|
+
notifyError(ctx, "Pull request failed", formatCommandFailure(result));
|
|
553
|
+
return false;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const detail = result.stdout.trim() || result.stderr.trim() || `Branch: ${branch}`;
|
|
557
|
+
notifySuccess(ctx, "Pull request opened", detail);
|
|
558
|
+
return true;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
async function offerPostCommitActions(
|
|
562
|
+
platform: Platform,
|
|
563
|
+
ctx: any,
|
|
564
|
+
cwd: string,
|
|
565
|
+
progress: ReturnType<typeof createProgress>,
|
|
566
|
+
): Promise<void> {
|
|
567
|
+
try {
|
|
568
|
+
const exec = platform.exec.bind(platform);
|
|
569
|
+
progress.activate(7, "detect branch");
|
|
570
|
+
const branch = await readCurrentBranch(exec, ctx, cwd);
|
|
571
|
+
if (!branch) {
|
|
572
|
+
progress.skip(7, "no branch");
|
|
573
|
+
progress.skip(8, "no branch");
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const yesPush = pushYesOption(branch);
|
|
578
|
+
progress.activate(7, "prompt");
|
|
579
|
+
const pushSelection = await ctx.ui.select("Push commits?", [
|
|
580
|
+
PUSH_NO_OPTION,
|
|
581
|
+
yesPush,
|
|
582
|
+
], {
|
|
583
|
+
helpText: `Current branch: ${branch}`,
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
if (!pushSelection) {
|
|
587
|
+
progress.skip(7, "cancelled");
|
|
588
|
+
progress.skip(8, "cancelled");
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
let pushed = false;
|
|
593
|
+
if (pushSelection === yesPush) {
|
|
594
|
+
progress.activate(7, `origin/${branch}`);
|
|
595
|
+
pushed = await pushCurrentBranch(exec, ctx, cwd, branch);
|
|
596
|
+
if (!pushed) {
|
|
597
|
+
progress.fail(7, "failed");
|
|
598
|
+
progress.skip(8, "push failed");
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
progress.complete(7, `origin/${branch}`);
|
|
602
|
+
} else {
|
|
603
|
+
progress.skip(7, "kept local");
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
if (isDefaultBranchName(branch)) {
|
|
607
|
+
progress.skip(8, "default branch");
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
progress.activate(8, "prompt");
|
|
612
|
+
const prSelection = await ctx.ui.select("Open a Pull Request?", [
|
|
613
|
+
PR_NO_OPTION,
|
|
614
|
+
PR_YES_OPTION,
|
|
615
|
+
], {
|
|
616
|
+
helpText: pushed
|
|
617
|
+
? `Branch: ${branch}`
|
|
618
|
+
: `Opening a PR will first push origin/${branch}.`,
|
|
619
|
+
});
|
|
620
|
+
if (!prSelection) {
|
|
621
|
+
progress.skip(8, "cancelled");
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
if (prSelection !== PR_YES_OPTION) {
|
|
625
|
+
progress.skip(8, "not requested");
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
if (!pushed) {
|
|
630
|
+
progress.activate(7, `origin/${branch}`);
|
|
631
|
+
pushed = await pushCurrentBranch(exec, ctx, cwd, branch);
|
|
632
|
+
if (!pushed) {
|
|
633
|
+
progress.fail(7, "failed");
|
|
634
|
+
progress.skip(8, "push failed");
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
progress.complete(7, `origin/${branch}`);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
progress.activate(8, "gh pr create");
|
|
641
|
+
if (!await createPullRequest(exec, ctx, cwd, branch)) {
|
|
642
|
+
progress.fail(8, "failed");
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
progress.complete(8, "created");
|
|
646
|
+
} catch (err) {
|
|
647
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
648
|
+
progress.fail(8, "error");
|
|
649
|
+
notifyError(ctx, "Post-commit action failed", message);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
491
653
|
|
|
492
654
|
// ── Commit execution ───────────────────────────────────────
|
|
493
655
|
|
|
@@ -548,12 +710,12 @@ async function executeCommitPlan(
|
|
|
548
710
|
await exec("git", ["read-tree", savedTree], { cwd });
|
|
549
711
|
|
|
550
712
|
progress.complete(6, `${committedMessages.length} done`);
|
|
551
|
-
progress.dispose();
|
|
552
713
|
notifySuccess(
|
|
553
714
|
ctx,
|
|
554
715
|
`${committedMessages.length} commit(s) created`,
|
|
555
716
|
committedMessages.map((m) => m.split("\n")[0]).join(" | "),
|
|
556
717
|
);
|
|
718
|
+
await offerPostCommitActions(platform, ctx, cwd, progress);
|
|
557
719
|
|
|
558
720
|
return { committed: committedMessages.length, messages: committedMessages };
|
|
559
721
|
}
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
type SlopBackendResult,
|
|
26
26
|
type SlopFinding,
|
|
27
27
|
} from "./backend.js";
|
|
28
|
+
import { execCli } from "../../utils/exec-cli.js";
|
|
28
29
|
|
|
29
30
|
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
30
31
|
|
|
@@ -149,7 +150,7 @@ async function resolveInvocation(
|
|
|
149
150
|
}
|
|
150
151
|
|
|
151
152
|
try {
|
|
152
|
-
const probe = await platform.exec("npx", ["--no-install", "fallow", "--version"], { timeout: 5000 });
|
|
153
|
+
const probe = await execCli((cmd, args, opts) => platform.exec(cmd, args, opts), "npx", ["--no-install", "fallow", "--version"], { timeout: 5000 });
|
|
153
154
|
if (probe.code === 0) {
|
|
154
155
|
availabilityCache = { ok: true, via: "npx" };
|
|
155
156
|
return { ok: true, cmd: "npx", baseArgs: ["--no-install", "fallow"], via: "npx" };
|
|
@@ -187,7 +188,7 @@ async function runFallow(
|
|
|
187
188
|
const startedAt = Date.now();
|
|
188
189
|
let result;
|
|
189
190
|
try {
|
|
190
|
-
result = await platform.exec(invocation.cmd, args, {
|
|
191
|
+
result = await execCli((cmd, args, opts) => platform.exec(cmd, args, opts), invocation.cmd, args, {
|
|
191
192
|
cwd: opts.cwd,
|
|
192
193
|
timeout: opts.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
193
194
|
});
|
|
@@ -269,7 +270,7 @@ export class FallowAdapter implements SlopBackend {
|
|
|
269
270
|
if (opts.subtree) args.push("--path", opts.subtree);
|
|
270
271
|
|
|
271
272
|
try {
|
|
272
|
-
const result = await platform.exec(invocation.cmd, args, {
|
|
273
|
+
const result = await execCli((cmd, args, opts) => platform.exec(cmd, args, opts), invocation.cmd, args, {
|
|
273
274
|
cwd: opts.cwd,
|
|
274
275
|
timeout: opts.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
275
276
|
});
|
package/src/harness/command.ts
CHANGED
|
@@ -45,7 +45,6 @@ import {
|
|
|
45
45
|
import { computeScore } from "./anti_slop/score.js";
|
|
46
46
|
import {
|
|
47
47
|
type BuildRunnerInput,
|
|
48
|
-
type HarnessPipelineProgressEvent,
|
|
49
48
|
type PipelineRunOutcome,
|
|
50
49
|
HARNESS_STAGE_ORDER,
|
|
51
50
|
runHarnessPipelineUntilGate,
|
|
@@ -58,7 +57,7 @@ import { DEFAULT_HARNESS_CONFIG } from "./hooks/register.js";
|
|
|
58
57
|
import { handlePrComment } from "./pr-comment/handler.js";
|
|
59
58
|
import { runGitVerificationQa } from "./git-verify-qa.js";
|
|
60
59
|
import { getHarnessSessionDir } from "./project-paths.js";
|
|
61
|
-
import type { HarnessDesignSpec, HarnessGateMode, HarnessSession, HarnessStage } from "../types.js";
|
|
60
|
+
import type { HarnessDesignSpec, HarnessGateMode, HarnessPipelineProgressEvent, HarnessSession, HarnessStage } from "../types.js";
|
|
62
61
|
|
|
63
62
|
modelRegistry.register({
|
|
64
63
|
id: "harness",
|
|
@@ -133,9 +132,11 @@ function createHarnessProgress(ctx: HarnessCommandContext) {
|
|
|
133
132
|
let done = 0;
|
|
134
133
|
let cur: HarnessStage | null = null;
|
|
135
134
|
const completed: string[] = [];
|
|
135
|
+
let liveDetail: string | null = null;
|
|
136
136
|
|
|
137
137
|
function refresh() {
|
|
138
|
-
const
|
|
138
|
+
const baseLabel = cur ? HARNESS_STAGE_LABELS[cur] : "Complete";
|
|
139
|
+
const label = cur && liveDetail ? `${baseLabel} — ${liveDetail}` : baseLabel;
|
|
139
140
|
const spinner = cur ? "\u25cc" : "\u2713";
|
|
140
141
|
(ctx.ui as any).setStatus?.("supi-harness", ` ${spinner} harness: ${label} (${done}/${SO.length})`);
|
|
141
142
|
}
|
|
@@ -147,22 +148,26 @@ function createHarnessProgress(ctx: HarnessCommandContext) {
|
|
|
147
148
|
case "stage-started":
|
|
148
149
|
cur = event.stage;
|
|
149
150
|
break;
|
|
151
|
+
case "stage-progress":
|
|
152
|
+
cur = event.stage;
|
|
153
|
+
liveDetail = event.detail;
|
|
154
|
+
break;
|
|
150
155
|
case "stage-completed": {
|
|
151
|
-
done += 1; cur = null;
|
|
156
|
+
done += 1; cur = null; liveDetail = null;
|
|
152
157
|
const mark = "\u2713";
|
|
153
158
|
completed.push(`${mark} ${HARNESS_STAGE_LABELS[event.stage]}: ${event.detail || "done"}`);
|
|
154
159
|
break;
|
|
155
160
|
}
|
|
156
161
|
case "stage-skipped":
|
|
157
|
-
done += 1; cur = null;
|
|
162
|
+
done += 1; cur = null; liveDetail = null;
|
|
158
163
|
completed.push(`\u2013 ${HARNESS_STAGE_LABELS[event.stage]}: skipped`);
|
|
159
164
|
break;
|
|
160
165
|
case "awaiting-user":
|
|
161
|
-
done += 1; cur = null;
|
|
166
|
+
done += 1; cur = null; liveDetail = null;
|
|
162
167
|
completed.push(`\u25cb ${HARNESS_STAGE_LABELS[event.stage]}: ${event.detail || "awaiting review"}`);
|
|
163
168
|
break;
|
|
164
169
|
case "stage-failed": case "stage-blocked":
|
|
165
|
-
cur = null;
|
|
170
|
+
cur = null; liveDetail = null;
|
|
166
171
|
completed.push(`\u2717 ${HARNESS_STAGE_LABELS[event.stage]}: ${event.detail || "failed"}`);
|
|
167
172
|
break;
|
|
168
173
|
}
|
package/src/harness/pipeline.ts
CHANGED
|
@@ -14,6 +14,7 @@ import * as path from "node:path";
|
|
|
14
14
|
import type { Platform, PlatformPaths } from "../platform/types.js";
|
|
15
15
|
import type {
|
|
16
16
|
HarnessGateMode,
|
|
17
|
+
HarnessPipelineProgressEvent,
|
|
17
18
|
HarnessStage,
|
|
18
19
|
ModelConfig,
|
|
19
20
|
} from "../types.js";
|
|
@@ -38,14 +39,6 @@ import { buildBackendAdapter } from "./anti_slop/backend-factory.js";
|
|
|
38
39
|
import { DEFAULT_HARNESS_CONFIG } from "./hooks/register.js";
|
|
39
40
|
import { getProjectStatePath } from "../workspace/state-paths.js";
|
|
40
41
|
|
|
41
|
-
/** Progress event emitted by the pipeline driver for UI feedback. */
|
|
42
|
-
export type HarnessPipelineProgressEvent =
|
|
43
|
-
| { type: "stage-started"; stage: HarnessStage }
|
|
44
|
-
| { type: "stage-skipped"; stage: HarnessStage }
|
|
45
|
-
| { type: "stage-completed"; stage: HarnessStage; detail?: string }
|
|
46
|
-
| { type: "stage-blocked"; stage: HarnessStage; detail: string }
|
|
47
|
-
| { type: "stage-failed"; stage: HarnessStage; detail: string }
|
|
48
|
-
| { type: "awaiting-user"; stage: HarnessStage; detail?: string };
|
|
49
42
|
|
|
50
43
|
const STAGE_ORDER: readonly HarnessStage[] = [
|
|
51
44
|
"discover",
|
|
@@ -301,6 +294,7 @@ export async function runHarnessPipelineUntilGate(
|
|
|
301
294
|
sessionId: input.sessionId,
|
|
302
295
|
modelConfig: input.modelConfig,
|
|
303
296
|
gateMode: input.gates,
|
|
297
|
+
onProgress: (event) => input.onProgress?.(event),
|
|
304
298
|
};
|
|
305
299
|
|
|
306
300
|
const result = await runner.run(ctx);
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
import type { Platform, PlatformPaths } from "../platform/types.js";
|
|
22
22
|
import type {
|
|
23
23
|
HarnessGateMode,
|
|
24
|
+
HarnessPipelineProgressEvent,
|
|
24
25
|
HarnessStage,
|
|
25
26
|
HarnessStageStatus,
|
|
26
27
|
ModelConfig,
|
|
@@ -46,6 +47,8 @@ export interface HarnessStageRunnerContext {
|
|
|
46
47
|
now?: () => string;
|
|
47
48
|
/** Optional override for the agent session model. Tests use this to bypass resolution. */
|
|
48
49
|
modelOverride?: { model: string; thinkingLevel: string | null };
|
|
50
|
+
/** Live progress sink for long-running stage internals such as subagent turns. */
|
|
51
|
+
onProgress?: (event: HarnessPipelineProgressEvent) => void;
|
|
49
52
|
}
|
|
50
53
|
|
|
51
54
|
export type HarnessStageRunStatus =
|
|
@@ -90,6 +90,7 @@ export interface DocsStageInput {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
interface AgentSessionLike {
|
|
93
|
+
subscribe?: (handler: (event: unknown) => void) => () => void;
|
|
93
94
|
prompt(text: string, opts?: { expandPromptTemplates?: boolean }): Promise<void>;
|
|
94
95
|
dispose(): Promise<void>;
|
|
95
96
|
}
|
|
@@ -536,6 +537,60 @@ async function orchestrateLayerSubagent(input: OrchestrateLayerInput): Promise<O
|
|
|
536
537
|
}
|
|
537
538
|
}
|
|
538
539
|
|
|
540
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
541
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function stringField(record: Record<string, unknown>, key: string): string | null {
|
|
545
|
+
const value = record[key];
|
|
546
|
+
return typeof value === "string" && value.trim().length > 0 ? value : null;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function nestedStringField(record: Record<string, unknown>, objectKey: string, fieldKey: string): string | null {
|
|
550
|
+
const nested = record[objectKey];
|
|
551
|
+
if (!isRecord(nested)) return null;
|
|
552
|
+
return stringField(nested, fieldKey);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function compactDetail(value: string, maxChars = 180): string {
|
|
556
|
+
const compact = value.replace(/\s+/g, " ").trim();
|
|
557
|
+
if (compact.length <= maxChars) return compact;
|
|
558
|
+
return `${compact.slice(0, maxChars - 1).trimEnd()}…`;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function summarizeAgentSessionEvent(event: unknown): string | null {
|
|
562
|
+
if (!isRecord(event)) return null;
|
|
563
|
+
const type = stringField(event, "type") ?? stringField(event, "kind") ?? stringField(event, "event");
|
|
564
|
+
const lowerType = type?.toLowerCase() ?? "";
|
|
565
|
+
const toolName =
|
|
566
|
+
stringField(event, "toolName") ??
|
|
567
|
+
stringField(event, "tool") ??
|
|
568
|
+
stringField(event, "name") ??
|
|
569
|
+
nestedStringField(event, "toolCall", "name") ??
|
|
570
|
+
nestedStringField(event, "tool_call", "name");
|
|
571
|
+
if (toolName && (lowerType.includes("tool") || lowerType.includes("call"))) {
|
|
572
|
+
return `tool ${compactDetail(toolName, 80)}`;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const text =
|
|
576
|
+
stringField(event, "text") ??
|
|
577
|
+
stringField(event, "delta") ??
|
|
578
|
+
stringField(event, "thought") ??
|
|
579
|
+
stringField(event, "content") ??
|
|
580
|
+
nestedStringField(event, "message", "text") ??
|
|
581
|
+
nestedStringField(event, "message", "content");
|
|
582
|
+
if (text) {
|
|
583
|
+
const prefix = lowerType.includes("thought") || lowerType.includes("reason")
|
|
584
|
+
? "thought"
|
|
585
|
+
: lowerType.includes("tool")
|
|
586
|
+
? "tool"
|
|
587
|
+
: "agent";
|
|
588
|
+
return `${prefix} ${compactDetail(text)}`;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
return type ? compactDetail(type, 80) : null;
|
|
592
|
+
}
|
|
593
|
+
|
|
539
594
|
async function dispatchSubagent(input: {
|
|
540
595
|
platform: Platform;
|
|
541
596
|
ctx: HarnessStageRunnerContext;
|
|
@@ -548,7 +603,13 @@ async function dispatchSubagent(input: {
|
|
|
548
603
|
const agentDisplayName = buildHarnessAgentDisplayName("docs", input.entry.layer.layer);
|
|
549
604
|
|
|
550
605
|
let session: AgentSessionLike | null = null;
|
|
606
|
+
let unsubscribe: (() => void) | null = null;
|
|
551
607
|
try {
|
|
608
|
+
input.ctx.onProgress?.({
|
|
609
|
+
type: "stage-progress",
|
|
610
|
+
stage: "docs",
|
|
611
|
+
detail: `${agentDisplayName}: starting attempt ${input.attempt}`,
|
|
612
|
+
});
|
|
552
613
|
if (input.factory) {
|
|
553
614
|
session = await input.factory(input.platform, {
|
|
554
615
|
cwd: input.ctx.cwd,
|
|
@@ -562,7 +623,21 @@ async function dispatchSubagent(input: {
|
|
|
562
623
|
agentDisplayName,
|
|
563
624
|
});
|
|
564
625
|
}
|
|
626
|
+
unsubscribe = session.subscribe?.((event) => {
|
|
627
|
+
const summary = summarizeAgentSessionEvent(event);
|
|
628
|
+
if (!summary) return;
|
|
629
|
+
input.ctx.onProgress?.({
|
|
630
|
+
type: "stage-progress",
|
|
631
|
+
stage: "docs",
|
|
632
|
+
detail: `${agentDisplayName}: ${summary}`,
|
|
633
|
+
});
|
|
634
|
+
}) ?? null;
|
|
565
635
|
await session.prompt(input.assignment, { expandPromptTemplates: false });
|
|
636
|
+
input.ctx.onProgress?.({
|
|
637
|
+
type: "stage-progress",
|
|
638
|
+
stage: "docs",
|
|
639
|
+
detail: `${agentDisplayName}: attempt ${input.attempt} complete`,
|
|
640
|
+
});
|
|
566
641
|
} catch (error) {
|
|
567
642
|
return {
|
|
568
643
|
ok: false,
|
|
@@ -571,6 +646,13 @@ async function dispatchSubagent(input: {
|
|
|
571
646
|
],
|
|
572
647
|
};
|
|
573
648
|
} finally {
|
|
649
|
+
if (unsubscribe) {
|
|
650
|
+
try {
|
|
651
|
+
unsubscribe();
|
|
652
|
+
} catch {
|
|
653
|
+
/* best-effort */
|
|
654
|
+
}
|
|
655
|
+
}
|
|
574
656
|
if (session) {
|
|
575
657
|
try {
|
|
576
658
|
await session.dispose();
|
package/src/lsp/capabilities.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// treated as fail-closed: NO_LSP_SUPPORT (gate skips rather than pretending
|
|
12
12
|
// it ran).
|
|
13
13
|
|
|
14
|
-
import {
|
|
14
|
+
import { z } from "zod/v4";
|
|
15
15
|
import {
|
|
16
16
|
parseStructuredOutput,
|
|
17
17
|
runWithOutputValidation,
|
|
@@ -20,18 +20,15 @@ import {
|
|
|
20
20
|
import { renderSchemaText } from "../ai/schema-text.js";
|
|
21
21
|
import type { GateExecutionContext } from "../types.js";
|
|
22
22
|
|
|
23
|
-
export const LspCapabilitiesSchema =
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
},
|
|
31
|
-
{ additionalProperties: false },
|
|
32
|
-
);
|
|
23
|
+
export const LspCapabilitiesSchema = z.object({
|
|
24
|
+
diagnostics: z.boolean(),
|
|
25
|
+
references: z.boolean(),
|
|
26
|
+
definition: z.boolean(),
|
|
27
|
+
hover: z.boolean(),
|
|
28
|
+
rename: z.boolean(),
|
|
29
|
+
}).strict();
|
|
33
30
|
|
|
34
|
-
export type LspCapabilities =
|
|
31
|
+
export type LspCapabilities = z.infer<typeof LspCapabilitiesSchema>;
|
|
35
32
|
|
|
36
33
|
const SCHEMA_TEXT = renderSchemaText(LspCapabilitiesSchema);
|
|
37
34
|
|