diff --git a/.changeset/sdk-saved-workflow-workdir.md b/.changeset/sdk-saved-workflow-workdir.md new file mode 100644 index 00000000..b8a9e870 --- /dev/null +++ b/.changeset/sdk-saved-workflow-workdir.md @@ -0,0 +1,5 @@ +--- +"@pythoughts/pythinker-code-sdk": minor +--- + +The saved-workflow write helper now takes the working directory and resolves the repository root itself, saved workflows can carry a size guideline, and the workflow size guideline resolver is exported. diff --git a/.changeset/workflow-save-scope-and-root.md b/.changeset/workflow-save-scope-and-root.md new file mode 100644 index 00000000..168fbd1c --- /dev/null +++ b/.changeset/workflow-save-scope-and-root.md @@ -0,0 +1,5 @@ +--- +"@pythoughts/pythinker-code": minor +--- + +`/workflow save` accepts `--personal` to save into the home skills directory, resolves the repository root when saving from a subdirectory so the saved skill is discoverable, and persists the workflow size guideline into the saved skill. diff --git a/apps/pythinker-code/src/tui/commands/dynamic-workflow.ts b/apps/pythinker-code/src/tui/commands/dynamic-workflow.ts index cf4b0d3f..ccfe0bc2 100644 --- a/apps/pythinker-code/src/tui/commands/dynamic-workflow.ts +++ b/apps/pythinker-code/src/tui/commands/dynamic-workflow.ts @@ -2,6 +2,7 @@ import { savedWorkflowSkillName, writeSavedWorkflowSkill, type PermissionMode, + type SavedWorkflowScope, } from '@pythoughts/pythinker-code-sdk'; import { getDataDir } from '#/utils/paths'; @@ -16,7 +17,7 @@ import { import { LLM_NOT_SET_MESSAGE, NO_ACTIVE_SESSION_MESSAGE } from '../constant/pythinker-tui'; import { formatErrorMessage } from '../utils/event-payload'; import type { SlashCommandHost } from './dispatch'; -import { isDynamicWorkflowDisabled } from './workflow-availability'; +import { currentWorkflowSizeGuideline, isDynamicWorkflowDisabled } from './workflow-availability'; export async function handleDynamicWorkflowCommand(host: SlashCommandHost, args: string): Promise { if (isDynamicWorkflowDisabled()) { @@ -121,8 +122,10 @@ function withWorkerModelInstruction(prompt: string, model: string | undefined): } /** - * `/workflow save ` writes the last run back out as a skill, so a fan-out - * that worked can be re-run by name instead of re-described. + * `/workflow save [--personal]` writes the last run back out as a + * skill, so a fan-out that worked can be re-run by name instead of + * re-described. Project scope is the default; `--personal` keeps the skill in + * the user's home skills directory instead of the repository. * * Returns true when the input was a `save` subcommand and has been handled. */ @@ -130,9 +133,18 @@ async function handleSaveSubcommand(host: SlashCommandHost, input: string): Prom const match = /^save(?:\s+(.*))?$/iu.exec(input); if (match === null) return false; - const name = match[1]?.trim() ?? ''; - if (name.length === 0) { - host.showError('Usage: /workflow save '); + const tokens = (match[1] ?? '').split(/\s+/u).filter((token) => token.length > 0); + // A name may contain spaces, so the flag is only recognised at either end. + // Anywhere else — or twice — it is a typo rather than part of the name, and + // folding it in would silently save under a different name and scope. + const personalFirst = tokens[0] === '--personal'; + const personalLast = !personalFirst && tokens.at(-1) === '--personal'; + if (personalFirst) tokens.shift(); + else if (personalLast) tokens.pop(); + const scope: SavedWorkflowScope = personalFirst || personalLast ? 'personal' : 'project'; + const name = tokens.join(' '); + if (name.length === 0 || tokens.includes('--personal')) { + host.showError('Usage: /workflow save [--personal]'); return true; } @@ -150,8 +162,8 @@ async function handleSaveSubcommand(host: SlashCommandHost, input: string): Prom try { const dir = await writeSavedWorkflowSkill({ - scope: 'project', - projectRoot: host.state.appState.workDir, + scope, + workDir: host.state.appState.workDir, brandHomeDir: getDataDir(), workflow: { name, @@ -161,6 +173,7 @@ async function handleSaveSubcommand(host: SlashCommandHost, input: string): Prom model: stringArg(args, 'model'), effort: stringArg(args, 'effort'), outputSchema: recordArg(args, 'output_schema'), + sizeGuideline: currentWorkflowSizeGuideline(), }, }); // The skill registry is built once when the session opens, so the file just diff --git a/apps/pythinker-code/src/tui/commands/workflow-availability.ts b/apps/pythinker-code/src/tui/commands/workflow-availability.ts index 5346b84c..b063b32f 100644 --- a/apps/pythinker-code/src/tui/commands/workflow-availability.ts +++ b/apps/pythinker-code/src/tui/commands/workflow-availability.ts @@ -1,3 +1,8 @@ +import { + resolveWorkflowSizeGuideline, + type WorkflowSizeGuideline, +} from '@pythoughts/pythinker-code-sdk'; + const DISABLE_WORKFLOWS_ENV = 'PYTHINKER_CODE_DISABLE_WORKFLOWS'; const TRUE_ENV_VALUES = new Set(['1', 'true', 'yes', 'on']); const FALSE_ENV_VALUES = new Set(['0', 'false', 'no', 'off']); @@ -23,3 +28,18 @@ export function setDynamicWorkflowDisabled(configValue: boolean | undefined, env export function isDynamicWorkflowDisabled(): boolean { return disabled; } + +let sizeGuideline: WorkflowSizeGuideline | undefined; + +/** Cache the resolved guideline. Call once at startup with the value from `harness.getConfig()`. */ +export function setWorkflowSizeGuideline( + configValue: WorkflowSizeGuideline | undefined, + env = process.env, +): void { + sizeGuideline = resolveWorkflowSizeGuideline({ workflowSizeGuideline: configValue }, env); +} + +/** The guideline in force for this session, for surfaces that persist it (e.g. `/workflow save`). */ +export function currentWorkflowSizeGuideline(): WorkflowSizeGuideline | undefined { + return sizeGuideline; +} diff --git a/apps/pythinker-code/src/tui/components/messages/dynamic-workflow-mission-control.ts b/apps/pythinker-code/src/tui/components/messages/dynamic-workflow-mission-control.ts index f6879087..497bd0e5 100644 --- a/apps/pythinker-code/src/tui/components/messages/dynamic-workflow-mission-control.ts +++ b/apps/pythinker-code/src/tui/components/messages/dynamic-workflow-mission-control.ts @@ -773,7 +773,7 @@ export class DynamicWorkflowMissionControlComponent implements Component { } /** Item list from the completed tool-call `items` argument. */ -export function dynamicWorkflowItemsFromArgs(args: Record): string[] { +function dynamicWorkflowItemsFromArgs(args: Record): string[] { const items = args['items']; if (!Array.isArray(items)) return []; // Blank entries are dropped by the engine before any agent is launched, so @@ -833,11 +833,6 @@ export function dynamicWorkflowPartialItemsFromArguments(argumentsText: string): return items; } -/** Count of `items` parsed so far from streaming arguments. */ -export function dynamicWorkflowPartialItemsCountFromArguments(argumentsText: string): number { - return dynamicWorkflowPartialItemsFromArguments(argumentsText).length; -} - /** Description from the completed tool-call `description` argument. */ export function dynamicWorkflowDescriptionFromArgs(args: Record): string { const description = args['description']; diff --git a/apps/pythinker-code/src/tui/pythinker-tui.ts b/apps/pythinker-code/src/tui/pythinker-tui.ts index 3ecbd19b..63b44b9c 100644 --- a/apps/pythinker-code/src/tui/pythinker-tui.ts +++ b/apps/pythinker-code/src/tui/pythinker-tui.ts @@ -57,6 +57,7 @@ import { import { isDynamicWorkflowDisabled, setDynamicWorkflowDisabled, + setWorkflowSizeGuideline, } from './commands/workflow-availability'; import * as slashCommands from './commands/dispatch'; import { BannerComponent } from './components/chrome/banner'; @@ -746,7 +747,9 @@ export class PythinkerTUI { private async init(): Promise { setExperimentalFeatures(await this.harness.getExperimentalFeatures()); - setDynamicWorkflowDisabled((await this.harness.getConfig()).disableWorkflows); + const pythinkerConfig = await this.harness.getConfig(); + setDynamicWorkflowDisabled(pythinkerConfig.disableWorkflows); + setWorkflowSizeGuideline(pythinkerConfig.workflowSizeGuideline); await this.authFlow.refreshAvailableModels(); void this.refreshProviderModelsInBackground(); diff --git a/apps/pythinker-code/test/tui/commands/dynamic-workflow.test.ts b/apps/pythinker-code/test/tui/commands/dynamic-workflow.test.ts index 515cab50..5a852f81 100644 --- a/apps/pythinker-code/test/tui/commands/dynamic-workflow.test.ts +++ b/apps/pythinker-code/test/tui/commands/dynamic-workflow.test.ts @@ -6,7 +6,7 @@ import { describe, expect, it, vi } from 'vitest'; import { handleDynamicWorkflowCommand } from '#/tui/commands/index'; import type { SlashCommandHost } from '#/tui/commands/dispatch'; -import { setDynamicWorkflowDisabled } from '#/tui/commands/workflow-availability'; +import { setDynamicWorkflowDisabled, setWorkflowSizeGuideline } from '#/tui/commands/workflow-availability'; import { currentTheme } from '#/tui/theme'; const ENTER = '\r'; @@ -497,6 +497,59 @@ describe('/workflow save', () => { await handleDynamicWorkflowCommand(host, 'save'); - expect(host.showError).toHaveBeenCalledWith('Usage: /workflow save '); + expect(host.showError).toHaveBeenCalledWith('Usage: /workflow save [--personal]'); + }); + + it('asks for a name when given only the --personal flag', async () => { + const { host } = makeHost({ permissionMode: 'auto' }); + + await handleDynamicWorkflowCommand(host, 'save --personal'); + + expect(host.showError).toHaveBeenCalledWith('Usage: /workflow save [--personal]'); + }); + + it('saves --personal into the data dir and records the size guideline', async () => { + const home = await fs.mkdtemp(join(tmpdir(), 'workflow-home-')); + vi.stubEnv('PYTHINKER_CODE_HOME', home); + // Explicit empty env: the default is process.env, where an exported + // PYTHINKER_CODE_WORKFLOW_SIZE_GUIDELINE would override 'small' and fail + // this test for reasons unrelated to the change under test. + setWorkflowSizeGuideline('small', {}); + try { + const { host, session } = makeHost({ + permissionMode: 'auto', + lastDynamicWorkflowArgs: { description: 'Audit routes for missing auth' }, + }); + + await handleDynamicWorkflowCommand(host, 'save --personal Audit Routes'); + + const saved = await fs.readFile(join(home, 'skills', 'audit-routes', 'SKILL.md'), 'utf8'); + expect(saved).toContain('name: "audit-routes"'); + expect(saved).toContain('size-guideline: "small"'); + // The body line is what shapes the re-run; the frontmatter alone is inert. + expect(saved).toContain('at most about 5 subagents'); + expect(session.reloadSkills).toHaveBeenCalledOnce(); + expect(host.showError).not.toHaveBeenCalled(); + } finally { + vi.unstubAllEnvs(); + // The module-level cache cannot return to unset; the resolved default + // ('medium') matches what TUI startup would have cached in production. + setWorkflowSizeGuideline(undefined, {}); + await fs.rm(home, { recursive: true, force: true }); + } + }); + + it('rejects --personal when it is repeated or not at either end', async () => { + for (const input of [ + 'save Audit --personal Routes', + 'save --personal Audit --personal', + 'save --personal --personal', + ]) { + const { host } = makeHost({ permissionMode: 'auto' }); + + await handleDynamicWorkflowCommand(host, input); + + expect(host.showError).toHaveBeenCalledWith('Usage: /workflow save [--personal]'); + } }); }); diff --git a/docs/configuration/env-vars.md b/docs/configuration/env-vars.md index 6c48fc11..c56924fc 100644 --- a/docs/configuration/env-vars.md +++ b/docs/configuration/env-vars.md @@ -131,6 +131,8 @@ Switches that control the behavior of subsystems such as telemetry, background t | `PYTHINKER_DISABLE_TELEMETRY` | Disable anonymous telemetry reporting | `1`, `true`, `yes`, `y` (case-insensitive) | | `PYTHINKER_CODE_BACKGROUND_KEEP_ALIVE_ON_EXIT` | Whether to keep background tasks when the session closes; takes higher priority than `config.toml`. The default is to stop them on exit | Truthy: `1`/`true`/`yes`/`on`; falsy: `0`/`false`/`no`/`off` | | `PYTHINKER_CODE_PLUGIN_MARKETPLACE_URL` | Override the plugin marketplace JSON loaded by `/plugins` | URL or local path | +| `PYTHINKER_CODE_DISABLE_WORKFLOWS` | Disable Dynamic Workflow: the `DynamicWorkflow` tool is not registered and `/workflow` is hidden; takes higher priority than `config.toml` | Truthy: `1`/`true`/`yes`/`on`; falsy: `0`/`false`/`no`/`off` | +| `PYTHINKER_CODE_WORKFLOW_SIZE_GUIDELINE` | Override the advisory Dynamic Workflow size guideline injected into the tool guidance; takes higher priority than `config.toml` | `small`, `medium`, `large`, `unrestricted` | | `PYTHINKER_CODE_EXPERIMENTAL_FLAG` | Enable all registered experimental features for this process; `micro_compaction` is already enabled by default | `1`, `true`, `yes`, `on` | | `PYTHINKER_CODE_EXPERIMENTAL_MICRO_COMPACTION` | Override [`[experimental].micro_compaction`](./config-files.md#experimental) for this process | Truthy or falsy | | `PYTHINKER_SHELL_PATH` | Override the Git Bash path on Windows (used when auto-detection fails) | Absolute path | diff --git a/docs/reference/slash-commands.md b/docs/reference/slash-commands.md index 5849d796..e4e96b99 100644 --- a/docs/reference/slash-commands.md +++ b/docs/reference/slash-commands.md @@ -52,6 +52,7 @@ Some commands are only available in the idle state. Executing these commands whi | `/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 | | `/workflow ` | — | Turn Dynamic Workflow mode on, then send `` 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 | | `/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 | +| `/workflow save [--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 (`/.pythinker-code/skills/`) by default; `--personal` saves into your home skills directory instead. | No | | `/goal [...]` | — | Start or manage an autonomous goal | See below | ::: info diff --git a/packages/agent-core/src/agent/dynamic-workflow/save-as-skill.ts b/packages/agent-core/src/agent/dynamic-workflow/save-as-skill.ts index 156cfbfb..fc6a0bea 100644 --- a/packages/agent-core/src/agent/dynamic-workflow/save-as-skill.ts +++ b/packages/agent-core/src/agent/dynamic-workflow/save-as-skill.ts @@ -2,8 +2,11 @@ import { constants, promises as fs } from 'node:fs'; import path from 'pathe'; +import type { WorkflowSizeGuideline } from '../../config'; import { resolveSafePath } from '../../services/fs/fsPathSafety'; +import { findProjectRoot } from '../../skill/scanner'; import { normalizeSkillName } from '../../skill/types'; +import { workflowSizeGuidelineTarget } from './size-guideline'; /** * A saved workflow's name becomes both a directory name and a slash command, @@ -40,6 +43,8 @@ export interface SavedWorkflow { readonly model?: string; readonly effort?: string; readonly outputSchema?: Record; + /** Size guideline in force when the workflow ran, so a re-run keeps the same fan-out expectation. */ + readonly sizeGuideline?: WorkflowSizeGuideline; } /** @@ -100,7 +105,22 @@ export function renderSavedWorkflowSkill(workflow: SavedWorkflow): string { if (workflow.effort !== undefined) { lines.push(`effort: ${quoteYamlScalar(workflow.effort)}`); } + if (workflow.sizeGuideline !== undefined) { + lines.push(`size-guideline: ${quoteYamlScalar(workflow.sizeGuideline)}`); + } lines.push('---', '', `# ${workflow.description}`); + // The body is what the model reads on invocation, so the guideline has to be + // stated there to shape the re-run; the frontmatter alone is inert metadata. + const sizeTarget = + workflow.sizeGuideline === undefined + ? undefined + : workflowSizeGuidelineTarget(workflow.sizeGuideline); + if (sizeTarget !== undefined) { + lines.push( + '', + `Size guideline: aim for at most about ${String(sizeTarget)} subagents in this workflow, preferring fewer, larger items over many tiny ones.`, + ); + } if (workflow.promptTemplate !== undefined) { const fence = renderFence(workflow.promptTemplate); lines.push('', '## Prompt template', '', fence, workflow.promptTemplate, fence); @@ -125,6 +145,12 @@ export function renderSavedWorkflowSkill(workflow: SavedWorkflow): string { * workflow can also keep one. The name is validated before any directory is * created, so a rejected name leaves nothing behind. * + * Project scope resolves the closest `.git` ancestor of `workDir` — the same + * rule the skill scanner uses to pick its project root — so a save made from a + * repository subdirectory lands where the scanner will look for it. Without + * that, the saved skill is invisible until the session is reopened at the + * repository root. + * * A validated name is not enough on its own. Agents work in repositories they * did not write, and a checked-out tree can already contain * `.pythinker-code/skills//SKILL.md` as a symlink pointing anywhere on @@ -136,15 +162,16 @@ export function renderSavedWorkflowSkill(workflow: SavedWorkflow): string { export async function writeSavedWorkflowSkill(input: { readonly scope: SavedWorkflowScope; readonly workflow: SavedWorkflow; - readonly projectRoot: string; + readonly workDir: string; readonly brandHomeDir: string; }): Promise { const name = savedWorkflowSkillName(input.workflow.name); - const root = input.scope === 'project' ? input.projectRoot : input.brandHomeDir; + const projectRoot = input.scope === 'project' ? await findProjectRoot(input.workDir) : input.workDir; + const root = input.scope === 'project' ? projectRoot : input.brandHomeDir; const dir = savedWorkflowSkillDir({ scope: input.scope, name: input.workflow.name, - projectRoot: input.projectRoot, + projectRoot, brandHomeDir: input.brandHomeDir, }); const content = renderSavedWorkflowSkill({ ...input.workflow, name }); diff --git a/packages/agent-core/src/agent/index.ts b/packages/agent-core/src/agent/index.ts index 6266e90d..b33a43ba 100644 --- a/packages/agent-core/src/agent/index.ts +++ b/packages/agent-core/src/agent/index.ts @@ -73,6 +73,7 @@ export { writeSavedWorkflowSkill, } from './dynamic-workflow/save-as-skill'; export type { SavedWorkflow, SavedWorkflowScope } from './dynamic-workflow/save-as-skill'; +export { resolveWorkflowSizeGuideline } from './dynamic-workflow/size-guideline'; export type { BuiltinTool, ToolInfo, ToolSource, UserToolRegistration } from './tool'; export * from './goal'; diff --git a/packages/agent-core/src/skill/scanner.ts b/packages/agent-core/src/skill/scanner.ts index 1a5822a5..1266c274 100644 --- a/packages/agent-core/src/skill/scanner.ts +++ b/packages/agent-core/src/skill/scanner.ts @@ -449,7 +449,14 @@ async function defaultIsFile(p: string): Promise { } } -async function findProjectRoot(workDir: string): Promise { +/** + * Closest `.git` ancestor of `workDir`, or `workDir` itself when none exists. + * + * Exported because it defines where project-scoped artifacts live: anything + * that writes into `/.pythinker-code` (e.g. saved workflows) must + * resolve the root the same way this scanner will later scan it. + */ +export async function findProjectRoot(workDir: string): Promise { const start = path.resolve(workDir); let current = start; while (true) { diff --git a/packages/agent-core/test/agent/dynamic-workflow-save.test.ts b/packages/agent-core/test/agent/dynamic-workflow-save.test.ts index 205b8dcb..9889358f 100644 --- a/packages/agent-core/test/agent/dynamic-workflow-save.test.ts +++ b/packages/agent-core/test/agent/dynamic-workflow-save.test.ts @@ -174,6 +174,35 @@ describe('renderSavedWorkflowSkill', () => { }); expect(rendered).toContain('\n---\n\n# Review the diff\n'); }); + + it('persists the size guideline in the frontmatter and states it in the body', () => { + const rendered = renderSavedWorkflowSkill({ + name: 'review', + description: 'Review the diff', + sizeGuideline: 'small', + }); + expect(frontmatterOf(rendered)).toContain('size-guideline: "small"'); + // The body is what the model reads on invocation; the frontmatter alone + // would record the guideline without ever applying it to a re-run. + expect(rendered).toContain('at most about 5 subagents'); + + const withoutGuideline = renderSavedWorkflowSkill({ + name: 'review', + description: 'Review the diff', + }); + expect(withoutGuideline).not.toContain('size-guideline'); + expect(withoutGuideline).not.toContain('subagents'); + }); + + it('records `unrestricted` in the frontmatter without a body target', () => { + const rendered = renderSavedWorkflowSkill({ + name: 'review', + description: 'Review the diff', + sizeGuideline: 'unrestricted', + }); + expect(frontmatterOf(rendered)).toContain('size-guideline: "unrestricted"'); + expect(rendered).not.toContain('at most about'); + }); }); // Agents work in repositories they did not write. A checked-out tree can @@ -196,7 +225,7 @@ describe('writeSavedWorkflowSkill refuses to write through a symlink', () => { writeSavedWorkflowSkill({ scope: 'project', workflow, - projectRoot: path.join(root, 'project'), + workDir: path.join(root, 'project'), brandHomeDir: path.join(root, 'home'), }), ).rejects.toThrow(/rejected \(symlink_outside_cwd\)/u); @@ -221,7 +250,7 @@ describe('writeSavedWorkflowSkill refuses to write through a symlink', () => { writeSavedWorkflowSkill({ scope: 'project', workflow, - projectRoot, + workDir: projectRoot, brandHomeDir: path.join(root, 'home'), }), ).rejects.toThrow(/rejected \(symlink_outside_cwd\)/u); @@ -238,14 +267,57 @@ describe('writeSavedWorkflowSkill refuses to write through a symlink', () => { const dir = await writeSavedWorkflowSkill({ scope: 'project', workflow, - projectRoot: root, + workDir: root, + brandHomeDir: path.join(root, 'home'), + }); + expect(await fs.readFile(path.join(dir, 'SKILL.md'), 'utf8')).toContain('name: "audit"'); + } finally { + await fs.rm(root, { recursive: true, force: true }); + } + }); +}); + +// The skill scanner resolves its project root from the closest `.git` +// ancestor. A save made from a repository subdirectory must land at that same +// root — a skill written to `/.pythinker-code/skills` would exist on +// disk and never be discovered. +describe('writeSavedWorkflowSkill resolves the project root like the scanner', () => { + it('saves from a repo subdirectory into the repo root skills directory', async () => { + const root = await fs.mkdtemp(path.join(tmpdir(), 'workflow-monorepo-')); + try { + const repoRoot = path.join(root, 'repo'); + const subDir = path.join(repoRoot, 'packages', 'app'); + await fs.mkdir(path.join(repoRoot, '.git'), { recursive: true }); + await fs.mkdir(subDir, { recursive: true }); + + const dir = await writeSavedWorkflowSkill({ + scope: 'project', + workflow: { name: 'audit', description: 'Audit routes' }, + workDir: subDir, brandHomeDir: path.join(root, 'home'), }); + + expect(dir).toBe(path.join(repoRoot, '.pythinker-code', 'skills', 'audit')); expect(await fs.readFile(path.join(dir, 'SKILL.md'), 'utf8')).toContain('name: "audit"'); } finally { await fs.rm(root, { recursive: true, force: true }); } }); + + it('falls back to the working directory itself outside any repository', async () => { + const root = await fs.mkdtemp(path.join(tmpdir(), 'workflow-no-repo-')); + try { + const dir = await writeSavedWorkflowSkill({ + scope: 'project', + workflow: { name: 'audit', description: 'Audit routes' }, + workDir: root, + brandHomeDir: path.join(root, 'home'), + }); + expect(dir).toBe(path.join(path.resolve(root), '.pythinker-code', 'skills', 'audit')); + } finally { + await fs.rm(root, { recursive: true, force: true }); + } + }); }); // Rendering valid-looking Markdown is not the contract — being loadable as a diff --git a/packages/node-sdk/src/index.ts b/packages/node-sdk/src/index.ts index ff42dfa9..248a69e7 100644 --- a/packages/node-sdk/src/index.ts +++ b/packages/node-sdk/src/index.ts @@ -44,11 +44,12 @@ export type { export { renderSavedWorkflowSkill, + resolveWorkflowSizeGuideline, savedWorkflowSkillDir, savedWorkflowSkillName, writeSavedWorkflowSkill, } from '@pythoughts/agent-core'; -export type { SavedWorkflow, SavedWorkflowScope } from '@pythoughts/agent-core'; +export type { SavedWorkflow, SavedWorkflowScope, WorkflowSizeGuideline } from '@pythoughts/agent-core'; export { buildSkillSlashCommands, isUserActivatableSkill } from '#/skill-commands'; export type { SkillSlashCommand, SkillSlashCommands } from '#/skill-commands';