pi-onedev-toolkit 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/README.md +9 -1
- package/package.json +5 -4
- package/skills/onedev-issue-to-pr/SKILL.md +16 -50
- package/skills/onedev-pr-review/SKILL.md +19 -62
- package/src/source-watch.ts +61 -19
- package/src/tools/build.ts +60 -40
- package/src/tools/common.ts +257 -5
- package/src/tools/index.ts +46 -7
- package/src/tools/issue.ts +34 -30
- package/src/tools/pull.ts +44 -59
- package/src/tools/watch.ts +3 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.0 — 2026-09-05
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- `issue`, `pull`, and `build` `list` actions now return a one-line-per-item digest instead of raw JSON (raw list items embed full Markdown descriptions); pass `full: true` for the raw payload. Digests fetch internally up to 512 KB; larger or non-JSON payloads fall back to the normal bounded raw output.
|
|
8
|
+
- Query parameter descriptions now include common OneDev query examples (`open`, `submitted by "user"`, `Assignees is "user"`, `order by`), so most queries no longer need the 8–11 KB grammar dump from `query_description`.
|
|
9
|
+
- `onedev_context` gains `list_projects` to discover accessible projects without activating a domain tool; it works from any repository once TOD is configured.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- One-shot watch notifications can no longer be lost when the emission consumer throws.
|
|
14
|
+
- Watch polls record `lastError` (visible in `onedev_watch list`) instead of failing silently, and clear it on recovery.
|
|
15
|
+
- Pull-attention watches read state only from top-level fields, so a nested build status can no longer masquerade as the PR state and cause spurious or missed wakes.
|
|
16
|
+
- Watch snapshot calls are bounded to 10 seconds so one slow TOD call cannot stall other watches.
|
|
17
|
+
|
|
18
|
+
## 0.2.2 — 2026-09-04
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- Compressed model-visible tool schemas and bundled workflow prompts without changing tool names, actions, validations, or approval gates; tests now enforce schema and skill byte budgets.
|
|
23
|
+
- Model-visible output now defaults to 16 KB for normal results and 32 KB for PR patches, build logs, and build-spec schemas. `max_bytes` can request up to 128 KB; internal watch polling remains independent.
|
|
24
|
+
|
|
3
25
|
## 0.2.1 — 2026-09-03
|
|
4
26
|
|
|
5
27
|
### Fixed
|
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ When the current directory is a OneDev repository, the status line shows the act
|
|
|
28
28
|
|
|
29
29
|
| Tool | Domains | Covers |
|
|
30
30
|
| --- | --- | --- |
|
|
31
|
-
| `onedev_context` (always active) | — | Readiness, authenticated health, login,
|
|
31
|
+
| `onedev_context` (always active) | — | Readiness, authenticated health, login, setup guidance, and accessible project discovery |
|
|
32
32
|
| `onedev_tools` (always active) | — | Activate only the issue, pull, build, and watch domains needed now |
|
|
33
33
|
| `onedev_issue` | issue | Query/get/comment issues, change fields/labels/state, log work, issue branches |
|
|
34
34
|
| `onedev_pull` | pull | PR metadata/comments/code comments/labels/builds/patch, create/edit, reply/resolve review comments, approve, request changes, merge/discard |
|
|
@@ -37,6 +37,8 @@ When the current directory is a OneDev repository, the status line shows the act
|
|
|
37
37
|
|
|
38
38
|
Lazy by design: domain tools activate only through `onedev_tools`, keeping the default tool list and system prompt light. Activation performs the first authenticated check and returns the server, project, and login context in the same response.
|
|
39
39
|
|
|
40
|
+
Model-visible issue, PR, and build output defaults to 16 KB. PR patches, build logs, and build-spec schemas default to 32 KB. Use `max_bytes` to raise or lower a call's budget up to 128 KB; truncation keeps the payload head/tail and is reported explicitly. List actions instead return one-line-per-item digests by default (pass `full` for the raw payload), so bulk queries stay small without losing items; payloads beyond the 512 KB internal fetch fall back to the normal bounded raw output. Internal watch polling remains separately bounded and is never copied into the model context.
|
|
41
|
+
|
|
40
42
|
Remote and local mutations use Pi's real interactive approval UI. An approval can apply once, for the session, or persist for the exact server/project/operation in `~/.pi/agent/onedev-toolkit.json`; remove that file to revoke all saved approvals. Non-interactive sessions reject unapproved mutations. `build run` in local mode uses an isolated temporary Git index so TOD cannot leave the user's index staged; `check_spec` is treated as a mutation because TOD may upgrade `.onedev-buildspec.yml`.
|
|
41
43
|
|
|
42
44
|
Watches are session-scoped and one-shot. They use one timer, bounded polling, expiration and backoff, and wake the agent with metadata only. The agent retrieves logs, comments, or build details with the corresponding read tool. At most ten watches may be active; session changes and shutdown clear them.
|
|
@@ -60,6 +62,12 @@ Attachment download is intentionally not exposed: current TOD releases may send
|
|
|
60
62
|
- `onedev-issue-to-pr` — gather issue context, branch, implement, present for review, then push and open the PR
|
|
61
63
|
- `onedev-pr-review` — full-context PR review with line-anchored findings; submits only on explicit request
|
|
62
64
|
|
|
65
|
+
If you use only tools and slash commands, omit both workflow entries from every prompt while keeping the extension and tools:
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
{ "source": "npm:pi-onedev-toolkit", "skills": [] }
|
|
69
|
+
```
|
|
70
|
+
|
|
63
71
|
## Development
|
|
64
72
|
|
|
65
73
|
```bash
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-onedev-toolkit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Lightweight OneDev integration for Pi via TOD: repo-aware issue, pull request, build, and watch tools",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": {
|
|
@@ -53,11 +53,12 @@
|
|
|
53
53
|
"typebox": "*"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
|
-
"@earendil-works/pi-ai": "0.
|
|
57
|
-
"@earendil-works/pi-
|
|
56
|
+
"@earendil-works/pi-ai": "0.85.0",
|
|
57
|
+
"@earendil-works/pi-server": "0.85.0",
|
|
58
|
+
"@earendil-works/pi-coding-agent": "0.85.0",
|
|
58
59
|
"@types/node": "^22.19.0",
|
|
59
60
|
"@types/proper-lockfile": "4.1.4",
|
|
60
|
-
"typebox": "1.3.
|
|
61
|
+
"typebox": "1.3.25",
|
|
61
62
|
"typescript": "^7.0.2",
|
|
62
63
|
"vitest": "^4.1.10"
|
|
63
64
|
},
|
|
@@ -1,60 +1,26 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: onedev-issue-to-pr
|
|
3
|
-
description: Implement a OneDev issue
|
|
3
|
+
description: "Implement a OneDev issue and open a linked PR only after user approval."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# OneDev
|
|
6
|
+
# OneDev Issue to Pull Request
|
|
7
7
|
|
|
8
|
-
##
|
|
8
|
+
## Rules
|
|
9
9
|
|
|
10
|
-
-
|
|
11
|
-
-
|
|
10
|
+
- Activate `issue` and `pull` together with `onedev_tools`; note the returned login. On readiness failure, use `onedev_context setup` and stop.
|
|
11
|
+
- Plain refs use the current repository's OneDev project. Never guess a missing ref or project.
|
|
12
|
+
- Preserve unrelated local changes. If checkout is dirty, report and stop; never stash automatically.
|
|
13
|
+
- Do not push, comment, or open a PR before the workflow checkpoint and explicit user approval. Toolkit mutations still require Pi UI approval.
|
|
12
14
|
|
|
13
15
|
## Workflow
|
|
14
16
|
|
|
15
|
-
1.
|
|
16
|
-
|
|
17
|
+
1. Resolve the prompt ref or use `onedev_issue current_reference`; stop if neither works.
|
|
18
|
+
2. Prepare the branch with `onedev_issue checkout` using `for_write: true`.
|
|
19
|
+
3. Read `onedev_issue get` and `get_comments`; use author/login context and derive acceptance criteria from the full discussion.
|
|
20
|
+
4. When failure evidence matters, activate `build`; inspect build metadata and start `get_log` with `max_bytes=32000`, raising it only if truncation hides required evidence. Follow same-commit dependency failures as the likely root cause; different-commit failures are inherited.
|
|
21
|
+
5. Implement the smallest complete change and run the narrowest relevant test. Optionally run local OneDev CI with `onedev_build run mode=local`.
|
|
22
|
+
6. **Stop for review.** Show the change, checks, and any issue comment you intend to post. Do not submit anything.
|
|
23
|
+
7. After explicit approval, read `onedev_pull commit_message_requirement` and `title_description_requirement`; empty output means no extra requirement. Commit and push with local Git, then create the PR against the intended target branch. Link the issue in the description when the issue branch does not imply it. Post only the approved issue comment.
|
|
24
|
+
8. For long CI, activate `watch` and start `onedev_watch watch_build`; re-read the build after the metadata-only notification.
|
|
17
25
|
|
|
18
|
-
|
|
19
|
-
`onedev_issue` action `checkout` with `ref` and `for_write: true`.
|
|
20
|
-
It creates the server-side issue branch and sets up tracking.
|
|
21
|
-
If the working copy is dirty, report it and stop — do not stash on your own.
|
|
22
|
-
|
|
23
|
-
3. **Read the full issue context** — never work from the prompt alone:
|
|
24
|
-
- `onedev_issue` `get` (title, description, fields, submitter)
|
|
25
|
-
- `onedev_issue` `get_comments`
|
|
26
|
-
- Match your login name against submitter/comment authors to understand your role.
|
|
27
|
-
- If a failed build is part of the issue, gather evidence with `onedev_build`
|
|
28
|
-
after activating the `build` domain (`get`, `get_log`, and a named
|
|
29
|
-
`get_unit_test_report` when useful) before planning changes. Follow `Dependency build … failed`
|
|
30
|
-
chains: same-commit dependency failures are the real target; different-commit
|
|
31
|
-
ones mean the failure is inherited.
|
|
32
|
-
|
|
33
|
-
4. **Implement the work.** Code changes, or an explanatory issue comment if no code
|
|
34
|
-
is needed. Run the smallest relevant test. Optionally validate CI locally with
|
|
35
|
-
`onedev_build` `run` (`mode: "local"`) before committing.
|
|
36
|
-
|
|
37
|
-
5. **STOP — present for review.** Summarize the change and the comment you intend
|
|
38
|
-
to post. Do not push or post anything yet. This is the mandatory checkpoint.
|
|
39
|
-
|
|
40
|
-
6. **After explicit user approval, submit:**
|
|
41
|
-
- Compose the commit message against the server requirements:
|
|
42
|
-
`onedev_pull` `commit_message_requirement` (and `title_description_requirement`
|
|
43
|
-
before creating the PR). Empty output means no requirement.
|
|
44
|
-
- `git add -A && git commit && git push` on the issue branch.
|
|
45
|
-
- Create the PR: `onedev_pull` `create` with `title`, `description`, and
|
|
46
|
-
`target_branch` (source branch defaults to the current branch). The issue link
|
|
47
|
-
is inferred from the branch; state it in the description if not.
|
|
48
|
-
- Post the drafted comment with `onedev_issue` `add_comment` if one was prepared.
|
|
49
|
-
|
|
50
|
-
7. **For long-running CI**, activate `watch` and use `onedev_watch` `watch_build`
|
|
51
|
-
after obtaining a build reference. The notification is one-shot and metadata-only;
|
|
52
|
-
inspect the build with `onedev_build` when it fires.
|
|
53
|
-
|
|
54
|
-
## Notes
|
|
55
|
-
|
|
56
|
-
- Write comment text in Markdown.
|
|
57
|
-
- Reference entities as `#123` (current project), `project#123`, or `PROJ-123`.
|
|
58
|
-
- If blocked mid-workflow, report the command and error, and stop.
|
|
59
|
-
- Toolkit mutations still open Pi's interactive approval UI. Do not try to bypass
|
|
60
|
-
it with a confirmation parameter.
|
|
26
|
+
Use Markdown for comments and `#123`, `project#123`, or `PROJ-123` for entities. On failure, report the command/error and stop; never invent a confirmation parameter.
|
|
@@ -1,71 +1,28 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: onedev-pr-review
|
|
3
|
-
description: Review a OneDev
|
|
3
|
+
description: "Review a OneDev PR; publish findings or merge only when asked."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# OneDev
|
|
6
|
+
# OneDev Pull Request Review
|
|
7
7
|
|
|
8
|
-
##
|
|
8
|
+
## Rules
|
|
9
9
|
|
|
10
|
-
-
|
|
11
|
-
-
|
|
10
|
+
- Activate `pull`, `issue`, and `build` together with `onedev_tools`; note the returned login. On readiness failure, use `onedev_context setup` and stop.
|
|
11
|
+
- Resolve the exact PR in its source/target repository. Never review from a patch alone.
|
|
12
|
+
- Analysis is read-only. Draft first; publish findings, change review state, or merge only after explicit user request and Pi UI approval.
|
|
13
|
+
- Never alter another user's code comments.
|
|
12
14
|
|
|
13
|
-
##
|
|
15
|
+
## Workflow
|
|
14
16
|
|
|
15
|
-
1.
|
|
17
|
+
1. Resolve the prompt ref or use `onedev_pull current_reference`.
|
|
18
|
+
2. Checkout for inspection; use `for_write: true` only when changing code. Dirty checkout means report and stop.
|
|
19
|
+
3. Read PR metadata, comments, code comments, and builds. Query fixed issues with `fixed in pull request "<ref>"`, then read their bodies/comments as the primary specification. Match authors against the returned login.
|
|
20
|
+
4. Investigate every failed build. Start `onedev_build get_log` with `max_bytes=32000`; raise it only if truncation hides required evidence. Follow same-commit dependency failures before blaming the current job.
|
|
21
|
+
5. Read `onedev_pull get_patch` with `for_code_review: true` and `max_bytes=32000`; inspect checkout files and callers when context is missing.
|
|
22
|
+
6. Keep only concrete correctness, regression, security, data-loss, error-handling, or coverage findings. Triage prior discussion: reply where needed, resolve only addressed comments you own, and leave unrelated comments alone.
|
|
23
|
+
7. Draft line findings with file/right-side lines, outside-patch findings as one overall comment, and `approve` or `request_changes` only when acting as a pending reviewer.
|
|
24
|
+
8. **Stop and present** findings, replies, outcome, and merge plan. Submit nothing.
|
|
25
|
+
9. On explicit request, publish the approved draft. For merge, satisfy `commit_message_requirement`; squash messages should reflect PR and fixed issues. Ask before rewriting commits for rebase-based conflict resolution.
|
|
26
|
+
10. When waiting, activate `watch` and use `watch_pull_attention`; re-read PR state after its metadata-only notification.
|
|
16
27
|
|
|
17
|
-
|
|
18
|
-
will change code). Dirty working copy → report and stop.
|
|
19
|
-
|
|
20
|
-
3. **Read everything:**
|
|
21
|
-
- `onedev_pull` `get` — title, description, source/target, merge strategy, status,
|
|
22
|
-
submitter, reviewers, review state.
|
|
23
|
-
- `onedev_pull` `get_comments` and `get_code_comments` — note code-comment `id`s,
|
|
24
|
-
file, line range, resolution state, replies.
|
|
25
|
-
- Fixed issues: `onedev_issue` `list` with query `fixed in pull request "<ref>"`,
|
|
26
|
-
then `get` / `get_comments` on each. Treat fixed issues as the primary spec
|
|
27
|
-
for what the PR should accomplish.
|
|
28
|
-
- `onedev_pull` `get_builds` — investigate every `FAILED` build with
|
|
29
|
-
`onedev_build` `get` / `get_log` before reviewing code.
|
|
30
|
-
- Match your login name against authors to understand your role
|
|
31
|
-
(pending reviewer, prior commenter, outsider).
|
|
32
|
-
|
|
33
|
-
## Review
|
|
34
|
-
|
|
35
|
-
1. Read the review-scoped patch: `onedev_pull` `get_patch` with
|
|
36
|
-
`for_code_review: true`. Read files in the checkout when the patch lacks context.
|
|
37
|
-
Check correctness, edge cases, security, style, and test coverage.
|
|
38
|
-
|
|
39
|
-
2. **Triage prior discussion:** resolve addressed code comments, reply where new
|
|
40
|
-
information is needed, leave unrelated unresolved ones alone. Never modify
|
|
41
|
-
another user's code comments. Use `reply_code_comment`, `resolve_code_comment`,
|
|
42
|
-
or `unresolve_code_comment` with the comment `id`.
|
|
43
|
-
|
|
44
|
-
3. **Draft findings:**
|
|
45
|
-
- Line-anchored findings → `onedev_pull` `add_code_comment` (`file`, `from_line`,
|
|
46
|
-
optional `to_line` — must be an added or context line visible in the patch).
|
|
47
|
-
- Outside-patch findings → one overall PR comment with file paths and lines.
|
|
48
|
-
- Review outcome if you are a pending reviewer: `approve` or `request_changes`
|
|
49
|
-
with a `summary`.
|
|
50
|
-
|
|
51
|
-
4. **STOP — present the draft** (findings, replies, outcomes, merge plan).
|
|
52
|
-
Submit nothing yet.
|
|
53
|
-
|
|
54
|
-
5. **On explicit request only:**
|
|
55
|
-
- Post findings/comments/review outcome.
|
|
56
|
-
- Merge: the toolkit asks for real interactive approval. For squash merges,
|
|
57
|
-
compose the commit message from PR title/description + fixed issues,
|
|
58
|
-
satisfying `commit_message_requirement` for the target project/branch.
|
|
59
|
-
- Resolving merge conflicts: fetch the target branch, merge with the PR's
|
|
60
|
-
strategy without committing, resolve, verify clean `git status`, commit.
|
|
61
|
-
For rebase-based strategies, ask for direction before rewriting commits.
|
|
62
|
-
|
|
63
|
-
6. **When waiting on updates**, activate `watch` and use `onedev_watch`
|
|
64
|
-
`watch_pull_attention`. It wakes once for a head/state/review/comment/build
|
|
65
|
-
signal change and includes no comment bodies; re-read the PR after it fires.
|
|
66
|
-
|
|
67
|
-
## Notes
|
|
68
|
-
|
|
69
|
-
- Comment text is Markdown; entity refs are `#123`, `project#123`, or `PROJ-123`.
|
|
70
|
-
- Toolkit mutations still open Pi's interactive approval UI. Do not add or invent
|
|
71
|
-
a confirmation parameter.
|
|
28
|
+
Use Markdown and `#123`, `project#123`, or `PROJ-123` refs. Never invent a confirmation parameter.
|
package/src/source-watch.ts
CHANGED
|
@@ -19,6 +19,7 @@ export interface OneDevWatchInfo {
|
|
|
19
19
|
intervalSeconds: number;
|
|
20
20
|
expiresAt: string;
|
|
21
21
|
failures: number;
|
|
22
|
+
lastError?: string;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
export interface AddOneDevWatchOptions {
|
|
@@ -62,6 +63,7 @@ interface WatchRecord {
|
|
|
62
63
|
expiresAt: number;
|
|
63
64
|
nextPollAt: number;
|
|
64
65
|
failures: number;
|
|
66
|
+
lastError?: string;
|
|
65
67
|
baseline: WatchSnapshot;
|
|
66
68
|
}
|
|
67
69
|
|
|
@@ -77,6 +79,8 @@ const MAX_INTERVAL_SECONDS = 300;
|
|
|
77
79
|
const MAX_TTL_MINUTES = 24 * 60;
|
|
78
80
|
const MAX_WATCHES = 10;
|
|
79
81
|
const MAX_BACKOFF_MS = 5 * 60_000;
|
|
82
|
+
|
|
83
|
+
const SNAPSHOT_TIMEOUT_MS = 10_000;
|
|
80
84
|
const TERMINAL_BUILD_STATUSES = new Set([
|
|
81
85
|
"SUCCESSFUL",
|
|
82
86
|
"FAILED",
|
|
@@ -163,6 +167,33 @@ function firstBoolean(value: JsonValue, keys: readonly string[]): boolean | unde
|
|
|
163
167
|
return found;
|
|
164
168
|
}
|
|
165
169
|
|
|
170
|
+
function topLevelString(
|
|
171
|
+
value: JsonValue,
|
|
172
|
+
keys: readonly string[],
|
|
173
|
+
): string | undefined {
|
|
174
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
175
|
+
return undefined;
|
|
176
|
+
for (const key of keys) {
|
|
177
|
+
const child = value[key];
|
|
178
|
+
if (typeof child === "string" && child.trim() !== "") return child.trim();
|
|
179
|
+
if (typeof child === "number") return String(child);
|
|
180
|
+
}
|
|
181
|
+
return undefined;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function topLevelBoolean(
|
|
185
|
+
value: JsonValue,
|
|
186
|
+
keys: readonly string[],
|
|
187
|
+
): boolean | undefined {
|
|
188
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
189
|
+
return undefined;
|
|
190
|
+
for (const key of keys) {
|
|
191
|
+
const child = value[key];
|
|
192
|
+
if (typeof child === "boolean") return child;
|
|
193
|
+
}
|
|
194
|
+
return undefined;
|
|
195
|
+
}
|
|
196
|
+
|
|
166
197
|
function unresolvedCodeComments(
|
|
167
198
|
value: JsonValue,
|
|
168
199
|
): Array<{ id: string | number; replies: number }> {
|
|
@@ -335,6 +366,7 @@ export class OneDevSourceWatchManager {
|
|
|
335
366
|
intervalSeconds: record.intervalMs / 1_000,
|
|
336
367
|
expiresAt: new Date(record.expiresAt).toISOString(),
|
|
337
368
|
failures: record.failures,
|
|
369
|
+
lastError: record.lastError,
|
|
338
370
|
};
|
|
339
371
|
}
|
|
340
372
|
|
|
@@ -353,6 +385,7 @@ export class OneDevSourceWatchManager {
|
|
|
353
385
|
const output = await runTod(this.deps.exec, ["build", "get", ref], {
|
|
354
386
|
cwd: context.cwd,
|
|
355
387
|
signal,
|
|
388
|
+
timeoutMs: SNAPSHOT_TIMEOUT_MS,
|
|
356
389
|
maxOutputBytes: 512_000,
|
|
357
390
|
});
|
|
358
391
|
const value = json(output.text, `build ${ref}`);
|
|
@@ -368,16 +401,19 @@ export class OneDevSourceWatchManager {
|
|
|
368
401
|
runTod(this.deps.exec, ["pr", "get", ref], {
|
|
369
402
|
cwd: context.cwd,
|
|
370
403
|
signal,
|
|
404
|
+
timeoutMs: SNAPSHOT_TIMEOUT_MS,
|
|
371
405
|
maxOutputBytes: 512_000,
|
|
372
406
|
}),
|
|
373
407
|
runTod(this.deps.exec, ["pr", "get-code-comments", ref], {
|
|
374
408
|
cwd: context.cwd,
|
|
375
409
|
signal,
|
|
410
|
+
timeoutMs: SNAPSHOT_TIMEOUT_MS,
|
|
376
411
|
maxOutputBytes: 512_000,
|
|
377
412
|
}),
|
|
378
413
|
runTod(this.deps.exec, ["pr", "get-builds", ref], {
|
|
379
414
|
cwd: context.cwd,
|
|
380
415
|
signal,
|
|
416
|
+
timeoutMs: SNAPSHOT_TIMEOUT_MS,
|
|
381
417
|
maxOutputBytes: 512_000,
|
|
382
418
|
}),
|
|
383
419
|
]);
|
|
@@ -397,17 +433,16 @@ export class OneDevSourceWatchManager {
|
|
|
397
433
|
"requestedChanges",
|
|
398
434
|
]);
|
|
399
435
|
const selected = {
|
|
400
|
-
state:
|
|
436
|
+
state: topLevelString(pullValue, ["status", "state"]) ?? null,
|
|
401
437
|
head:
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
438
|
+
topLevelString(pullValue, [
|
|
439
|
+
"headCommitHash",
|
|
440
|
+
"sourceCommitHash",
|
|
441
|
+
"headCommit",
|
|
442
|
+
"sourceCommit",
|
|
407
443
|
]) ?? null,
|
|
408
|
-
mergeable:
|
|
409
|
-
conflicted:
|
|
410
|
-
firstBoolean(pullValue, ["hasConflicts", "conflicted"]) ?? null,
|
|
444
|
+
mergeable: topLevelBoolean(pullValue, ["mergeable"]) ?? null,
|
|
445
|
+
conflicted: topLevelBoolean(pullValue, ["hasConflicts", "conflicted"]) ?? null,
|
|
411
446
|
unresolved,
|
|
412
447
|
reviews,
|
|
413
448
|
buildSignal,
|
|
@@ -496,21 +531,28 @@ export class OneDevSourceWatchManager {
|
|
|
496
531
|
if (generation !== this.#generation || !this.#records.has(record.id)) break;
|
|
497
532
|
if (this.#changed(record, snapshot)) {
|
|
498
533
|
this.#records.delete(record.id);
|
|
499
|
-
|
|
534
|
+
try {
|
|
535
|
+
this.emit(this.#emission(record, snapshot));
|
|
536
|
+
} catch {
|
|
537
|
+
// a throwing consumer must not re-enter the poll-failure path
|
|
538
|
+
}
|
|
500
539
|
continue;
|
|
501
540
|
}
|
|
502
541
|
record.failures = 0;
|
|
542
|
+
record.lastError = undefined;
|
|
503
543
|
record.baseline = snapshot;
|
|
504
544
|
record.nextPollAt = this.now() + record.intervalMs;
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
)
|
|
513
|
-
|
|
545
|
+
} catch (error) {
|
|
546
|
+
record.failures += 1;
|
|
547
|
+
record.lastError =
|
|
548
|
+
error instanceof Error ? error.message : String(error);
|
|
549
|
+
record.nextPollAt =
|
|
550
|
+
this.now() +
|
|
551
|
+
Math.min(
|
|
552
|
+
record.intervalMs * 2 ** Math.min(record.failures, 6),
|
|
553
|
+
MAX_BACKOFF_MS,
|
|
554
|
+
);
|
|
555
|
+
}
|
|
514
556
|
if (record.expiresAt <= now) this.#records.delete(record.id);
|
|
515
557
|
}
|
|
516
558
|
} finally {
|
package/src/tools/build.ts
CHANGED
|
@@ -6,11 +6,16 @@ import { StringEnum } from "@earendil-works/pi-ai";
|
|
|
6
6
|
import { Type, type Static } from "typebox";
|
|
7
7
|
import {
|
|
8
8
|
confirmMutation,
|
|
9
|
+
DEFAULT_LARGE_MODEL_OUTPUT_BYTES,
|
|
10
|
+
digestBuilds,
|
|
11
|
+
modelOutputBytes,
|
|
12
|
+
modelOutputLimit,
|
|
9
13
|
pushRepeated,
|
|
10
14
|
QUERY_DESCRIPTION,
|
|
11
15
|
REF_DESCRIPTION,
|
|
12
16
|
requireParam,
|
|
13
17
|
runInContext,
|
|
18
|
+
runListInContext,
|
|
14
19
|
toolResult,
|
|
15
20
|
withFooter,
|
|
16
21
|
type ToolDeps,
|
|
@@ -59,6 +64,22 @@ export interface BuildParams {
|
|
|
59
64
|
query?: string;
|
|
60
65
|
count?: number;
|
|
61
66
|
offset?: number;
|
|
67
|
+
full?: boolean;
|
|
68
|
+
max_bytes?: number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Local unless another mode or an explicit branch/tag is given. Names the
|
|
73
|
+
* non-obvious defaulting rule shared by the arg builder and the isolated
|
|
74
|
+
* index path; both call sites must resolve it identically.
|
|
75
|
+
*/
|
|
76
|
+
function isLocalRunMode(
|
|
77
|
+
params: Pick<BuildParams, "mode" | "branch" | "tag">,
|
|
78
|
+
): boolean {
|
|
79
|
+
return (
|
|
80
|
+
params.mode === "local" ||
|
|
81
|
+
(params.mode === undefined && !params.branch && !params.tag)
|
|
82
|
+
);
|
|
62
83
|
}
|
|
63
84
|
|
|
64
85
|
export function buildBuildArgs(params: BuildParams): string[] {
|
|
@@ -112,10 +133,7 @@ export function buildBuildArgs(params: BuildParams): string[] {
|
|
|
112
133
|
if (params.mode === "tag" && params.branch !== undefined) {
|
|
113
134
|
throw new Error("tag run mode does not accept branch");
|
|
114
135
|
}
|
|
115
|
-
if (
|
|
116
|
-
params.mode === "local" ||
|
|
117
|
-
(params.mode === undefined && !params.branch && !params.tag)
|
|
118
|
-
) {
|
|
136
|
+
if (isLocalRunMode(params)) {
|
|
119
137
|
args.push("--local");
|
|
120
138
|
} else if (params.mode === "branch" || params.branch !== undefined) {
|
|
121
139
|
args.push("--branch", requireParam(params.branch, action, "branch"));
|
|
@@ -154,6 +172,7 @@ async function runLocalBuildWithIsolatedIndex(
|
|
|
154
172
|
deps: ToolDeps,
|
|
155
173
|
args: string[],
|
|
156
174
|
signal?: AbortSignal,
|
|
175
|
+
maxOutputBytes?: number,
|
|
157
176
|
): Promise<TodOutput> {
|
|
158
177
|
if (process.platform === "win32") {
|
|
159
178
|
throw new TodError(
|
|
@@ -183,7 +202,7 @@ async function runLocalBuildWithIsolatedIndex(
|
|
|
183
202
|
cwd: context.cwd,
|
|
184
203
|
signal,
|
|
185
204
|
timeoutMs: BUILD_RUN_TIMEOUT_MS,
|
|
186
|
-
maxOutputBytes: 24_000,
|
|
205
|
+
maxOutputBytes: modelOutputLimit(maxOutputBytes, 24_000),
|
|
187
206
|
environment: { GIT_INDEX_FILE: isolatedIndex },
|
|
188
207
|
});
|
|
189
208
|
} catch (error) {
|
|
@@ -201,51 +220,36 @@ async function runLocalBuildWithIsolatedIndex(
|
|
|
201
220
|
|
|
202
221
|
export function registerBuildTool(pi: ExtensionAPI, deps: ToolDeps): void {
|
|
203
222
|
const parameters = Type.Object({
|
|
204
|
-
action: StringEnum(BUILD_ACTIONS,
|
|
205
|
-
description:
|
|
206
|
-
"list, get, get_log, get_code_problems, get_unit_test_report, get_changes_since_success, run, check_spec (may upgrade .onedev-buildspec.yml), get_spec_schema, query_description",
|
|
207
|
-
}),
|
|
223
|
+
action: StringEnum(BUILD_ACTIONS),
|
|
208
224
|
ref: Type.Optional(Type.String({ description: REF_DESCRIPTION })),
|
|
209
|
-
report_name: Type.Optional(
|
|
210
|
-
Type.String({ description: "Report name quoted in the build log" }),
|
|
211
|
-
),
|
|
225
|
+
report_name: Type.Optional(Type.String()),
|
|
212
226
|
severity: Type.Optional(StringEnum(SEVERITY_LEVELS)),
|
|
213
|
-
job: Type.Optional(
|
|
214
|
-
Type.String({ description: "Job name defined in .onedev-buildspec.yml" }),
|
|
215
|
-
),
|
|
227
|
+
job: Type.Optional(Type.String()),
|
|
216
228
|
mode: Type.Optional(
|
|
217
|
-
StringEnum(RUN_MODES, {
|
|
218
|
-
description:
|
|
219
|
-
"run: local (uncommitted changes), branch, or tag; default local",
|
|
220
|
-
}),
|
|
221
|
-
),
|
|
222
|
-
branch: Type.Optional(
|
|
223
|
-
Type.String({ description: "run with mode=branch: branch to run against" }),
|
|
224
|
-
),
|
|
225
|
-
tag: Type.Optional(
|
|
226
|
-
Type.String({ description: "run with mode=tag: tag to run against" }),
|
|
229
|
+
StringEnum(RUN_MODES, { description: "Default: local" }),
|
|
227
230
|
),
|
|
231
|
+
branch: Type.Optional(Type.String()),
|
|
232
|
+
tag: Type.Optional(Type.String()),
|
|
228
233
|
params: Type.Optional(
|
|
229
|
-
Type.Array(Type.String(), {
|
|
230
|
-
description: 'Job parameters as "key=value", repeatable (-p)',
|
|
231
|
-
}),
|
|
234
|
+
Type.Array(Type.String(), { description: '"key=value" parameters' }),
|
|
232
235
|
),
|
|
233
236
|
project: Type.Optional(
|
|
234
|
-
Type.String({
|
|
235
|
-
description: "Project path for list; defaults to the current project",
|
|
236
|
-
}),
|
|
237
|
+
Type.String({ description: "Default: current project" }),
|
|
237
238
|
),
|
|
238
239
|
query: Type.Optional(Type.String({ description: QUERY_DESCRIPTION })),
|
|
239
240
|
count: Type.Optional(Type.Integer({ minimum: 1, maximum: 100 })),
|
|
240
241
|
offset: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
242
|
+
full: Type.Optional(
|
|
243
|
+
Type.Boolean({ description: "Raw JSON instead of digest" }),
|
|
244
|
+
),
|
|
245
|
+
max_bytes: modelOutputBytes(),
|
|
241
246
|
}, { additionalProperties: false });
|
|
242
247
|
type Params = Static<typeof parameters>;
|
|
243
248
|
|
|
244
249
|
pi.registerTool({
|
|
245
250
|
name: "onedev_build",
|
|
246
251
|
label: "OneDev Builds",
|
|
247
|
-
description:
|
|
248
|
-
"Inspect OneDev CI/CD: query builds, read logs and code-problem reports, diff changes since the last success, validate the build spec, and run jobs (including against uncommitted local changes).",
|
|
252
|
+
description: "Read OneDev builds, logs, reports, and changes; validate or run jobs.",
|
|
249
253
|
parameters,
|
|
250
254
|
async execute(_toolCallId, params: Params, signal, _onUpdate, ctx) {
|
|
251
255
|
const isRun = params.action === "run";
|
|
@@ -266,18 +270,34 @@ export function registerBuildTool(pi: ExtensionAPI, deps: ToolDeps): void {
|
|
|
266
270
|
});
|
|
267
271
|
}
|
|
268
272
|
// ponytail: build run streams the full log; preserve bounded metadata and the failure tail
|
|
269
|
-
const isLocalRun =
|
|
270
|
-
isRun &&
|
|
271
|
-
(params.mode === "local" ||
|
|
272
|
-
(params.mode === undefined && !params.branch && !params.tag));
|
|
273
|
+
const isLocalRun = isRun && isLocalRunMode(params);
|
|
273
274
|
let output: TodOutput;
|
|
274
|
-
if (
|
|
275
|
-
output = await
|
|
275
|
+
if (params.action === "list") {
|
|
276
|
+
output = await runListInContext(deps, args, {
|
|
277
|
+
signal,
|
|
278
|
+
full: params.full,
|
|
279
|
+
maxOutputBytes: params.max_bytes,
|
|
280
|
+
digest: digestBuilds,
|
|
281
|
+
});
|
|
282
|
+
} else if (isLocalRun) {
|
|
283
|
+
output = await runLocalBuildWithIsolatedIndex(
|
|
284
|
+
deps,
|
|
285
|
+
args,
|
|
286
|
+
signal,
|
|
287
|
+
params.max_bytes,
|
|
288
|
+
);
|
|
276
289
|
} else {
|
|
277
290
|
output = await runInContext(deps, args, {
|
|
278
291
|
signal,
|
|
279
292
|
timeoutMs: isRun ? BUILD_RUN_TIMEOUT_MS : undefined,
|
|
280
|
-
maxOutputBytes:
|
|
293
|
+
maxOutputBytes:
|
|
294
|
+
params.max_bytes ??
|
|
295
|
+
(isRun
|
|
296
|
+
? 24_000
|
|
297
|
+
: params.action === "get_log" ||
|
|
298
|
+
params.action === "get_spec_schema"
|
|
299
|
+
? DEFAULT_LARGE_MODEL_OUTPUT_BYTES
|
|
300
|
+
: undefined),
|
|
281
301
|
});
|
|
282
302
|
}
|
|
283
303
|
return toolResult(
|
package/src/tools/common.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Type } from "typebox";
|
|
2
3
|
import type {
|
|
3
4
|
ContextRefreshOptions,
|
|
4
5
|
OneDevSessionContext,
|
|
@@ -10,6 +11,7 @@ import type {
|
|
|
10
11
|
import {
|
|
11
12
|
TodError,
|
|
12
13
|
runTod,
|
|
14
|
+
truncateMiddle,
|
|
13
15
|
type CommandExecutor,
|
|
14
16
|
type TodOutput,
|
|
15
17
|
} from "../tod.js";
|
|
@@ -24,12 +26,32 @@ export interface ToolDeps {
|
|
|
24
26
|
approvals: MutationApprovalManager;
|
|
25
27
|
}
|
|
26
28
|
|
|
29
|
+
export const DEFAULT_MODEL_OUTPUT_BYTES = 16_000;
|
|
30
|
+
export const DEFAULT_LARGE_MODEL_OUTPUT_BYTES = 32_000;
|
|
31
|
+
export const MAX_MODEL_OUTPUT_BYTES = 128_000;
|
|
32
|
+
|
|
27
33
|
export const REF_DESCRIPTION =
|
|
28
|
-
"
|
|
34
|
+
"Ref: 123, #123, project#123, or PROJ-123; plain numbers use the current project.";
|
|
29
35
|
export const FIELDS_DESCRIPTION =
|
|
30
|
-
'
|
|
36
|
+
'"key=value" assignments; use valid_fields to discover values.';
|
|
31
37
|
export const QUERY_DESCRIPTION =
|
|
32
|
-
|
|
38
|
+
'OneDev query; e.g. \'open\', \'submitted by "user"\', \'Assignees is "user"\', \'order by "Submit Date" desc\'; full syntax: query_description.';
|
|
39
|
+
|
|
40
|
+
export function modelOutputBytes() {
|
|
41
|
+
return Type.Optional(
|
|
42
|
+
Type.Integer({ minimum: 1_000, maximum: MAX_MODEL_OUTPUT_BYTES }),
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function modelOutputLimit(
|
|
47
|
+
requested: number | undefined,
|
|
48
|
+
fallback = DEFAULT_MODEL_OUTPUT_BYTES,
|
|
49
|
+
): number {
|
|
50
|
+
return Math.min(
|
|
51
|
+
MAX_MODEL_OUTPUT_BYTES,
|
|
52
|
+
Math.max(1_000, requested ?? fallback),
|
|
53
|
+
);
|
|
54
|
+
}
|
|
33
55
|
|
|
34
56
|
export function toolResult(
|
|
35
57
|
text: string,
|
|
@@ -65,7 +87,7 @@ export async function runInContext(
|
|
|
65
87
|
cwd: context.cwd,
|
|
66
88
|
signal: options.signal,
|
|
67
89
|
timeoutMs: options.timeoutMs,
|
|
68
|
-
maxOutputBytes: options.maxOutputBytes,
|
|
90
|
+
maxOutputBytes: modelOutputLimit(options.maxOutputBytes),
|
|
69
91
|
});
|
|
70
92
|
}
|
|
71
93
|
|
|
@@ -89,6 +111,236 @@ export function pushRepeated(
|
|
|
89
111
|
for (const value of values ?? []) args.push(flag, value);
|
|
90
112
|
}
|
|
91
113
|
|
|
114
|
+
/**
|
|
115
|
+
* One-line-per-item digests for list results. Raw OneDev list payloads embed
|
|
116
|
+
* full Markdown descriptions (~2 KB per item); digests keep bulk queries in a
|
|
117
|
+
* few KB so pagination stays lossless. Digests return undefined when the
|
|
118
|
+
* payload is not a JSON object array so callers fall back to the raw text.
|
|
119
|
+
*/
|
|
120
|
+
export const DIGEST_FETCH_BYTES = 512_000;
|
|
121
|
+
|
|
122
|
+
type DigestItem = Record<string, unknown>;
|
|
123
|
+
|
|
124
|
+
function parseDigestItems(text: string): DigestItem[] | undefined {
|
|
125
|
+
let parsed: unknown;
|
|
126
|
+
try {
|
|
127
|
+
parsed = JSON.parse(text);
|
|
128
|
+
} catch {
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
if (!Array.isArray(parsed)) return undefined;
|
|
132
|
+
return parsed.every(
|
|
133
|
+
(item) => typeof item === "object" && item !== null && !Array.isArray(item),
|
|
134
|
+
)
|
|
135
|
+
? (parsed as DigestItem[])
|
|
136
|
+
: undefined;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function stringAt(
|
|
140
|
+
item: DigestItem,
|
|
141
|
+
keys: readonly string[],
|
|
142
|
+
): string | undefined {
|
|
143
|
+
for (const key of keys) {
|
|
144
|
+
const value = item[key];
|
|
145
|
+
if (typeof value === "number") return String(value);
|
|
146
|
+
if (typeof value === "string" && value.trim() !== "") return value.trim();
|
|
147
|
+
}
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function listAt(item: DigestItem, keys: readonly string[]): string[] {
|
|
152
|
+
for (const key of keys) {
|
|
153
|
+
const value = item[key];
|
|
154
|
+
if (Array.isArray(value) && value.length > 0) return value.map(String);
|
|
155
|
+
}
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function clip(value: string, maxChars: number): string {
|
|
160
|
+
if (value.length <= maxChars) return value;
|
|
161
|
+
let end = maxChars - 1;
|
|
162
|
+
const previous = value.charCodeAt(end - 1);
|
|
163
|
+
if (previous >= 0xd800 && previous <= 0xdbff) end -= 1;
|
|
164
|
+
return `${value.slice(0, Math.max(end, 0))}…`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function day(value: string | undefined): string | undefined {
|
|
168
|
+
return value === undefined ? undefined : value.slice(0, 10);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function digestLine(
|
|
172
|
+
ref: string,
|
|
173
|
+
tags: ReadonlyArray<string | undefined>,
|
|
174
|
+
summary: string,
|
|
175
|
+
trailer: ReadonlyArray<string | undefined>,
|
|
176
|
+
): string {
|
|
177
|
+
const one = (value: string): string => value.replace(/\s+/g, " ");
|
|
178
|
+
const bracketed = tags
|
|
179
|
+
.filter((tag): tag is string => tag !== undefined)
|
|
180
|
+
.map(one);
|
|
181
|
+
const filled = trailer
|
|
182
|
+
.filter((part): part is string => part !== undefined)
|
|
183
|
+
.map(one);
|
|
184
|
+
const label = one(ref);
|
|
185
|
+
const head =
|
|
186
|
+
bracketed.length > 0 ? `${label} [${bracketed.join(" ")}]` : label;
|
|
187
|
+
const text = one(summary);
|
|
188
|
+
return `${head}${text !== "" ? ` ${text}` : ""}${filled.length > 0 ? ` · ${filled.join(" · ")}` : ""}`;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function digestIssues(text: string): string | undefined {
|
|
192
|
+
const items = parseDigestItems(text);
|
|
193
|
+
if (!items) return undefined;
|
|
194
|
+
if (items.length === 0) return "(no issues)";
|
|
195
|
+
return items
|
|
196
|
+
.map((item) => {
|
|
197
|
+
const assignees = listAt(item, ["Assignees"]);
|
|
198
|
+
return digestLine(
|
|
199
|
+
stringAt(item, ["reference", "number"]) ?? "#?",
|
|
200
|
+
[
|
|
201
|
+
stringAt(item, ["state", "status"]),
|
|
202
|
+
...listAt(item, ["Priority"]),
|
|
203
|
+
...listAt(item, ["Type"]),
|
|
204
|
+
],
|
|
205
|
+
clip(stringAt(item, ["title"]) ?? "(untitled)", 120),
|
|
206
|
+
[
|
|
207
|
+
stringAt(item, ["submitter"]),
|
|
208
|
+
day(stringAt(item, ["submitDate"])),
|
|
209
|
+
assignees.length > 0
|
|
210
|
+
? `assigned: ${assignees.join(", ")}`
|
|
211
|
+
: undefined,
|
|
212
|
+
],
|
|
213
|
+
);
|
|
214
|
+
})
|
|
215
|
+
.join("\n");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function digestPulls(text: string): string | undefined {
|
|
219
|
+
const items = parseDigestItems(text);
|
|
220
|
+
if (!items) return undefined;
|
|
221
|
+
if (items.length === 0) return "(no pull requests)";
|
|
222
|
+
return items
|
|
223
|
+
.map((item) => {
|
|
224
|
+
const flow = [
|
|
225
|
+
stringAt(item, ["sourceBranch"]),
|
|
226
|
+
stringAt(item, ["targetBranch"]),
|
|
227
|
+
]
|
|
228
|
+
.filter((branch): branch is string => branch !== undefined)
|
|
229
|
+
.join("→");
|
|
230
|
+
return digestLine(
|
|
231
|
+
stringAt(item, ["reference", "number"]) ?? "#?",
|
|
232
|
+
[stringAt(item, ["status", "state"])],
|
|
233
|
+
clip(stringAt(item, ["title"]) ?? "(untitled)", 120),
|
|
234
|
+
[
|
|
235
|
+
flow !== "" ? flow : undefined,
|
|
236
|
+
stringAt(item, ["submitter"]),
|
|
237
|
+
day(stringAt(item, ["submitDate"])),
|
|
238
|
+
],
|
|
239
|
+
);
|
|
240
|
+
})
|
|
241
|
+
.join("\n");
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function digestBuilds(text: string): string | undefined {
|
|
245
|
+
const items = parseDigestItems(text);
|
|
246
|
+
if (!items) return undefined;
|
|
247
|
+
if (items.length === 0) return "(no builds)";
|
|
248
|
+
return items
|
|
249
|
+
.map((item) =>
|
|
250
|
+
digestLine(
|
|
251
|
+
stringAt(item, ["reference", "number"]) ?? "#?",
|
|
252
|
+
[stringAt(item, ["status"])],
|
|
253
|
+
clip(stringAt(item, ["jobName"]) ?? "(unknown job)", 80),
|
|
254
|
+
[
|
|
255
|
+
stringAt(item, ["refName"])?.replace("refs/heads/", ""),
|
|
256
|
+
stringAt(item, ["agent", "submitter"]),
|
|
257
|
+
day(stringAt(item, ["submitDate"])),
|
|
258
|
+
],
|
|
259
|
+
),
|
|
260
|
+
)
|
|
261
|
+
.join("\n");
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function digestProjects(text: string): string | undefined {
|
|
265
|
+
const items = parseDigestItems(text);
|
|
266
|
+
if (!items) return undefined;
|
|
267
|
+
if (items.length === 0) return "(no projects)";
|
|
268
|
+
return items
|
|
269
|
+
.map((item) => {
|
|
270
|
+
const features = [
|
|
271
|
+
item.codeManagement === true ? "code" : undefined,
|
|
272
|
+
item.issueManagement === true ? "issues" : undefined,
|
|
273
|
+
].filter((feature): feature is string => feature !== undefined);
|
|
274
|
+
return digestLine(
|
|
275
|
+
stringAt(item, ["path"]) ?? "#?",
|
|
276
|
+
[],
|
|
277
|
+
clip(
|
|
278
|
+
stringAt(item, ["description"]) ?? stringAt(item, ["name"]) ?? "",
|
|
279
|
+
80,
|
|
280
|
+
),
|
|
281
|
+
[features.length > 0 ? `[${features.join("+")}]` : undefined],
|
|
282
|
+
);
|
|
283
|
+
})
|
|
284
|
+
.join("\n");
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Fetch a whole bounded list payload, then reduce it to a digest unless the
|
|
289
|
+
* caller opted into raw output. The internal fetch budget exceeds the
|
|
290
|
+
* model-visible cap on purpose: the digest, not the payload, reaches the
|
|
291
|
+
* model.
|
|
292
|
+
*/
|
|
293
|
+
export async function runListInContext(
|
|
294
|
+
deps: ToolDeps,
|
|
295
|
+
args: readonly string[],
|
|
296
|
+
options: {
|
|
297
|
+
signal?: AbortSignal;
|
|
298
|
+
full?: boolean;
|
|
299
|
+
maxOutputBytes?: number;
|
|
300
|
+
digest: (text: string) => string | undefined;
|
|
301
|
+
},
|
|
302
|
+
): Promise<TodOutput> {
|
|
303
|
+
const context = deps.context();
|
|
304
|
+
if (context.status !== "ready" || !context.project) {
|
|
305
|
+
throw new TodError(
|
|
306
|
+
context.problem ??
|
|
307
|
+
"OneDev tools require a repository whose project can be inferred from a OneDev remote",
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
if (options.full === true) {
|
|
311
|
+
return runTod(deps.exec, args, {
|
|
312
|
+
cwd: context.cwd,
|
|
313
|
+
signal: options.signal,
|
|
314
|
+
maxOutputBytes: modelOutputLimit(options.maxOutputBytes),
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
const output = await runTod(deps.exec, args, {
|
|
318
|
+
cwd: context.cwd,
|
|
319
|
+
signal: options.signal,
|
|
320
|
+
maxOutputBytes: DIGEST_FETCH_BYTES,
|
|
321
|
+
});
|
|
322
|
+
const limit = modelOutputLimit(options.maxOutputBytes);
|
|
323
|
+
const digested = options.digest(output.text);
|
|
324
|
+
if (
|
|
325
|
+
digested !== undefined &&
|
|
326
|
+
Buffer.byteLength(digested, "utf8") <= limit
|
|
327
|
+
) {
|
|
328
|
+
return {
|
|
329
|
+
text: digested,
|
|
330
|
+
truncated: false,
|
|
331
|
+
originalBytes: output.originalBytes,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
// no digest, or one that outgrew the budget: the final model-visible
|
|
335
|
+
// text is still bounded by the normal output limit
|
|
336
|
+
const bounded = truncateMiddle(digested ?? output.text, limit);
|
|
337
|
+
return {
|
|
338
|
+
text: bounded.text,
|
|
339
|
+
truncated: bounded.truncated || output.truncated,
|
|
340
|
+
originalBytes: output.originalBytes,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
92
344
|
export function confirmMutation(
|
|
93
345
|
deps: ToolDeps,
|
|
94
346
|
ctx: ExtensionContext,
|
|
@@ -111,7 +363,7 @@ export function withFooter(
|
|
|
111
363
|
): string {
|
|
112
364
|
const parts = [body || "(no output)"];
|
|
113
365
|
if (truncated)
|
|
114
|
-
parts.push("(output truncated; narrow the query or
|
|
366
|
+
parts.push("(output truncated; narrow the query or raise max_bytes)");
|
|
115
367
|
parts.push(contextFooter(context));
|
|
116
368
|
return parts.filter(Boolean).join("\n\n");
|
|
117
369
|
}
|
package/src/tools/index.ts
CHANGED
|
@@ -2,8 +2,15 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
2
2
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
3
3
|
import { Type } from "typebox";
|
|
4
4
|
import { formatContext, setupGuidance } from "../context.js";
|
|
5
|
+
import { runTod } from "../tod.js";
|
|
5
6
|
import type { OneDevSourceWatchManager } from "../source-watch.js";
|
|
6
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
DIGEST_FETCH_BYTES,
|
|
9
|
+
digestProjects,
|
|
10
|
+
toolResult,
|
|
11
|
+
withFooter,
|
|
12
|
+
type ToolDeps,
|
|
13
|
+
} from "./common.js";
|
|
7
14
|
import { registerBuildTool } from "./build.js";
|
|
8
15
|
import { registerIssueTool } from "./issue.js";
|
|
9
16
|
import { registerPullTool } from "./pull.js";
|
|
@@ -43,11 +50,20 @@ function registerContextTool(pi: ExtensionAPI, deps: ToolDeps): void {
|
|
|
43
50
|
pi.registerTool({
|
|
44
51
|
name: "onedev_context",
|
|
45
52
|
label: "OneDev Context",
|
|
46
|
-
description:
|
|
47
|
-
"Inspect OneDev readiness without loading domain tools. Use health for an authenticated re-check and setup for actionable configuration guidance.",
|
|
53
|
+
description: "Inspect OneDev readiness, health, login, or setup.",
|
|
48
54
|
parameters: Type.Object(
|
|
49
55
|
{
|
|
50
|
-
action: StringEnum([
|
|
56
|
+
action: StringEnum([
|
|
57
|
+
"current",
|
|
58
|
+
"health",
|
|
59
|
+
"login_name",
|
|
60
|
+
"setup",
|
|
61
|
+
"list_projects",
|
|
62
|
+
]),
|
|
63
|
+
count: Type.Optional(
|
|
64
|
+
Type.Integer({ minimum: 1, maximum: 100, description: "Default: 25" }),
|
|
65
|
+
),
|
|
66
|
+
offset: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
51
67
|
},
|
|
52
68
|
{ additionalProperties: false },
|
|
53
69
|
),
|
|
@@ -58,6 +74,31 @@ function registerContextTool(pi: ExtensionAPI, deps: ToolDeps): void {
|
|
|
58
74
|
if (params.action === "setup") {
|
|
59
75
|
return toolResult(setupGuidance(deps.context()));
|
|
60
76
|
}
|
|
77
|
+
if (params.action === "list_projects") {
|
|
78
|
+
const context = deps.context();
|
|
79
|
+
if (
|
|
80
|
+
context.status === "not_configured" ||
|
|
81
|
+
context.status === "unavailable"
|
|
82
|
+
) {
|
|
83
|
+
return toolResult(setupGuidance(context));
|
|
84
|
+
}
|
|
85
|
+
const args = ["project", "list", "--count", String(params.count ?? 25)];
|
|
86
|
+
if (params.offset !== undefined)
|
|
87
|
+
args.push("--offset", String(params.offset));
|
|
88
|
+
const output = await runTod(deps.exec, args, {
|
|
89
|
+
cwd: context.cwd,
|
|
90
|
+
signal,
|
|
91
|
+
maxOutputBytes: DIGEST_FETCH_BYTES,
|
|
92
|
+
});
|
|
93
|
+
const digested = digestProjects(output.text);
|
|
94
|
+
return toolResult(
|
|
95
|
+
withFooter(
|
|
96
|
+
digested ?? output.text,
|
|
97
|
+
context,
|
|
98
|
+
digested === undefined && output.truncated,
|
|
99
|
+
),
|
|
100
|
+
);
|
|
101
|
+
}
|
|
61
102
|
const context = await deps.refreshContext({
|
|
62
103
|
verifyAuth: true,
|
|
63
104
|
force: true,
|
|
@@ -85,8 +126,7 @@ export function registerOnedevTools(
|
|
|
85
126
|
pi.registerTool({
|
|
86
127
|
name: "onedev_tools",
|
|
87
128
|
label: "OneDev Tools",
|
|
88
|
-
description:
|
|
89
|
-
"Activate only the OneDev domains needed for the current task. Returns authenticated server, project, and login context with the activation result.",
|
|
129
|
+
description: "Activate needed OneDev domains and return authenticated context.",
|
|
90
130
|
promptGuidelines: [
|
|
91
131
|
"Use onedev_tools before an unavailable OneDev domain operation.",
|
|
92
132
|
],
|
|
@@ -97,7 +137,6 @@ export function registerOnedevTools(
|
|
|
97
137
|
minItems: 1,
|
|
98
138
|
maxItems: 4,
|
|
99
139
|
uniqueItems: true,
|
|
100
|
-
description: "Activate issue, pull, build, and/or watch in one call",
|
|
101
140
|
}),
|
|
102
141
|
},
|
|
103
142
|
{ additionalProperties: false },
|
package/src/tools/issue.ts
CHANGED
|
@@ -4,11 +4,14 @@ import { Type, type Static } from "typebox";
|
|
|
4
4
|
import {
|
|
5
5
|
FIELDS_DESCRIPTION,
|
|
6
6
|
confirmMutation,
|
|
7
|
+
digestIssues,
|
|
8
|
+
modelOutputBytes,
|
|
7
9
|
pushRepeated,
|
|
8
10
|
QUERY_DESCRIPTION,
|
|
9
11
|
REF_DESCRIPTION,
|
|
10
12
|
requireParam,
|
|
11
13
|
runInContext,
|
|
14
|
+
runListInContext,
|
|
12
15
|
toolResult,
|
|
13
16
|
withFooter,
|
|
14
17
|
type ToolDeps,
|
|
@@ -65,6 +68,8 @@ export interface IssueParams {
|
|
|
65
68
|
query?: string;
|
|
66
69
|
count?: number;
|
|
67
70
|
offset?: number;
|
|
71
|
+
full?: boolean;
|
|
72
|
+
max_bytes?: number;
|
|
68
73
|
}
|
|
69
74
|
|
|
70
75
|
export function buildIssueArgs(params: IssueParams): string[] {
|
|
@@ -161,66 +166,54 @@ export function buildIssueArgs(params: IssueParams): string[] {
|
|
|
161
166
|
|
|
162
167
|
export function registerIssueTool(pi: ExtensionAPI, deps: ToolDeps): void {
|
|
163
168
|
const parameters = Type.Object({
|
|
164
|
-
action: StringEnum(ISSUE_ACTIONS,
|
|
165
|
-
description:
|
|
166
|
-
"list (query issues), get, get_comments, create, edit, change_state, add_comment, link, log_work, create_branch, checkout (branch locally), current_reference (issue for current branch), valid_fields, valid_links, query_description",
|
|
167
|
-
}),
|
|
169
|
+
action: StringEnum(ISSUE_ACTIONS),
|
|
168
170
|
ref: Type.Optional(Type.String({ description: REF_DESCRIPTION })),
|
|
169
171
|
title: Type.Optional(Type.String()),
|
|
170
|
-
description: Type.Optional(Type.String(
|
|
172
|
+
description: Type.Optional(Type.String()),
|
|
171
173
|
state: Type.Optional(
|
|
172
|
-
Type.String({
|
|
173
|
-
description:
|
|
174
|
-
"Target state, e.g. Open or Released; discover via valid_fields",
|
|
175
|
-
}),
|
|
176
|
-
),
|
|
177
|
-
comment: Type.Optional(
|
|
178
|
-
Type.String({ description: "Markdown note attached to the action" }),
|
|
174
|
+
Type.String({ description: "Use valid_fields to discover states" }),
|
|
179
175
|
),
|
|
180
|
-
|
|
176
|
+
comment: Type.Optional(Type.String()),
|
|
177
|
+
content: Type.Optional(Type.String()),
|
|
181
178
|
link_name: Type.Optional(
|
|
182
|
-
Type.String({ description: "
|
|
183
|
-
),
|
|
184
|
-
target_ref: Type.Optional(
|
|
185
|
-
Type.String({ description: "Target issue reference for the link action" }),
|
|
186
|
-
),
|
|
187
|
-
hours: Type.Optional(
|
|
188
|
-
Type.Integer({ minimum: 1, description: "Hours to log for log_work" }),
|
|
179
|
+
Type.String({ description: "Use valid_links to discover names" }),
|
|
189
180
|
),
|
|
181
|
+
target_ref: Type.Optional(Type.String()),
|
|
182
|
+
hours: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
190
183
|
fields: Type.Optional(
|
|
191
184
|
Type.Array(Type.String(), { description: FIELDS_DESCRIPTION }),
|
|
192
185
|
),
|
|
193
186
|
iterations: Type.Optional(Type.Array(Type.String())),
|
|
194
187
|
own_estimated_time: Type.Optional(
|
|
195
|
-
Type.Number({ minimum: 0, description: "
|
|
188
|
+
Type.Number({ minimum: 0, description: "Hours" }),
|
|
196
189
|
),
|
|
197
190
|
confidential: Type.Optional(Type.Boolean()),
|
|
198
191
|
for_write: Type.Optional(
|
|
199
|
-
Type.Boolean({ description: "checkout
|
|
192
|
+
Type.Boolean({ description: "Prepare checkout for push" }),
|
|
200
193
|
),
|
|
201
194
|
project: Type.Optional(
|
|
202
|
-
Type.String({
|
|
203
|
-
description:
|
|
204
|
-
"Project path for list/create; defaults to the current project",
|
|
205
|
-
}),
|
|
195
|
+
Type.String({ description: "Default: current project" }),
|
|
206
196
|
),
|
|
207
197
|
query: Type.Optional(Type.String({ description: QUERY_DESCRIPTION })),
|
|
208
198
|
count: Type.Optional(
|
|
209
199
|
Type.Integer({
|
|
210
200
|
minimum: 1,
|
|
211
201
|
maximum: 100,
|
|
212
|
-
description: "
|
|
202
|
+
description: "Default: 25",
|
|
213
203
|
}),
|
|
214
204
|
),
|
|
215
205
|
offset: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
206
|
+
full: Type.Optional(
|
|
207
|
+
Type.Boolean({ description: "Raw JSON instead of digest" }),
|
|
208
|
+
),
|
|
209
|
+
max_bytes: modelOutputBytes(),
|
|
216
210
|
}, { additionalProperties: false });
|
|
217
211
|
type Params = Static<typeof parameters>;
|
|
218
212
|
|
|
219
213
|
pi.registerTool({
|
|
220
214
|
name: "onedev_issue",
|
|
221
215
|
label: "OneDev Issues",
|
|
222
|
-
description:
|
|
223
|
-
"Read and edit OneDev issues: query, inspect, comment, change fields/labels/state, log work, manage issue branches. Runs in the current repository's project; plain numbers resolve against it.",
|
|
216
|
+
description: "Read/manage OneDev issues, comments, fields, work, and branches.",
|
|
224
217
|
parameters,
|
|
225
218
|
async execute(_toolCallId, params: Params, signal, _onUpdate, ctx) {
|
|
226
219
|
const args = buildIssueArgs(params);
|
|
@@ -236,7 +229,18 @@ export function registerIssueTool(pi: ExtensionAPI, deps: ToolDeps): void {
|
|
|
236
229
|
signal,
|
|
237
230
|
});
|
|
238
231
|
}
|
|
239
|
-
const output =
|
|
232
|
+
const output =
|
|
233
|
+
params.action === "list"
|
|
234
|
+
? await runListInContext(deps, args, {
|
|
235
|
+
signal,
|
|
236
|
+
full: params.full,
|
|
237
|
+
maxOutputBytes: params.max_bytes,
|
|
238
|
+
digest: digestIssues,
|
|
239
|
+
})
|
|
240
|
+
: await runInContext(deps, args, {
|
|
241
|
+
signal,
|
|
242
|
+
maxOutputBytes: params.max_bytes,
|
|
243
|
+
});
|
|
240
244
|
return toolResult(
|
|
241
245
|
withFooter(output.text, deps.context(), output.truncated),
|
|
242
246
|
{
|
package/src/tools/pull.ts
CHANGED
|
@@ -3,11 +3,15 @@ import { StringEnum } from "@earendil-works/pi-ai";
|
|
|
3
3
|
import { Type, type Static } from "typebox";
|
|
4
4
|
import {
|
|
5
5
|
confirmMutation,
|
|
6
|
+
DEFAULT_LARGE_MODEL_OUTPUT_BYTES,
|
|
7
|
+
digestPulls,
|
|
8
|
+
modelOutputBytes,
|
|
6
9
|
pushRepeated,
|
|
7
10
|
QUERY_DESCRIPTION,
|
|
8
11
|
REF_DESCRIPTION,
|
|
9
12
|
requireParam,
|
|
10
13
|
runInContext,
|
|
14
|
+
runListInContext,
|
|
11
15
|
toolResult,
|
|
12
16
|
withFooter,
|
|
13
17
|
type ToolDeps,
|
|
@@ -86,6 +90,8 @@ export interface PullParams {
|
|
|
86
90
|
query?: string;
|
|
87
91
|
count?: number;
|
|
88
92
|
offset?: number;
|
|
93
|
+
full?: boolean;
|
|
94
|
+
max_bytes?: number;
|
|
89
95
|
}
|
|
90
96
|
|
|
91
97
|
export function buildPullArgs(params: PullParams): string[] {
|
|
@@ -232,92 +238,56 @@ export function buildPullArgs(params: PullParams): string[] {
|
|
|
232
238
|
|
|
233
239
|
export function registerPullTool(pi: ExtensionAPI, deps: ToolDeps): void {
|
|
234
240
|
const parameters = Type.Object({
|
|
235
|
-
action: StringEnum(PULL_ACTIONS,
|
|
236
|
-
description:
|
|
237
|
-
"list, get, get_comments, get_code_comments, get_builds, get_patch, create, edit, add_comment, add_code_comment, reply_code_comment, resolve_code_comment, unresolve_code_comment, approve, request_changes, merge, discard, checkout, current_reference, valid_labels, query_description, title_description_requirement, commit_message_requirement",
|
|
238
|
-
}),
|
|
241
|
+
action: StringEnum(PULL_ACTIONS),
|
|
239
242
|
ref: Type.Optional(Type.String({ description: REF_DESCRIPTION })),
|
|
240
243
|
title: Type.Optional(Type.String()),
|
|
241
|
-
description: Type.Optional(Type.String(
|
|
242
|
-
content: Type.Optional(Type.String(
|
|
243
|
-
summary: Type.Optional(
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
),
|
|
248
|
-
comment: Type.Optional(
|
|
249
|
-
Type.String({ description: "Markdown note for discard" }),
|
|
250
|
-
),
|
|
251
|
-
commit_message: Type.Optional(
|
|
252
|
-
Type.String({
|
|
253
|
-
description: "Commit message for merge (required by squash merges)",
|
|
254
|
-
}),
|
|
255
|
-
),
|
|
256
|
-
file: Type.Optional(
|
|
257
|
-
Type.String({ description: "add_code_comment: file path in the PR patch" }),
|
|
258
|
-
),
|
|
244
|
+
description: Type.Optional(Type.String()),
|
|
245
|
+
content: Type.Optional(Type.String()),
|
|
246
|
+
summary: Type.Optional(Type.String()),
|
|
247
|
+
comment: Type.Optional(Type.String()),
|
|
248
|
+
commit_message: Type.Optional(Type.String()),
|
|
249
|
+
file: Type.Optional(Type.String()),
|
|
259
250
|
from_line: Type.Optional(
|
|
260
|
-
Type.Integer({
|
|
261
|
-
minimum: 1,
|
|
262
|
-
description: "add_code_comment: 1-based start line (right side of patch)",
|
|
263
|
-
}),
|
|
264
|
-
),
|
|
265
|
-
to_line: Type.Optional(
|
|
266
|
-
Type.Integer({
|
|
267
|
-
minimum: 1,
|
|
268
|
-
description: "add_code_comment: 1-based end line; defaults to from_line",
|
|
269
|
-
}),
|
|
270
|
-
),
|
|
271
|
-
comment_id: Type.Optional(
|
|
272
|
-
Type.Integer({
|
|
273
|
-
minimum: 1,
|
|
274
|
-
description:
|
|
275
|
-
"Code comment id for reply_code_comment, resolve_code_comment, or unresolve_code_comment",
|
|
276
|
-
}),
|
|
251
|
+
Type.Integer({ minimum: 1, description: "Right-side line" }),
|
|
277
252
|
),
|
|
253
|
+
to_line: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
254
|
+
comment_id: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
278
255
|
source_branch: Type.Optional(Type.String()),
|
|
279
256
|
target_branch: Type.Optional(Type.String()),
|
|
280
257
|
source_project: Type.Optional(Type.String()),
|
|
281
258
|
target_project: Type.Optional(Type.String()),
|
|
282
|
-
assignees: Type.Optional(
|
|
283
|
-
|
|
284
|
-
),
|
|
285
|
-
reviewers: Type.Optional(
|
|
286
|
-
Type.Array(Type.String(), { description: "User logins, repeatable" }),
|
|
287
|
-
),
|
|
288
|
-
remove_reviewers: Type.Optional(
|
|
289
|
-
Type.Array(Type.String(), { description: "edit: user logins to remove" }),
|
|
290
|
-
),
|
|
259
|
+
assignees: Type.Optional(Type.Array(Type.String())),
|
|
260
|
+
reviewers: Type.Optional(Type.Array(Type.String())),
|
|
261
|
+
remove_reviewers: Type.Optional(Type.Array(Type.String())),
|
|
291
262
|
labels: Type.Optional(
|
|
292
|
-
Type.Array(Type.String(), {
|
|
293
|
-
description: "Label names; discover via valid_labels",
|
|
294
|
-
}),
|
|
263
|
+
Type.Array(Type.String(), { description: "Use valid_labels" }),
|
|
295
264
|
),
|
|
296
265
|
merge_strategy: Type.Optional(Type.String()),
|
|
297
266
|
auto_merge: Type.Optional(Type.Boolean()),
|
|
298
267
|
auto_merge_commit_message: Type.Optional(Type.String()),
|
|
299
268
|
for_code_review: Type.Optional(
|
|
300
|
-
Type.Boolean({ description: "
|
|
269
|
+
Type.Boolean({ description: "Review-scoped patch" }),
|
|
301
270
|
),
|
|
302
271
|
for_write: Type.Optional(
|
|
303
|
-
Type.Boolean({ description: "checkout
|
|
272
|
+
Type.Boolean({ description: "Prepare checkout for push" }),
|
|
304
273
|
),
|
|
305
274
|
project: Type.Optional(
|
|
306
|
-
Type.String({
|
|
307
|
-
description: "Project path for list; defaults to the current project",
|
|
308
|
-
}),
|
|
275
|
+
Type.String({ description: "Default: current project" }),
|
|
309
276
|
),
|
|
310
277
|
query: Type.Optional(Type.String({ description: QUERY_DESCRIPTION })),
|
|
311
278
|
count: Type.Optional(Type.Integer({ minimum: 1, maximum: 100 })),
|
|
312
279
|
offset: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
280
|
+
full: Type.Optional(
|
|
281
|
+
Type.Boolean({ description: "Raw JSON instead of digest" }),
|
|
282
|
+
),
|
|
283
|
+
max_bytes: modelOutputBytes(),
|
|
313
284
|
}, { additionalProperties: false });
|
|
314
285
|
type Params = Static<typeof parameters>;
|
|
315
286
|
|
|
316
287
|
pi.registerTool({
|
|
317
288
|
name: "onedev_pull",
|
|
318
289
|
label: "OneDev Pull Requests",
|
|
319
|
-
description:
|
|
320
|
-
"Read and edit OneDev pull requests: metadata, labels, comments, line-anchored code comments, builds, patch, review outcomes, and guarded merge/discard. Runs in the current repository's project.",
|
|
290
|
+
description: "Read/manage OneDev PRs, patch, comments, reviews, and guarded merge.",
|
|
321
291
|
parameters,
|
|
322
292
|
async execute(_toolCallId, params: Params, signal, _onUpdate, ctx) {
|
|
323
293
|
const args = buildPullArgs(params);
|
|
@@ -334,7 +304,22 @@ export function registerPullTool(pi: ExtensionAPI, deps: ToolDeps): void {
|
|
|
334
304
|
signal,
|
|
335
305
|
});
|
|
336
306
|
}
|
|
337
|
-
const output =
|
|
307
|
+
const output =
|
|
308
|
+
params.action === "list"
|
|
309
|
+
? await runListInContext(deps, args, {
|
|
310
|
+
signal,
|
|
311
|
+
full: params.full,
|
|
312
|
+
maxOutputBytes: params.max_bytes,
|
|
313
|
+
digest: digestPulls,
|
|
314
|
+
})
|
|
315
|
+
: await runInContext(deps, args, {
|
|
316
|
+
signal,
|
|
317
|
+
maxOutputBytes:
|
|
318
|
+
params.max_bytes ??
|
|
319
|
+
(params.action === "get_patch"
|
|
320
|
+
? DEFAULT_LARGE_MODEL_OUTPUT_BYTES
|
|
321
|
+
: undefined),
|
|
322
|
+
});
|
|
338
323
|
return toolResult(
|
|
339
324
|
withFooter(output.text, deps.context(), output.truncated),
|
|
340
325
|
{
|
package/src/tools/watch.ts
CHANGED
|
@@ -11,8 +11,7 @@ export function registerWatchTool(
|
|
|
11
11
|
pi.registerTool({
|
|
12
12
|
name: "onedev_watch",
|
|
13
13
|
label: "OneDev Watch",
|
|
14
|
-
description:
|
|
15
|
-
"Create session-scoped one-shot watches for a build result or pull-request attention, or list/stop active watches. Notifications contain bounded metadata; use read tools for details.",
|
|
14
|
+
description: "Start/list/stop one-shot build or PR-attention watches.",
|
|
16
15
|
parameters: Type.Object(
|
|
17
16
|
{
|
|
18
17
|
action: StringEnum([
|
|
@@ -21,12 +20,8 @@ export function registerWatchTool(
|
|
|
21
20
|
"list",
|
|
22
21
|
"stop",
|
|
23
22
|
]),
|
|
24
|
-
ref: Type.Optional(
|
|
25
|
-
|
|
26
|
-
),
|
|
27
|
-
watch_id: Type.Optional(
|
|
28
|
-
Type.String({ description: "Watch id required by stop" }),
|
|
29
|
-
),
|
|
23
|
+
ref: Type.Optional(Type.String()),
|
|
24
|
+
watch_id: Type.Optional(Type.String()),
|
|
30
25
|
interval_seconds: Type.Optional(
|
|
31
26
|
Type.Integer({ minimum: 15, maximum: 300, default: 30 }),
|
|
32
27
|
),
|