@nt-ai-lab/opencode-skillz 0.3.15 → 0.4.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.
Files changed (44) hide show
  1. package/AGENTS.md +31 -0
  2. package/agents/default.md +4 -0
  3. package/agents/facilitator.md +63 -0
  4. package/agents/tdd.md +1 -0
  5. package/commands/plan.md +135 -45
  6. package/commands/resolve-pr-feedback.md +138 -0
  7. package/dist/commands/dont-stop/hooks.js +4 -10
  8. package/dist/git-workflow-gates.d.ts +21 -0
  9. package/dist/git-workflow-gates.js +103 -0
  10. package/dist/plugin-registry/agents.js +0 -4
  11. package/dist/plugin-registry/commands.js +0 -2
  12. package/dist/plugin-registry/index.js +29 -2
  13. package/dist/tools/create-pr-tool.d.ts +4 -0
  14. package/dist/tools/create-pr-tool.js +22 -0
  15. package/dist/tools/infra/lint/guidance.d.ts +8 -0
  16. package/dist/tools/infra/lint/guidance.js +98 -0
  17. package/dist/tools/{lint-review.d.ts → infra/lint/review.d.ts} +1 -1
  18. package/dist/tools/{lint-review.js → infra/lint/review.js} +1 -1
  19. package/dist/tools/infra/pull-request/create-draft-pull-request.d.ts +11 -0
  20. package/dist/tools/infra/pull-request/create-draft-pull-request.js +115 -0
  21. package/dist/tools/infra/pull-request/feedback.d.ts +14 -0
  22. package/dist/tools/infra/pull-request/feedback.js +280 -0
  23. package/dist/tools/infra/vitest-coverage/command.d.ts +16 -0
  24. package/dist/tools/infra/vitest-coverage/command.js +85 -0
  25. package/dist/tools/{vitest-coverage.d.ts → infra/vitest-coverage/review.d.ts} +1 -18
  26. package/dist/tools/{vitest-coverage.js → infra/vitest-coverage/review.js} +15 -52
  27. package/dist/tools/infra/vitest-coverage/test-support.d.ts +15 -0
  28. package/dist/tools/infra/vitest-coverage/test-support.js +156 -0
  29. package/dist/tools/lint.d.ts +3 -17
  30. package/dist/tools/lint.js +29 -18
  31. package/dist/tools/pull-request-feedback-tool.d.ts +5 -0
  32. package/dist/tools/pull-request-feedback-tool.js +23 -0
  33. package/dist/tools/vitest-coverage-tool.d.ts +4 -0
  34. package/dist/tools/vitest-coverage-tool.js +31 -0
  35. package/dist/types.d.ts +8 -0
  36. package/package.json +4 -3
  37. package/scripts/check-tools-folder-boundary.mjs +78 -0
  38. package/scripts/install-git-hooks.mjs +42 -4
  39. package/scripts/lint-ts.mjs +32 -7
  40. package/scripts/living-architecture-eslint.config.mjs +2 -2
  41. package/scripts/no-generic-names-eslint-rule.mjs +2 -24
  42. package/scripts/no-generic-names-eslint-rule.mjs.d.ts +28 -0
  43. /package/dist/tools/{pull-request-files.d.ts → infra/source-control/changed-files.d.ts} +0 -0
  44. /package/dist/tools/{pull-request-files.js → infra/source-control/changed-files.js} +0 -0
