@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.
@@ -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
- pi.registerTool({
43
- name: 'slope_briefing',
44
- label: 'Slope Briefing',
45
- description: 'Get sprint briefing — handicap, hazards, claims, roadmap context. Use compact for ~200 token summary.',
46
- parameters: Type.Object({
47
- compact: Type.Optional(Type.Boolean({ description: 'Compact mode (~200 tokens instead of full briefing)' })),
48
- }),
49
- async execute(_id, params, _signal, _update, ctx) {
50
- const result = slopeCmd(`briefing${params.compact ? ' --compact' : ''}`, ctx.cwd);
51
- return { content: [{ type: 'text', text: result }], details: {} };
52
- },
53
- });
54
- pi.registerTool({
55
- name: 'slope_card',
56
- label: 'Slope Card',
57
- description: 'Display handicap card — rolling performance stats across sprints',
58
- parameters: Type.Object({}),
59
- async execute(_id, _params, _signal, _update, ctx) {
60
- const result = slopeCmd('card', ctx.cwd);
61
- return { content: [{ type: 'text', text: result }], details: {} };
62
- },
63
- });
64
- pi.registerTool({
65
- name: 'slope_guard_check',
66
- label: 'Slope Guard Check',
67
- description: 'Run standalone guard validation: typecheck, tests, uncommitted changes, unpushed commits. Call before committing.',
68
- parameters: Type.Object({
69
- json: Type.Optional(Type.Boolean({ description: 'Machine-readable JSON output' })),
70
- }),
71
- async execute(_id, params, _signal, _update, ctx) {
72
- const result = slopeCmd(`guard check${params.json ? ' --json' : ''}`, ctx.cwd);
73
- return { content: [{ type: 'text', text: result }], details: {} };
74
- },
75
- });
76
- pi.registerTool({
77
- name: 'slope_sprint_context',
78
- label: 'Slope Sprint Context',
79
- description: 'Get remaining workflow steps for the current sprint include in subagent prompts for workflow awareness.',
80
- parameters: Type.Object({
81
- sprint_id: Type.String({ description: 'Sprint ID (e.g., S80)' }),
82
- }),
83
- async execute(_id, params, _signal, _update, ctx) {
84
- const result = slopeCmd(`sprint context ${params.sprint_id}`, ctx.cwd);
85
- return { content: [{ type: 'text', text: result }], details: {} };
86
- },
87
- });
88
- pi.registerTool({
89
- name: 'slope_sprint_validate',
90
- label: 'Slope Sprint Validate',
91
- description: 'Post-hoc validation: check workflow complete, scorecard exists, plan exists, tests pass.',
92
- parameters: Type.Object({
93
- sprint_id: Type.String({ description: 'Sprint ID (e.g., S80)' }),
94
- }),
95
- async execute(_id, params, _signal, _update, ctx) {
96
- const result = slopeCmd(`sprint validate ${params.sprint_id}`, ctx.cwd);
97
- return { content: [{ type: 'text', text: result }], details: {} };
98
- },
99
- });
100
- pi.registerTool({
101
- name: 'slope_review_run',
102
- label: 'Slope Review Run',
103
- description: 'Generate isolated review prompts from a PR diff for subagent-based code/architect reviews.',
104
- parameters: Type.Object({
105
- pr: Type.Optional(Type.Number({ description: 'PR number (default: current branch)' })),
106
- type: Type.Optional(Type.Union([Type.Literal('architect'), Type.Literal('code'), Type.Literal('both')], { description: 'Review type' })),
107
- }),
108
- async execute(_id, params, _signal, _update, ctx) {
109
- const args = [
110
- params.pr ? `--pr=${params.pr}` : '',
111
- params.type ? `--type=${params.type}` : '',
112
- ].filter(Boolean).join(' ');
113
- const result = slopeCmd(`review run ${args}`, ctx.cwd);
114
- return { content: [{ type: 'text', text: result }], details: {} };
115
- },
116
- });
117
- pi.registerTool({
118
- name: 'slope_guard_metrics',
119
- label: 'Slope Guard Metrics',
120
- description: 'Display guard execution metrics — per-guard totals, allow/deny rates, most active/blocking.',
121
- parameters: Type.Object({}),
122
- async execute(_id, _params, _signal, _update, ctx) {
123
- const result = slopeCmd('guard metrics', ctx.cwd);
124
- return { content: [{ type: 'text', text: result }], details: {} };
125
- },
126
- });
127
- pi.registerTool({
128
- name: 'slope_convergence',
129
- label: 'Slope Convergence',
130
- description: 'Detect convergence patterns: improvement rate, plateau, reversion. Requires 10+ scorecards.',
131
- parameters: Type.Object({
132
- json: Type.Optional(Type.Boolean({ description: 'JSON output' })),
133
- }),
134
- async execute(_id, params, _signal, _update, ctx) {
135
- const result = slopeCmd(`loop convergence${params.json ? ' --json' : ''}`, ctx.cwd);
136
- return { content: [{ type: 'text', text: result }], details: {} };
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 multiselectask 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
- // Guard: hazard warning on write/edit; commit discipline nudge on bash
141
- pi.on('tool_call', async (event, ctx) => {
142
- const { toolName, input } = event;
143
- const inp = input;
144
- // Hazard warning on file writes/edits
145
- if ((toolName === 'write' || toolName === 'edit') && typeof inp.path === 'string') {
146
- try {
147
- const payload = JSON.stringify({
148
- session_id: 'pi-session',
149
- cwd: ctx.cwd,
150
- hook_event_name: 'PreToolUse',
151
- tool_name: toolName === 'write' ? 'Write' : 'Edit',
152
- tool_input: { file_path: inp.path },
153
- });
154
- const result = execSync(`echo ${JSON.stringify(payload)} | slope guard hazard`, {
155
- cwd: ctx.cwd,
156
- encoding: 'utf8',
157
- timeout: 5000,
158
- }).trim();
159
- if (result.includes('additionalContext')) {
160
- const match = result.match(/"additionalContext":"([^"]+)"/);
161
- if (match) {
162
- const context = match[1].replace(/\\n/g, '\n');
163
- pi.sendMessage({ customType: 'slope-hazard', content: context, display: true }, { deliverAs: 'steer' });
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
- catch { /* guard failure should never block */ }
168
- }
169
- // Commit discipline: warn on direct main/master commits
170
- if (toolName === 'bash' && typeof inp.command === 'string' && /git\s+commit/.test(inp.command)) {
171
- try {
172
- const branch = execSync('git rev-parse --abbrev-ref HEAD', { cwd: ctx.cwd, encoding: 'utf8' }).trim();
173
- if (branch === 'main' || branch === 'master') {
174
- pi.sendMessage({
175
- customType: 'slope-guard',
176
- content: 'SLOPE: Committing directly on main/master. Create a feature branch first: git checkout -b feat/<description>',
177
- display: true,
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
- catch { /* not in git repo */ }
182
- }
183
- });
184
- // Guard: post-push sprint nudge
185
- pi.on('tool_result', async (event, ctx) => {
186
- const inp = event.input;
187
- if (event.toolName === 'bash' && typeof inp.command === 'string' && /git\s+push/.test(inp.command)) {
188
- try {
189
- const status = slopeCmd('sprint status', ctx.cwd);
190
- if (status && !status.includes('Error')) {
191
- pi.sendMessage({
192
- customType: 'slope-post-push',
193
- content: 'SLOPE post-push: Sprint active. Run `slope guard check` to verify, or `slope sprint context` for next steps.',
194
- display: true,
195
- }, { deliverAs: 'steer' });
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
- catch { /* ignore */ }
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
- pi.on('before_agent_start', async (_event, ctx) => {
223
- if (briefingInjected)
224
- return;
225
- briefingInjected = true;
226
- const briefing = slopeCmd('briefing --compact', ctx.cwd);
227
- return {
228
- message: {
229
- customType: 'slope-briefing',
230
- content: `SLOPE Session Briefing:\n${briefing}`,
231
- display: true,
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
  }