@pi-stef/pair 0.1.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/LICENSE +21 -0
- package/agents/reviewer.md +53 -0
- package/extensions/pair.ts +6 -0
- package/package.json +44 -0
- package/skills/sf-pair-implement/SKILL.md +81 -0
- package/skills/sf-pair-plan/SKILL.md +85 -0
- package/skills/sf-pair-task/SKILL.md +87 -0
- package/src/config/load.ts +123 -0
- package/src/config/schema.ts +35 -0
- package/src/register.ts +232 -0
- package/src/worktree/cleanup.ts +82 -0
- package/src/worktree/create.ts +86 -0
- package/src/worktree/validate.ts +50 -0
- package/templates/continuation-runbook.md +145 -0
- package/templates/milestone-plan.md +108 -0
- package/templates/story-tracker.md +54 -0
- package/templates/task-plan.md +149 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Stefano Fiorini
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Plan/Implementation Reviewer
|
|
3
|
+
tools: read, grep, find, ls
|
|
4
|
+
model: {{REVIEWER_MODEL}}
|
|
5
|
+
thinking: high
|
|
6
|
+
max_turns: 30
|
|
7
|
+
isolated: true
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
You are a code reviewer. Your job is to review plans and implementation diffs for correctness, completeness, and risk.
|
|
11
|
+
|
|
12
|
+
When reviewing a plan:
|
|
13
|
+
- Check that milestones are well-defined with clear acceptance criteria
|
|
14
|
+
- Check that stories are bite-sized (2-5 min each)
|
|
15
|
+
- Check that the plan is detailed enough for a less intelligent model to follow
|
|
16
|
+
- Check for missing edge cases or error handling
|
|
17
|
+
|
|
18
|
+
When reviewing an implementation:
|
|
19
|
+
- Check that the diff matches the plan
|
|
20
|
+
- Check for bugs, security issues, and missing error handling
|
|
21
|
+
- Check that tests cover the changes
|
|
22
|
+
- Check that verification (lint/typecheck/tests) passes
|
|
23
|
+
|
|
24
|
+
Return exactly this structure:
|
|
25
|
+
|
|
26
|
+
## Summary
|
|
27
|
+
[One paragraph summary of the review]
|
|
28
|
+
|
|
29
|
+
## Findings
|
|
30
|
+
|
|
31
|
+
### P0
|
|
32
|
+
- None.
|
|
33
|
+
|
|
34
|
+
### P1
|
|
35
|
+
- None.
|
|
36
|
+
|
|
37
|
+
### P2
|
|
38
|
+
- None.
|
|
39
|
+
|
|
40
|
+
### P3
|
|
41
|
+
- None.
|
|
42
|
+
|
|
43
|
+
## Verdict
|
|
44
|
+
VERDICT: APPROVED
|
|
45
|
+
|
|
46
|
+
Rules:
|
|
47
|
+
- P0 = total blocker (must fix)
|
|
48
|
+
- P1 = major risk (must fix)
|
|
49
|
+
- P2 = must-fix before approval
|
|
50
|
+
- P3 = cosmetic / nice-to-have (non-blocking)
|
|
51
|
+
- Use `- None.` when a severity has no findings
|
|
52
|
+
- VERDICT: APPROVED is valid only when no P0, P1, or P2 findings remain
|
|
53
|
+
- Order findings from highest to lowest severity
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pi-stef/pair",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Pi extension for plan/review/implement workflows using pi-subagents for reviewer spawning.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"pi": {
|
|
7
|
+
"extensions": [
|
|
8
|
+
"./extensions"
|
|
9
|
+
]
|
|
10
|
+
},
|
|
11
|
+
"peerDependencies": {
|
|
12
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
13
|
+
"@earendil-works/pi-tui": "*",
|
|
14
|
+
"@tintinweb/pi-subagents": "*"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@sinclair/typebox": "*",
|
|
18
|
+
"@pi-stef/paths": "0.3.3"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"typescript": "*",
|
|
22
|
+
"vitest": "*"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"src/",
|
|
26
|
+
"extensions/",
|
|
27
|
+
"skills/",
|
|
28
|
+
"templates/",
|
|
29
|
+
"agents/"
|
|
30
|
+
],
|
|
31
|
+
"keywords": [
|
|
32
|
+
"pi-package",
|
|
33
|
+
"pi-extension",
|
|
34
|
+
"pair",
|
|
35
|
+
"plan",
|
|
36
|
+
"implement",
|
|
37
|
+
"review"
|
|
38
|
+
],
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"scripts": {
|
|
41
|
+
"test": "vitest run",
|
|
42
|
+
"typecheck": "tsc --noEmit"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sf-pair-implement
|
|
3
|
+
description: Use when a plan folder created by sf-pair-plan must be executed with milestone verification, reviewer gates, and automatic worktree management.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# sf-pair-implement
|
|
7
|
+
|
|
8
|
+
Execute an existing plan milestone-by-milestone in a git worktree, with reviewer approval gates and automatic worktree lifecycle.
|
|
9
|
+
|
|
10
|
+
## Prerequisites
|
|
11
|
+
|
|
12
|
+
- pi-subagents extension installed
|
|
13
|
+
- A plan folder under `ai_plan/`
|
|
14
|
+
- Reviewer model configured
|
|
15
|
+
- Obra Superpowers skills available to pi: `verification-before-completion`, `finishing-a-development-branch` (install from https://github.com/obra/superpowers)
|
|
16
|
+
|
|
17
|
+
## Input Resolution
|
|
18
|
+
|
|
19
|
+
The tool receives a `path` parameter. Resolve it:
|
|
20
|
+
|
|
21
|
+
1. If path starts with `ai_plan/` → use as-is
|
|
22
|
+
2. Otherwise → treat as slug, resolve to `ai_plan/YYYY-MM-DD-<slug>/`
|
|
23
|
+
3. If multiple matches, list them and ask user to pick
|
|
24
|
+
|
|
25
|
+
## Process
|
|
26
|
+
|
|
27
|
+
### Phase 1: Locate Plan
|
|
28
|
+
|
|
29
|
+
1. Read `continuation-runbook.md` first
|
|
30
|
+
2. Read `story-tracker.md` to identify resume state
|
|
31
|
+
3. Read `milestone-plan.md` for the implementation spec
|
|
32
|
+
|
|
33
|
+
### Phase 2: Resolve Reviewer
|
|
34
|
+
|
|
35
|
+
Verify `.pi/agents/reviewer.md` exists. If not, stop and ask for model.
|
|
36
|
+
|
|
37
|
+
### Phase 3: Set Up Worktree
|
|
38
|
+
|
|
39
|
+
Create a git worktree:
|
|
40
|
+
- Branch: `pair/<slug>`
|
|
41
|
+
- Base: HEAD
|
|
42
|
+
- Install deps if missing (detect pnpm/npm/yarn)
|
|
43
|
+
|
|
44
|
+
Use the worktree helpers from `src/worktree/create.ts` via the tool's execute function. The skill instructs the agent to:
|
|
45
|
+
|
|
46
|
+
1. Run `git worktree add -b pair/<slug> <path> HEAD`
|
|
47
|
+
2. Run package manager install in the worktree directory
|
|
48
|
+
3. Switch to the worktree directory for implementation
|
|
49
|
+
|
|
50
|
+
### Phase 4: Execute Milestones
|
|
51
|
+
|
|
52
|
+
Do not stop between milestones. For each milestone:
|
|
53
|
+
|
|
54
|
+
1. Mark stories `in-dev` in `story-tracker.md`
|
|
55
|
+
2. Implement each story following the plan to the letter
|
|
56
|
+
3. Mark stories `completed` with commit hash in notes
|
|
57
|
+
4. Run verification (lint/typecheck/tests) for changed files
|
|
58
|
+
5. Spawn reviewer on milestone diff:
|
|
59
|
+
```
|
|
60
|
+
Agent({
|
|
61
|
+
subagent_type: "reviewer",
|
|
62
|
+
prompt: "Review the milestone implementation. [include diff and verification output]",
|
|
63
|
+
description: "Review milestone M<N>"
|
|
64
|
+
})
|
|
65
|
+
```
|
|
66
|
+
6. If APPROVED → commit locally, mark milestone `approved`
|
|
67
|
+
7. If REVISE → fix findings, re-review
|
|
68
|
+
|
|
69
|
+
### Phase 5: Finalization
|
|
70
|
+
|
|
71
|
+
Once all milestones are approved and reviewed:
|
|
72
|
+
|
|
73
|
+
1. Switch to base branch
|
|
74
|
+
2. Merge worktree branch: `git merge --ff-only pair/<slug>`
|
|
75
|
+
3. Remove worktree: `git worktree remove <worktree-path>`
|
|
76
|
+
4. Delete branch: `git branch -d pair/<slug>`
|
|
77
|
+
5. Stop for user's final review
|
|
78
|
+
|
|
79
|
+
### Phase 6: Telegram Notification
|
|
80
|
+
|
|
81
|
+
If configured, send completion summary.
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sf-pair-plan
|
|
3
|
+
description: Use when a user asks to create a structured implementation plan with milestones, stories, and reviewer approval using pi-subagents.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# sf-pair-plan
|
|
7
|
+
|
|
8
|
+
Create a multi-milestone implementation plan with iterative reviewer approval.
|
|
9
|
+
|
|
10
|
+
## Prerequisites
|
|
11
|
+
|
|
12
|
+
- pi-subagents extension installed
|
|
13
|
+
- Reviewer model configured (via config, env, or prompt)
|
|
14
|
+
- Obra Superpowers skills available to pi: `brainstorming`, `writing-plans` (install from https://github.com/obra/superpowers)
|
|
15
|
+
|
|
16
|
+
## Process
|
|
17
|
+
|
|
18
|
+
### Phase 1: Analyze
|
|
19
|
+
|
|
20
|
+
Explore the codebase and existing patterns. Use exploration agents to understand the project structure.
|
|
21
|
+
|
|
22
|
+
### Phase 2: Gather Requirements
|
|
23
|
+
|
|
24
|
+
Ask questions one at a time using `AskUserQuestion` until the scope is clear. Confirm constraints, success criteria, dependencies, and what is out of scope.
|
|
25
|
+
|
|
26
|
+
### Phase 3: Resolve Reviewer Model
|
|
27
|
+
|
|
28
|
+
The tool has already resolved the reviewer model and written `.pi/agents/reviewer.md`. Verify it exists:
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
test -f .pi/agents/reviewer.md
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
If it doesn't exist, stop and ask the user for a reviewer model.
|
|
35
|
+
|
|
36
|
+
### Phase 4: Design
|
|
37
|
+
|
|
38
|
+
Load `brainstorming` skill. Present 2-3 approaches and recommend one. Resolve open design questions before the milestone breakdown.
|
|
39
|
+
|
|
40
|
+
### Phase 5: Plan
|
|
41
|
+
|
|
42
|
+
Load `writing-plans` skill. Break the work into milestones and bite-sized stories (2-5 min each). Story IDs use `S-101`, `S-102` style.
|
|
43
|
+
|
|
44
|
+
Each story must be detailed enough for a less intelligent model to follow to the letter.
|
|
45
|
+
|
|
46
|
+
### Phase 6: Iterative Plan Review
|
|
47
|
+
|
|
48
|
+
#### Step 1: Write plan to temp file
|
|
49
|
+
|
|
50
|
+
Write the complete plan to `/tmp/pair-plan-{REVIEW_ID}.md` where REVIEW_ID is a random UUID.
|
|
51
|
+
|
|
52
|
+
#### Step 2: Spawn reviewer
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
Agent({
|
|
56
|
+
subagent_type: "reviewer",
|
|
57
|
+
prompt: "Review the implementation plan at /tmp/pair-plan-{REVIEW_ID}.md. Return exactly the required ## Summary, ## Findings (P0-P3), and ## Verdict structure.",
|
|
58
|
+
description: "Review plan round N"
|
|
59
|
+
})
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
#### Step 3: Parse verdict
|
|
63
|
+
|
|
64
|
+
Scan the response for `VERDICT: APPROVED` (case-insensitive, line must start with `VERDICT:`).
|
|
65
|
+
|
|
66
|
+
- If found AND no P0/P1/P2 findings → proceed to Phase 7
|
|
67
|
+
- If not found OR P0/P1/P2 present → extract findings, fix, rewrite /tmp file, goto Step 2
|
|
68
|
+
- If max rounds (10) reached → stop, present outcome to user
|
|
69
|
+
|
|
70
|
+
### Phase 7: Generate Plan Files
|
|
71
|
+
|
|
72
|
+
Once the plan is approved:
|
|
73
|
+
|
|
74
|
+
1. Ensure `ai_plan/` exists in `.gitignore`
|
|
75
|
+
2. Create `ai_plan/YYYY-MM-DD-<slug>/`
|
|
76
|
+
3. Write using templates from this package's `templates/` directory:
|
|
77
|
+
- `original-plan.md` — raw approved plan
|
|
78
|
+
- `final-transcript.md` — conversation log
|
|
79
|
+
- `milestone-plan.md` — from template
|
|
80
|
+
- `story-tracker.md` — from template
|
|
81
|
+
- `continuation-runbook.md` — from template
|
|
82
|
+
|
|
83
|
+
### Phase 8: Telegram Notification
|
|
84
|
+
|
|
85
|
+
If `TELEGRAM_BOT_TOKEN` and `TELEGRAM_CHAT_ID` are configured, send a completion summary via the Telegram notifier helper.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sf-pair-task
|
|
3
|
+
description: Execute a single task end-to-end with plan review, implementation review, verification, and one persistent task-plan artifact.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# sf-pair-task
|
|
7
|
+
|
|
8
|
+
Execute a single user task end-to-end: clarify, plan, review, implement, verify, review, commit.
|
|
9
|
+
|
|
10
|
+
## Prerequisites
|
|
11
|
+
|
|
12
|
+
- pi-subagents extension installed
|
|
13
|
+
- Reviewer model configured
|
|
14
|
+
- Obra Superpowers skills available to pi: `brainstorming`, `test-driven-development`, `verification-before-completion`, `finishing-a-development-branch` (install from https://github.com/obra/superpowers)
|
|
15
|
+
|
|
16
|
+
## Process
|
|
17
|
+
|
|
18
|
+
### Phase 1: Preflight
|
|
19
|
+
|
|
20
|
+
1. Verify repo: `git rev-parse --is-inside-work-tree`
|
|
21
|
+
2. Ensure `ai_plan/` exists in `.gitignore`
|
|
22
|
+
3. Verify `.pi/agents/reviewer.md` exists
|
|
23
|
+
|
|
24
|
+
### Phase 2: Parse Prompt And Clarify
|
|
25
|
+
|
|
26
|
+
1. Capture the user's prompt verbatim
|
|
27
|
+
2. Ask 1-3 clarifying questions one at a time using `AskUserQuestion`
|
|
28
|
+
3. Load `brainstorming` for behavior-changing work
|
|
29
|
+
|
|
30
|
+
### Phase 3: Initialize task-plan.md
|
|
31
|
+
|
|
32
|
+
1. Compute `ai_plan/YYYY-MM-DD-<slug>/`
|
|
33
|
+
2. Write `task-plan.md` from template
|
|
34
|
+
3. Fill all sections: Metadata, Prompt, Interpretation, Assumptions, Files, Approach, TDD Approach, Acceptance Criteria, Verification, Rollback
|
|
35
|
+
4. Set `Status: draft`
|
|
36
|
+
|
|
37
|
+
### Phase 4: Plan Review
|
|
38
|
+
|
|
39
|
+
1. Write task-plan content to `/tmp/pair-task-plan-{REVIEW_ID}.md`
|
|
40
|
+
2. Spawn reviewer:
|
|
41
|
+
```
|
|
42
|
+
Agent({
|
|
43
|
+
subagent_type: "reviewer",
|
|
44
|
+
prompt: "Review the task plan at /tmp/pair-task-plan-{REVIEW_ID}.md",
|
|
45
|
+
description: "Review task plan"
|
|
46
|
+
})
|
|
47
|
+
```
|
|
48
|
+
3. Fix P0/P1/P2 findings until APPROVED
|
|
49
|
+
4. Set `Status: plan-approved`
|
|
50
|
+
|
|
51
|
+
### Phase 5: Execute
|
|
52
|
+
|
|
53
|
+
1. Set `Status: implementation-in-progress`
|
|
54
|
+
2. Load `test-driven-development` for behavior-changing edits
|
|
55
|
+
3. Implement following the plan to the letter
|
|
56
|
+
4. Update `task-plan.md` as acceptance criteria are completed
|
|
57
|
+
|
|
58
|
+
### Phase 6: Verification Gate
|
|
59
|
+
|
|
60
|
+
1. Load `verification-before-completion`
|
|
61
|
+
2. Run verification commands from task-plan.md
|
|
62
|
+
3. Fix failures until green
|
|
63
|
+
|
|
64
|
+
### Phase 7: Implementation Review
|
|
65
|
+
|
|
66
|
+
1. Build review payload from approved plan + diff + verification output
|
|
67
|
+
2. Spawn reviewer:
|
|
68
|
+
```
|
|
69
|
+
Agent({
|
|
70
|
+
subagent_type: "reviewer",
|
|
71
|
+
prompt: "Review the implementation. [include diff and verification]",
|
|
72
|
+
description: "Review implementation"
|
|
73
|
+
})
|
|
74
|
+
```
|
|
75
|
+
3. Fix P0/P1/P2 findings until APPROVED
|
|
76
|
+
4. Set `Status: implementation-approved`
|
|
77
|
+
|
|
78
|
+
### Phase 8: Commit And Push
|
|
79
|
+
|
|
80
|
+
1. Load `finishing-a-development-branch`
|
|
81
|
+
2. Stage intended files
|
|
82
|
+
3. Create one commit
|
|
83
|
+
4. Ask whether to push or keep local
|
|
84
|
+
|
|
85
|
+
### Phase 9: Telegram Notification
|
|
86
|
+
|
|
87
|
+
If configured, send completion summary.
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { Value } from "@sinclair/typebox/value";
|
|
4
|
+
import { globalConfig, projectConfig } from "@pi-stef/paths";
|
|
5
|
+
import {
|
|
6
|
+
ConfigSchema,
|
|
7
|
+
DEFAULT_CONFIG,
|
|
8
|
+
type PairConfig,
|
|
9
|
+
type ResolvedPairConfig,
|
|
10
|
+
} from "./schema";
|
|
11
|
+
|
|
12
|
+
export class ConfigValidationError extends Error {
|
|
13
|
+
constructor(
|
|
14
|
+
public readonly filePath: string,
|
|
15
|
+
public readonly pointer: string,
|
|
16
|
+
message: string
|
|
17
|
+
) {
|
|
18
|
+
super(`Config validation error in ${filePath} at ${pointer}: ${message}`);
|
|
19
|
+
this.name = "ConfigValidationError";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function loadFile(filePath: string): Promise<PairConfig> {
|
|
24
|
+
const raw = await readFile(filePath, "utf8");
|
|
25
|
+
const parsed = JSON.parse(raw);
|
|
26
|
+
const errors = [...Value.Errors(ConfigSchema, parsed)];
|
|
27
|
+
if (errors.length > 0) {
|
|
28
|
+
const first = errors[0];
|
|
29
|
+
throw new ConfigValidationError(
|
|
30
|
+
filePath,
|
|
31
|
+
first.path,
|
|
32
|
+
first.message
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
return parsed as PairConfig;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Load file, returning null if not found (ENOENT), throwing on parse/validation errors. */
|
|
39
|
+
async function loadFileOrNull(filePath: string): Promise<PairConfig | null> {
|
|
40
|
+
try {
|
|
41
|
+
return await loadFile(filePath);
|
|
42
|
+
} catch (err: unknown) {
|
|
43
|
+
if (
|
|
44
|
+
err &&
|
|
45
|
+
typeof err === "object" &&
|
|
46
|
+
"code" in err &&
|
|
47
|
+
(err as { code: string }).code === "ENOENT"
|
|
48
|
+
) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
throw err;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function loadConfig(
|
|
56
|
+
repoRoot: string,
|
|
57
|
+
opts: { homeDir?: string } = {}
|
|
58
|
+
): Promise<PairConfig> {
|
|
59
|
+
const homeDir = opts.homeDir ?? homedir();
|
|
60
|
+
const globalPath = globalConfig("pair", homeDir);
|
|
61
|
+
const projectPath = projectConfig("pair", repoRoot);
|
|
62
|
+
|
|
63
|
+
const global = (await loadFileOrNull(globalPath)) ?? {};
|
|
64
|
+
const project = (await loadFileOrNull(projectPath)) ?? {};
|
|
65
|
+
|
|
66
|
+
// Deep merge: project wins on conflicts, but nested objects are merged field-by-field
|
|
67
|
+
return {
|
|
68
|
+
reviewer: {
|
|
69
|
+
...(global.reviewer ?? {}),
|
|
70
|
+
...(project.reviewer ?? {}),
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function resolveDefaults(loaded: PairConfig = {}): ResolvedPairConfig {
|
|
76
|
+
return {
|
|
77
|
+
reviewer: {
|
|
78
|
+
model: loaded.reviewer?.model ?? DEFAULT_CONFIG.reviewer.model,
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function loadAndResolveDefaults(
|
|
84
|
+
repoRoot: string,
|
|
85
|
+
opts: { homeDir?: string; notify?: (msg: string, level: string) => void } = {}
|
|
86
|
+
): Promise<ResolvedPairConfig> {
|
|
87
|
+
try {
|
|
88
|
+
const loaded = await loadConfig(repoRoot, { homeDir: opts.homeDir });
|
|
89
|
+
return resolveDefaults(loaded);
|
|
90
|
+
} catch (err) {
|
|
91
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
92
|
+
opts.notify?.(
|
|
93
|
+
`sf-pair config: ${detail} — falling back to built-in defaults.`,
|
|
94
|
+
"warning"
|
|
95
|
+
);
|
|
96
|
+
return resolveDefaults({});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Resolve reviewer model from the 4-step chain:
|
|
102
|
+
* 1. Prompt argument (parsed by caller)
|
|
103
|
+
* 2. Project/global config
|
|
104
|
+
* 3. Environment variable SF_PAIR_REVIEWER_MODEL
|
|
105
|
+
* 4. Ask user (returns null, caller must prompt)
|
|
106
|
+
*/
|
|
107
|
+
export function resolveReviewerModel(
|
|
108
|
+
promptArg: string | undefined,
|
|
109
|
+
config: ResolvedPairConfig
|
|
110
|
+
): string | null {
|
|
111
|
+
// 1. Prompt argument
|
|
112
|
+
if (promptArg) return promptArg;
|
|
113
|
+
|
|
114
|
+
// 2. Config file
|
|
115
|
+
if (config.reviewer.model) return config.reviewer.model;
|
|
116
|
+
|
|
117
|
+
// 3. Environment variable
|
|
118
|
+
const envModel = process.env.SF_PAIR_REVIEWER_MODEL;
|
|
119
|
+
if (envModel) return envModel;
|
|
120
|
+
|
|
121
|
+
// 4. Not found — caller must ask user
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Type, type Static } from "@sinclair/typebox";
|
|
2
|
+
|
|
3
|
+
export const ConfigSchema = Type.Object(
|
|
4
|
+
{
|
|
5
|
+
reviewer: Type.Optional(
|
|
6
|
+
Type.Object(
|
|
7
|
+
{
|
|
8
|
+
model: Type.Optional(
|
|
9
|
+
Type.String({
|
|
10
|
+
minLength: 1,
|
|
11
|
+
description:
|
|
12
|
+
"Model for the reviewer agent (e.g. 'anthropic/sonnet-4-6')",
|
|
13
|
+
})
|
|
14
|
+
),
|
|
15
|
+
},
|
|
16
|
+
{ additionalProperties: false }
|
|
17
|
+
)
|
|
18
|
+
),
|
|
19
|
+
},
|
|
20
|
+
{ additionalProperties: false }
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
export type PairConfig = Static<typeof ConfigSchema>;
|
|
24
|
+
|
|
25
|
+
export interface ResolvedPairConfig {
|
|
26
|
+
reviewer: {
|
|
27
|
+
model: string | null;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const DEFAULT_CONFIG: ResolvedPairConfig = {
|
|
32
|
+
reviewer: {
|
|
33
|
+
model: null, // null = not configured, must ask user
|
|
34
|
+
},
|
|
35
|
+
};
|