@justjoshn/pi-pstack 0.15.36-pi.11 → 0.15.36-pi.13
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 +28 -0
- package/README.md +14 -14
- package/agents/thermo-nuclear-code-quality-review.md +1 -1
- package/automations/benny/FOR_AGENTS.md +1 -1
- package/automations/benny/skills/reproduce-and-fix-issues/SKILL.md +3 -3
- package/automations/benny/skills/setup-benny/SKILL.md +3 -3
- package/automations/benny/skills/triage-issue-reports/SKILL.md +2 -2
- package/docs/native-gaps.md +1 -1
- package/docs/parity-status.md +55 -11
- package/package.json +4 -3
- package/skills/make-bot-ui/SKILL.md +1 -1
- package/skills/pr-review-canvas/SKILL.md +1 -1
- package/skills/statechart/scripts/test/cli.test.ts +10 -1
- package/skills/teach/SKILL.md +1 -1
- package/src/ask.ts +24 -10
- package/src/child-env.ts +4 -0
- package/src/generate-image.ts +98 -0
- package/src/index.ts +19 -11
- package/src/loop.ts +113 -60
- package/src/mode.ts +84 -46
- package/src/plan.ts +338 -46
- package/src/process-lock.ts +76 -0
- package/src/readonly-shell.ts +6 -8
- package/src/sections.ts +6 -1
- package/src/subscriptions/durable-store.ts +2 -75
- package/src/subscriptions/index.ts +2 -17
- package/src/subscriptions/supervisor.ts +1 -1
- package/src/task/agents.ts +39 -3
- package/src/task/card.ts +4 -2
- package/src/task/format.ts +17 -3
- package/src/task/records.ts +37 -32
- package/src/task/registry.ts +2 -1
- package/src/task/runner.ts +4 -1
- package/src/task/stop.ts +3 -3
- package/src/task/tool.ts +62 -22
- package/src/todo.ts +50 -7
- package/src/path-skills.ts +0 -104
package/src/mode.ts
CHANGED
|
@@ -13,6 +13,32 @@ export interface ActiveMode {
|
|
|
13
13
|
path?: string;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Cursor's custom-mode lifecycle: a mode is `entered` until a request carries it, then `active`. Leaving or switching
|
|
18
|
+
* away from an `active` mode leaves it `exited` until the next request says so once.
|
|
19
|
+
*/
|
|
20
|
+
export interface ModeState {
|
|
21
|
+
active?: ActiveMode & { lifecycle: 'entered' | 'active' };
|
|
22
|
+
exited?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const left = (state: ModeState): string | undefined => (state.active?.lifecycle === 'active' ? state.active.skill : state.exited);
|
|
26
|
+
|
|
27
|
+
export function enterMode(state: ModeState, mode: ActiveMode): ModeState {
|
|
28
|
+
const exited = left(state);
|
|
29
|
+
return { active: { ...mode, lifecycle: 'entered' }, ...(exited === undefined ? {} : { exited }) };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function exitMode(state: ModeState): ModeState {
|
|
33
|
+
const exited = left(state);
|
|
34
|
+
return exited === undefined ? {} : { exited };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** What a request leaves behind: the mode it carried is now active, and its exit notice has been sent. */
|
|
38
|
+
export function afterRequest(state: ModeState): ModeState {
|
|
39
|
+
return state.active === undefined ? {} : { active: { ...state.active, lifecycle: 'active' } };
|
|
40
|
+
}
|
|
41
|
+
|
|
16
42
|
export type ModeCommand = { kind: 'status' } | { kind: 'off' } | { kind: 'set'; skill: string; task: string };
|
|
17
43
|
|
|
18
44
|
export function parseModeArgs(args: string): ModeCommand {
|
|
@@ -31,41 +57,53 @@ interface BranchEntry {
|
|
|
31
57
|
|
|
32
58
|
const SKILL_NAME = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$/;
|
|
33
59
|
|
|
60
|
+
/** A stored mode as a skill name only: a session file can come from elsewhere, so a stored path is ignored and a value that is not a plausible skill name (it would reach the system prompt) counts as no mode. */
|
|
61
|
+
function storedSkill(data: unknown): string | undefined {
|
|
62
|
+
const skill = (data as { skill?: unknown } | null)?.skill;
|
|
63
|
+
return typeof skill === 'string' && SKILL_NAME.test(skill) ? skill : undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
34
66
|
/**
|
|
35
|
-
*
|
|
36
|
-
*
|
|
67
|
+
* Replays the branch into the mode state: each mode entry enters or leaves a mode, and an assistant message after it
|
|
68
|
+
* shows a request carried the mode. Skills are resolved by name, never by a stored path.
|
|
37
69
|
*/
|
|
38
|
-
export function modeFromBranch(entries: readonly BranchEntry[]
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if (entry
|
|
42
|
-
|
|
43
|
-
|
|
70
|
+
export function modeFromBranch(entries: readonly BranchEntry[], resolve: (skill: string) => ActiveMode = (skill) => ({ skill })): ModeState {
|
|
71
|
+
let state: ModeState = {};
|
|
72
|
+
for (const entry of entries) {
|
|
73
|
+
if (entry.type === 'custom' && entry.customType === MODE_ENTRY) {
|
|
74
|
+
const skill = storedSkill(entry.data);
|
|
75
|
+
state = skill === undefined ? exitMode(state) : enterMode(state, resolve(skill));
|
|
76
|
+
} else if (entry.type === 'message' && (entry as { message?: { role?: string } }).message?.role === 'assistant') state = afterRequest(state);
|
|
44
77
|
}
|
|
45
|
-
return
|
|
78
|
+
return state;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Cursor's custom-mode notices for the next request (CLI `custom-modes` module). */
|
|
82
|
+
export function modeNotice(state: ModeState): string | undefined {
|
|
83
|
+
const lines: string[] = [];
|
|
84
|
+
const { active, exited } = state;
|
|
85
|
+
if (exited !== undefined && exited !== active?.skill) lines.push(`The user has exited ${exited} custom mode. Stop following that mode's skill. Continue without those constraints.`);
|
|
86
|
+
if (active?.path === undefined && active !== undefined) lines.push(`Mode ${active.skill} is on, but no loaded skill has that name. Tell the user to run /mode off or /mode <skill>.`);
|
|
87
|
+
else if (active?.path !== undefined && !existsSync(active.path)) lines.push(`Mode ${active.skill} is on, but its skill file ${active.path} no longer exists. Tell the user to run /mode off or /mode <skill>.`);
|
|
88
|
+
else if (active?.lifecycle === 'entered')
|
|
89
|
+
lines.push(`The user has entered ${active.skill} custom mode. Follow the skill instructions in ${JSON.stringify(active.path)} until the user exits or switches modes. Do not drift back to default agent behavior.`);
|
|
90
|
+
else if (active !== undefined)
|
|
91
|
+
lines.push(
|
|
92
|
+
`Reminder: you are still in ${active.skill} mode. Follow that mode's skill on this turn. Do not drop it because the request looks small or you already have context. Do not drift back to default agent behavior. If you cannot state the mode's hard rules, read ${JSON.stringify(active.path)} before acting.`,
|
|
93
|
+
);
|
|
94
|
+
return lines.length === 0 ? undefined : lines.join('\n\n');
|
|
46
95
|
}
|
|
47
96
|
|
|
48
97
|
/**
|
|
49
|
-
* The
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* unread.
|
|
98
|
+
* The skill a mode keeps in context on every request until the user leaves it, in the block Pi's own /skill:name
|
|
99
|
+
* expansion builds. Cursor keeps the attached skill durable in the conversation; a section keeps it in every request,
|
|
100
|
+
* through compaction and restarts, and Pi re-sends a section only when it changes.
|
|
53
101
|
*/
|
|
54
|
-
export function
|
|
55
|
-
if (mode.path === undefined
|
|
56
|
-
|
|
57
|
-
if (!includeBody)
|
|
58
|
-
return `Reminder: you are still in ${mode.skill} mode. Follow that mode's skill on this turn. Do not drop it because the request looks small or you already have context. Do not drift back to default agent behavior. If you cannot state the mode's hard rules, read ${JSON.stringify(mode.path)} before acting.`;
|
|
59
|
-
// The frontmatter, including `reminder`, stays out of the model's context in a mode (MODE-004).
|
|
102
|
+
export function modeSkill(mode: ActiveMode): string | undefined {
|
|
103
|
+
if (mode.path === undefined || !existsSync(mode.path)) return undefined;
|
|
104
|
+
// The frontmatter stays out of the model's context in a mode (MODE-004).
|
|
60
105
|
const body = stripFrontmatter(readFileSync(mode.path, 'utf8')).trim();
|
|
61
|
-
return [
|
|
62
|
-
`The user has entered ${mode.skill} custom mode. Follow the skill instructions in ${JSON.stringify(mode.path)} until the user exits or switches modes. Do not drift back to default agent behavior.`,
|
|
63
|
-
`<skill name="${mode.skill}" location="${mode.path}">`,
|
|
64
|
-
`References are relative to ${dirname(mode.path)}.`,
|
|
65
|
-
'',
|
|
66
|
-
body,
|
|
67
|
-
'</skill>',
|
|
68
|
-
].join('\n');
|
|
106
|
+
return [`<skill name="${mode.skill}" location="${mode.path}">`, `References are relative to ${dirname(mode.path)}.`, '', body, '</skill>'].join('\n');
|
|
69
107
|
}
|
|
70
108
|
|
|
71
109
|
// A mode's badge shows the skill's `icon` (default: lightning) and `color`. Icon names map to terminal glyphs and
|
|
@@ -108,20 +146,22 @@ function loadedSkills(pi: ExtensionAPI): Map<string, string> {
|
|
|
108
146
|
return skills;
|
|
109
147
|
}
|
|
110
148
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
149
|
+
/** The mode sections of one request: Cursor's notice, and the skill the mode keeps in context. */
|
|
150
|
+
export interface ModeSections {
|
|
151
|
+
pstack_mode?: string;
|
|
152
|
+
pstack_mode_skill?: string;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function registerMode(pi: ExtensionAPI): { sections: () => ModeSections } {
|
|
156
|
+
let state: ModeState = {};
|
|
115
157
|
const show = (ctx: ExtensionContext) => {
|
|
116
158
|
if (!ctx.hasUI) return;
|
|
117
|
-
const badge = active === undefined ? undefined : modeBadge(active);
|
|
159
|
+
const badge = state.active === undefined ? undefined : modeBadge(state.active);
|
|
118
160
|
ctx.ui.setStatus(STATUS_KEY, badge === undefined ? undefined : badge.color === undefined ? badge.text : ctx.ui.theme.fg(badge.color, badge.text));
|
|
119
161
|
};
|
|
120
162
|
// The skill is looked up by name among loaded skills; a stored path is never read, since it can be stale or crafted.
|
|
121
163
|
const restore = (ctx: ExtensionContext) => {
|
|
122
|
-
|
|
123
|
-
active = stored === undefined ? undefined : (tryResolve(stored.skill) ?? { skill: stored.skill });
|
|
124
|
-
bodySent = undefined;
|
|
164
|
+
state = modeFromBranch(ctx.sessionManager.getBranch(), (skill) => tryResolve(skill) ?? { skill });
|
|
125
165
|
show(ctx);
|
|
126
166
|
};
|
|
127
167
|
pi.on('session_start', (_event, ctx) => restore(ctx));
|
|
@@ -145,12 +185,12 @@ export function registerMode(pi: ExtensionAPI): { section: () => string | undefi
|
|
|
145
185
|
|
|
146
186
|
const handle = (args: string, ctx: ExtensionCommandContext) => {
|
|
147
187
|
const command = parseModeArgs(args);
|
|
188
|
+
const { active } = state;
|
|
148
189
|
if (command.kind === 'status') return ctx.ui.notify(active === undefined ? 'No mode is on.' : `Mode on: ${active.skill} (${active.path ?? 'no loaded skill has that name'}).`, 'info');
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
pi.appendEntry(MODE_ENTRY, active === undefined ? null : { skill: active.skill });
|
|
190
|
+
state = command.kind === 'off' ? exitMode(state) : enterMode(state, resolveSkill(command.skill));
|
|
191
|
+
pi.appendEntry(MODE_ENTRY, state.active === undefined ? null : { skill: state.active.skill });
|
|
152
192
|
show(ctx);
|
|
153
|
-
ctx.ui.notify(active === undefined ? 'Mode off.' : `Mode on: ${active.skill}. /mode off exits.`, 'info');
|
|
193
|
+
ctx.ui.notify(state.active === undefined ? 'Mode off.' : `Mode on: ${state.active.skill}. /mode off exits.`, 'info');
|
|
154
194
|
if (command.kind === 'set' && command.task !== '') pi.sendUserMessage(command.task, { expandPromptTemplates: true, ...(ctx.isIdle() ? {} : { deliverAs: 'followUp' as const }) });
|
|
155
195
|
return undefined;
|
|
156
196
|
};
|
|
@@ -168,13 +208,11 @@ export function registerMode(pi: ExtensionAPI): { section: () => string | undefi
|
|
|
168
208
|
});
|
|
169
209
|
|
|
170
210
|
return {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
if (includeBody && active.path !== undefined && existsSync(active.path)) bodySent = active.skill;
|
|
177
|
-
return modeSection(active, includeBody);
|
|
211
|
+
sections: () => {
|
|
212
|
+
const notice = modeNotice(state);
|
|
213
|
+
const skill = state.active === undefined ? undefined : modeSkill(state.active);
|
|
214
|
+
state = afterRequest(state);
|
|
215
|
+
return { ...(notice === undefined ? {} : { pstack_mode: notice }), ...(skill === undefined ? {} : { pstack_mode_skill: skill }) };
|
|
178
216
|
},
|
|
179
217
|
};
|
|
180
218
|
}
|