@slope-dev/slope 1.51.4 → 1.53.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.
Files changed (54) hide show
  1. package/dist/cli/commands/interview.d.ts +4 -0
  2. package/dist/cli/commands/interview.d.ts.map +1 -0
  3. package/dist/cli/commands/interview.js +230 -0
  4. package/dist/cli/commands/interview.js.map +1 -0
  5. package/dist/cli/commands/memory.d.ts +14 -0
  6. package/dist/cli/commands/memory.d.ts.map +1 -0
  7. package/dist/cli/commands/memory.js +230 -0
  8. package/dist/cli/commands/memory.js.map +1 -0
  9. package/dist/cli/commands/review-state.d.ts.map +1 -1
  10. package/dist/cli/commands/review-state.js +9 -1
  11. package/dist/cli/commands/review-state.js.map +1 -1
  12. package/dist/cli/commands/sprint.d.ts.map +1 -1
  13. package/dist/cli/commands/sprint.js +10 -1
  14. package/dist/cli/commands/sprint.js.map +1 -1
  15. package/dist/cli/guards/claim-required.d.ts +6 -0
  16. package/dist/cli/guards/claim-required.d.ts.map +1 -1
  17. package/dist/cli/guards/claim-required.js +13 -3
  18. package/dist/cli/guards/claim-required.js.map +1 -1
  19. package/dist/cli/index.js +14 -0
  20. package/dist/cli/index.js.map +1 -1
  21. package/dist/cli/interactive-init.d.ts.map +1 -1
  22. package/dist/cli/interactive-init.js +12 -7
  23. package/dist/cli/interactive-init.js.map +1 -1
  24. package/dist/cli/registry.d.ts.map +1 -1
  25. package/dist/cli/registry.js +28 -0
  26. package/dist/cli/registry.js.map +1 -1
  27. package/dist/core/auto-memory.d.ts +19 -0
  28. package/dist/core/auto-memory.d.ts.map +1 -0
  29. package/dist/core/auto-memory.js +142 -0
  30. package/dist/core/auto-memory.js.map +1 -0
  31. package/dist/core/index.d.ts +7 -0
  32. package/dist/core/index.d.ts.map +1 -1
  33. package/dist/core/index.js +8 -0
  34. package/dist/core/index.js.map +1 -1
  35. package/dist/core/interview-state-machine.d.ts +59 -0
  36. package/dist/core/interview-state-machine.d.ts.map +1 -0
  37. package/dist/core/interview-state-machine.js +129 -0
  38. package/dist/core/interview-state-machine.js.map +1 -0
  39. package/dist/core/interview.d.ts.map +1 -1
  40. package/dist/core/interview.js +13 -0
  41. package/dist/core/interview.js.map +1 -1
  42. package/dist/core/memory.d.ts +57 -0
  43. package/dist/core/memory.d.ts.map +1 -0
  44. package/dist/core/memory.js +217 -0
  45. package/dist/core/memory.js.map +1 -0
  46. package/dist/core/pi-settings.d.ts +18 -0
  47. package/dist/core/pi-settings.d.ts.map +1 -0
  48. package/dist/core/pi-settings.js +80 -0
  49. package/dist/core/pi-settings.js.map +1 -0
  50. package/dist/core/review.d.ts.map +1 -1
  51. package/dist/core/review.js +5 -0
  52. package/dist/core/review.js.map +1 -1
  53. package/package.json +1 -1
  54. package/packages/pi-extension/dist/index.js +519 -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, searchMemories, addMemory, removeMemory, } 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,330 @@ 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, 'memory')) {
261
+ pi.registerTool({
262
+ name: 'slope_memory',
263
+ label: 'Slope Memory',
264
+ description: 'List, add, search, or remove cross-session memories stored in .slope/memories.json',
265
+ parameters: Type.Object({
266
+ action: Type.Union([Type.Literal('list'), Type.Literal('add'), Type.Literal('search'), Type.Literal('remove')], { description: 'Memory action' }),
267
+ text: Type.Optional(Type.String({ description: 'Memory text (for add)' })),
268
+ query: Type.Optional(Type.String({ description: 'Search query (for search)' })),
269
+ id: Type.Optional(Type.String({ description: 'Memory ID (for remove)' })),
270
+ category: Type.Optional(Type.String({ description: 'Category filter' })),
271
+ limit: Type.Optional(Type.Number({ description: 'Max results' })),
272
+ }),
273
+ async execute(_id, params, _signal, _update, ctx) {
274
+ switch (params.action) {
275
+ case 'list': {
276
+ const results = searchMemories(ctx.cwd, {
277
+ category: params.category,
278
+ limit: params.limit ?? 10,
279
+ });
280
+ const lines = results.map(m => `[${m.category}] w:${m.weight} ${m.text.slice(0, 80)}`);
281
+ return { content: [{ type: 'text', text: lines.join('\n') || 'No memories.' }], details: {} };
282
+ }
283
+ case 'add': {
284
+ if (!params.text)
285
+ return { content: [{ type: 'text', text: 'Error: text required for add' }], details: {} };
286
+ const mem = addMemory(ctx.cwd, params.text, {
287
+ category: params.category,
288
+ source: 'manual',
289
+ });
290
+ return { content: [{ type: 'text', text: `Added: ${mem.id.slice(0, 12)}… [${mem.category}]` }], details: {} };
291
+ }
292
+ case 'search': {
293
+ const results = searchMemories(ctx.cwd, {
294
+ query: params.query,
295
+ category: params.category,
296
+ limit: params.limit ?? 10,
297
+ });
298
+ const lines = results.map(m => `[${m.category}] w:${m.weight} ${m.text.slice(0, 80)}`);
299
+ return { content: [{ type: 'text', text: lines.join('\n') || 'No memories found.' }], details: {} };
300
+ }
301
+ case 'remove': {
302
+ if (!params.id)
303
+ return { content: [{ type: 'text', text: 'Error: id required for remove' }], details: {} };
304
+ const ok = removeMemory(ctx.cwd, params.id);
305
+ return { content: [{ type: 'text', text: ok ? 'Removed.' : 'Memory not found.' }], details: {} };
306
+ }
307
+ }
308
+ },
309
+ });
310
+ } // end memory skill
311
+ if (isSkillEnabled(settings, 'guards')) {
312
+ pi.registerTool({
313
+ name: 'slope_guard_metrics',
314
+ label: 'Slope Guard Metrics',
315
+ description: 'Display guard execution metrics — per-guard totals, allow/deny rates, most active/blocking.',
316
+ parameters: Type.Object({}),
317
+ async execute(_id, _params, _signal, _update, ctx) {
318
+ const result = slopeCmd('guard metrics', ctx.cwd);
319
+ return { content: [{ type: 'text', text: result }], details: {} };
320
+ },
321
+ });
322
+ } // end guards skill (metrics)
323
+ if (isSkillEnabled(settings, 'scorecard')) {
324
+ pi.registerTool({
325
+ name: 'slope_convergence',
326
+ label: 'Slope Convergence',
327
+ description: 'Detect convergence patterns: improvement rate, plateau, reversion. Requires 10+ scorecards.',
328
+ parameters: Type.Object({
329
+ json: Type.Optional(Type.Boolean({ description: 'JSON output' })),
330
+ }),
331
+ async execute(_id, params, _signal, _update, ctx) {
332
+ const result = slopeCmd(`loop convergence${params.json ? ' --json' : ''}`, ctx.cwd);
333
+ return { content: [{ type: 'text', text: result }], details: {} };
334
+ },
335
+ });
336
+ } // end scorecard skill (convergence)
139
337
  // ── 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' });
