@xzzpig/pi-openspec-x 0.2.1 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/README.md +3 -0
- package/package.json +8 -6
- package/src/plan-gate.ts +47 -30
- package/src/report-tools.ts +68 -66
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.2 — 2026-11-05
|
|
4
|
+
|
|
5
|
+
- Require Pi 1.5.0 or newer and build tool parameter schemas with
|
|
6
|
+
`@earendil-works/pi-schema` instead of TypeBox. Preserve existing required
|
|
7
|
+
fields, bounds, and open/closed-object validation semantics.
|
|
8
|
+
|
|
3
9
|
## 0.2.1
|
|
4
10
|
|
|
5
11
|
- Support Pi 1.2+ custom-message wakeups in child report handlers. A wakeup
|
package/README.md
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
把 [OpenSpec](https://github.com/Fission-AI/OpenSpec) 的规范驱动流程接进 pi coding agent:从**当前安装的 openspec CLI** 现场提取官方 skill,并提供 `/opsx:plan`(深度计划 + 缺口分析 / 计划审查门)与 `/opsx:implement`(代理实现 / 主会话直实现 + 最终整体审查门)两条增强流程。
|
|
4
4
|
|
|
5
|
+
Requires **Pi >=1.5.0 <2**. Tool schemas use `@earendil-works/pi-schema`,
|
|
6
|
+
which is declared as a Pi peer dependency.
|
|
7
|
+
|
|
5
8
|
```bash
|
|
6
9
|
pi install npm:@xzzpig/pi-openspec-x
|
|
7
10
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xzzpig/pi-openspec-x",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "OpenSpec workflow for the pi coding agent: spec-driven planning and implementation via dedicated subagents, sandboxed /opsx:plan and /opsx:implement commands.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -37,19 +37,21 @@
|
|
|
37
37
|
]
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
|
-
"@earendil-works/pi-ai": ">=1.
|
|
41
|
-
"@earendil-works/pi-coding-agent": ">=1.
|
|
40
|
+
"@earendil-works/pi-ai": ">=1.5.0 <2",
|
|
41
|
+
"@earendil-works/pi-coding-agent": ">=1.5.0 <2",
|
|
42
42
|
"@xzzpig/pi-goal-x": "*",
|
|
43
43
|
"@xzzpig/pi-sandbox": "*",
|
|
44
|
-
"@xzzpig/pi-subagents": "*"
|
|
44
|
+
"@xzzpig/pi-subagents": "*",
|
|
45
|
+
"@earendil-works/pi-schema": ">=1.5.0 <2"
|
|
45
46
|
},
|
|
46
47
|
"devDependencies": {
|
|
47
|
-
"@earendil-works/pi-coding-agent": "1.
|
|
48
|
+
"@earendil-works/pi-coding-agent": "1.5.0",
|
|
48
49
|
"@types/node": "24.12.4",
|
|
49
50
|
"rollup": "^4.62.2",
|
|
50
51
|
"rollup-plugin-dts": "^6.4.1",
|
|
51
52
|
"typescript": "5.9.3",
|
|
52
|
-
"vitest": "4.1.8"
|
|
53
|
+
"vitest": "4.1.8",
|
|
54
|
+
"@earendil-works/pi-schema": "1.5.0"
|
|
53
55
|
},
|
|
54
56
|
"scripts": {
|
|
55
57
|
"test": "vitest run",
|
package/src/plan-gate.ts
CHANGED
|
@@ -17,7 +17,16 @@
|
|
|
17
17
|
* reads the recorded approval from disk, so approval survives a restart and a
|
|
18
18
|
* fresh session cannot bypass the gate.
|
|
19
19
|
*/
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
arr,
|
|
22
|
+
lit,
|
|
23
|
+
num,
|
|
24
|
+
obj,
|
|
25
|
+
opt,
|
|
26
|
+
str,
|
|
27
|
+
union,
|
|
28
|
+
type Static,
|
|
29
|
+
} from "@earendil-works/pi-schema";
|
|
21
30
|
import {
|
|
22
31
|
defineTool,
|
|
23
32
|
type ExtensionAPI,
|
|
@@ -43,61 +52,57 @@ import {
|
|
|
43
52
|
export const OPSX_PLAN_GAP_ANALYSIS_TOOL_NAME = "opsx_plan_gap_analysis";
|
|
44
53
|
export const OPSX_PLAN_VERDICT_TOOL_NAME = "opsx_plan_verdict";
|
|
45
54
|
|
|
46
|
-
const usageSchema =
|
|
47
|
-
|
|
55
|
+
const usageSchema = opt(
|
|
56
|
+
obj(
|
|
48
57
|
{
|
|
49
|
-
input:
|
|
50
|
-
output:
|
|
51
|
-
cacheRead:
|
|
52
|
-
cacheWrite:
|
|
53
|
-
turns:
|
|
58
|
+
input: num(),
|
|
59
|
+
output: num(),
|
|
60
|
+
cacheRead: num(),
|
|
61
|
+
cacheWrite: num(),
|
|
62
|
+
turns: num(),
|
|
54
63
|
},
|
|
55
|
-
{
|
|
64
|
+
{ open: false },
|
|
56
65
|
),
|
|
57
66
|
);
|
|
58
67
|
|
|
59
|
-
const gapAnalysisParams =
|
|
68
|
+
const gapAnalysisParams = obj(
|
|
60
69
|
{
|
|
61
|
-
findings:
|
|
62
|
-
|
|
70
|
+
findings: arr(
|
|
71
|
+
str({
|
|
63
72
|
description: "One concrete gap, specific enough to act on.",
|
|
64
73
|
}),
|
|
65
74
|
{ description: "Gap-analysis findings; may be empty." },
|
|
66
75
|
),
|
|
67
|
-
summary:
|
|
68
|
-
changeId:
|
|
69
|
-
|
|
76
|
+
summary: opt(str()),
|
|
77
|
+
changeId: opt(
|
|
78
|
+
str({
|
|
70
79
|
description:
|
|
71
80
|
"OpenSpec change id; defaults to this session's active plan flow.",
|
|
72
81
|
}),
|
|
73
82
|
),
|
|
74
83
|
},
|
|
75
|
-
{
|
|
84
|
+
{ open: false },
|
|
76
85
|
);
|
|
77
86
|
|
|
78
|
-
const planReviewParams =
|
|
87
|
+
const planReviewParams = obj(
|
|
79
88
|
{
|
|
80
|
-
verdict:
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
Type.Literal("REJECT"),
|
|
84
|
-
]),
|
|
85
|
-
blockers: Type.Optional(
|
|
86
|
-
Type.Array(Type.String(), {
|
|
89
|
+
verdict: union([lit("OKAY"), lit("ITERATE"), lit("REJECT")]),
|
|
90
|
+
blockers: opt(
|
|
91
|
+
arr(str(), {
|
|
87
92
|
description:
|
|
88
93
|
"Blocking issues; at most three, each new relative to earlier rounds.",
|
|
89
94
|
}),
|
|
90
95
|
),
|
|
91
|
-
summary:
|
|
96
|
+
summary: opt(str()),
|
|
92
97
|
usage: usageSchema,
|
|
93
|
-
changeId:
|
|
94
|
-
|
|
98
|
+
changeId: opt(
|
|
99
|
+
str({
|
|
95
100
|
description:
|
|
96
101
|
"OpenSpec change id; defaults to this session's active plan flow.",
|
|
97
102
|
}),
|
|
98
103
|
),
|
|
99
104
|
},
|
|
100
|
-
{
|
|
105
|
+
{ open: false },
|
|
101
106
|
);
|
|
102
107
|
|
|
103
108
|
/**
|
|
@@ -149,7 +154,13 @@ export function registerPlanGateTools(pi: ExtensionAPI): void {
|
|
|
149
154
|
],
|
|
150
155
|
parameters: gapAnalysisParams,
|
|
151
156
|
executionMode: "sequential",
|
|
152
|
-
async execute(
|
|
157
|
+
async execute(
|
|
158
|
+
_toolCallId,
|
|
159
|
+
params: Static<typeof gapAnalysisParams>,
|
|
160
|
+
_signal,
|
|
161
|
+
_onUpdate,
|
|
162
|
+
ctx,
|
|
163
|
+
) {
|
|
153
164
|
const resolved = requireChangeId(ctx, params.changeId);
|
|
154
165
|
if ("error" in resolved) {
|
|
155
166
|
return toolText(resolved.error);
|
|
@@ -181,7 +192,13 @@ export function registerPlanGateTools(pi: ExtensionAPI): void {
|
|
|
181
192
|
],
|
|
182
193
|
parameters: planReviewParams,
|
|
183
194
|
executionMode: "sequential",
|
|
184
|
-
async execute(
|
|
195
|
+
async execute(
|
|
196
|
+
_toolCallId,
|
|
197
|
+
params: Static<typeof planReviewParams>,
|
|
198
|
+
_signal,
|
|
199
|
+
_onUpdate,
|
|
200
|
+
ctx,
|
|
201
|
+
) {
|
|
185
202
|
const resolved = requireChangeId(ctx, params.changeId);
|
|
186
203
|
if ("error" in resolved) {
|
|
187
204
|
return toolText(resolved.error);
|
package/src/report-tools.ts
CHANGED
|
@@ -22,7 +22,17 @@
|
|
|
22
22
|
* call returns. There is no progress bar here — a tool may be called any
|
|
23
23
|
* number of times; projection of the calls is a later concern.
|
|
24
24
|
*/
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
arr,
|
|
27
|
+
bool,
|
|
28
|
+
lit,
|
|
29
|
+
num,
|
|
30
|
+
obj,
|
|
31
|
+
opt,
|
|
32
|
+
str,
|
|
33
|
+
union,
|
|
34
|
+
type Static,
|
|
35
|
+
} from "@earendil-works/pi-schema";
|
|
26
36
|
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
27
37
|
|
|
28
38
|
export const REPORT_GAP_ANALYSIS_TOOL_NAME = "report_gap_analysis";
|
|
@@ -33,20 +43,20 @@ export const REPORT_WORK_TOOL_NAME = "report_work";
|
|
|
33
43
|
* One located finding. `location` is free-form ("src/cli.ts:42",
|
|
34
44
|
* "tasks.md#Step 3") because artifact sections do not always map to lines.
|
|
35
45
|
*/
|
|
36
|
-
const findingItem =
|
|
46
|
+
const findingItem = obj(
|
|
37
47
|
{
|
|
38
|
-
description:
|
|
48
|
+
description: str({
|
|
39
49
|
description:
|
|
40
50
|
"The finding itself, specific enough to act on without re-deriving it.",
|
|
41
51
|
}),
|
|
42
|
-
location:
|
|
43
|
-
|
|
52
|
+
location: opt(
|
|
53
|
+
str({
|
|
44
54
|
description:
|
|
45
55
|
"Where the finding applies: file path, and line or section when known.",
|
|
46
56
|
}),
|
|
47
57
|
),
|
|
48
58
|
},
|
|
49
|
-
{
|
|
59
|
+
{ open: false },
|
|
50
60
|
);
|
|
51
61
|
|
|
52
62
|
/**
|
|
@@ -54,158 +64,150 @@ const findingItem = Type.Object(
|
|
|
54
64
|
* 百分比,或等效字段). Optional: a report that carries no progress simply leaves
|
|
55
65
|
* the parent's board unchanged.
|
|
56
66
|
*/
|
|
57
|
-
const progressField =
|
|
58
|
-
|
|
67
|
+
const progressField = opt(
|
|
68
|
+
obj(
|
|
59
69
|
{
|
|
60
|
-
phase:
|
|
70
|
+
phase: str({
|
|
61
71
|
description:
|
|
62
72
|
"The stage this update belongs to, e.g. reading, analyzing, editing, verifying.",
|
|
63
73
|
}),
|
|
64
|
-
percentage:
|
|
74
|
+
percentage: num({
|
|
65
75
|
description: "How far through this stage the agent is, 0 to 100.",
|
|
66
76
|
minimum: 0,
|
|
67
77
|
maximum: 100,
|
|
68
78
|
}),
|
|
69
|
-
label:
|
|
70
|
-
|
|
79
|
+
label: opt(
|
|
80
|
+
str({
|
|
71
81
|
description:
|
|
72
82
|
"Short human-readable detail for the board; defaults to the phase name.",
|
|
73
83
|
}),
|
|
74
84
|
),
|
|
75
85
|
},
|
|
76
|
-
{
|
|
86
|
+
{ open: false },
|
|
77
87
|
),
|
|
78
88
|
);
|
|
79
89
|
|
|
80
|
-
const gapAnalysisParams =
|
|
90
|
+
const gapAnalysisParams = obj(
|
|
81
91
|
{
|
|
82
92
|
progress: progressField,
|
|
83
|
-
intentType:
|
|
93
|
+
intentType: str({
|
|
84
94
|
description:
|
|
85
95
|
"Classified user intent, e.g. refactor, build-from-scratch, mid-sized, collaborative, architecture, or research.",
|
|
86
96
|
}),
|
|
87
|
-
confidence:
|
|
97
|
+
confidence: num({
|
|
88
98
|
description: "Confidence in the intent classification, from 0 to 1.",
|
|
89
99
|
minimum: 0,
|
|
90
100
|
maximum: 1,
|
|
91
101
|
}),
|
|
92
|
-
contradictions:
|
|
102
|
+
contradictions: arr(findingItem, {
|
|
93
103
|
description: "Requirements or statements that conflict with each other.",
|
|
94
104
|
}),
|
|
95
|
-
missingConstraints:
|
|
105
|
+
missingConstraints: arr(findingItem, {
|
|
96
106
|
description:
|
|
97
107
|
"Constraints the request implies but never states (platform, compatibility, performance, security, ...).",
|
|
98
108
|
}),
|
|
99
|
-
scopeRisks:
|
|
109
|
+
scopeRisks: arr(findingItem, {
|
|
100
110
|
description:
|
|
101
111
|
"Scope-inflation or gold-plating risks, including premature abstraction and over-validation.",
|
|
102
112
|
}),
|
|
103
|
-
unvalidatedAssumptions:
|
|
113
|
+
unvalidatedAssumptions: arr(findingItem, {
|
|
104
114
|
description:
|
|
105
115
|
"Assumptions the plan relies on without evidence (environment, behavior, availability).",
|
|
106
116
|
}),
|
|
107
|
-
missingAcceptanceCriteria:
|
|
117
|
+
missingAcceptanceCriteria: arr(findingItem, {
|
|
108
118
|
description:
|
|
109
119
|
"Outcomes that lack an agent-executable acceptance criterion (no 'user manually tests' criteria).",
|
|
110
120
|
}),
|
|
111
|
-
summary:
|
|
121
|
+
summary: str({
|
|
112
122
|
description: "One-paragraph synthesis of the analysis.",
|
|
113
123
|
}),
|
|
114
124
|
},
|
|
115
|
-
{
|
|
125
|
+
{ open: false },
|
|
116
126
|
);
|
|
117
127
|
|
|
118
|
-
const planReviewParams =
|
|
128
|
+
const planReviewParams = obj(
|
|
119
129
|
{
|
|
120
130
|
progress: progressField,
|
|
121
|
-
verdict:
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
),
|
|
128
|
-
issues: Type.Array(
|
|
129
|
-
Type.Object(
|
|
131
|
+
verdict: union([lit("OKAY"), lit("ITERATE"), lit("REJECT")], {
|
|
132
|
+
description:
|
|
133
|
+
"OKAY: executable as written. ITERATE: workable after targeted, non-blocking revisions. REJECT: not executable as written.",
|
|
134
|
+
}),
|
|
135
|
+
issues: arr(
|
|
136
|
+
obj(
|
|
130
137
|
{
|
|
131
|
-
severity:
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
],
|
|
137
|
-
{ description: "How badly the issue threatens execution." },
|
|
138
|
-
),
|
|
139
|
-
file: Type.Optional(
|
|
140
|
-
Type.String({
|
|
138
|
+
severity: union([lit("blocker"), lit("major"), lit("minor")], {
|
|
139
|
+
description: "How badly the issue threatens execution.",
|
|
140
|
+
}),
|
|
141
|
+
file: opt(
|
|
142
|
+
str({
|
|
141
143
|
description: "Plan file the issue applies to, when located.",
|
|
142
144
|
}),
|
|
143
145
|
),
|
|
144
|
-
line:
|
|
145
|
-
|
|
146
|
+
line: opt(
|
|
147
|
+
num({
|
|
146
148
|
description: "Line number in that file, when known.",
|
|
147
149
|
}),
|
|
148
150
|
),
|
|
149
|
-
description:
|
|
151
|
+
description: str({
|
|
150
152
|
description:
|
|
151
153
|
"The issue, concrete and actionable (what to change, not a taste judgment).",
|
|
152
154
|
}),
|
|
153
|
-
blocking:
|
|
155
|
+
blocking: bool({
|
|
154
156
|
description:
|
|
155
157
|
"True only for REJECT blockers; ITERATE/OKAY issues are never blocking.",
|
|
156
158
|
}),
|
|
157
159
|
},
|
|
158
|
-
{
|
|
160
|
+
{ open: false },
|
|
159
161
|
),
|
|
160
162
|
{ description: "Found issues; may be empty for a clean OKAY." },
|
|
161
163
|
),
|
|
162
|
-
summary:
|
|
164
|
+
summary: str({
|
|
163
165
|
description:
|
|
164
166
|
"One-paragraph verdict rationale; for REJECT it must reference which earlier blockers (if any) are now resolved.",
|
|
165
167
|
}),
|
|
166
168
|
},
|
|
167
|
-
{
|
|
169
|
+
{ open: false },
|
|
168
170
|
);
|
|
169
171
|
|
|
170
|
-
const workReportParams =
|
|
172
|
+
const workReportParams = obj(
|
|
171
173
|
{
|
|
172
174
|
progress: progressField,
|
|
173
|
-
taskId:
|
|
175
|
+
taskId: str({
|
|
174
176
|
description: "The dispatched task identifier (e.g. the tasks.md item).",
|
|
175
177
|
}),
|
|
176
|
-
changedFiles:
|
|
178
|
+
changedFiles: arr(str(), {
|
|
177
179
|
description:
|
|
178
180
|
"Every file this task created or modified, with paths relative to the project root.",
|
|
179
181
|
}),
|
|
180
|
-
claims:
|
|
182
|
+
claims: str({
|
|
181
183
|
description:
|
|
182
184
|
"What was done and what the result is. Only claims backed by evidence below.",
|
|
183
185
|
}),
|
|
184
|
-
selfVerification:
|
|
186
|
+
selfVerification: obj(
|
|
185
187
|
{
|
|
186
|
-
command:
|
|
187
|
-
|
|
188
|
+
command: opt(
|
|
189
|
+
str({
|
|
188
190
|
description:
|
|
189
191
|
"The verification command actually run (e.g. build/test).",
|
|
190
192
|
}),
|
|
191
193
|
),
|
|
192
|
-
result:
|
|
193
|
-
|
|
194
|
+
result: opt(
|
|
195
|
+
str({
|
|
194
196
|
description:
|
|
195
197
|
"The real command output summary (exit status, failing/passing counts). Never invented.",
|
|
196
198
|
}),
|
|
197
199
|
),
|
|
198
200
|
},
|
|
199
|
-
{
|
|
201
|
+
{ open: false },
|
|
200
202
|
),
|
|
201
|
-
verificationPassed:
|
|
202
|
-
|
|
203
|
+
verificationPassed: opt(
|
|
204
|
+
bool({
|
|
203
205
|
description:
|
|
204
206
|
"Structured verdict: whether the verification actually passed. When provided the gate trusts it and does not parse the result text; when absent the result text is scanned heuristically.",
|
|
205
207
|
}),
|
|
206
208
|
),
|
|
207
209
|
},
|
|
208
|
-
{
|
|
210
|
+
{ open: false },
|
|
209
211
|
);
|
|
210
212
|
|
|
211
213
|
/** Count of non-empty finding lists, for the one-line confirmation text. */
|
|
@@ -238,7 +240,7 @@ export const reportGapAnalysisTool = defineTool({
|
|
|
238
240
|
],
|
|
239
241
|
parameters: gapAnalysisParams,
|
|
240
242
|
executionMode: "sequential",
|
|
241
|
-
async execute(_toolCallId, params) {
|
|
243
|
+
async execute(_toolCallId, params: Static<typeof gapAnalysisParams>) {
|
|
242
244
|
return {
|
|
243
245
|
content: [
|
|
244
246
|
{
|
|
@@ -264,7 +266,7 @@ export const reportPlanReviewTool = defineTool({
|
|
|
264
266
|
],
|
|
265
267
|
parameters: planReviewParams,
|
|
266
268
|
executionMode: "sequential",
|
|
267
|
-
async execute(_toolCallId, params) {
|
|
269
|
+
async execute(_toolCallId, params: Static<typeof planReviewParams>) {
|
|
268
270
|
const blocking = params.issues.filter((issue) => issue.blocking).length;
|
|
269
271
|
return {
|
|
270
272
|
content: [
|
|
@@ -291,7 +293,7 @@ export const reportWorkTool = defineTool({
|
|
|
291
293
|
],
|
|
292
294
|
parameters: workReportParams,
|
|
293
295
|
executionMode: "sequential",
|
|
294
|
-
async execute(_toolCallId, params) {
|
|
296
|
+
async execute(_toolCallId, params: Static<typeof workReportParams>) {
|
|
295
297
|
return {
|
|
296
298
|
content: [
|
|
297
299
|
{
|