@slope-dev/slope 1.51.4 → 1.52.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/dist/cli/commands/interview.d.ts +4 -0
- package/dist/cli/commands/interview.d.ts.map +1 -0
- package/dist/cli/commands/interview.js +230 -0
- package/dist/cli/commands/interview.js.map +1 -0
- package/dist/cli/commands/sprint.d.ts.map +1 -1
- package/dist/cli/commands/sprint.js +10 -1
- package/dist/cli/commands/sprint.js.map +1 -1
- package/dist/cli/index.js +7 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/interactive-init.d.ts.map +1 -1
- package/dist/cli/interactive-init.js +12 -7
- package/dist/cli/interactive-init.js.map +1 -1
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/cli/registry.js +7 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/core/index.d.ts +4 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +4 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/interview-state-machine.d.ts +59 -0
- package/dist/core/interview-state-machine.d.ts.map +1 -0
- package/dist/core/interview-state-machine.js +129 -0
- package/dist/core/interview-state-machine.js.map +1 -0
- package/dist/core/interview.d.ts.map +1 -1
- package/dist/core/interview.js +13 -0
- package/dist/core/interview.js.map +1 -1
- package/dist/core/pi-settings.d.ts +18 -0
- package/dist/core/pi-settings.d.ts.map +1 -0
- package/dist/core/pi-settings.js +76 -0
- package/dist/core/pi-settings.js.map +1 -0
- package/package.json +1 -1
- package/packages/pi-extension/dist/index.js +425 -166
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
* Install: pi install . (project-local) or pi install npm:@slope-dev/slope
|
|
6
6
|
*/
|
|
7
7
|
import { execSync } from 'node:child_process';
|
|
8
|
-
import { existsSync } from 'node:fs';
|
|
8
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
9
9
|
import { join } from 'node:path';
|
|
10
10
|
import { Type } from '@sinclair/typebox';
|
|
11
|
+
import { InterviewStateMachine, buildInterviewContext, generateInterviewSteps, initFromAnswers, loadPiSettings, savePiSettings, isSkillEnabled, setSkillEnabled, listSkills, } from '@slope-dev/slope';
|
|
11
12
|
// ── Helpers ─────────────────────────────────────────
|
|
12
13
|
function slopeCmd(args, cwd) {
|
|
13
14
|
try {
|
|
@@ -21,11 +22,50 @@ function slopeCmd(args, cwd) {
|
|
|
21
22
|
function hasSlopeProject(cwd) {
|
|
22
23
|
return existsSync(join(cwd, '.slope', 'config.json'));
|
|
23
24
|
}
|
|
25
|
+
// ── Onboarding State ────────────────────────────────
|
|
26
|
+
const ONBOARDING_STATE_FILE = '.slope/.pi-onboarding.json';
|
|
27
|
+
function loadOnboardingState(cwd) {
|
|
28
|
+
const path = join(cwd, ONBOARDING_STATE_FILE);
|
|
29
|
+
if (!existsSync(path))
|
|
30
|
+
return {};
|
|
31
|
+
try {
|
|
32
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return {};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function saveOnboardingState(cwd, state) {
|
|
39
|
+
const dir = join(cwd, '.slope');
|
|
40
|
+
mkdirSync(dir, { recursive: true });
|
|
41
|
+
writeFileSync(join(cwd, ONBOARDING_STATE_FILE), JSON.stringify(state, null, 2) + '\n');
|
|
42
|
+
}
|
|
43
|
+
/** Determine the SLOPE project state for this workspace */
|
|
44
|
+
function getProjectState(cwd) {
|
|
45
|
+
const sprintStatePath = join(cwd, '.slope', 'sprint-state.json');
|
|
46
|
+
if (!existsSync(sprintStatePath)) {
|
|
47
|
+
return 'fresh';
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
const state = JSON.parse(readFileSync(sprintStatePath, 'utf8'));
|
|
51
|
+
if (state.phase === 'planning' ||
|
|
52
|
+
state.phase === 'implementing' ||
|
|
53
|
+
state.phase === 'scoring') {
|
|
54
|
+
return 'active';
|
|
55
|
+
}
|
|
56
|
+
return 'complete';
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return 'fresh';
|
|
60
|
+
}
|
|
61
|
+
}
|
|
24
62
|
// ── Extension Entry Point ───────────────────────────
|
|
25
63
|
export default function slopeExtension(pi, _cwdOverride) {
|
|
26
64
|
const cwd = _cwdOverride ?? process.cwd();
|
|
65
|
+
// Load Pi settings (always, even without a SLOPE project)
|
|
66
|
+
const settings = loadPiSettings(cwd);
|
|
27
67
|
if (!hasSlopeProject(cwd)) {
|
|
28
|
-
// Not a SLOPE project — register only the init tool
|
|
68
|
+
// Not a SLOPE project — register only the init tool and settings command
|
|
29
69
|
pi.registerTool({
|
|
30
70
|
name: 'slope_init',
|
|
31
71
|
label: 'Slope Init',
|
|
@@ -36,168 +76,279 @@ export default function slopeExtension(pi, _cwdOverride) {
|
|
|
36
76
|
return { content: [{ type: 'text', text: result }], details: {} };
|
|
37
77
|
},
|
|
38
78
|
});
|
|
79
|
+
// Settings command is always available
|
|
80
|
+
registerSettingsCommand(pi, settings, cwd);
|
|
39
81
|
return;
|
|
40
82
|
}
|
|
41
83
|
// ── SLOPE Tools ───────────────────────────────────
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
84
|
+
if (isSkillEnabled(settings, 'interview')) {
|
|
85
|
+
pi.registerTool({
|
|
86
|
+
name: 'slope_interview',
|
|
87
|
+
label: 'Slope Interview',
|
|
88
|
+
description: 'Run the project interview directly in Pi. Uses native dialogs (input, select, confirm). No shell-out. Creates .slope/config.json and starter files on completion.',
|
|
89
|
+
parameters: Type.Object({
|
|
90
|
+
resume: Type.Optional(Type.Boolean({ description: 'Resume a previously started interview' })),
|
|
91
|
+
}),
|
|
92
|
+
async execute(_id, _params, _signal, _update, ctx) {
|
|
93
|
+
const steps = generateInterviewSteps(buildInterviewContext(ctx.cwd));
|
|
94
|
+
const sm = new InterviewStateMachine(steps);
|
|
95
|
+
let step;
|
|
96
|
+
while ((step = sm.nextQuestion()) !== null) {
|
|
97
|
+
let value;
|
|
98
|
+
switch (step.type) {
|
|
99
|
+
case 'text': {
|
|
100
|
+
const placeholder = step.description ?? '';
|
|
101
|
+
const def = typeof step.default === 'string' ? step.default : undefined;
|
|
102
|
+
const reply = await ctx.ui.input(step.question, placeholder + (def ? ` (default: ${def})` : ''));
|
|
103
|
+
value = reply ?? def ?? '';
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
case 'select': {
|
|
107
|
+
const options = (step.options ?? []).map((o) => o.hint ? `${o.label} ${o.hint}` : o.label);
|
|
108
|
+
const reply = await ctx.ui.select(step.question, options);
|
|
109
|
+
if (reply === undefined) {
|
|
110
|
+
return {
|
|
111
|
+
content: [{ type: 'text', text: 'Interview cancelled.' }],
|
|
112
|
+
details: {},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
// Map display label back to value
|
|
116
|
+
const chosen = step.options?.find((o) => (o.hint ? `${o.label} ${o.hint}` : o.label) === reply);
|
|
117
|
+
value = chosen?.value ?? reply;
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
case 'multiselect': {
|
|
121
|
+
// Pi has no multiselect — ask for comma-separated values
|
|
122
|
+
const optionsText = (step.options ?? [])
|
|
123
|
+
.map((o) => `${o.label}${o.hint ? ` ${o.hint}` : ''}`)
|
|
124
|
+
.join(', ');
|
|
125
|
+
const reply = await ctx.ui.input(`${step.question} (comma-separated)`, optionsText);
|
|
126
|
+
if (reply === undefined) {
|
|
127
|
+
return {
|
|
128
|
+
content: [{ type: 'text', text: 'Interview cancelled.' }],
|
|
129
|
+
details: {},
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
value = reply.split(',').map((s) => s.trim()).filter(Boolean);
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
case 'confirm': {
|
|
136
|
+
value = await ctx.ui.confirm(step.question, step.description ?? '');
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
default: {
|
|
140
|
+
const reply = await ctx.ui.input(step.question);
|
|
141
|
+
value = reply ?? '';
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const submitResult = sm.submitAnswer(step.id, value);
|
|
145
|
+
if (!submitResult.success) {
|
|
146
|
+
return {
|
|
147
|
+
content: [{ type: 'text', text: `Validation error: ${submitResult.error}` }],
|
|
148
|
+
details: {},
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const answers = sm.getResultUnknown();
|
|
153
|
+
const platformsVal = answers.platforms;
|
|
154
|
+
const result = await initFromAnswers(ctx.cwd, answers, Array.isArray(platformsVal) ? platformsVal : undefined);
|
|
155
|
+
if (!result.success) {
|
|
156
|
+
return {
|
|
157
|
+
content: [{ type: 'text', text: `Interview failed:\n${result.errors.map((e) => ` ${e.field}: ${e.message}`).join('\n')}` }],
|
|
158
|
+
details: {},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
// Clear onboarding flag so future sessions show planning briefing
|
|
162
|
+
saveOnboardingState(ctx.cwd, { shownAt: new Date().toISOString() });
|
|
163
|
+
return {
|
|
164
|
+
content: [{
|
|
165
|
+
type: 'text',
|
|
166
|
+
text: 'Project initialized from interview.\n\n' +
|
|
167
|
+
`Files created:\n${result.filesCreated.map((f) => ` ${f}`).join('\n')}\n\n` +
|
|
168
|
+
'Next: run `slope sprint start` to begin your first sprint.',
|
|
169
|
+
}],
|
|
170
|
+
details: {},
|
|
171
|
+
};
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
} // end interview skill
|
|
175
|
+
if (isSkillEnabled(settings, 'briefing')) {
|
|
176
|
+
pi.registerTool({
|
|
177
|
+
name: 'slope_briefing',
|
|
178
|
+
label: 'Slope Briefing',
|
|
179
|
+
description: 'Get sprint briefing — handicap, hazards, claims, roadmap context. Use compact for ~200 token summary.',
|
|
180
|
+
parameters: Type.Object({
|
|
181
|
+
compact: Type.Optional(Type.Boolean({ description: 'Compact mode (~200 tokens instead of full briefing)' })),
|
|
182
|
+
}),
|
|
183
|
+
async execute(_id, params, _signal, _update, ctx) {
|
|
184
|
+
const result = slopeCmd(`briefing${params.compact ? ' --compact' : ''}`, ctx.cwd);
|
|
185
|
+
return { content: [{ type: 'text', text: result }], details: {} };
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
} // end briefing skill
|
|
189
|
+
if (isSkillEnabled(settings, 'scorecard')) {
|
|
190
|
+
pi.registerTool({
|
|
191
|
+
name: 'slope_card',
|
|
192
|
+
label: 'Slope Card',
|
|
193
|
+
description: 'Display handicap card — rolling performance stats across sprints',
|
|
194
|
+
parameters: Type.Object({}),
|
|
195
|
+
async execute(_id, _params, _signal, _update, ctx) {
|
|
196
|
+
const result = slopeCmd('card', ctx.cwd);
|
|
197
|
+
return { content: [{ type: 'text', text: result }], details: {} };
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
} // end scorecard skill
|
|
201
|
+
if (isSkillEnabled(settings, 'guards')) {
|
|
202
|
+
pi.registerTool({
|
|
203
|
+
name: 'slope_guard_check',
|
|
204
|
+
label: 'Slope Guard Check',
|
|
205
|
+
description: 'Run standalone guard validation: typecheck, tests, uncommitted changes, unpushed commits. Call before committing.',
|
|
206
|
+
parameters: Type.Object({
|
|
207
|
+
json: Type.Optional(Type.Boolean({ description: 'Machine-readable JSON output' })),
|
|
208
|
+
}),
|
|
209
|
+
async execute(_id, params, _signal, _update, ctx) {
|
|
210
|
+
const result = slopeCmd(`guard check${params.json ? ' --json' : ''}`, ctx.cwd);
|
|
211
|
+
return { content: [{ type: 'text', text: result }], details: {} };
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
} // end guards skill
|
|
215
|
+
if (isSkillEnabled(settings, 'planning')) {
|
|
216
|
+
pi.registerTool({
|
|
217
|
+
name: 'slope_sprint_context',
|
|
218
|
+
label: 'Slope Sprint Context',
|
|
219
|
+
description: 'Get remaining workflow steps for the current sprint — include in subagent prompts for workflow awareness.',
|
|
220
|
+
parameters: Type.Object({
|
|
221
|
+
sprint_id: Type.String({ description: 'Sprint ID (e.g., S80)' }),
|
|
222
|
+
}),
|
|
223
|
+
async execute(_id, params, _signal, _update, ctx) {
|
|
224
|
+
const result = slopeCmd(`sprint context ${params.sprint_id}`, ctx.cwd);
|
|
225
|
+
return { content: [{ type: 'text', text: result }], details: {} };
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
pi.registerTool({
|
|
229
|
+
name: 'slope_sprint_validate',
|
|
230
|
+
label: 'Slope Sprint Validate',
|
|
231
|
+
description: 'Post-hoc validation: check workflow complete, scorecard exists, plan exists, tests pass.',
|
|
232
|
+
parameters: Type.Object({
|
|
233
|
+
sprint_id: Type.String({ description: 'Sprint ID (e.g., S80)' }),
|
|
234
|
+
}),
|
|
235
|
+
async execute(_id, params, _signal, _update, ctx) {
|
|
236
|
+
const result = slopeCmd(`sprint validate ${params.sprint_id}`, ctx.cwd);
|
|
237
|
+
return { content: [{ type: 'text', text: result }], details: {} };
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
} // end planning skill
|
|
241
|
+
if (isSkillEnabled(settings, 'review')) {
|
|
242
|
+
pi.registerTool({
|
|
243
|
+
name: 'slope_review_run',
|
|
244
|
+
label: 'Slope Review Run',
|
|
245
|
+
description: 'Generate isolated review prompts from a PR diff for subagent-based code/architect reviews.',
|
|
246
|
+
parameters: Type.Object({
|
|
247
|
+
pr: Type.Optional(Type.Number({ description: 'PR number (default: current branch)' })),
|
|
248
|
+
type: Type.Optional(Type.Union([Type.Literal('architect'), Type.Literal('code'), Type.Literal('both')], { description: 'Review type' })),
|
|
249
|
+
}),
|
|
250
|
+
async execute(_id, params, _signal, _update, ctx) {
|
|
251
|
+
const args = [
|
|
252
|
+
params.pr ? `--pr=${params.pr}` : '',
|
|
253
|
+
params.type ? `--type=${params.type}` : '',
|
|
254
|
+
].filter(Boolean).join(' ');
|
|
255
|
+
const result = slopeCmd(`review run ${args}`, ctx.cwd);
|
|
256
|
+
return { content: [{ type: 'text', text: result }], details: {} };
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
} // end review skill
|
|
260
|
+
if (isSkillEnabled(settings, 'guards')) {
|
|
261
|
+
pi.registerTool({
|
|
262
|
+
name: 'slope_guard_metrics',
|
|
263
|
+
label: 'Slope Guard Metrics',
|
|
264
|
+
description: 'Display guard execution metrics — per-guard totals, allow/deny rates, most active/blocking.',
|
|
265
|
+
parameters: Type.Object({}),
|
|
266
|
+
async execute(_id, _params, _signal, _update, ctx) {
|
|
267
|
+
const result = slopeCmd('guard metrics', ctx.cwd);
|
|
268
|
+
return { content: [{ type: 'text', text: result }], details: {} };
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
} // end guards skill (metrics)
|
|
272
|
+
if (isSkillEnabled(settings, 'scorecard')) {
|
|
273
|
+
pi.registerTool({
|
|
274
|
+
name: 'slope_convergence',
|
|
275
|
+
label: 'Slope Convergence',
|
|
276
|
+
description: 'Detect convergence patterns: improvement rate, plateau, reversion. Requires 10+ scorecards.',
|
|
277
|
+
parameters: Type.Object({
|
|
278
|
+
json: Type.Optional(Type.Boolean({ description: 'JSON output' })),
|
|
279
|
+
}),
|
|
280
|
+
async execute(_id, params, _signal, _update, ctx) {
|
|
281
|
+
const result = slopeCmd(`loop convergence${params.json ? ' --json' : ''}`, ctx.cwd);
|
|
282
|
+
return { content: [{ type: 'text', text: result }], details: {} };
|
|
283
|
+
},
|
|
284
|
+
});
|
|
285
|
+
} // end scorecard skill (convergence)
|
|
139
286
|
// ── Guard Enforcement via Events ──────────────────
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
287
|
+
if (isSkillEnabled(settings, 'guards')) {
|
|
288
|
+
// Guard: hazard warning on write/edit; commit discipline nudge on bash
|
|
289
|
+
pi.on('tool_call', async (event, ctx) => {
|
|
290
|
+
const { toolName, input } = event;
|
|
291
|
+
const inp = input;
|
|
292
|
+
// Hazard warning on file writes/edits
|
|
293
|
+
if ((toolName === 'write' || toolName === 'edit') && typeof inp.path === 'string') {
|
|
294
|
+
try {
|
|
295
|
+
const payload = JSON.stringify({
|
|
296
|
+
session_id: 'pi-session',
|
|
297
|
+
cwd: ctx.cwd,
|
|
298
|
+
hook_event_name: 'PreToolUse',
|
|
299
|
+
tool_name: toolName === 'write' ? 'Write' : 'Edit',
|
|
300
|
+
tool_input: { file_path: inp.path },
|
|
301
|
+
});
|
|
302
|
+
const result = execSync(`echo ${JSON.stringify(payload)} | slope guard hazard`, {
|
|
303
|
+
cwd: ctx.cwd,
|
|
304
|
+
encoding: 'utf8',
|
|
305
|
+
timeout: 5000,
|
|
306
|
+
}).trim();
|
|
307
|
+
if (result.includes('additionalContext')) {
|
|
308
|
+
const match = result.match(/"additionalContext":"([^"]+)"/);
|
|
309
|
+
if (match) {
|
|
310
|
+
const context = match[1].replace(/\\n/g, '\n');
|
|
311
|
+
pi.sendMessage({ customType: 'slope-hazard', content: context, display: true }, { deliverAs: 'steer' });
|
|
312
|
+
}
|
|
164
313
|
}
|
|
165
314
|
}
|
|
315
|
+
catch { /* guard failure should never block */ }
|
|
166
316
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
}, { deliverAs: 'steer' });
|
|
317
|
+
// Commit discipline: warn on direct main/master commits
|
|
318
|
+
if (toolName === 'bash' && typeof inp.command === 'string' && /git\s+commit/.test(inp.command)) {
|
|
319
|
+
try {
|
|
320
|
+
const branch = execSync('git rev-parse --abbrev-ref HEAD', { cwd: ctx.cwd, encoding: 'utf8' }).trim();
|
|
321
|
+
if (branch === 'main' || branch === 'master') {
|
|
322
|
+
pi.sendMessage({
|
|
323
|
+
customType: 'slope-guard',
|
|
324
|
+
content: 'SLOPE: Committing directly on main/master. Create a feature branch first: git checkout -b feat/<description>',
|
|
325
|
+
display: true,
|
|
326
|
+
}, { deliverAs: 'steer' });
|
|
327
|
+
}
|
|
179
328
|
}
|
|
329
|
+
catch { /* not in git repo */ }
|
|
180
330
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
331
|
+
});
|
|
332
|
+
} // end guards event handlers
|
|
333
|
+
if (isSkillEnabled(settings, 'planning')) {
|
|
334
|
+
// Guard: post-push sprint nudge
|
|
335
|
+
pi.on('tool_result', async (event, ctx) => {
|
|
336
|
+
const inp = event.input;
|
|
337
|
+
if (event.toolName === 'bash' && typeof inp.command === 'string' && /git\s+push/.test(inp.command)) {
|
|
338
|
+
try {
|
|
339
|
+
const status = slopeCmd('sprint status', ctx.cwd);
|
|
340
|
+
if (status && !status.includes('Error')) {
|
|
341
|
+
pi.sendMessage({
|
|
342
|
+
customType: 'slope-post-push',
|
|
343
|
+
content: 'SLOPE post-push: Sprint active. Run `slope guard check` to verify, or `slope sprint context` for next steps.',
|
|
344
|
+
display: true,
|
|
345
|
+
}, { deliverAs: 'steer' });
|
|
346
|
+
}
|
|
196
347
|
}
|
|
348
|
+
catch { /* ignore */ }
|
|
197
349
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
});
|
|
350
|
+
});
|
|
351
|
+
} // end planning event handlers
|
|
201
352
|
// ── Slash Commands ────────────────────────────────
|
|
202
353
|
pi.registerCommand('slope', {
|
|
203
354
|
description: 'Run any SLOPE CLI command (default: briefing --compact)',
|
|
@@ -213,23 +364,131 @@ export default function slopeExtension(pi, _cwdOverride) {
|
|
|
213
364
|
ctx.ui.notify(output, 'info');
|
|
214
365
|
},
|
|
215
366
|
});
|
|
367
|
+
// Settings command
|
|
368
|
+
registerSettingsCommand(pi, settings, cwd);
|
|
216
369
|
// ── Session Start: Inject Briefing on First Turn ──
|
|
217
370
|
let briefingInjected = false;
|
|
218
371
|
pi.on('session_start', async (_event, ctx) => {
|
|
219
372
|
briefingInjected = false;
|
|
220
|
-
ctx.ui.notify('SLOPE loaded — use /slope, /sprint, or ask for slope_* tools', 'info');
|
|
373
|
+
ctx.ui.notify('SLOPE loaded — use /slope, /sprint, /slope-settings, or ask for slope_* tools', 'info');
|
|
221
374
|
});
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
375
|
+
if (isSkillEnabled(settings, 'briefing')) {
|
|
376
|
+
pi.on('before_agent_start', async (_event, ctx) => {
|
|
377
|
+
if (briefingInjected)
|
|
378
|
+
return;
|
|
379
|
+
briefingInjected = true;
|
|
380
|
+
const projectState = getProjectState(ctx.cwd);
|
|
381
|
+
// Fresh project: onboarding instead of briefing
|
|
382
|
+
if (projectState === 'fresh') {
|
|
383
|
+
const onboarding = loadOnboardingState(ctx.cwd);
|
|
384
|
+
if (!onboarding.shownAt) {
|
|
385
|
+
onboarding.shownAt = new Date().toISOString();
|
|
386
|
+
saveOnboardingState(ctx.cwd, onboarding);
|
|
387
|
+
return {
|
|
388
|
+
message: {
|
|
389
|
+
customType: 'slope-onboarding',
|
|
390
|
+
content: '🎯 SLOPE Onboarding\n\n' +
|
|
391
|
+
'This project has SLOPE initialized, but no sprint is active yet. Please help get things set up:\n\n' +
|
|
392
|
+
'1. **Understand the project**: Read README.md, package.json, and explore key source files to understand what\'s been built.\n' +
|
|
393
|
+
'2. **Index the codebase**: Run `slope map` to generate CODEBASE.md for architectural context.\n' +
|
|
394
|
+
'3. **Interview the user**: Ask 2–3 quick questions about the project goals, team size, and current priorities.\n' +
|
|
395
|
+
'4. **Start Sprint 1**: Once you have context, suggest creating a sprint plan with `slope sprint start` or by editing docs/backlog/roadmap.json.\n\n' +
|
|
396
|
+
'Session mode: adhoc (sprint-workflow guards are silenced until a sprint begins).\n' +
|
|
397
|
+
'Use /slope or the slope_* tools anytime for SLOPE commands.',
|
|
398
|
+
display: true,
|
|
399
|
+
},
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
// Onboarding already shown this project — stay silent in adhoc mode
|
|
403
|
+
return {};
|
|
404
|
+
}
|
|
405
|
+
// Sprint complete but no active one
|
|
406
|
+
if (projectState === 'complete') {
|
|
407
|
+
return {
|
|
408
|
+
message: {
|
|
409
|
+
customType: 'slope-briefing',
|
|
410
|
+
content: 'SLOPE: No active sprint (previous sprint complete). ' +
|
|
411
|
+
'Start the next sprint with `slope sprint start`, or continue in adhoc mode.',
|
|
412
|
+
display: true,
|
|
413
|
+
},
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
// Active sprint — check if in planning phase
|
|
417
|
+
const sprintStatePath = join(ctx.cwd, '.slope', 'sprint-state.json');
|
|
418
|
+
let phase;
|
|
419
|
+
try {
|
|
420
|
+
const raw = JSON.parse(readFileSync(sprintStatePath, 'utf8'));
|
|
421
|
+
phase = raw.phase;
|
|
422
|
+
}
|
|
423
|
+
catch { /* ignore */ }
|
|
424
|
+
if (phase === 'planning') {
|
|
425
|
+
return {
|
|
426
|
+
message: {
|
|
427
|
+
customType: 'slope-planning',
|
|
428
|
+
content: '📋 SLOPE Planning Phase\n\n' +
|
|
429
|
+
'The project is initialized and ready for sprint planning.\n\n' +
|
|
430
|
+
'Suggested next steps:\n' +
|
|
431
|
+
'1. Review docs/backlog/roadmap.json and refine the starter sprint\n' +
|
|
432
|
+
'2. Run `slope sprint start --number=1` to activate Sprint 1\n' +
|
|
433
|
+
'3. Use `slope sprint run S1 --workflow=sprint-standard` to begin execution\n\n' +
|
|
434
|
+
'Session mode: planning (workflow guards are relaxed).',
|
|
435
|
+
display: true,
|
|
436
|
+
},
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
// Normal active sprint — compact briefing
|
|
440
|
+
const briefing = slopeCmd('briefing --compact', ctx.cwd);
|
|
441
|
+
if (briefing.startsWith('Error:')) {
|
|
442
|
+
return {
|
|
443
|
+
message: {
|
|
444
|
+
customType: 'slope-briefing',
|
|
445
|
+
content: `SLOPE: Could not load briefing (${briefing}). Run \`slope briefing\` manually.`,
|
|
446
|
+
display: true,
|
|
447
|
+
},
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
return {
|
|
451
|
+
message: {
|
|
452
|
+
customType: 'slope-briefing',
|
|
453
|
+
content: `SLOPE Session Briefing:\n${briefing}`,
|
|
454
|
+
display: true,
|
|
455
|
+
},
|
|
456
|
+
};
|
|
457
|
+
});
|
|
458
|
+
} // end briefing event handlers
|
|
459
|
+
}
|
|
460
|
+
// ── Settings Command ──────────────────────────────
|
|
461
|
+
function registerSettingsCommand(pi, settings, cwd) {
|
|
462
|
+
pi.registerCommand('slope-settings', {
|
|
463
|
+
description: 'Show and manage SLOPE Pi feature settings',
|
|
464
|
+
handler: async (_args, ctx) => {
|
|
465
|
+
const skills = listSkills(settings);
|
|
466
|
+
// Build interactive select options showing current state
|
|
467
|
+
const options = skills.map((s) => {
|
|
468
|
+
const status = s.enabled ? '✓ ON ' : '✗ OFF';
|
|
469
|
+
return `${s.name.padEnd(12)} ${status} ${s.description}`;
|
|
470
|
+
});
|
|
471
|
+
const choice = await ctx.ui.select('SLOPE Features — select one to toggle', options);
|
|
472
|
+
if (choice === undefined) {
|
|
473
|
+
ctx.ui.notify('Settings unchanged.', 'info');
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
// Extract skill name from selection
|
|
477
|
+
const skillName = skills[options.indexOf(choice)]?.name;
|
|
478
|
+
if (!skillName || !settings.skills[skillName]) {
|
|
479
|
+
ctx.ui.notify('Invalid selection.', 'error');
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
const wasEnabled = settings.skills[skillName].enabled;
|
|
483
|
+
const confirm = await ctx.ui.confirm(`Toggle "${skillName}" ${wasEnabled ? 'OFF' : 'ON'}?`, `Currently: ${wasEnabled ? 'enabled' : 'disabled'}. Changes take effect after session restart.`);
|
|
484
|
+
if (!confirm) {
|
|
485
|
+
ctx.ui.notify('Settings unchanged.', 'info');
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
setSkillEnabled(settings, skillName, !wasEnabled);
|
|
489
|
+
savePiSettings(cwd, settings);
|
|
490
|
+
ctx.ui.notify(`SLOPE: "${skillName}" is now ${!wasEnabled ? 'ENABLED' : 'DISABLED'}. ` +
|
|
491
|
+
'Restart the session for changes to take full effect.', 'info');
|
|
492
|
+
},
|
|
234
493
|
});
|
|
235
494
|
}
|