@@ -0,0 +1,103 @@
1
+ export const directPullRequestCreationBlockedMessage = [
2
+ "Direct gh pr create is banned for this workspace.",
3
+ "Use nt_skillz_create_pr instead.",
4
+ ].join("\n");
5
+ class GitWorkflowGateError extends Error {
6
+ constructor(message) {
7
+ super(message);
8
+ }
9
+ }
10
+ function readBashCommandText(value) {
11
+ if (typeof value !== "string") {
12
+ throw new GitWorkflowGateError(`Expected bash command text. Got ${String(value)}.`);
13
+ }
14
+ return value.trim();
15
+ }
16
+ function readCommandFailureMessage(commandResult) {
17
+ const stderr = commandResult.stderr.trim();
18
+ if (stderr) {
19
+ return stderr;
20
+ }
21
+ const stdout = commandResult.stdout.trim();
22
+ if (stdout) {
23
+ return stdout;
24
+ }
25
+ return `exit status ${commandResult.status}`;
26
+ }
27
+ function isDirectPullRequestCreation(commandText) {
28
+ return /(?:^|[;&|()]|\s)gh\s+pr\s+create(?:\s|$|[;&|()])/.test(commandText);
29
+ }
30
+ function isGitCommit(commandText) {
31
+ return /(?:^|[;&|()]|\s)git\s+commit(?:\s|$|[;&|()])/.test(commandText);
32
+ }
33
+ function normalizeCommandOutput(commandResult, description) {
34
+ if (commandResult.errorMessage) {
35
+ throw new GitWorkflowGateError(`Expected ${description} to run. Got ${commandResult.errorMessage}.`);
36
+ }
37
+ if (commandResult.status !== 0) {
38
+ throw new GitWorkflowGateError(`Expected ${description} to succeed. Got ${readCommandFailureMessage(commandResult)}.`);
39
+ }
40
+ return commandResult.stdout.trim();
41
+ }
42
+ function isTypeScriptFilePath(filePath) {
43
+ return filePath.endsWith(".ts") || filePath.endsWith(".tsx");
44
+ }
45
+ function createUnlintedCommitMessage(filePaths) {
46
+ return [
47
+ "Commit blocked: TypeScript files changed without nt_skillz_lint validation.",
48
+ `Run nt_skillz_lint with files: ${JSON.stringify(filePaths)}`,
49
+ "Then retry the commit.",
50
+ ].join("\n");
51
+ }
52
+ export function createGitWorkflowGate(repositoryRoot, commandRunner) {
53
+ const lintedFingerprints = new Map();
54
+ function runGit(commandArguments, description) {
55
+ return normalizeCommandOutput(commandRunner.run("git", commandArguments, repositoryRoot), description);
56
+ }
57
+ function readWorkingTreeFingerprint(filePath) {
58
+ return runGit(["hash-object", "--", filePath], `working tree hash for ${filePath}`);
59
+ }
60
+ function readStagedFingerprint(filePath) {
61
+ return runGit(["rev-parse", `:${filePath}`], `staged hash for ${filePath}`);
62
+ }
63
+ function readStagedTypeScriptFilePaths() {
64
+ const output = runGit([
65
+ "diff",
66
+ "--name-only",
67
+ "--cached",
68
+ "--diff-filter=ACMR",
69
+ "--",
70
+ "*.ts",
71
+ "*.tsx",
72
+ ], "staged TypeScript file discovery");
73
+ return output.split("\n").map((filePath) => filePath.trim()).filter(Boolean);
74
+ }
75
+ function ensureStagedTypeScriptFilesAreLinted() {
76
+ const stagedFilePaths = readStagedTypeScriptFilePaths();
77
+ const unlintedFilePaths = stagedFilePaths.filter((filePath) => lintedFingerprints.get(filePath) !== readStagedFingerprint(filePath));
78
+ if (unlintedFilePaths.length === 0) {
79
+ return;
80
+ }
81
+ throw new GitWorkflowGateError(createUnlintedCommitMessage(unlintedFilePaths));
82
+ }
83
+ return {
84
+ beforeToolExecution(input, output) {
85
+ if (input.tool !== "bash") {
86
+ return;
87
+ }
88
+ const commandText = readBashCommandText(output.args.command);
89
+ if (isDirectPullRequestCreation(commandText)) {
90
+ throw new GitWorkflowGateError(directPullRequestCreationBlockedMessage);
91
+ }
92
+ if (isGitCommit(commandText)) {
93
+ ensureStagedTypeScriptFilesAreLinted();
94
+ }
95
+ },
96
+ recordLintedFiles(filePaths) {
97
+ const typeScriptFilePaths = filePaths.filter(isTypeScriptFilePath);
98
+ for (const filePath of typeScriptFilePaths) {
99
+ lintedFingerprints.set(filePath, readWorkingTreeFingerprint(filePath));
100
+ }
101
+ },
102
+ };
103
+ }
@@ -15,8 +15,6 @@ function getParentAgentName(rawAgent) {
15
15
  }
