@robota-sdk/agent-command 3.0.0-beta.100
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 +1351 -0
- package/LICENSE +661 -0
- package/README.md +100 -0
- package/dist/node/index.cjs +121 -0
- package/dist/node/index.d.cts +1053 -0
- package/dist/node/index.d.cts.map +1 -0
- package/dist/node/index.d.ts +1053 -0
- package/dist/node/index.d.ts.map +1 -0
- package/dist/node/index.js +122 -0
- package/dist/node/index.js.map +1 -0
- package/package.json +83 -0
- package/src/advisor/advisor-command-module.ts +47 -0
- package/src/advisor/advisor-command.test.ts +102 -0
- package/src/advisor/advisor-command.ts +55 -0
- package/src/advisor/advisor-cost-functional.test.ts +92 -0
- package/src/advisor/index.ts +6 -0
- package/src/agent/__tests__/agent-command.test.ts +597 -0
- package/src/agent/agent-command-module.ts +89 -0
- package/src/agent/agent-command-parser.ts +180 -0
- package/src/agent/agent-command.ts +255 -0
- package/src/agent/index.ts +7 -0
- package/src/background/__tests__/background-command-module.test.ts +261 -0
- package/src/background/background-command-module.ts +52 -0
- package/src/background/background-command.ts +65 -0
- package/src/background/index.ts +6 -0
- package/src/command-module-utils.test.ts +37 -0
- package/src/command-module-utils.ts +35 -0
- package/src/compact/__tests__/compact-command-module.test.ts +181 -0
- package/src/compact/compact-command-module.ts +59 -0
- package/src/compact/compact-command.ts +33 -0
- package/src/compact/index.ts +6 -0
- package/src/context/__tests__/context-command-module.test.ts +502 -0
- package/src/context/auto-compact-format.ts +25 -0
- package/src/context/context-breakdown.ts +295 -0
- package/src/context/context-command-module.ts +82 -0
- package/src/context/context-command.ts +291 -0
- package/src/context/index.ts +6 -0
- package/src/default/__tests__/__snapshots__/model-exposure.test.ts.snap +121 -0
- package/src/default/__tests__/default-command-modules.test.ts +192 -0
- package/src/default/__tests__/model-exposure.test.ts +269 -0
- package/src/default/__tests__/org-policy-forwarding.test.ts +106 -0
- package/src/default/__tests__/provider-environment-forwarding.test.ts +98 -0
- package/src/default/default-command-modules.ts +247 -0
- package/src/default/index.ts +5 -0
- package/src/devices/__tests__/devices-command-module.test.ts +299 -0
- package/src/devices/devices-command-module.ts +308 -0
- package/src/devices/devices-command-port.ts +143 -0
- package/src/devices/index.ts +14 -0
- package/src/doctor/__tests__/doctor-command-module.test.ts +145 -0
- package/src/doctor/__tests__/doctor-display-vocabulary.test.ts +38 -0
- package/src/doctor/__tests__/doctor-fixture.ts +146 -0
- package/src/doctor/__tests__/doctor-redaction.test.ts +29 -0
- package/src/doctor/__tests__/doctor-runner.test.ts +484 -0
- package/src/doctor/doctor-command-module.ts +132 -0
- package/src/doctor/doctor-extensions-probe.ts +323 -0
- package/src/doctor/doctor-node-deps.ts +88 -0
- package/src/doctor/doctor-redaction.ts +99 -0
- package/src/doctor/doctor-render.ts +70 -0
- package/src/doctor/doctor-repair.ts +136 -0
- package/src/doctor/doctor-runner.ts +122 -0
- package/src/doctor/doctor-settings-probe.ts +239 -0
- package/src/doctor/doctor-storage-probe.ts +187 -0
- package/src/doctor/doctor-types.ts +109 -0
- package/src/doctor/index.ts +32 -0
- package/src/editor/__tests__/editor-command-functional.test.ts +124 -0
- package/src/editor/editor-command-module.ts +53 -0
- package/src/editor/editor-command.ts +58 -0
- package/src/editor/index.ts +7 -0
- package/src/editor/resolve-editor.ts +21 -0
- package/src/effort/effort-command-module.ts +51 -0
- package/src/effort/effort-command.test.ts +203 -0
- package/src/effort/effort-command.ts +136 -0
- package/src/effort/index.ts +6 -0
- package/src/events/__tests__/events-command.test.ts +73 -0
- package/src/events/events-command-module.ts +49 -0
- package/src/events/events-command.ts +69 -0
- package/src/events/index.ts +2 -0
- package/src/exit/__tests__/exit-command-module.test.ts +61 -0
- package/src/exit/exit-command-module.ts +47 -0
- package/src/exit/exit-command.ts +26 -0
- package/src/exit/index.ts +6 -0
- package/src/fork/__tests__/fork-command.test.ts +184 -0
- package/src/fork/fork-command-module.ts +61 -0
- package/src/fork/fork-command.ts +162 -0
- package/src/fork/index.ts +8 -0
- package/src/git/__tests__/fake-git-port.ts +35 -0
- package/src/git/__tests__/git-command-module.test.ts +111 -0
- package/src/git/__tests__/git-commit.test.ts +185 -0
- package/src/git/git-command-module.ts +138 -0
- package/src/git/git-commit.ts +254 -0
- package/src/git/index.ts +20 -0
- package/src/goal/__tests__/goal-command.test.ts +80 -0
- package/src/goal/goal-command-module.ts +49 -0
- package/src/goal/goal-command.ts +69 -0
- package/src/goal/index.ts +6 -0
- package/src/handoff/__tests__/handoff-command-module.test.ts +25 -0
- package/src/handoff/__tests__/handoff-command.test.ts +218 -0
- package/src/handoff/handoff-command-module.ts +49 -0
- package/src/handoff/handoff-command.ts +187 -0
- package/src/handoff/index.ts +6 -0
- package/src/help/__tests__/help-command-module.test.ts +134 -0
- package/src/help/__tests__/help-command.test.ts +130 -0
- package/src/help/help-command-module.ts +47 -0
- package/src/help/help-command.ts +11 -0
- package/src/help/index.ts +6 -0
- package/src/index.ts +163 -0
- package/src/keybindings/__tests__/keybindings-command-module.test.ts +57 -0
- package/src/keybindings/index.ts +6 -0
- package/src/keybindings/keybindings-command-module.ts +88 -0
- package/src/language/__tests__/language-command-module.test.ts +126 -0
- package/src/language/index.ts +6 -0
- package/src/language/language-command-module.ts +55 -0
- package/src/language/language-command.ts +52 -0
- package/src/mcp-activation/__tests__/mcp-activation-command.test.ts +895 -0
- package/src/mcp-activation/__tests__/mcp-model-view.test.ts +242 -0
- package/src/mcp-activation/__tests__/mcp-skill-command.test.ts +141 -0
- package/src/mcp-activation/index.ts +12 -0
- package/src/mcp-activation/mcp-activation-command-module.ts +123 -0
- package/src/mcp-activation/mcp-activation-command.ts +662 -0
- package/src/mcp-activation/mcp-model-notice.ts +99 -0
- package/src/mcp-activation/mcp-model-status.ts +104 -0
- package/src/memory/__tests__/memory-command-module.test.ts +476 -0
- package/src/memory/__tests__/memory-command-unavailable.test.ts +63 -0
- package/src/memory/index.ts +6 -0
- package/src/memory/memory-command-module.ts +77 -0
- package/src/memory/memory-command.ts +326 -0
- package/src/mode/__tests__/mode-command-module.test.ts +230 -0
- package/src/mode/index.ts +6 -0
- package/src/mode/mode-command-module.ts +55 -0
- package/src/mode/mode-command.ts +67 -0
- package/src/model/__tests__/model-command-module.test.ts +80 -0
- package/src/model/__tests__/model-command.test.ts +265 -0
- package/src/model/index.ts +6 -0
- package/src/model/model-command-module.ts +56 -0
- package/src/model/model-command.ts +249 -0
- package/src/output-style/__tests__/output-style-command.test.ts +96 -0
- package/src/output-style/index.ts +6 -0
- package/src/output-style/output-style-command-module.ts +143 -0
- package/src/peers/__tests__/peers-command-module.test.ts +24 -0
- package/src/peers/__tests__/peers-command.test.ts +399 -0
- package/src/peers/index.ts +9 -0
- package/src/peers/peers-command-module.ts +49 -0
- package/src/peers/peers-command.ts +220 -0
- package/src/permissions/__tests__/permissions-command-module.test.ts +276 -0
- package/src/permissions/index.ts +6 -0
- package/src/permissions/permissions-command-module.ts +55 -0
- package/src/permissions/permissions-command.ts +89 -0
- package/src/plan/__tests__/plan-command.test.ts +129 -0
- package/src/plan/index.ts +9 -0
- package/src/plan/plan-command-module.ts +49 -0
- package/src/plan/plan-command.ts +92 -0
- package/src/plugin/__tests__/plugin-active-turn.test.ts +100 -0
- package/src/plugin/__tests__/plugin-command-module.test.ts +300 -0
- package/src/plugin/__tests__/plugin-host-boundary.test.ts +11 -0
- package/src/plugin/index.ts +7 -0
- package/src/plugin/plugin-command-module.ts +85 -0
- package/src/plugin/plugin-command.ts +271 -0
- package/src/preset/__tests__/host-supplied-preset-registry.test.ts +98 -0
- package/src/preset/__tests__/preset-command-module.test.ts +220 -0
- package/src/preset/index.ts +6 -0
- package/src/preset/preset-command-module.ts +80 -0
- package/src/preset/preset-command.ts +148 -0
- package/src/provider/__tests__/optional-provider-credentials.test.ts +130 -0
- package/src/provider/__tests__/org-policy.test.ts +174 -0
- package/src/provider/__tests__/provider-command-module.test.ts +884 -0
- package/src/provider/__tests__/provider-connection-setup.test.ts +567 -0
- package/src/provider/__tests__/provider-reference-switch.test.ts +113 -0
- package/src/provider/__tests__/provider-setup-flow.test.ts +244 -0
- package/src/provider/__tests__/provider-startup-host-settings.test.ts +16 -0
- package/src/provider/__tests__/scripted-interaction.ts +40 -0
- package/src/provider/index.ts +32 -0
- package/src/provider/provider-command-execution.ts +235 -0
- package/src/provider/provider-command-module.ts +82 -0
- package/src/provider/provider-command-profile-lifecycle.ts +239 -0
- package/src/provider/provider-command-profile-operations.ts +218 -0
- package/src/provider/provider-command-profile.ts +116 -0
- package/src/provider/provider-command-setup.ts +79 -0
- package/src/provider/provider-connection-setup.ts +287 -0
- package/src/provider/provider-connection-validation.ts +38 -0
- package/src/provider/provider-onboarding.ts +73 -0
- package/src/provider/provider-prompt-interaction.ts +28 -0
- package/src/provider/provider-setup-ask.ts +67 -0
- package/src/provider/provider-setup-flow.ts +326 -0
- package/src/provider/provider-startup.ts +225 -0
- package/src/remote-control/__tests__/remote-control-command-module.test.ts +36 -0
- package/src/remote-control/__tests__/remote-control-command.test.ts +216 -0
- package/src/remote-control/index.ts +8 -0
- package/src/remote-control/remote-control-command-module.ts +61 -0
- package/src/remote-control/remote-control-command.ts +138 -0
- package/src/reset/__tests__/reset-command-module.test.ts +63 -0
- package/src/reset/index.ts +2 -0
- package/src/reset/reset-command-module.ts +47 -0
- package/src/reset/reset-command.ts +10 -0
- package/src/rewind/__tests__/rewind-command-module.test.ts +287 -0
- package/src/rewind/index.ts +2 -0
- package/src/rewind/rewind-command-module.ts +56 -0
- package/src/rewind/rewind-command.ts +279 -0
- package/src/sandbox/__tests__/sandbox-command.test.ts +80 -0
- package/src/sandbox/index.ts +2 -0
- package/src/sandbox/sandbox-command-module.ts +56 -0
- package/src/sandbox/sandbox-command.ts +94 -0
- package/src/schedule/__tests__/loop-command-functional.test.ts +399 -0
- package/src/schedule/__tests__/loop-command.test.ts +594 -0
- package/src/schedule/__tests__/loop-jitter.test.ts +25 -0
- package/src/schedule/__tests__/monitor-model-permission-functional.test.ts +126 -0
- package/src/schedule/__tests__/schedule-command.test.ts +247 -0
- package/src/schedule/__tests__/schedule-redos.test.ts +114 -0
- package/src/schedule/index.ts +12 -0
- package/src/schedule/loop-command.ts +427 -0
- package/src/schedule/loop-jitter.ts +41 -0
- package/src/schedule/schedule-command-module.ts +184 -0
- package/src/schedule/schedule-command.ts +161 -0
- package/src/schedule/schedule-spec-parser.ts +70 -0
- package/src/session/__tests__/model-pricing.test.ts +66 -0
- package/src/session/__tests__/rename-host-persistence.test.ts +72 -0
- package/src/session/__tests__/session-command-module.test.ts +589 -0
- package/src/session/index.ts +17 -0
- package/src/session/model-pricing.ts +26 -0
- package/src/session/session-command-module.ts +197 -0
- package/src/session/session-command.ts +272 -0
- package/src/settings/index.ts +7 -0
- package/src/settings/settings-command-module.ts +47 -0
- package/src/shell/__tests__/resolve-shell-host.test.ts +11 -0
- package/src/shell/__tests__/shell-command-functional.test.ts +118 -0
- package/src/shell/index.ts +8 -0
- package/src/shell/resolve-shell.ts +25 -0
- package/src/shell/shell-command-module.ts +53 -0
- package/src/shell/shell-command.ts +48 -0
- package/src/shell/spawn-inherited.ts +32 -0
- package/src/skills/__tests__/skill-read-command.test.ts +64 -0
- package/src/skills/__tests__/skills-command-module.test.ts +200 -0
- package/src/skills/index.ts +7 -0
- package/src/skills/skill-read-command.ts +60 -0
- package/src/skills/skills-command-module.ts +76 -0
- package/src/skills/skills-command.ts +108 -0
- package/src/statusline/__tests__/statusline-command-module.test.ts +96 -0
- package/src/statusline/index.ts +6 -0
- package/src/statusline/statusline-command-module.ts +55 -0
- package/src/statusline/statusline-command.ts +80 -0
- package/src/terminal-client/__tests__/terminal-client-commands.test.ts +216 -0
- package/src/terminal-client/index.ts +5 -0
- package/src/terminal-client/terminal-client-commands.ts +109 -0
- package/src/theme/__tests__/theme-command.test.ts +204 -0
- package/src/theme/index.ts +12 -0
- package/src/theme/theme-command-module.ts +221 -0
- package/src/user-local/__tests__/user-local-command.test.ts +180 -0
- package/src/user-local/index.ts +13 -0
- package/src/user-local/user-local-command-constants.ts +5 -0
- package/src/user-local/user-local-command-module.ts +65 -0
- package/src/user-local/user-local-command.ts +214 -0
- package/src/user-local/user-local-memory-command.ts +160 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BEHAVIOR-2437: `/git` — one command whose `execute` parses its verb (`status` | `diff` | `commit`),
|
|
3
|
+
* with the three verbs on `subcommands` for `/help` and autocomplete. Host-only (`modelInvocable:
|
|
4
|
+
* false`); the confirmation lives in the `commit` verb, because `requiresPermission` is not enforced
|
|
5
|
+
* on the user path (`SystemCommandExecutor.executeCommand` calls `execute` directly).
|
|
6
|
+
*/
|
|
7
|
+
import { executeGitCommit } from './git-commit.js';
|
|
8
|
+
|
|
9
|
+
import { createGitProcess, executeGitDiff, executeGitStatus } from '@robota-sdk/agent-framework';
|
|
10
|
+
|
|
11
|
+
import type {
|
|
12
|
+
ICommandHostUserInteraction,
|
|
13
|
+
ICommandHostWorkspace,
|
|
14
|
+
ICommandModule,
|
|
15
|
+
IGitProcessPort,
|
|
16
|
+
ISystemCommand,
|
|
17
|
+
} from '@robota-sdk/agent-framework';
|
|
18
|
+
import type { ICommand, ICommandResult, ICommandSource } from '@robota-sdk/agent-interface-command';
|
|
19
|
+
|
|
20
|
+
export const GIT_COMMAND_DESCRIPTION =
|
|
21
|
+
'Show the git status, view a diff, or commit the staged changes after confirming';
|
|
22
|
+
export const GIT_COMMAND_ARGUMENT_HINT =
|
|
23
|
+
'status | diff [--staged | <rev> | <a>..<b>] [-- <path> ...] | commit [<subject>]';
|
|
24
|
+
export const GIT_COMMAND_USAGE =
|
|
25
|
+
'Usage: /git status | /git diff [--staged | <rev> | <a>..<b>] [-- <path> ...] | /git commit [<subject>]';
|
|
26
|
+
|
|
27
|
+
const SOURCE = 'git';
|
|
28
|
+
|
|
29
|
+
function verbEntry(name: string, description: string, argumentHint?: string): ICommand {
|
|
30
|
+
return {
|
|
31
|
+
name,
|
|
32
|
+
description,
|
|
33
|
+
source: SOURCE,
|
|
34
|
+
modelInvocable: false,
|
|
35
|
+
...(argumentHint ? { argumentHint } : {}),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function createGitCommandEntry(): ICommand {
|
|
40
|
+
return {
|
|
41
|
+
name: 'git',
|
|
42
|
+
displayName: 'Git',
|
|
43
|
+
description: GIT_COMMAND_DESCRIPTION,
|
|
44
|
+
source: SOURCE,
|
|
45
|
+
modelInvocable: false,
|
|
46
|
+
argumentHint: GIT_COMMAND_ARGUMENT_HINT,
|
|
47
|
+
subcommands: [
|
|
48
|
+
verbEntry('status', 'Show the branch and the staged, unstaged and untracked paths'),
|
|
49
|
+
verbEntry(
|
|
50
|
+
'diff',
|
|
51
|
+
'Show the unstaged diff, the staged diff, or a diff against one or two revisions',
|
|
52
|
+
'[--staged | <rev> | <a>..<b>] [-- <path> ...]',
|
|
53
|
+
),
|
|
54
|
+
verbEntry(
|
|
55
|
+
'commit',
|
|
56
|
+
'Commit the staged changes with a Conventional Commits subject, after confirming',
|
|
57
|
+
'[<subject>]',
|
|
58
|
+
),
|
|
59
|
+
],
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** What the verbs need from the host: the working directory and the ask-the-user port. */
|
|
64
|
+
export type TGitCommandContext = Pick<ICommandHostWorkspace, 'getCwd'> &
|
|
65
|
+
ICommandHostUserInteraction;
|
|
66
|
+
|
|
67
|
+
export async function executeGitCommand(
|
|
68
|
+
context: TGitCommandContext,
|
|
69
|
+
args: string,
|
|
70
|
+
port: IGitProcessPort = createGitProcess(),
|
|
71
|
+
): Promise<ICommandResult> {
|
|
72
|
+
const trimmed = args.trim();
|
|
73
|
+
const split = trimmed.search(/\s/);
|
|
74
|
+
const verb = split === -1 ? trimmed : trimmed.slice(0, split);
|
|
75
|
+
const rest = split === -1 ? '' : trimmed.slice(split + 1).trim();
|
|
76
|
+
const cwd = context.getCwd();
|
|
77
|
+
|
|
78
|
+
switch (verb) {
|
|
79
|
+
case 'status':
|
|
80
|
+
if (rest.length > 0) {
|
|
81
|
+
return {
|
|
82
|
+
success: false,
|
|
83
|
+
message: `\`/git status\` takes no arguments.\n${GIT_COMMAND_USAGE}`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
return executeGitStatus(port, cwd);
|
|
87
|
+
case 'diff':
|
|
88
|
+
return executeGitDiff(port, cwd, rest);
|
|
89
|
+
case 'commit':
|
|
90
|
+
return executeGitCommit(port, cwd, rest, context.getUserInteraction());
|
|
91
|
+
default:
|
|
92
|
+
return {
|
|
93
|
+
success: false,
|
|
94
|
+
message:
|
|
95
|
+
verb.length === 0
|
|
96
|
+
? GIT_COMMAND_USAGE
|
|
97
|
+
: `Unknown /git verb "${verb}".\n${GIT_COMMAND_USAGE}`,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function createGitSystemCommand(port: IGitProcessPort): ISystemCommand {
|
|
103
|
+
const entry = createGitCommandEntry();
|
|
104
|
+
return {
|
|
105
|
+
name: entry.name,
|
|
106
|
+
displayName: entry.displayName,
|
|
107
|
+
description: entry.description,
|
|
108
|
+
argumentHint: entry.argumentHint,
|
|
109
|
+
example: '/git status',
|
|
110
|
+
subcommands: entry.subcommands,
|
|
111
|
+
requiresPermission: true,
|
|
112
|
+
userInvocable: true,
|
|
113
|
+
modelInvocable: false,
|
|
114
|
+
lifecycle: 'blocking',
|
|
115
|
+
execute: (context, args) => executeGitCommand(context, args, port),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export class GitCommandSource implements ICommandSource {
|
|
120
|
+
readonly name = SOURCE;
|
|
121
|
+
|
|
122
|
+
getCommands(): ICommand[] {
|
|
123
|
+
return [createGitCommandEntry()];
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface IGitCommandModuleOptions {
|
|
128
|
+
/** The process seam; the production `createGitProcess()` by default, injectable for tests. */
|
|
129
|
+
port?: IGitProcessPort;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function createGitCommandModule(options: IGitCommandModuleOptions = {}): ICommandModule {
|
|
133
|
+
return {
|
|
134
|
+
name: 'agent-command-git',
|
|
135
|
+
commandSources: [new GitCommandSource()],
|
|
136
|
+
systemCommands: [createGitSystemCommand(options.port ?? createGitProcess())],
|
|
137
|
+
};
|
|
138
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BEHAVIOR-2437: `/git commit [<subject>]` — the staged set, and only ever the staged set.
|
|
3
|
+
*
|
|
4
|
+
* No `-a`, no `add`, no paths. Nothing staged is answered with guidance that says why; the subject
|
|
5
|
+
* is held to the Conventional Commits v1.0.0 MUST rules and nothing more (the commitlint conventions
|
|
6
|
+
* are warnings, never refusals); the confirmation lists the message and exactly the files
|
|
7
|
+
* `git diff --cached --name-status` reports; an absent `IUserInteraction` is a cancellation — the
|
|
8
|
+
* `/doctor repair` precedent — never a guess. Only after "Yes" does `git commit -m <subject>` run.
|
|
9
|
+
*/
|
|
10
|
+
import { confirmAction, isConfirmed, textAction } from '@robota-sdk/agent-core';
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
gitFailureMessage,
|
|
14
|
+
parseStatusPorcelainV2,
|
|
15
|
+
GIT_STATUS_ARGS,
|
|
16
|
+
} from '@robota-sdk/agent-framework';
|
|
17
|
+
|
|
18
|
+
import type { IGitProcessPort } from '@robota-sdk/agent-framework';
|
|
19
|
+
import type { IUserInteraction } from '@robota-sdk/agent-core';
|
|
20
|
+
import type { ICommandResult } from '@robota-sdk/agent-interface-command';
|
|
21
|
+
|
|
22
|
+
const GIT_COMMIT_CONVENTIONAL_TYPES: readonly string[] = [
|
|
23
|
+
'build',
|
|
24
|
+
'chore',
|
|
25
|
+
'ci',
|
|
26
|
+
'docs',
|
|
27
|
+
'feat',
|
|
28
|
+
'fix',
|
|
29
|
+
'perf',
|
|
30
|
+
'refactor',
|
|
31
|
+
'revert',
|
|
32
|
+
'style',
|
|
33
|
+
'test',
|
|
34
|
+
];
|
|
35
|
+
const GIT_COMMIT_SUBJECT_LIMIT = 72;
|
|
36
|
+
|
|
37
|
+
export type TConventionalSubjectCheck =
|
|
38
|
+
{ ok: true; warnings: readonly string[] } | { ok: false; reason: string };
|
|
39
|
+
|
|
40
|
+
/** `<type>[(scope)][!]: <description>` — the MUST rules, and only those. */
|
|
41
|
+
const CONVENTIONAL_SUBJECT = /^([^\s():!]+)(\(([^\s()]+)\))?(!)?: (.*)$/;
|
|
42
|
+
|
|
43
|
+
export function validateConventionalSubject(subject: string): TConventionalSubjectCheck {
|
|
44
|
+
if (subject.trim().length === 0) return { ok: false, reason: 'the subject is empty' };
|
|
45
|
+
const separator = subject.indexOf(': ');
|
|
46
|
+
if (separator === -1) {
|
|
47
|
+
return {
|
|
48
|
+
ok: false,
|
|
49
|
+
reason:
|
|
50
|
+
'the subject has no `: ` type separator — the form is `<type>[(scope)][!]: <description>`',
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const match = CONVENTIONAL_SUBJECT.exec(subject);
|
|
54
|
+
if (!match) {
|
|
55
|
+
return {
|
|
56
|
+
ok: false,
|
|
57
|
+
reason:
|
|
58
|
+
'the part before `: ` must be a type word with an optional `(scope)` and `!` — the form is `<type>[(scope)][!]: <description>`',
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
const type = match[1] ?? '';
|
|
62
|
+
const description = match[5] ?? '';
|
|
63
|
+
if (description.trim().length === 0) {
|
|
64
|
+
return { ok: false, reason: 'the description after `: ` is empty' };
|
|
65
|
+
}
|
|
66
|
+
const warnings: string[] = [];
|
|
67
|
+
if (!GIT_COMMIT_CONVENTIONAL_TYPES.includes(type)) {
|
|
68
|
+
warnings.push(
|
|
69
|
+
`type "${type}" is not one of the conventional types (${GIT_COMMIT_CONVENTIONAL_TYPES.join(', ')})`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
if (subject.length > GIT_COMMIT_SUBJECT_LIMIT) {
|
|
73
|
+
warnings.push(
|
|
74
|
+
`the subject is ${subject.length} characters; ${GIT_COMMIT_SUBJECT_LIMIT} is the conventional limit`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
if (subject.endsWith('.')) warnings.push('the subject ends with a period');
|
|
78
|
+
return { ok: true, warnings };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The inline subject: an optional leading `-m`, then one matching pair of outer quotes stripped. */
|
|
82
|
+
export function normalizeCommitSubject(args: string): string {
|
|
83
|
+
let subject = args.trim();
|
|
84
|
+
if (subject === '-m') return '';
|
|
85
|
+
if (subject.startsWith('-m ')) subject = subject.slice('-m '.length).trim();
|
|
86
|
+
if (
|
|
87
|
+
subject.length >= 2 &&
|
|
88
|
+
(subject.startsWith('"') || subject.startsWith("'")) &&
|
|
89
|
+
subject.endsWith(subject[0] ?? '')
|
|
90
|
+
) {
|
|
91
|
+
subject = subject.slice(1, -1).trim();
|
|
92
|
+
}
|
|
93
|
+
return subject;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function cancelled(message: string): ICommandResult {
|
|
97
|
+
return { success: false, message, data: { git: 'commit', committed: false } };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function askSubject(ui: IUserInteraction): Promise<string | undefined> {
|
|
101
|
+
const response = await ui.ask(
|
|
102
|
+
textAction('git-commit-subject', 'Commit message', {
|
|
103
|
+
description: 'Conventional Commits form: <type>[(scope)][!]: <description>',
|
|
104
|
+
placeholder: 'feat: describe the change',
|
|
105
|
+
}),
|
|
106
|
+
);
|
|
107
|
+
if (response.type !== 'answer') return undefined;
|
|
108
|
+
const text = response.text ?? response.values[0] ?? '';
|
|
109
|
+
return normalizeCommitSubject(text);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
type TStagedCheck = { staged: true } | { staged: false; result: ICommandResult };
|
|
113
|
+
|
|
114
|
+
/** Step 1 — the staged set decides everything; exit 1 means something is staged. */
|
|
115
|
+
async function checkStaged(port: IGitProcessPort, cwd: string): Promise<TStagedCheck> {
|
|
116
|
+
const staged = await port.run(['diff', '--cached', '--quiet'], { cwd });
|
|
117
|
+
if (staged.kind === 'failed' || (staged.exitCode !== 0 && staged.exitCode !== 1)) {
|
|
118
|
+
return {
|
|
119
|
+
staged: false,
|
|
120
|
+
result: {
|
|
121
|
+
success: false,
|
|
122
|
+
message: gitFailureMessage('diff --cached', staged) ?? 'git failed',
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
if (staged.exitCode === 1) return { staged: true };
|
|
127
|
+
const status = await port.run(GIT_STATUS_ARGS, { cwd });
|
|
128
|
+
const failure = gitFailureMessage('status', status);
|
|
129
|
+
if (failure !== undefined || status.kind !== 'exited') {
|
|
130
|
+
return { staged: false, result: { success: false, message: failure ?? 'git status failed' } };
|
|
131
|
+
}
|
|
132
|
+
const summary = parseStatusPorcelainV2(status.stdout);
|
|
133
|
+
return {
|
|
134
|
+
staged: false,
|
|
135
|
+
result: {
|
|
136
|
+
success: false,
|
|
137
|
+
message: `Nothing is staged (${summary.unstaged.length} unstaged, ${summary.untracked.length} untracked). Stage files with \`git add\` or \`/shell git add ...\` and run \`/git commit\` again.`,
|
|
138
|
+
data: {
|
|
139
|
+
git: 'commit',
|
|
140
|
+
committed: false,
|
|
141
|
+
unstaged: summary.unstaged.length,
|
|
142
|
+
untracked: summary.untracked.length,
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
type TSubjectResolution = { subject: string } | { result: ICommandResult };
|
|
149
|
+
|
|
150
|
+
/** Step 2 — the subject: inline, or asked for as free text; then the MUST rules. */
|
|
151
|
+
async function resolveSubject(
|
|
152
|
+
args: string,
|
|
153
|
+
ui: IUserInteraction | undefined,
|
|
154
|
+
): Promise<TSubjectResolution> {
|
|
155
|
+
let subject = normalizeCommitSubject(args);
|
|
156
|
+
if (subject.length === 0) {
|
|
157
|
+
if (ui === undefined) {
|
|
158
|
+
return {
|
|
159
|
+
result: cancelled(
|
|
160
|
+
'Commit cancelled: a commit message was needed and none could be asked for (no interactive session). Run `/git commit <subject>`.',
|
|
161
|
+
),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
const asked = await askSubject(ui);
|
|
165
|
+
if (asked === undefined || asked.length === 0)
|
|
166
|
+
return { result: cancelled('Commit cancelled.') };
|
|
167
|
+
subject = asked;
|
|
168
|
+
}
|
|
169
|
+
return { subject };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Step 3 — what is confirmed is what is committed: the listing comes from git, not from a guess. */
|
|
173
|
+
async function confirmCommit(
|
|
174
|
+
port: IGitProcessPort,
|
|
175
|
+
cwd: string,
|
|
176
|
+
subject: string,
|
|
177
|
+
warnings: readonly string[],
|
|
178
|
+
ui: IUserInteraction | undefined,
|
|
179
|
+
): Promise<
|
|
180
|
+
{ confirmed: true; files: readonly string[] } | { confirmed: false; result: ICommandResult }
|
|
181
|
+
> {
|
|
182
|
+
if (ui === undefined) {
|
|
183
|
+
return {
|
|
184
|
+
confirmed: false,
|
|
185
|
+
result: cancelled(
|
|
186
|
+
'Commit cancelled: a confirmation was needed and none could be asked for (no interactive session).',
|
|
187
|
+
),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
const listing = await port.run(['diff', '--cached', '--name-status'], { cwd });
|
|
191
|
+
const listingFailure = gitFailureMessage('diff --cached --name-status', listing);
|
|
192
|
+
if (listingFailure !== undefined || listing.kind !== 'exited') {
|
|
193
|
+
return {
|
|
194
|
+
confirmed: false,
|
|
195
|
+
result: { success: false, message: listingFailure ?? 'git failed' },
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
const files = listing.stdout
|
|
199
|
+
.split('\n')
|
|
200
|
+
.filter((line) => line.length > 0)
|
|
201
|
+
.map((line) => line.replace(/\t/g, ' '));
|
|
202
|
+
const description = [
|
|
203
|
+
`Message: ${subject}`,
|
|
204
|
+
...warnings.map((warning) => `Warning: ${warning}`),
|
|
205
|
+
`Staged (${files.length}):`,
|
|
206
|
+
...files.map((file) => ` ${file}`),
|
|
207
|
+
].join('\n');
|
|
208
|
+
const response = await ui.ask(
|
|
209
|
+
confirmAction('git-commit', `Commit ${files.length} staged file(s)?`, {
|
|
210
|
+
description,
|
|
211
|
+
defaultYes: false,
|
|
212
|
+
}),
|
|
213
|
+
);
|
|
214
|
+
if (!isConfirmed(response)) return { confirmed: false, result: cancelled('Commit cancelled.') };
|
|
215
|
+
return { confirmed: true, files };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export async function executeGitCommit(
|
|
219
|
+
port: IGitProcessPort,
|
|
220
|
+
cwd: string,
|
|
221
|
+
args: string,
|
|
222
|
+
ui: IUserInteraction | undefined,
|
|
223
|
+
): Promise<ICommandResult> {
|
|
224
|
+
const staged = await checkStaged(port, cwd);
|
|
225
|
+
if (!staged.staged) return staged.result;
|
|
226
|
+
|
|
227
|
+
const resolved = await resolveSubject(args, ui);
|
|
228
|
+
if (!('subject' in resolved)) return resolved.result;
|
|
229
|
+
const { subject } = resolved;
|
|
230
|
+
const check = validateConventionalSubject(subject);
|
|
231
|
+
if (!check.ok) {
|
|
232
|
+
return {
|
|
233
|
+
success: false,
|
|
234
|
+
message: `Commit refused: ${check.reason}.`,
|
|
235
|
+
data: { git: 'commit', committed: false, subject },
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const confirmation = await confirmCommit(port, cwd, subject, check.warnings, ui);
|
|
240
|
+
if (!confirmation.confirmed) return confirmation.result;
|
|
241
|
+
|
|
242
|
+
// Step 4 — exactly `commit -m <subject>`: never `-a`, never `add`.
|
|
243
|
+
const commit = await port.run(['commit', '-m', subject], { cwd });
|
|
244
|
+
const failure = gitFailureMessage('commit', commit);
|
|
245
|
+
if (failure !== undefined || commit.kind !== 'exited') {
|
|
246
|
+
return { success: false, message: failure ?? 'git commit failed' };
|
|
247
|
+
}
|
|
248
|
+
const firstLine = commit.stdout.split('\n').find((line) => line.trim().length > 0) ?? '';
|
|
249
|
+
return {
|
|
250
|
+
success: true,
|
|
251
|
+
message: `Committed: ${firstLine.length > 0 ? firstLine : subject}`,
|
|
252
|
+
data: { git: 'commit', committed: true, subject, files: confirmation.files },
|
|
253
|
+
};
|
|
254
|
+
}
|
package/src/git/index.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// The git process port and the status/diff readers moved to `@robota-sdk/agent-framework` (#3282
|
|
2
|
+
// §4c) so the Project panel's session-level reads and this command run through the exact same
|
|
3
|
+
// functions; re-exported here so an existing `from '@robota-sdk/agent-command'` import keeps working.
|
|
4
|
+
export {
|
|
5
|
+
createGitProcess,
|
|
6
|
+
gitEnvironment,
|
|
7
|
+
type ICreateGitProcessOptions,
|
|
8
|
+
type IGitProcessPort,
|
|
9
|
+
type IGitProcessRunOptions,
|
|
10
|
+
type TGitProcessFailureReason,
|
|
11
|
+
type TGitProcessOutcome,
|
|
12
|
+
} from '@robota-sdk/agent-framework';
|
|
13
|
+
export {
|
|
14
|
+
createGitCommandEntry,
|
|
15
|
+
createGitCommandModule,
|
|
16
|
+
executeGitCommand,
|
|
17
|
+
GitCommandSource,
|
|
18
|
+
type IGitCommandModuleOptions,
|
|
19
|
+
type TGitCommandContext,
|
|
20
|
+
} from './git-command-module.js';
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { executeGoalCommand } from '../goal-command.js';
|
|
4
|
+
|
|
5
|
+
import type { IGoalState } from '@robota-sdk/agent-interface-session';
|
|
6
|
+
import {
|
|
7
|
+
createTestCommandHost,
|
|
8
|
+
type ICreateTestCommandHostOptions,
|
|
9
|
+
} from '@robota-sdk/agent-framework/testing';
|
|
10
|
+
|
|
11
|
+
function goal(overrides: Partial<IGoalState> = {}): IGoalState {
|
|
12
|
+
return {
|
|
13
|
+
id: 'g1',
|
|
14
|
+
objective: 'write a file',
|
|
15
|
+
status: 'active',
|
|
16
|
+
iterations: 1,
|
|
17
|
+
maxIterations: 25,
|
|
18
|
+
startedAt: '2026-06-27T00:00:00.000Z',
|
|
19
|
+
progress: [{ iteration: 1, signal: 'continue', reason: 'started' }],
|
|
20
|
+
...overrides,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function host(overrides: ICreateTestCommandHostOptions['overrides'] = {}) {
|
|
25
|
+
// ARCH-029: the partial is now an OVERRIDE over a conformant host, not a cast that turns the
|
|
26
|
+
// check off. A fixture that names three members no longer claims to satisfy 46.
|
|
27
|
+
return createTestCommandHost({ overrides });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe('executeGoalCommand', () => {
|
|
31
|
+
it('assigns a goal from the objective text', async () => {
|
|
32
|
+
const setGoal = vi.fn().mockResolvedValue(goal());
|
|
33
|
+
const result = await executeGoalCommand(host({ setGoal }), 'write a file');
|
|
34
|
+
expect(setGoal).toHaveBeenCalledWith('write a file');
|
|
35
|
+
expect(result.success).toBe(true);
|
|
36
|
+
expect(result.message).toContain('pursuing autonomously');
|
|
37
|
+
expect(result.data).toMatchObject({ goalId: 'g1' });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('shows status when given "status"', async () => {
|
|
41
|
+
const result = await executeGoalCommand(host({ getGoalState: () => goal() }), 'status');
|
|
42
|
+
expect(result.success).toBe(true);
|
|
43
|
+
expect(result.message).toContain('write a file');
|
|
44
|
+
expect(result.message).toContain('Iterations: 1 / 25');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('reports no goal when status is requested with none set', async () => {
|
|
48
|
+
const result = await executeGoalCommand(host({ getGoalState: () => null }), 'status');
|
|
49
|
+
expect(result.message).toBe('No goal is set.');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('cancels an active goal', async () => {
|
|
53
|
+
const cancelGoal = vi
|
|
54
|
+
.fn()
|
|
55
|
+
.mockReturnValue(goal({ status: 'stopped', stopReason: 'cancelled' }));
|
|
56
|
+
const result = await executeGoalCommand(host({ cancelGoal }), 'cancel');
|
|
57
|
+
expect(cancelGoal).toHaveBeenCalled();
|
|
58
|
+
expect(result.success).toBe(true);
|
|
59
|
+
expect(result.message).toContain('Goal cancelled');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('reports failure when cancelling with no active goal', async () => {
|
|
63
|
+
const result = await executeGoalCommand(host({ cancelGoal: () => null }), 'stop');
|
|
64
|
+
expect(result.success).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('shows usage on an empty argument', async () => {
|
|
68
|
+
const result = await executeGoalCommand(host({}), '');
|
|
69
|
+
expect(result.message).toContain('Usage');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('surfaces an invalid-objective error as a failed result (not a crash)', async () => {
|
|
73
|
+
const setGoal = vi
|
|
74
|
+
.fn()
|
|
75
|
+
.mockRejectedValue(new Error('A goal objective must be a non-empty string.'));
|
|
76
|
+
const result = await executeGoalCommand(host({ setGoal }), 'x');
|
|
77
|
+
expect(result.success).toBe(false);
|
|
78
|
+
expect(result.message).toContain('non-empty');
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GOAL-001: `/goal` command module registration.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { executeGoalCommand, GOAL_COMMAND_DESCRIPTION } from './goal-command.js';
|
|
6
|
+
|
|
7
|
+
import type { ICommandModule, ISystemCommand } from '@robota-sdk/agent-framework';
|
|
8
|
+
import type { ICommand, ICommandSource } from '@robota-sdk/agent-interface-command';
|
|
9
|
+
|
|
10
|
+
export function createGoalCommandEntry(): ICommand {
|
|
11
|
+
return {
|
|
12
|
+
name: 'goal',
|
|
13
|
+
displayName: 'Autonomous Goal',
|
|
14
|
+
description: GOAL_COMMAND_DESCRIPTION,
|
|
15
|
+
source: 'goal',
|
|
16
|
+
// User-only: assigning an autonomous goal widens what the agent pursues without asking; the user sets it.
|
|
17
|
+
modelInvocable: false,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function createGoalSystemCommand(): ISystemCommand {
|
|
22
|
+
const entry = createGoalCommandEntry();
|
|
23
|
+
return {
|
|
24
|
+
name: entry.name,
|
|
25
|
+
displayName: entry.displayName,
|
|
26
|
+
description: entry.description,
|
|
27
|
+
requiresPermission: false,
|
|
28
|
+
userInvocable: true,
|
|
29
|
+
modelInvocable: false,
|
|
30
|
+
lifecycle: 'inline',
|
|
31
|
+
execute: executeGoalCommand,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class GoalCommandSource implements ICommandSource {
|
|
36
|
+
readonly name = 'goal';
|
|
37
|
+
|
|
38
|
+
getCommands(): ICommand[] {
|
|
39
|
+
return [createGoalCommandEntry()];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function createGoalCommandModule(): ICommandModule {
|
|
44
|
+
return {
|
|
45
|
+
name: 'agent-command-goal',
|
|
46
|
+
commandSources: [new GoalCommandSource()],
|
|
47
|
+
systemCommands: [createGoalSystemCommand()],
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GOAL-001: the `/goal` slash command. Assigns an autonomous objective the agent pursues across
|
|
3
|
+
* turns until satisfied or a bound fires. Delegates to the framework-owned goal controller via the
|
|
4
|
+
* command host context; all loop logic lives in agent-framework.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { isGoalCancelVerb } from '@robota-sdk/agent-framework';
|
|
8
|
+
|
|
9
|
+
import type { ICommandHostGoal } from '@robota-sdk/agent-framework';
|
|
10
|
+
import type { ICommandResult } from '@robota-sdk/agent-interface-command';
|
|
11
|
+
import type { IGoalState } from '@robota-sdk/agent-interface-session';
|
|
12
|
+
|
|
13
|
+
export const GOAL_COMMAND_DESCRIPTION =
|
|
14
|
+
'Assign an autonomous goal the agent pursues across turns until satisfied.';
|
|
15
|
+
|
|
16
|
+
const GOAL_COMMAND_USAGE =
|
|
17
|
+
'Usage:\n /goal <objective> assign a goal and pursue it autonomously\n' +
|
|
18
|
+
' /goal status show the current goal and progress\n' +
|
|
19
|
+
' /goal cancel stop the current goal';
|
|
20
|
+
|
|
21
|
+
function formatGoalState(goal: IGoalState): string {
|
|
22
|
+
const lines = [
|
|
23
|
+
`Goal: ${goal.objective}`,
|
|
24
|
+
`Status: ${goal.status}${goal.stopReason ? ` (${goal.stopReason})` : ''}`,
|
|
25
|
+
`Iterations: ${goal.iterations} / ${goal.maxIterations}`,
|
|
26
|
+
];
|
|
27
|
+
const last = goal.progress[goal.progress.length - 1];
|
|
28
|
+
if (last?.reason) lines.push(`Latest: ${last.reason}`);
|
|
29
|
+
return lines.join('\n');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function executeGoalCommand(
|
|
33
|
+
context: ICommandHostGoal,
|
|
34
|
+
args: string,
|
|
35
|
+
): Promise<ICommandResult> {
|
|
36
|
+
const trimmed = args.trim();
|
|
37
|
+
const verb = trimmed.split(/\s+/)[0]?.toLowerCase() ?? '';
|
|
38
|
+
|
|
39
|
+
if (trimmed.length === 0 || verb === 'help') {
|
|
40
|
+
return { message: GOAL_COMMAND_USAGE, success: true };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (verb === 'status') {
|
|
44
|
+
const goal = context.getGoalState();
|
|
45
|
+
return goal
|
|
46
|
+
? { message: formatGoalState(goal), success: true, data: { status: goal.status } }
|
|
47
|
+
: { message: 'No goal is set.', success: true };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (isGoalCancelVerb(trimmed)) {
|
|
51
|
+
const stopped = context.cancelGoal();
|
|
52
|
+
return stopped
|
|
53
|
+
? { message: `Goal cancelled: ${stopped.objective}`, success: true }
|
|
54
|
+
: { message: 'No active goal to cancel.', success: false };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let goal: IGoalState;
|
|
58
|
+
try {
|
|
59
|
+
goal = await context.setGoal(trimmed);
|
|
60
|
+
} catch (error) {
|
|
61
|
+
// allow-fallback: surface an invalid-objective error to the user as a failed command result, not a crash
|
|
62
|
+
return { message: error instanceof Error ? error.message : String(error), success: false };
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
message: `Goal set — pursuing autonomously (up to ${goal.maxIterations} iterations):\n${goal.objective}`,
|
|
66
|
+
success: true,
|
|
67
|
+
data: { goalId: goal.id },
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { createHandoffCommandModule } from '../handoff-command-module.js';
|
|
4
|
+
|
|
5
|
+
describe('/handoff command module', () => {
|
|
6
|
+
it('is user-only: never model-invocable, and palette metadata matches the executable', () => {
|
|
7
|
+
const module = createHandoffCommandModule();
|
|
8
|
+
const palette = module.commandSources?.[0]?.getCommands()[0];
|
|
9
|
+
const executable = module.systemCommands?.[0];
|
|
10
|
+
expect(executable?.modelInvocable).toBe(false);
|
|
11
|
+
expect(palette?.modelInvocable).toBe(false);
|
|
12
|
+
expect(executable?.userInvocable).toBe(true);
|
|
13
|
+
expect(palette?.userInvocable).toBe(true);
|
|
14
|
+
expect(executable?.description).toBe(palette?.description);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('tells the model what it does and which command to suggest, since it cannot run it', () => {
|
|
18
|
+
const description =
|
|
19
|
+
createHandoffCommandModule().commandSources?.[0]?.getCommands()[0]?.description;
|
|
20
|
+
expect(description).toContain('/handoff <session-or-device-id>');
|
|
21
|
+
expect(description).toMatch(/another linked device/);
|
|
22
|
+
expect(description).toMatch(/user-only/i);
|
|
23
|
+
expect(description).toContain('saved, not started');
|
|
24
|
+
});
|
|
25
|
+
});
|