338
+ if (isSkillEnabled(settings, 'guards')) {
339
+ // Guard: hazard warning on write/edit; commit discipline nudge on bash
340
+ pi.on('tool_call', async (event, ctx) => {
341
+ const { toolName, input } = event;
342
+ const inp = input;
343
+ // Hazard warning on file writes/edits
344
+ if ((toolName === 'write' || toolName === 'edit') && typeof inp.path === 'string') {
345
+ try {
346
+ const payload = JSON.stringify({
347
+ session_id: 'pi-session',
348
+ cwd: ctx.cwd,
349
+ hook_event_name: 'PreToolUse',
350
+ tool_name: toolName === 'write' ? 'Write' : 'Edit',
351
+ tool_input: { file_path: inp.path },
352
+ });
353
+ const result = execSync(`echo ${JSON.stringify(payload)} | slope guard hazard`, {
354
+ cwd: ctx.cwd,
355
+ encoding: 'utf8',
356
+ timeout: 5000,
357
+ }).trim();
358
+ if (result.includes('additionalContext')) {
359
+ const match = result.match(/"additionalContext":"([^"]+)"/);
360
+ if (match) {
361
+ const context = match[1].replace(/\\n/g, '\n');
362
+ pi.sendMessage({ customType: 'slope-hazard', content: context, display: true }, { deliverAs: 'steer' });
363
+ }
164
364
  }
165
365
  }
366
+ catch { /* guard failure should never block */ }
166
367
  }
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' });
368
+ // Commit discipline: warn on direct main/master commits
369
+ if (toolName === 'bash' && typeof inp.command === 'string' && /git\s+commit/.test(inp.command)) {
370
+ try {
371
+ const branch = execSync('git rev-parse --abbrev-ref HEAD', { cwd: ctx.cwd, encoding: 'utf8' }).trim();
372
+ if (branch === 'main' || branch === 'master') {
373
+ pi.sendMessage({
374
+ customType: 'slope-guard',
375
+ content: 'SLOPE: Committing directly on main/master. Create a feature branch first: git checkout -b feat/<description>',
376
+ display: true,
377
+ }, { deliverAs: 'steer' });
378
+ }
179
379
  }
380
+ catch { /* not in git repo */ }
180
381
  }
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' });
382
+ });
383
+ } // end guards event handlers
384
+ if (isSkillEnabled(settings, 'planning')) {
385
+ // Guard: post-push sprint nudge
386
+ pi.on('tool_result', async (event, ctx) => {
387
+ const inp = event.input;
388
+ if (event.toolName === 'bash' && typeof inp.command === 'string' && /git\s+push/.test(inp.command)) {
389
+ try {
390
+ const status = slopeCmd('sprint status', ctx.cwd);
391
+ if (status && !status.includes('Error')) {
392
+ pi.sendMessage({
393
+ customType: 'slope-post-push',
394
+ content: 'SLOPE post-push: Sprint active. Run `slope guard check` to verify, or `slope sprint context` for next steps.',
395
+ display: true,
396
+ }, { deliverAs: 'steer' });
397
+ }
196
398
  }
399
+ catch { /* ignore */ }
197
400
  }
198
- catch { /* ignore */ }
199
- }
200
- });
401
+ });
402
+ } // end planning event handlers
201
403
  // ── Slash Commands ────────────────────────────────
