supipowers 2.1.0 → 2.2.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 +71 -12
- package/package.json +4 -8
- 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 +3 -0
- package/src/commands/fix-pr.ts +166 -26
- package/src/commands/optimize-context.ts +153 -16
- package/src/commands/runbook.ts +511 -0
- package/src/config/schema.ts +102 -139
- package/src/context/rule-renderer.ts +274 -2
- package/src/context/runbook-extension-template.ts +193 -0
- package/src/context/startup-check.ts +197 -2
- package/src/context/startup-optimizer.ts +133 -10
- 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/command.ts +98 -6
- package/src/harness/git-verification.ts +515 -0
- package/src/harness/git-verify-qa.ts +406 -0
- package/src/harness/pipeline.ts +17 -8
- package/src/harness/stages/implement-apply.ts +61 -4
- package/src/harness/stages/validate.ts +108 -0
- package/src/lsp/capabilities.ts +9 -12
- package/src/lsp/contracts.ts +15 -23
- 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 +45 -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/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
|
}
|
package/src/harness/command.ts
CHANGED
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
loadHarnessSession,
|
|
40
40
|
loadHarnessValidateReport,
|
|
41
41
|
readSlopQueue,
|
|
42
|
+
saveHarnessDesignSpecJson,
|
|
42
43
|
saveHarnessSession,
|
|
43
44
|
} from "./storage.js";
|
|
44
45
|
import { computeScore } from "./anti_slop/score.js";
|
|
@@ -55,6 +56,8 @@ import { buildBackendAdapter } from "./anti_slop/backend-factory.js";
|
|
|
55
56
|
import { getWorkingTreeStatus } from "../git/status.js";
|
|
56
57
|
import { DEFAULT_HARNESS_CONFIG } from "./hooks/register.js";
|
|
57
58
|
import { handlePrComment } from "./pr-comment/handler.js";
|
|
59
|
+
import { runGitVerificationQa } from "./git-verify-qa.js";
|
|
60
|
+
import { getHarnessSessionDir } from "./project-paths.js";
|
|
58
61
|
import type { HarnessDesignSpec, HarnessGateMode, HarnessSession, HarnessStage } from "../types.js";
|
|
59
62
|
|
|
60
63
|
modelRegistry.register({
|
|
@@ -223,12 +226,13 @@ async function runPipelineWithProgress(
|
|
|
223
226
|
gates: HarnessGateMode,
|
|
224
227
|
stageInputs: BuildRunnerInput,
|
|
225
228
|
startStage?: HarnessStage,
|
|
229
|
+
forceStages?: ReadonlySet<HarnessStage>,
|
|
226
230
|
): Promise<PipelineRunOutcome> {
|
|
227
231
|
const harnessProgress = createHarnessProgress(ctx);
|
|
228
232
|
const modelConfig = loadModelConfig(platform.paths, ctx.cwd);
|
|
229
233
|
const outcome = await pipelineDriver({
|
|
230
234
|
platform, paths: platform.paths, cwd: ctx.cwd, sessionId,
|
|
231
|
-
modelConfig, gates, stageInputs, startStage,
|
|
235
|
+
modelConfig, gates, stageInputs, startStage, forceStages,
|
|
232
236
|
onProgress: harnessProgress.onProgress,
|
|
233
237
|
});
|
|
234
238
|
// Single consolidated notification.
|
|
@@ -558,12 +562,12 @@ async function runDesignQa(
|
|
|
558
562
|
|
|
559
563
|
if (choice === "Accept all suggestions") {
|
|
560
564
|
applyDesignAnalysis(base, analysis);
|
|
561
|
-
await askCiAndTooling(ctx, base);
|
|
565
|
+
await askCiAndTooling(platform, ctx, base);
|
|
562
566
|
return base;
|
|
563
567
|
}
|
|
564
568
|
|
|
565
569
|
if (choice === "Skip — use bare defaults") {
|
|
566
|
-
await askCiAndTooling(ctx, base);
|
|
570
|
+
await askCiAndTooling(platform, ctx, base);
|
|
567
571
|
return base;
|
|
568
572
|
}
|
|
569
573
|
}
|
|
@@ -627,7 +631,7 @@ async function runDesignQa(
|
|
|
627
631
|
}
|
|
628
632
|
}
|
|
629
633
|
|
|
630
|
-
await askCiAndTooling(ctx, base);
|
|
634
|
+
await askCiAndTooling(platform, ctx, base);
|
|
631
635
|
return base;
|
|
632
636
|
}
|
|
633
637
|
|
|
@@ -707,7 +711,7 @@ function localCommandOptions(base: HarnessDesignSpec): string[] {
|
|
|
707
711
|
]));
|
|
708
712
|
}
|
|
709
713
|
|
|
710
|
-
async function askCiAndTooling(ctx: HarnessCommandContext, base: HarnessDesignSpec): Promise<void> {
|
|
714
|
+
async function askCiAndTooling(platform: Platform, ctx: HarnessCommandContext, base: HarnessDesignSpec): Promise<void> {
|
|
711
715
|
if (!ctx.ui.select) return;
|
|
712
716
|
|
|
713
717
|
const triggerChoice = await ctx.ui.select(
|
|
@@ -741,6 +745,84 @@ async function askCiAndTooling(ctx: HarnessCommandContext, base: HarnessDesignSp
|
|
|
741
745
|
} else if (toolChoice) {
|
|
742
746
|
base.ci.localCommand = toolChoice.replace(/\s+\(.+\)$/, "");
|
|
743
747
|
}
|
|
748
|
+
|
|
749
|
+
// After the user picks their CI trigger and local command, offer the optional Git
|
|
750
|
+
// verification flow. This populates `base.ci.git` so the implement stage renders the
|
|
751
|
+
// PR-source guardrail and validate confirms the wiring. Skippable; declining leaves
|
|
752
|
+
// `git` unset and the rest of the pipeline behaves identically to before this feature.
|
|
753
|
+
await runGitVerificationStep(platform, ctx, base);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* Adapter around `runGitVerificationQa` that fits the harness command UI. The QA helper
|
|
758
|
+
* expects an `ExecFn`-shaped function plus a `select / input / notify` UI trio; we wrap
|
|
759
|
+
* `platform.exec` and `ctx.ui` so the helper stays independent of the OMP plumbing.
|
|
760
|
+
*
|
|
761
|
+
* Persists any returned `HarnessCiGitConfig` onto the in-memory `base` spec; the design
|
|
762
|
+
* stage runner persists the spec to disk so no extra storage call is needed here. We
|
|
763
|
+
* also widen `base.ci.trigger.branches` to include both `mainBranch` and `devBranch`
|
|
764
|
+
* so the rendered workflow runs CI on both PR targets — matching the rule "CI runs on
|
|
765
|
+
* both the dev branch PR and the main branch".
|
|
766
|
+
*/
|
|
767
|
+
async function runGitVerificationStep(
|
|
768
|
+
platform: Platform,
|
|
769
|
+
ctx: HarnessCommandContext,
|
|
770
|
+
base: HarnessDesignSpec,
|
|
771
|
+
): Promise<void> {
|
|
772
|
+
if (!ctx.ui.select || !ctx.ui.input) return; // No interactive UI — skip silently.
|
|
773
|
+
|
|
774
|
+
const sessionDir = getHarnessSessionDir(platform.paths, ctx.cwd, base.sessionId);
|
|
775
|
+
|
|
776
|
+
const result = await runGitVerificationQa({
|
|
777
|
+
exec: (cmd, args, opts) => platform.exec(cmd, args, opts),
|
|
778
|
+
cwd: ctx.cwd,
|
|
779
|
+
sessionDir,
|
|
780
|
+
ui: {
|
|
781
|
+
select: (title, options) => ctx.ui.select!(title, options as unknown as string[]),
|
|
782
|
+
input: (label) => ctx.ui.input!(label),
|
|
783
|
+
notify: (message) => notifyInfo(ctx, "Git verification", message),
|
|
784
|
+
},
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
if (!result) return;
|
|
788
|
+
base.ci.git = result;
|
|
789
|
+
|
|
790
|
+
// Ensure the CI trigger includes both branches so the workflow runs on PRs targeting
|
|
791
|
+
// either. Preserve any user-customized branches the prior step already picked.
|
|
792
|
+
if (base.ci.trigger.mode === "branches") {
|
|
793
|
+
const next = new Set(base.ci.trigger.branches);
|
|
794
|
+
next.add(result.mainBranch);
|
|
795
|
+
if (result.devBranch) next.add(result.devBranch);
|
|
796
|
+
base.ci.trigger = { mode: "branches", branches: Array.from(next) };
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* Harden-mode entry point for Git verification. Mutates the persisted design spec in
|
|
802
|
+
* place so the downstream implement stage re-renders the workflow with the new `git`
|
|
803
|
+
* block. Returns true when a new `ci.git` block was captured and persisted so the
|
|
804
|
+
* caller can force-re-run the affected stages; false when the user declined or no UI
|
|
805
|
+
* is available.
|
|
806
|
+
*/
|
|
807
|
+
async function runGitVerificationOnHarden(
|
|
808
|
+
platform: Platform,
|
|
809
|
+
ctx: HarnessCommandContext,
|
|
810
|
+
sessionId: string,
|
|
811
|
+
spec: HarnessDesignSpec,
|
|
812
|
+
): Promise<boolean> {
|
|
813
|
+
await runGitVerificationStep(platform, ctx, spec);
|
|
814
|
+
if (!spec.ci.git) return false; // user declined
|
|
815
|
+
const persisted = saveHarnessDesignSpecJson(platform.paths, ctx.cwd, sessionId, spec);
|
|
816
|
+
if (!persisted.ok) {
|
|
817
|
+
notifyInfo(
|
|
818
|
+
ctx,
|
|
819
|
+
"Git verification persisted partially",
|
|
820
|
+
`In-memory spec updated, but persistence failed: ${persisted.error.message}. ` +
|
|
821
|
+
`Re-run /supi:harness to retry.`,
|
|
822
|
+
);
|
|
823
|
+
return false;
|
|
824
|
+
}
|
|
825
|
+
return true;
|
|
744
826
|
}
|
|
745
827
|
|
|
746
828
|
|
|
@@ -902,7 +984,17 @@ async function handleBareEntry(platform: Platform, ctx: HarnessCommandContext):
|
|
|
902
984
|
if (layerCount >= 2) {
|
|
903
985
|
await promptDocsTierIfNeeded(platform, ctx, sessionId, layerCount);
|
|
904
986
|
}
|
|
905
|
-
|
|
987
|
+
// Offer the optional Git verification flow on harden when the existing spec has no
|
|
988
|
+
// `ci.git` block. Keeps the harden path lightweight by skipping silently when the
|
|
989
|
+
// user previously declined or completed the verification. When the user captures a
|
|
990
|
+
// new `ci.git` block, force implement + validate to re-run so the workflow file is
|
|
991
|
+
// re-rendered with the `verify-pr-source` job and the validate cross-check fires.
|
|
992
|
+
let forceStages: ReadonlySet<HarnessStage> | undefined;
|
|
993
|
+
if (designSpec.ok && !designSpec.value.ci.git) {
|
|
994
|
+
const captured = await runGitVerificationOnHarden(platform, ctx, sessionId, designSpec.value);
|
|
995
|
+
if (captured) forceStages = new Set<HarnessStage>(["implement", "validate"]);
|
|
996
|
+
}
|
|
997
|
+
await runPipelineWithProgress(platform, ctx, sessionId, "auto", {}, undefined, forceStages);
|
|
906
998
|
} else {
|
|
907
999
|
// Rebuild: full regeneration with user gates at each stage.
|
|
908
1000
|
await runRebuildWithGates(platform, ctx, sessionId);
|