Skip to content

Commit b04404c

Browse files
committed
fix(workflow): reject a misplaced --personal flag in /workflow save
A workflow name may contain spaces, so the flag was stripped wherever it appeared. That let 'save report --personal extra' save under the name 'report extra' in personal scope without a word to the user. Recognise --personal only as the first or last token and reject the rest, and document that a saved workflow is invoked under its generated skill name rather than the name as typed.
1 parent e6395e9 commit b04404c

3 files changed

Lines changed: 26 additions & 12 deletions

File tree

apps/pythinker-code/src/tui/commands/dynamic-workflow.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -134,11 +134,16 @@ async function handleSaveSubcommand(host: SlashCommandHost, input: string): Prom
134134
if (match === null) return false;
135135

136136
const tokens = (match[1] ?? '').split(/\s+/u).filter((token) => token.length > 0);
137-
const personalIndex = tokens.indexOf('--personal');
138-
const scope: SavedWorkflowScope = personalIndex === -1 ? 'project' : 'personal';
139-
if (personalIndex !== -1) tokens.splice(personalIndex, 1);
137+
// A name may contain spaces, so the flag is only recognised at either end.
138+
// Anywhere else — or twice — it is a typo rather than part of the name, and
139+
// folding it in would silently save under a different name and scope.
140+
const personalFirst = tokens[0] === '--personal';
141+
const personalLast = !personalFirst && tokens.at(-1) === '--personal';
142+
if (personalFirst) tokens.shift();
143+
else if (personalLast) tokens.pop();
144+
const scope: SavedWorkflowScope = personalFirst || personalLast ? 'personal' : 'project';
140145
const name = tokens.join(' ');
141-
if (name.length === 0) {
146+
if (name.length === 0 || tokens.includes('--personal')) {
142147
host.showError('Usage: /workflow save <name> [--personal]');
143148
return true;
144149
}

apps/pythinker-code/test/tui/commands/dynamic-workflow.test.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -510,8 +510,7 @@ describe('/workflow save', () => {
510510

511511
it('saves --personal into the data dir and records the size guideline', async () => {
512512
const home = await fs.mkdtemp(join(tmpdir(), 'workflow-home-'));
513-
const previousHome = process.env['PYTHINKER_CODE_HOME'];
514-
process.env['PYTHINKER_CODE_HOME'] = home;
513+
vi.stubEnv('PYTHINKER_CODE_HOME', home);
515514
// Explicit empty env: the default is process.env, where an exported
516515
// PYTHINKER_CODE_WORKFLOW_SIZE_GUIDELINE would override 'small' and fail
517516
// this test for reasons unrelated to the change under test.
@@ -532,15 +531,25 @@ describe('/workflow save', () => {
532531
expect(session.reloadSkills).toHaveBeenCalledOnce();
533532
expect(host.showError).not.toHaveBeenCalled();
534533
} finally {
535-
if (previousHome === undefined) {
536-
delete process.env['PYTHINKER_CODE_HOME'];
537-
} else {
538-
process.env['PYTHINKER_CODE_HOME'] = previousHome;
539-
}
534+
vi.unstubAllEnvs();
540535
// The module-level cache cannot return to unset; the resolved default
541536
// ('medium') matches what TUI startup would have cached in production.
542537
setWorkflowSizeGuideline(undefined, {});
543538
await fs.rm(home, { recursive: true, force: true });
544539
}
545540
});
541+
542+
it('rejects --personal when it is repeated or not at either end', async () => {
543+
for (const input of [
544+
'save Audit --personal Routes',
545+
'save --personal Audit --personal',
546+
'save --personal --personal',
547+
]) {
548+
const { host } = makeHost({ permissionMode: 'auto' });
549+
550+
await handleDynamicWorkflowCommand(host, input);
551+
552+
expect(host.showError).toHaveBeenCalledWith('Usage: /workflow save <name> [--personal]');
553+
}
554+
});
546555
});

docs/reference/slash-commands.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ Some commands are only available in the idle state. Executing these commands whi
5252
| `/workflow [on\|off]` || Toggle Dynamic Workflow mode without sending a prompt. Without arguments, flips the current state; explicitly passing `on`/`off` forces the setting. | No |
5353
| `/workflow <task>` || Turn Dynamic Workflow mode on, then send `<task>` as a normal prompt. If the turn completes normally, Dynamic Workflow mode turns off automatically. In `manual` permission mode, Pythinker Code asks whether to switch to `auto` or `yolo` before starting. | No |
5454
| `/workflow model [alias\|off]` || Ask Dynamic Workflow subagents to run on `alias` instead of the session model, so workers can use a cheaper or faster model than the agent orchestrating them. Without arguments, shows the current setting; `off` clears it. Lasts for the session. | No |
55-
| `/workflow save <name> [--personal]` || Save the last Dynamic Workflow that ran in this session as a skill, immediately invocable as `/<name>`. Saves into the project (`<repo root>/.pythinker-code/skills/`) by default; `--personal` saves into your home skills directory instead. | No |
55+
| `/workflow save <name> [--personal]` || Save the last Dynamic Workflow that ran in this session as a skill, immediately invocable under its generated skill name — `Audit Routes` becomes `/audit-routes`. Saves into the project (`<repo root>/.pythinker-code/skills/`) by default; `--personal` saves into your home skills directory instead. | No |
5656
| `/goal [...]` || Start or manage an autonomous goal | See below |
5757

5858
::: info

0 commit comments

Comments
 (0)