202
404
  pi.registerCommand('slope', {
203
405
  description: 'Run any SLOPE CLI command (default: briefing --compact)',
@@ -213,23 +415,174 @@ export default function slopeExtension(pi, _cwdOverride) {
213
415
  ctx.ui.notify(output, 'info');
214
416
  },
215
417
  });
418
+ if (isSkillEnabled(settings, 'memory')) {
419
+ pi.registerCommand('slope-memory', {
420
+ description: 'List or add cross-session memories',
421
+ handler: async (args, ctx) => {
422
+ const tokens = (args ?? '').trim().split(/\s+/).filter(Boolean);
423
+ const sub = tokens[0];
424
+ if (sub === 'add') {
425
+ const text = tokens.slice(1).join(' ').trim();
426
+ if (!text) {
427
+ ctx.ui.notify('Usage: /slope-memory add <text>', 'info');
428
+ return;
429
+ }
430
+ try {
431
+ const mem = addMemory(ctx.cwd, text, { source: 'manual' });
432
+ ctx.ui.notify(`Memory added: ${mem.id.slice(0, 12)}…`, 'info');
433
+ }
434
+ catch (err) {
435
+ ctx.ui.notify(`Memory not added: ${err.message}`, 'warning');
436
+ }
437
+ }
438
+ else {
439
+ const results = searchMemories(ctx.cwd, { limit: 10 });
440
+ if (results.length === 0) {
441
+ ctx.ui.notify('No memories stored.', 'info');
442
+ return;
443
+ }
444
+ const lines = results.map(m => `[${m.category}] w:${m.weight} ${m.text.slice(0, 60)}`);
445
+ ctx.ui.notify(`Memories:\n${lines.join('\n')}`, 'info');
446
+ }
447
+ },
448
+ });
449
+ } // end memory slash command
450
+ // Settings command
451
+ registerSettingsCommand(pi, settings, cwd);
216
452
  // ── Session Start: Inject Briefing on First Turn ──