16
16
  function appendAgentPromptParts(promptParts, rawAgents, name) {
17
17
  const rawAgent = rawAgents[name];
18
- if (!rawAgent)
19
- return;
20
18
  const parentAgentName = getParentAgentName(rawAgent);
21
19
  if (parentAgentName && rawAgents[parentAgentName]?.body) {
22
20
  promptParts.push(rawAgents[parentAgentName].body);
@@ -47,8 +45,6 @@ export function registerAgents(agentConfig, pluginRoot, commands) {
47
45
  const rawAgents = readMarkdownEntries(pluginRoot, "agents");
48
46
  function collectPreloadedCommands(agentName, stack = new Set()) {
49
47
  const rawAgent = rawAgents[agentName];
50
- if (!rawAgent)
51
- return [];
52
48
  if (stack.has(agentName))
53
49
  return parseCsvList(rawAgent.meta.preload_commands);
54
50
  stack.add(agentName);
@@ -19,8 +19,6 @@ function loadMarkdownCommands(pluginRoot) {
19
19
  const commands = {};
20
20
  function buildComposedTemplate(name, stack = new Set()) {
21
21
  const rawCommand = rawCommands[name];
22
- if (!rawCommand)
23
- return "";
24
22
  if (stack.has(name))
25
23
  return rawCommand.body;
26
24
  stack.add(name);
@@ -1,16 +1,43 @@
1
+ import process from "node:process";
1
2
  import { createDontStopHooks } from "../commands/dont-stop/index.js";
3
+ import { createGitWorkflowGate, } from "../git-workflow-gates.js";
4
+ import { createPullRequestTool, } from "../tools/create-pr-tool.js";
2
5
  import { LINT_TOOL_NAME, lintTool, } from "../tools/lint.js";
3
- import { VITEST_COVERAGE_TOOL_NAME, vitestCoverageTool, } from "../tools/vitest-coverage.js";
6
+ import { PULL_REQUEST_FEEDBACK_TOOL_NAME, pullRequestFeedbackTool, } from "../tools/pull-request-feedback-tool.js";
7
+ import { VITEST_COVERAGE_TOOL_NAME, vitestCoverageTool, } from "../tools/vitest-coverage-tool.js";
4
8
  import { registerAgents } from "./agents.js";
5
9
  import { registerCommands } from "./commands.js";
10
+ import { childProcessCommandRunner } from "../tools/infra/source-control/changed-files.js";
11
+ function readLintedFilePaths(request) {
12
+ if (!Array.isArray(request.files)) {
13
+ return [];
14
+ }
15
+ return request.files.filter((filePath) => typeof filePath === "string");
16
+ }
17
+ function createGateAwareLintTool(gate) {
18
+ return {
19
+ ...lintTool,
20
+ async execute(request, context) {
21
+ const result = await lintTool.execute(request, context);
22
+ gate.recordLintedFiles(readLintedFilePaths(request));
23
+ return result;
24
+ },
25
+ };
26
+ }
6
27
  export function createPluginRegistry(input, pluginRoot) {
7
28
  const dontStopHooks = createDontStopHooks(input.client);
29
+ const gitWorkflowGate = createGitWorkflowGate(input.worktree ?? process.cwd(), childProcessCommandRunner);
8
30
  return {
9
31
  ...dontStopHooks,
10
32
  tool: {
11
- [LINT_TOOL_NAME]: lintTool,
33
+ nt_skillz_create_pr: createPullRequestTool,
34
+ [LINT_TOOL_NAME]: createGateAwareLintTool(gitWorkflowGate),
35
+ [PULL_REQUEST_FEEDBACK_TOOL_NAME]: pullRequestFeedbackTool,
12
36
  [VITEST_COVERAGE_TOOL_NAME]: vitestCoverageTool,
13
37
  },
38
+ "tool.execute.before": async (hookInput, hookOutput) => {
39
+ gitWorkflowGate.beforeToolExecution(hookInput, hookOutput);
40
+ },
14
41
  config: async (config) => {
15
42
  config.command ??= {};
16
43
  config.agent ??= {};
@@ -0,0 +1,4 @@
1
+ import { type ToolDefinition } from "@opencode-ai/plugin";
2
+ import type { CommandRunner } from "../git-workflow-gates.js";
3
+ export declare function createPullRequestToolWithRunner(commandRunner: CommandRunner): ToolDefinition;
4
+ export declare const createPullRequestTool: ToolDefinition;
@@ -0,0 +1,22 @@
1
+ import { tool } from "@opencode-ai/plugin";
2
+ import { createDraftPullRequest } from "./infra/pull-request/create-draft-pull-request.js";
3
+ import { childProcessCommandRunner } from "./infra/source-control/changed-files.js";
4
+ export function createPullRequestToolWithRunner(commandRunner) {
5
+ return tool({
6
+ description: "Create a validated draft pull request with required OpenCode workflow gates.",
7
+ args: {
8
+ base: tool.schema.string().describe("Base branch for the pull request."),
9
+ title: tool.schema.string().describe("Pull request title."),
10
+ problem: tool.schema.string().describe("Problem section content."),
11
+ solution: tool.schema.string().describe("Solution section content."),
12
+ acceptanceCriteria: tool.schema.string().describe("Acceptance Criteria section content."),
13
+ architectureAndSoftwareDesign: tool.schema.string().describe("Architecture and software design section content."),
14
+ },
15
+ async execute(request, context) {
16
+ return {
17
+ output: createDraftPullRequest(context.worktree, request, commandRunner),
18
+ };
19
+ },
20
+ });
21
+ }
22
+ export const createPullRequestTool = createPullRequestToolWithRunner(childProcessCommandRunner);
@@ -0,0 +1,8 @@
1
+ interface LintMessageWithRule {
2
+ ruleId: string | null;
3
+ }
4
+ interface LintResultWithMessages {
5
+ messages: readonly LintMessageWithRule[];
6
+ }
7
+ export declare function createLintFailureGuidance(lintResults: readonly LintResultWithMessages[]): string;
8
+ export {};
@@ -0,0 +1,98 @@
1
+ const genericLintFailureGuidance = [
2
+ "Lint remediation guidance:",
3
+ "- Do not sacrifice code quality or test coverage to satisfy lint rules.",
4
+ "- These rules are not objectives; they are signs that code needs to be split, simplified, or clarified.",
5
+ "- Fix the underlying design or test issue instead of deleting assertions, disabling rules, or reducing coverage.",
6
+ ];
7
+ const targetedLintGuidance = [
8
+ {
9
+ ruleIds: ["vitest/max-expects"],
10
+ guidance: [
11
+ "Do not delete required assertions or weaken expected values to reduce assertion count.",
12
+ "If several assertions describe one observable result, prefer one whole-result assertion such as `toEqual`, `toStrictEqual`, `toMatchObject`, or a domain-specific matcher.",
13
+ "If the test verifies multiple behaviours, split it into separate tests with outcome-focused names.",
14
+ "Do not combine unrelated checks into one object assertion just to satisfy this rule.",
15
+ ],
16
+ },
17
+ {
18
+ ruleIds: ["max-lines", "sonarjs/max-lines", "sonarjs/max-lines-per-function"],
19
+ guidance: [
20
+ "Do not delete required behavior, tests, setup, edge cases, or assertions to reduce line count.",
21
+ "Split by cohesive responsibility instead: production behavior, test fixture construction, assertions, adapters, or domain concepts.",
22
+ "For long spec files, extract fixture builders or split scenarios by behavior.",
23
+ "For long source files, extract named concepts that can be tested independently.",
24
+ "Do not move unrelated code into vague files such as utils, helpers, common, shared, or lib.",
25
+ ],
26
+ },
27
+ {
28
+ ruleIds: [
29
+ "complexity",
30
+ "max-depth",
31
+ "sonarjs/cognitive-complexity",
32
+ "sonarjs/cyclomatic-complexity",
33
+ "sonarjs/nested-control-flow",
34
+ ],
35
+ guidance: [
36
+ "Do not remove branches, states, error handling, or edge cases to reduce complexity.",
37
+ "Reduce complexity by making decisions easier to read without changing behavior.",
38
+ "Use guard clauses, early returns, named predicates, or discriminated unions where they clarify the code.",
39
+ "If complexity comes from multiple states, model those states directly instead of stacking conditionals.",
40
+ "Keep all existing behavior, tests, and edge cases intact.",
41
+ ],
42
+ },
43
+ {
44
+ ruleIds: [
45
+ "@typescript-eslint/no-explicit-any",
46
+ "@typescript-eslint/no-unsafe-assignment",
47
+ "@typescript-eslint/no-unsafe-call",
48
+ "@typescript-eslint/no-unsafe-member-access",
49
+ "@typescript-eslint/no-unsafe-return",
50
+ "@typescript-eslint/consistent-type-assertions",
51
+ "@typescript-eslint/no-non-null-assertion",
52
+ "sonarjs/no-return-type-any",
53
+ ],
54
+ guidance: [
55
+ "Do not replace `any` with `unknown as X`, non-null assertions, type assertions, or broader unsafe types.",
56
+ "Add precise types at the boundary where the value enters the system.",
57
+ "For external input, parse with Zod or an existing runtime validator.",
58
+ "For impossible states, change the type model instead of silencing the error.",
59
+ "Keep behavior unchanged and preserve existing validation.",
60
+ ],
61
+ },
62
+ {
63
+ ruleIds: [
64
+ "@eslint-community/eslint-comments/no-use",
65
+ "no-inline-comments",
66
+ "sonarjs/no-commented-code",
67
+ "sonarjs/no-sonar-comments",
68
+ "@typescript-eslint/ban-ts-comment",
69
+ ],
70
+ guidance: [
71
+ "Do not add code comments. They make the code noisy and are a workaround for poor code. Make code intention revealing so that comments are not necessary.",
72
+ "Do not add lint or TypeScript suppressions. The rules are non-negotiable. If the solution cannot be implemented while following the codebase rules, stop and ask for help.",
73
+ ],
74
+ },
75
+ ];
76
+ function isPresentRuleId(ruleId) {
77
+ return typeof ruleId === "string" && ruleId.length > 0;
78
+ }
79
+ function readTriggeredRuleIds(lintResults) {
80
+ return new Set(lintResults.flatMap((lintResult) => {
81
+ return lintResult.messages.map((message) => message.ruleId).filter(isPresentRuleId);
82
+ }));
83
+ }
84
+ export function createLintFailureGuidance(lintResults) {
85
+ const triggeredRuleIds = readTriggeredRuleIds(lintResults);
86
+ const specificGuidance = targetedLintGuidance
87
+ .filter((targetedGuidance) => targetedGuidance.ruleIds.some((ruleId) => triggeredRuleIds.has(ruleId)))
88
+ .flatMap((targetedGuidance) => targetedGuidance.guidance);
89
+ if (specificGuidance.length === 0) {
90
+ return genericLintFailureGuidance.join("\n");
91
+ }
92
+ return [
93
+ ...genericLintFailureGuidance,
94
+ "",
95
+ "Rule-specific guidance:",
96
+ ...specificGuidance.map((guidance) => `- ${guidance}`),
97
+ ].join("\n");
98
+ }
@@ -1,4 +1,4 @@
1
- import { type CommandRunner } from "./pull-request-files.js";
1
+ import { type CommandRunner } from "../source-control/changed-files.js";
2
2
  export interface LintReviewRequest {
3
3
  repositoryRoot?: string;
4
4
  pullRequest?: string;
@@ -1,6 +1,6 @@
1
1
  import path from "node:path";
2
2
  import process from "node:process";
3
- import { resolvePullRequestChangedFiles, } from "./pull-request-files.js";
3
+ import { resolvePullRequestChangedFiles, } from "../source-control/changed-files.js";
4
4
  function normalizeTypeScriptFiles(filePaths) {
5
5
  return filePaths.filter((filePath) => filePath.endsWith(".ts") || filePath.endsWith(".tsx"));
6
6
  }
@@ -0,0 +1,11 @@
1
+ import type { CommandRunner } from "../../../git-workflow-gates.js";
2
+ export declare const CREATE_PULL_REQUEST_TOOL_NAME = "nt_skillz_create_pr";
3
+ export interface CreatePullRequestRequest {
4
+ base: string;
5
+ title: string;
6
+ problem: string;
7
+ solution: string;
8
+ acceptanceCriteria: string;
9
+ architectureAndSoftwareDesign: string;
10
+ }
11
+ export declare function createDraftPullRequest(repositoryRoot: string, request: CreatePullRequestRequest, commandRunner: CommandRunner): string;
@@ -0,0 +1,115 @@
1
+ export const CREATE_PULL_REQUEST_TOOL_NAME = "nt_skillz_create_pr";
2
+ const semanticCommitTitlePattern = /^(feat|fix|refactor|perf|docs|test|build|ci|release|chore)\([^)]+\): .+/;
3
+ class CreatePullRequestError extends Error {
4
+ constructor(message) {
5
+ super(message);
6
+ }
7
+ }
8
+ function normalizeRequiredText(value, fieldName) {
9
+ const trimmedValue = value.trim();
10
+ if (trimmedValue) {
11
+ return trimmedValue;
12
+ }
13
+ throw new CreatePullRequestError(`Expected ${fieldName} to be provided. Got empty text.`);
14
+ }
15
+ function runCommand(repositoryRoot, commandRunner, executable, commandArguments, description) {
16
+ const commandResult = commandRunner.run(executable, commandArguments, repositoryRoot);
17
+ if (commandResult.errorMessage) {
18
+ throw new CreatePullRequestError(`Expected ${description} to run. Got ${commandResult.errorMessage}.`);
19
+ }
20
+ if (commandResult.status !== 0) {
21
+ throw new CreatePullRequestError(`Expected ${description} to succeed. Got ${readCommandFailureMessage(commandResult)}.`);
22
+ }
23
+ return commandResult.stdout.trim();
24
+ }
25
+ function readCommandFailureMessage(commandResult) {
26
+ const stderr = commandResult.stderr.trim();
27
+ if (stderr) {
28
+ return stderr;
29
+ }
30
+ const stdout = commandResult.stdout.trim();
31
+ if (stdout) {
32
+ return stdout;
33
+ }
34
+ return `exit status ${commandResult.status}`;
35
+ }
36
+ function validateCleanWorkingTree(repositoryRoot, commandRunner) {
37
+ const statusOutput = runCommand(repositoryRoot, commandRunner, "git", ["status", "--porcelain"], "working tree status check");
38
+ if (!statusOutput) {
39
+ return;
40
+ }
41
+ throw new CreatePullRequestError(`Expected working tree to be clean before pull request creation. Got ${statusOutput}.`);
42
+ }
43
+ function validateSemanticCommitTitles(repositoryRoot, commandRunner, base) {
44
+ const commitTitleOutput = runCommand(repositoryRoot, commandRunner, "git", ["log", "--format=%s", `${base}..HEAD`], "pull request commit title discovery");
45
+ const commitTitles = commitTitleOutput.split("\n").map((commitTitle) => commitTitle.trim()).filter(Boolean);
46
+ const invalidCommitTitle = commitTitles.find((commitTitle) => !semanticCommitTitlePattern.test(commitTitle));
47
+ if (!invalidCommitTitle) {
48
+ return;
49
+ }
50
+ throw new CreatePullRequestError(`Expected semantic commit title format <type>(<scope>): <short summary>. Got ${invalidCommitTitle}.`);
51
+ }
52
+ function readCurrentBranch(repositoryRoot, commandRunner) {
53
+ const branchName = runCommand(repositoryRoot, commandRunner, "git", ["branch", "--show-current"], "current branch discovery");
54
+ if (branchName) {
55
+ return branchName;
56
+ }
57
+ throw new CreatePullRequestError("Expected current branch name to be non-empty. Got empty text.");
58
+ }
59
+ function isMissingUpstreamResult(commandResult) {
60
+ if (commandResult.errorMessage) {
61
+ return false;
62
+ }
63
+ return readCommandFailureMessage(commandResult).includes("no upstream");
64
+ }
65
+ function readUpstreamLookupFailureMessage(commandResult) {
66
+ if (commandResult.errorMessage) {
67
+ return commandResult.errorMessage;
68
+ }
69
+ return readCommandFailureMessage(commandResult);
70
+ }
71
+ function pushBranchWhenUpstreamIsMissing(repositoryRoot, commandRunner, branchName) {
72
+ const upstreamResult = commandRunner.run("git", ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"], repositoryRoot);
73
+ if (upstreamResult.status === 0) {
74
+ return;
75
+ }
76
+ if (!isMissingUpstreamResult(upstreamResult)) {
77
+ throw new CreatePullRequestError(`Expected upstream lookup to succeed or report missing upstream. Got ${readUpstreamLookupFailureMessage(upstreamResult)}.`);
78
+ }
79
+ runCommand(repositoryRoot, commandRunner, "git", ["push", "-u", "origin", branchName], "branch push");
80
+ }
81
+ function createPullRequestBody(request) {
82
+ return [
83
+ "## Problem",
84
+ normalizeRequiredText(request.problem, "problem"),
85
+ "",
86
+ "## Solution",
87
+ normalizeRequiredText(request.solution, "solution"),
88
+ "",
89
+ "## Acceptance Criteria",
90
+ normalizeRequiredText(request.acceptanceCriteria, "acceptanceCriteria"),
91
+ "",
92
+ "## Architecture and software design",
93
+ normalizeRequiredText(request.architectureAndSoftwareDesign, "architectureAndSoftwareDesign"),
94
+ ].join("\n");
95
+ }
96
+ export function createDraftPullRequest(repositoryRoot, request, commandRunner) {
97
+ const base = normalizeRequiredText(request.base, "base");
98
+ const title = normalizeRequiredText(request.title, "title");
99
+ const body = createPullRequestBody(request);
100
+ validateCleanWorkingTree(repositoryRoot, commandRunner);
101
+ validateSemanticCommitTitles(repositoryRoot, commandRunner, base);
102
+ const branchName = readCurrentBranch(repositoryRoot, commandRunner);
103
+ pushBranchWhenUpstreamIsMissing(repositoryRoot, commandRunner, branchName);
104
+ return runCommand(repositoryRoot, commandRunner, "gh", [
105
+ "pr",
106
+ "create",
107
+ "--draft",
108
+ "--base",
109
+ base,
110
+ "--title",
111
+ title,
112
+ "--body",
113
+ body,
114
+ ], "draft pull request creation");
115
+ }
@@ -0,0 +1,14 @@
1
+ import type { CommandRunner } from "../source-control/changed-files.js";
2
+ export declare const PULL_REQUEST_FEEDBACK_TOOL_NAME = "nt_skillz_pr_feedback";
3
+ export declare class PullRequestFeedbackError extends Error {
4
+ constructor(message: string);
5
+ }
6
+ export interface PullRequestFeedbackRequest {
7
+ repositoryRoot: string;
8
+ pullRequestNumber: string;
9
+ pullRequestUrl: string;
10
+ }
11
+ export interface PullRequestFeedbackDependencies {
12
+ commandRunner?: CommandRunner;
13
+ }
14
+ export declare function readPullRequestFeedback(request: PullRequestFeedbackRequest, dependencies?: PullRequestFeedbackDependencies): string;