@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 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.1",
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.0.0 <2",
41
- "@earendil-works/pi-coding-agent": ">=1.0.0 <2",
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.1.0",
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 { Type } from "@earendil-works/pi-ai";
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 = Type.Optional(
47
- Type.Object(
55
+ const usageSchema = opt(
56
+ obj(
48
57
  {
49
- input: Type.Number(),
50
- output: Type.Number(),
51
- cacheRead: Type.Number(),
52
- cacheWrite: Type.Number(),
53
- turns: Type.Number(),
58
+ input: num(),
59
+ output: num(),
60
+ cacheRead: num(),
61
+ cacheWrite: num(),
62
+ turns: num(),
54
63
  },
55
- { additionalProperties: false },
64
+ { open: false },
56
65
  ),
57
66
  );
58
67
 
59
- const gapAnalysisParams = Type.Object(
68
+ const gapAnalysisParams = obj(
60
69
  {
61
- findings: Type.Array(
62
- Type.String({
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: Type.Optional(Type.String()),
68
- changeId: Type.Optional(
69
- Type.String({
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
- { additionalProperties: false },
84
+ { open: false },
76
85
  );
77
86
 
78
- const planReviewParams = Type.Object(
87
+ const planReviewParams = obj(
79
88
  {
80
- verdict: Type.Union([
81
- Type.Literal("OKAY"),
82
- Type.Literal("ITERATE"),
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: Type.Optional(Type.String()),
96
+ summary: opt(str()),
92
97
  usage: usageSchema,
93
- changeId: Type.Optional(
94
- Type.String({
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
- { additionalProperties: false },
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(_toolCallId, params, _signal, _onUpdate, ctx) {
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(_toolCallId, params, _signal, _onUpdate, ctx) {
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);
@@ -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 { Type } from "@earendil-works/pi-ai";
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 = Type.Object(
46
+ const findingItem = obj(
37
47
  {
38
- description: Type.String({
48
+ description: str({
39
49
  description:
40
50
  "The finding itself, specific enough to act on without re-deriving it.",
41
51
  }),
42
- location: Type.Optional(
43
- Type.String({
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
- { additionalProperties: false },
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 = Type.Optional(
58
- Type.Object(
67
+ const progressField = opt(
68
+ obj(
59
69
  {
60
- phase: Type.String({
70
+ phase: str({
61
71
  description:
62
72
  "The stage this update belongs to, e.g. reading, analyzing, editing, verifying.",
63
73
  }),
64
- percentage: Type.Number({
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: Type.Optional(
70
- Type.String({
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
- { additionalProperties: false },
86
+ { open: false },
77
87
  ),
78
88
  );
79
89
 
80
- const gapAnalysisParams = Type.Object(
90
+ const gapAnalysisParams = obj(
81
91
  {
82
92
  progress: progressField,
83
- intentType: Type.String({
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: Type.Number({
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: Type.Array(findingItem, {
102
+ contradictions: arr(findingItem, {
93
103
  description: "Requirements or statements that conflict with each other.",
94
104
  }),
95
- missingConstraints: Type.Array(findingItem, {
105
+ missingConstraints: arr(findingItem, {
96
106
  description:
97
107
  "Constraints the request implies but never states (platform, compatibility, performance, security, ...).",
98
108
  }),
99
- scopeRisks: Type.Array(findingItem, {
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: Type.Array(findingItem, {
113
+ unvalidatedAssumptions: arr(findingItem, {
104
114
  description:
105
115
  "Assumptions the plan relies on without evidence (environment, behavior, availability).",
106
116
  }),
107
- missingAcceptanceCriteria: Type.Array(findingItem, {
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: Type.String({
121
+ summary: str({
112
122
  description: "One-paragraph synthesis of the analysis.",
113
123
  }),
114
124
  },
115
- { additionalProperties: false },
125
+ { open: false },
116
126
  );
117
127
 
118
- const planReviewParams = Type.Object(
128
+ const planReviewParams = obj(
119
129
  {
120
130
  progress: progressField,
121
- verdict: Type.Union(
122
- [Type.Literal("OKAY"), Type.Literal("ITERATE"), Type.Literal("REJECT")],
123
- {
124
- description:
125
- "OKAY: executable as written. ITERATE: workable after targeted, non-blocking revisions. REJECT: not executable as written.",
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: Type.Union(
132
- [
133
- Type.Literal("blocker"),
134
- Type.Literal("major"),
135
- Type.Literal("minor"),
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: Type.Optional(
145
- Type.Number({
146
+ line: opt(
147
+ num({
146
148
  description: "Line number in that file, when known.",
147
149
  }),
148
150
  ),
149
- description: Type.String({
151
+ description: str({
150
152
  description:
151
153
  "The issue, concrete and actionable (what to change, not a taste judgment).",
152
154
  }),
153
- blocking: Type.Boolean({
155
+ blocking: bool({
154
156
  description:
155
157
  "True only for REJECT blockers; ITERATE/OKAY issues are never blocking.",
156
158
  }),
157
159
  },
158
- { additionalProperties: false },
160
+ { open: false },
159
161
  ),
160
162
  { description: "Found issues; may be empty for a clean OKAY." },
161
163
  ),
162
- summary: Type.String({
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
- { additionalProperties: false },
169
+ { open: false },
168
170
  );
169
171
 
170
- const workReportParams = Type.Object(
172
+ const workReportParams = obj(
171
173
  {
172
174
  progress: progressField,
173
- taskId: Type.String({
175
+ taskId: str({
174
176
  description: "The dispatched task identifier (e.g. the tasks.md item).",
175
177
  }),
176
- changedFiles: Type.Array(Type.String(), {
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: Type.String({
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: Type.Object(
186
+ selfVerification: obj(
185
187
  {
186
- command: Type.Optional(
187
- Type.String({
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: Type.Optional(
193
- Type.String({
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
- { additionalProperties: false },
201
+ { open: false },
200
202
  ),
201
- verificationPassed: Type.Optional(
202
- Type.Boolean({
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
- { additionalProperties: false },
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
  {