217
453
  let briefingInjected = false;
218
454
  pi.on('session_start', async (_event, ctx) => {
219
455
  briefingInjected = false;
220
- ctx.ui.notify('SLOPE loaded — use /slope, /sprint, or ask for slope_* tools', 'info');
456
+ ctx.ui.notify('SLOPE loaded — use /slope, /sprint, /slope-settings, or ask for slope_* tools', 'info');
221
457
  });
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
- };
458
+ if (isSkillEnabled(settings, 'briefing')) {
459
+ pi.on('before_agent_start', async (_event, ctx) => {
460
+ if (briefingInjected)
461
+ return;
462
+ briefingInjected = true;
463
+ const projectState = getProjectState(ctx.cwd);
464
+ // Fresh project: onboarding instead of briefing
465
+ if (projectState === 'fresh') {
466
+ const onboarding = loadOnboardingState(ctx.cwd);
467
+ if (!onboarding.shownAt) {
468
+ onboarding.shownAt = new Date().toISOString();
469
+ saveOnboardingState(ctx.cwd, onboarding);
470
+ return {
471
+ message: {
472
+ customType: 'slope-onboarding',
473
+ content: '🎯 SLOPE Onboarding\n\n' +
474
+ 'This project has SLOPE initialized, but no sprint is active yet. Please help get things set up:\n\n' +
475
+ '1. **Understand the project**: Read README.md, package.json, and explore key source files to understand what\'s been built.\n' +
476
+ '2. **Index the codebase**: Run `slope map` to generate CODEBASE.md for architectural context.\n' +
477
+ '3. **Interview the user**: Ask 2–3 quick questions about the project goals, team size, and current priorities.\n' +
478
+ '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' +
479
+ 'Session mode: adhoc (sprint-workflow guards are silenced until a sprint begins).\n' +
480
+ 'Use /slope or the slope_* tools anytime for SLOPE commands.',
481
+ display: true,
482
+ },
483
+ };
484
+ }
485
+ // Onboarding already shown this project — stay silent in adhoc mode
486
+ return {};
487
+ }
488
+ // Sprint complete but no active one
489
+ if (projectState === 'complete') {
490
+ return {
491
+ message: {
492
+ customType: 'slope-briefing',
493
+ content: 'SLOPE: No active sprint (previous sprint complete). ' +
494
+ 'Start the next sprint with `slope sprint start`, or continue in adhoc mode.',
495
+ display: true,
496
+ },
497
+ };
498
+ }
499
+ // Active sprint — check if in planning phase
500
+ const sprintStatePath = join(ctx.cwd, '.slope', 'sprint-state.json');
501
+ let phase;
502
+ try {
503
+ const raw = JSON.parse(readFileSync(sprintStatePath, 'utf8'));
504
+ phase = raw.phase;
505
+ }
506
+ catch { /* ignore */ }
507
+ if (phase === 'planning') {
508
+ return {
509
+ message: {
510
+ customType: 'slope-planning',
511
+ content: '📋 SLOPE Planning Phase\n\n' +
512
+ 'The project is initialized and ready for sprint planning.\n\n' +
513
+ 'Suggested next steps:\n' +
514
+ '1. Review docs/backlog/roadmap.json and refine the starter sprint\n' +
515
+ '2. Run `slope sprint start --number=1` to activate Sprint 1\n' +
516
+ '3. Use `slope sprint run S1 --workflow=sprint-standard` to begin execution\n\n' +
517
+ 'Session mode: planning (workflow guards are relaxed).',
518
+ display: true,
519
+ },
520
+ };
521
+ }
522
+ // Normal active sprint — compact briefing
523
+ const briefing = slopeCmd('briefing --compact', ctx.cwd);
524
+ if (briefing.startsWith('Error:')) {
525
+ return {
526
+ message: {
527
+ customType: 'slope-briefing',
528
+ content: `SLOPE: Could not load briefing (${briefing}). Run \`slope briefing\` manually.`,
529
+ display: true,
530
+ },
531
+ };
532
+ }
533
+ // Inject top memories if memory skill is enabled
534
+ let memoryContext = '';
535
+ if (isSkillEnabled(settings, 'memory')) {
536
+ try {
537
+ const memories = searchMemories(ctx.cwd, { limit: 5, minWeight: 5 });
538
+ if (memories.length > 0) {
539
+ memoryContext = '\n\n📌 Relevant Memories:\n' + memories.map(m => ` • [${m.category}] ${m.text.slice(0, 100)}`).join('\n');
540
+ }
541
+ }
542
+ catch { /* ignore memory errors */ }
543
+ }
544
+ return {
545
+ message: {
546
+ customType: 'slope-briefing',
547
+ content: `SLOPE Session Briefing:\n${briefing}${memoryContext}`,
548
+ display: true,
549
+ },
550
+ };
551
+ });
552
+ } // end briefing event handlers
553
+ }
554
+ // ── Settings Command ──────────────────────────────
555
+ function registerSettingsCommand(pi, settings, cwd) {
556
+ pi.registerCommand('slope-settings', {
557
+ description: 'Show and manage SLOPE Pi feature settings',
558
+ handler: async (_args, ctx) => {
559
+ const skills = listSkills(settings);
560
+ // Build interactive select options showing current state
561
+ const options = skills.map((s) => {
562
+ const status = s.enabled ? '✓ ON ' : '✗ OFF';
563
+ return `${s.name.padEnd(12)} ${status} ${s.description}`;
564
+ });
565
+ const choice = await ctx.ui.select('SLOPE Features — select one to toggle', options);
566
+ if (choice === undefined) {
567
+ ctx.ui.notify('Settings unchanged.', 'info');
568
+ return;
569
+ }
570
+ // Extract skill name from selection
571
+ const skillName = skills[options.indexOf(choice)]?.name;
572
+ if (!skillName || !settings.skills[skillName]) {
573
+ ctx.ui.notify('Invalid selection.', 'error');
574
+ return;
575
+ }
576
+ const wasEnabled = settings.skills[skillName].enabled;
577
+ const confirm = await ctx.ui.confirm(`Toggle "${skillName}" ${wasEnabled ? 'OFF' : 'ON'}?`, `Currently: ${wasEnabled ? 'enabled' : 'disabled'}. Changes take effect after session restart.`);
578
+ if (!confirm) {
579
+ ctx.ui.notify('Settings unchanged.', 'info');
580
+ return;
581
+ }
582
+ setSkillEnabled(settings, skillName, !wasEnabled);
583
+ savePiSettings(cwd, settings);
584
+ ctx.ui.notify(`SLOPE: "${skillName}" is now ${!wasEnabled ? 'ENABLED' : 'DISABLED'}. ` +
585
+ 'Restart the session for changes to take full effect.', 'info');
586
+ },
234
587
  });
235
588
  }