@kynetic-ai/spec 0.9.1 → 0.11.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.
- package/README.md +2 -1
- package/dist/acp/client.d.ts +6 -1
- package/dist/acp/client.d.ts.map +1 -1
- package/dist/acp/client.js +7 -2
- package/dist/acp/client.js.map +1 -1
- package/dist/acp/framing.d.ts +12 -1
- package/dist/acp/framing.d.ts.map +1 -1
- package/dist/acp/framing.js +27 -4
- package/dist/acp/framing.js.map +1 -1
- package/dist/agent-runtime/dispatch.d.ts +292 -0
- package/dist/agent-runtime/dispatch.d.ts.map +1 -0
- package/dist/agent-runtime/dispatch.js +860 -0
- package/dist/agent-runtime/dispatch.js.map +1 -0
- package/dist/agent-runtime/index.d.ts +11 -0
- package/dist/agent-runtime/index.d.ts.map +1 -0
- package/dist/agent-runtime/index.js +11 -0
- package/dist/agent-runtime/index.js.map +1 -0
- package/dist/agent-runtime/invocation.d.ts +86 -0
- package/dist/agent-runtime/invocation.d.ts.map +1 -0
- package/dist/agent-runtime/invocation.js +442 -0
- package/dist/agent-runtime/invocation.js.map +1 -0
- package/dist/agent-runtime/prompts.d.ts +50 -0
- package/dist/agent-runtime/prompts.d.ts.map +1 -0
- package/dist/agent-runtime/prompts.js +108 -0
- package/dist/agent-runtime/prompts.js.map +1 -0
- package/dist/agents/spawner.d.ts.map +1 -1
- package/dist/agents/spawner.js +60 -4
- package/dist/agents/spawner.js.map +1 -1
- package/dist/cli/batch-exec.d.ts.map +1 -1
- package/dist/cli/batch-exec.js +140 -62
- package/dist/cli/batch-exec.js.map +1 -1
- package/dist/cli/batch-write-buffer.d.ts +141 -0
- package/dist/cli/batch-write-buffer.d.ts.map +1 -0
- package/dist/cli/batch-write-buffer.js +400 -0
- package/dist/cli/batch-write-buffer.js.map +1 -0
- package/dist/cli/commands/agent.d.ts +20 -0
- package/dist/cli/commands/agent.d.ts.map +1 -0
- package/dist/cli/commands/agent.js +831 -0
- package/dist/cli/commands/agent.js.map +1 -0
- package/dist/cli/commands/inbox.d.ts.map +1 -1
- package/dist/cli/commands/inbox.js +46 -22
- package/dist/cli/commands/inbox.js.map +1 -1
- package/dist/cli/commands/index.d.ts +1 -0
- package/dist/cli/commands/index.d.ts.map +1 -1
- package/dist/cli/commands/index.js +1 -0
- package/dist/cli/commands/index.js.map +1 -1
- package/dist/cli/commands/item.d.ts.map +1 -1
- package/dist/cli/commands/item.js +22 -16
- package/dist/cli/commands/item.js.map +1 -1
- package/dist/cli/commands/log.js +1 -1
- package/dist/cli/commands/log.js.map +1 -1
- package/dist/cli/commands/meta.d.ts.map +1 -1
- package/dist/cli/commands/meta.js +168 -6
- package/dist/cli/commands/meta.js.map +1 -1
- package/dist/cli/commands/module.d.ts.map +1 -1
- package/dist/cli/commands/module.js +2 -1
- package/dist/cli/commands/module.js.map +1 -1
- package/dist/cli/commands/plan-import.js +19 -3
- package/dist/cli/commands/plan-import.js.map +1 -1
- package/dist/cli/commands/plan.d.ts.map +1 -1
- package/dist/cli/commands/plan.js +87 -43
- package/dist/cli/commands/plan.js.map +1 -1
- package/dist/cli/commands/ralph.d.ts +5 -56
- package/dist/cli/commands/ralph.d.ts.map +1 -1
- package/dist/cli/commands/ralph.js +52 -1502
- package/dist/cli/commands/ralph.js.map +1 -1
- package/dist/cli/commands/search.d.ts.map +1 -1
- package/dist/cli/commands/search.js +22 -13
- package/dist/cli/commands/search.js.map +1 -1
- package/dist/cli/commands/serve.d.ts.map +1 -1
- package/dist/cli/commands/serve.js +70 -11
- package/dist/cli/commands/serve.js.map +1 -1
- package/dist/cli/commands/session/checkpoint.d.ts.map +1 -1
- package/dist/cli/commands/session/checkpoint.js +7 -2
- package/dist/cli/commands/session/checkpoint.js.map +1 -1
- package/dist/cli/commands/session/commands.d.ts.map +1 -1
- package/dist/cli/commands/session/commands.js +15 -0
- package/dist/cli/commands/session/commands.js.map +1 -1
- package/dist/cli/commands/session/context.d.ts.map +1 -1
- package/dist/cli/commands/session/context.js +10 -5
- package/dist/cli/commands/session/context.js.map +1 -1
- package/dist/cli/commands/session/log.d.ts +1 -0
- package/dist/cli/commands/session/log.d.ts.map +1 -1
- package/dist/cli/commands/session/log.js +124 -8
- package/dist/cli/commands/session/log.js.map +1 -1
- package/dist/cli/commands/session/stale-close.d.ts +17 -0
- package/dist/cli/commands/session/stale-close.d.ts.map +1 -0
- package/dist/cli/commands/session/stale-close.js +378 -0
- package/dist/cli/commands/session/stale-close.js.map +1 -0
- package/dist/cli/commands/setup.d.ts.map +1 -1
- package/dist/cli/commands/setup.js +95 -0
- package/dist/cli/commands/setup.js.map +1 -1
- package/dist/cli/commands/skill-crud.d.ts.map +1 -1
- package/dist/cli/commands/skill-crud.js +4 -3
- package/dist/cli/commands/skill-crud.js.map +1 -1
- package/dist/cli/commands/skill-diff.d.ts.map +1 -1
- package/dist/cli/commands/skill-diff.js +15 -0
- package/dist/cli/commands/skill-diff.js.map +1 -1
- package/dist/cli/commands/skill-install.d.ts.map +1 -1
- package/dist/cli/commands/skill-install.js +50 -18
- package/dist/cli/commands/skill-install.js.map +1 -1
- package/dist/cli/commands/task.d.ts.map +1 -1
- package/dist/cli/commands/task.js +536 -310
- package/dist/cli/commands/task.js.map +1 -1
- package/dist/cli/commands/tasks.js +1 -1
- package/dist/cli/commands/tasks.js.map +1 -1
- package/dist/cli/commands/triage.d.ts.map +1 -1
- package/dist/cli/commands/triage.js +37 -13
- package/dist/cli/commands/triage.js.map +1 -1
- package/dist/cli/commands/validate.d.ts.map +1 -1
- package/dist/cli/commands/validate.js +65 -25
- package/dist/cli/commands/validate.js.map +1 -1
- package/dist/cli/help/content.d.ts.map +1 -1
- package/dist/cli/help/content.js +5 -0
- package/dist/cli/help/content.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +2 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/output.d.ts.map +1 -1
- package/dist/cli/output.js +5 -1
- package/dist/cli/output.js.map +1 -1
- package/dist/daemon/project-context.ts +22 -0
- package/dist/daemon/routes/agent-dispatch.ts +279 -0
- package/dist/daemon/routes/items.ts +22 -0
- package/dist/daemon/routes/meta.ts +141 -1
- package/dist/daemon/routes/plans.ts +147 -0
- package/dist/daemon/routes/sessions.ts +180 -0
- package/dist/daemon/routes/tasks.ts +198 -0
- package/dist/daemon/routes/validation.ts +1 -1
- package/dist/daemon/server.ts +77 -21
- package/dist/daemon/websocket/handler.ts +67 -6
- package/dist/daemon/websocket/lifecycle.ts +19 -0
- package/dist/daemon/websocket/pubsub.ts +74 -3
- package/dist/export/html.d.ts.map +1 -1
- package/dist/export/html.js +5 -2
- package/dist/export/html.js.map +1 -1
- package/dist/export/triage.d.ts +1 -1
- package/dist/export/triage.d.ts.map +1 -1
- package/dist/export/triage.js +5 -3
- package/dist/export/triage.js.map +1 -1
- package/dist/parser/alignment.d.ts.map +1 -1
- package/dist/parser/alignment.js +10 -5
- package/dist/parser/alignment.js.map +1 -1
- package/dist/parser/assess.js +1 -1
- package/dist/parser/assess.js.map +1 -1
- package/dist/parser/config.d.ts +6 -6
- package/dist/parser/meta.d.ts.map +1 -1
- package/dist/parser/meta.js +9 -8
- package/dist/parser/meta.js.map +1 -1
- package/dist/parser/plan-document.d.ts +12 -12
- package/dist/parser/plans.d.ts +7 -0
- package/dist/parser/plans.d.ts.map +1 -1
- package/dist/parser/plans.js +100 -15
- package/dist/parser/plans.js.map +1 -1
- package/dist/parser/refs.d.ts +5 -0
- package/dist/parser/refs.d.ts.map +1 -1
- package/dist/parser/refs.js +17 -12
- package/dist/parser/refs.js.map +1 -1
- package/dist/parser/shadow.d.ts +1 -1
- package/dist/parser/shadow.d.ts.map +1 -1
- package/dist/parser/shadow.js +71 -4
- package/dist/parser/shadow.js.map +1 -1
- package/dist/parser/skill-render.d.ts.map +1 -1
- package/dist/parser/skill-render.js +6 -3
- package/dist/parser/skill-render.js.map +1 -1
- package/dist/parser/validate.d.ts.map +1 -1
- package/dist/parser/validate.js +35 -76
- package/dist/parser/validate.js.map +1 -1
- package/dist/parser/yaml.d.ts +24 -5
- package/dist/parser/yaml.d.ts.map +1 -1
- package/dist/parser/yaml.js +224 -64
- package/dist/parser/yaml.js.map +1 -1
- package/dist/schema/meta.d.ts +457 -119
- package/dist/schema/meta.d.ts.map +1 -1
- package/dist/schema/meta.js +56 -0
- package/dist/schema/meta.js.map +1 -1
- package/dist/schema/plan.d.ts +22 -22
- package/dist/schema/spec.d.ts +39 -39
- package/dist/schema/task.d.ts +43 -32
- package/dist/schema/task.d.ts.map +1 -1
- package/dist/schema/task.js +5 -0
- package/dist/schema/task.js.map +1 -1
- package/dist/sessions/store.d.ts +126 -0
- package/dist/sessions/store.d.ts.map +1 -1
- package/dist/sessions/store.js +440 -22
- package/dist/sessions/store.js.map +1 -1
- package/dist/sessions/types.d.ts +75 -17
- package/dist/sessions/types.d.ts.map +1 -1
- package/dist/sessions/types.js +51 -1
- package/dist/sessions/types.js.map +1 -1
- package/dist/triage/actions.d.ts +1 -0
- package/dist/triage/actions.d.ts.map +1 -1
- package/dist/triage/actions.js +34 -7
- package/dist/triage/actions.js.map +1 -1
- package/dist/utils/commit.js +1 -1
- package/dist/utils/commit.js.map +1 -1
- package/dist/web-ui/_app/env.js +1 -0
- package/dist/web-ui/_app/immutable/assets/0.BJaYkGW2.css +1 -0
- package/dist/web-ui/_app/immutable/assets/9.SzGLxi4x.css +1 -0
- package/dist/web-ui/_app/immutable/assets/select-trigger.CV-KWLNP.css +1 -0
- package/dist/web-ui/_app/immutable/chunks/-lc0BifF.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/62JVKtnb.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/8RBjHMN1.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/B5LJFxqa.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/B5wTVqxm.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/B6VSmczZ.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/B8a0xDxR.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/BEOQc37C.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/BHtYorjv.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/BJ0JX3ea.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/BMuCqDX8.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/BP352uRn.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/BUZujXJ2.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/BVA9Exy-.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/BWET-efb.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/BXkNecpt.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/BYzrIfX8.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/BkOJ8DkV.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/BpuwufMc.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/BwMO4RrG.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/BysXJlZb.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/C076q4JN.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/C33JaVbg.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/CGtqifKp.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/CHDZZ7OG.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/CPPfDSei.js +5 -0
- package/dist/web-ui/_app/immutable/chunks/CUir3f4J.js +60 -0
- package/dist/web-ui/_app/immutable/chunks/Cncwi6fQ.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/CrCIbn0C.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/CwELQvbx.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/D3vxvonu.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/D6TVmR9T.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/D7LTux4W.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/D82RulSH.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/D9QNBZM2.js +2 -0
- package/dist/web-ui/_app/immutable/chunks/DAMmvwn4.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/DAh4Wfku.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/DAx07bEQ.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/DBYE9jOd.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/DOno4cA2.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/DQA8NZIH.js +2 -0
- package/dist/web-ui/_app/immutable/chunks/DRfPm2bo.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/DhQhksaB.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/DjG7s6hm.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/DjcCz-PU.js +2 -0
- package/dist/web-ui/_app/immutable/chunks/DkltRNvh.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/DlaTnPKL.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/DvA-KON-.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/DxCk-KHc.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/DzO4hlg9.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/Eo4gF7ih.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/ExCq5swK.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/T3zZGv51.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/XZumBYeP.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/_ySfNjkF.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/iEtR5cV6.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/k_Qegko0.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/pE6cYWlS.js +1 -0
- package/dist/web-ui/_app/immutable/entry/app.Cgu6uKeS.js +2 -0
- package/dist/web-ui/_app/immutable/entry/start.9XifnLoB.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/0.DISwcKSK.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/1.Cx2Ufqp1.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/10.C3z8ijXL.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/11.DZdIjZmM.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/12.FsIGfAOa.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/13.DZoFwagf.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/14.DaIzDKbQ.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/15.BYyt4XWF.js +2 -0
- package/dist/web-ui/_app/immutable/nodes/16.CQkSqpOe.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/2.Bkf_j2UJ.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/3.kaMCurJG.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/4.BSsFPTHG.js +2 -0
- package/dist/web-ui/_app/immutable/nodes/5.CpPlcCEZ.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/6.BN4FqQmY.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/7.9kBYIZik.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/8.BuijtZ6B.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/9.C-Weba8R.js +1 -0
- package/dist/web-ui/_app/version.json +1 -0
- package/dist/web-ui/index.html +39 -0
- package/dist/web-ui/robots.txt +3 -0
- package/package.json +4 -2
- package/plugin/.claude-plugin/marketplace.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/plugins/kspec/skills/task-work/SKILL.md +25 -2
- package/templates/agents-sections/06-ralph-loop.md +64 -11
- package/templates/skills/task-work/SKILL.md +25 -2
- package/dist/ralph/cli-renderer.d.ts +0 -27
- package/dist/ralph/cli-renderer.d.ts.map +0 -1
- package/dist/ralph/cli-renderer.js +0 -250
- package/dist/ralph/cli-renderer.js.map +0 -1
- package/dist/ralph/events.d.ts +0 -65
- package/dist/ralph/events.d.ts.map +0 -1
- package/dist/ralph/events.js +0 -600
- package/dist/ralph/events.js.map +0 -1
- package/dist/ralph/index.d.ts +0 -11
- package/dist/ralph/index.d.ts.map +0 -1
- package/dist/ralph/index.js +0 -16
- package/dist/ralph/index.js.map +0 -1
- package/dist/ralph/loop-errors.d.ts +0 -83
- package/dist/ralph/loop-errors.d.ts.map +0 -1
- package/dist/ralph/loop-errors.js +0 -150
- package/dist/ralph/loop-errors.js.map +0 -1
- package/dist/ralph/subagent.d.ts +0 -127
- package/dist/ralph/subagent.d.ts.map +0 -1
- package/dist/ralph/subagent.js +0 -268
- package/dist/ralph/subagent.js.map +0 -1
- package/dist/ralph/wrap-up.d.ts +0 -127
- package/dist/ralph/wrap-up.d.ts.map +0 -1
- package/dist/ralph/wrap-up.js +0 -271
- package/dist/ralph/wrap-up.js.map +0 -1
|
@@ -0,0 +1,831 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* kspec agent commands — manage and run agents.
|
|
3
|
+
*
|
|
4
|
+
* Provides subcommands for listing agent definitions, running one-shot
|
|
5
|
+
* invocations, and managing the dispatch engine lifecycle via the daemon.
|
|
6
|
+
*
|
|
7
|
+
* AC: @cli-agent-commands ac-1 through ac-10
|
|
8
|
+
* AC: @trait-json-output ac-1 through ac-6
|
|
9
|
+
* AC: @trait-semantic-exit-codes ac-1 through ac-8
|
|
10
|
+
* AC: @trait-error-guidance ac-1 through ac-6
|
|
11
|
+
* AC: @trait-dry-run ac-1 through ac-6
|
|
12
|
+
* AC: @trait-filterable-list ac-1 through ac-8
|
|
13
|
+
*/
|
|
14
|
+
import chalk from "chalk";
|
|
15
|
+
import { initContext, loadMetaContext, } from "../../parser/index.js";
|
|
16
|
+
import { runInvocation } from "../../agent-runtime/invocation.js";
|
|
17
|
+
import { buildPromptWithSkills } from "../../agent-runtime/prompts.js";
|
|
18
|
+
import { resolveAdapter } from "../../agents/adapters.js";
|
|
19
|
+
import { EXIT_CODES } from "../exit-codes.js";
|
|
20
|
+
import { error, info, output, success, warn, isJsonMode } from "../output.js";
|
|
21
|
+
import { parseIntOption } from "../validators.js";
|
|
22
|
+
import { PidFileManager } from "../pid-utils.js";
|
|
23
|
+
import { errors } from "../../strings/errors.js";
|
|
24
|
+
import { requestEndLoop } from "../../sessions/index.js";
|
|
25
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
26
|
+
/**
|
|
27
|
+
* Get the daemon URL from the PID file manager.
|
|
28
|
+
* Returns null if the daemon is not running.
|
|
29
|
+
* AC: @cli-agent-commands ac-10
|
|
30
|
+
*/
|
|
31
|
+
function getDaemonUrl() {
|
|
32
|
+
const pidManager = new PidFileManager();
|
|
33
|
+
if (!pidManager.isDaemonRunning())
|
|
34
|
+
return null;
|
|
35
|
+
try {
|
|
36
|
+
const port = pidManager.readPort();
|
|
37
|
+
return { url: `http://localhost:${port}`, port };
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Format dispatch rules summary for display.
|
|
45
|
+
*/
|
|
46
|
+
function formatDispatchRules(agent) {
|
|
47
|
+
if (!agent.dispatch || agent.dispatch.length === 0) {
|
|
48
|
+
return "(none)";
|
|
49
|
+
}
|
|
50
|
+
return agent.dispatch.map((r) => {
|
|
51
|
+
const filterParts = [];
|
|
52
|
+
if (r.filter?.automation)
|
|
53
|
+
filterParts.push(`automation=${r.filter.automation}`);
|
|
54
|
+
if (r.filter?.tags?.length)
|
|
55
|
+
filterParts.push(`tags=${r.filter.tags.join(",")}`);
|
|
56
|
+
if (r.filter?.priority !== undefined)
|
|
57
|
+
filterParts.push(`priority=${r.filter.priority}`);
|
|
58
|
+
const filterStr = filterParts.length > 0 ? ` [${filterParts.join(", ")}]` : "";
|
|
59
|
+
return `${r.on}${filterStr}`;
|
|
60
|
+
}).join(", ");
|
|
61
|
+
}
|
|
62
|
+
// ─── Command Registration ─────────────────────────────────────────────────────
|
|
63
|
+
/**
|
|
64
|
+
* Register the kspec agent command family.
|
|
65
|
+
* AC: @cli-agent-commands ac-1 through ac-10
|
|
66
|
+
*/
|
|
67
|
+
export function registerAgentCommands(program) {
|
|
68
|
+
const agent = program
|
|
69
|
+
.command("agent")
|
|
70
|
+
.description("Manage and run agents");
|
|
71
|
+
// ─── kspec agent list ─────────────────────────────────────────────────────
|
|
72
|
+
// AC: @cli-agent-commands ac-1
|
|
73
|
+
// AC: @trait-filterable-list ac-1 through ac-8
|
|
74
|
+
agent
|
|
75
|
+
.command("list")
|
|
76
|
+
.description("List all agent definitions")
|
|
77
|
+
.option("--json", "Output as JSON")
|
|
78
|
+
.option("--status <status>", "Filter by automation status (eligible|ineligible)")
|
|
79
|
+
.option("--tag <tag>", "Filter by tag (repeatable)", (val, arr) => [...arr, val], [])
|
|
80
|
+
.option("--limit <n>", "Maximum number of results")
|
|
81
|
+
.option("--offset <n>", "Skip first N results")
|
|
82
|
+
.option("--count", "Output only the count")
|
|
83
|
+
.action(async (opts) => {
|
|
84
|
+
try {
|
|
85
|
+
const ctx = await initContext();
|
|
86
|
+
if (!ctx.manifestPath) {
|
|
87
|
+
error(errors.project.noKspecProject);
|
|
88
|
+
process.exit(EXIT_CODES.VALIDATION_FAILED);
|
|
89
|
+
}
|
|
90
|
+
const meta = await loadMetaContext(ctx);
|
|
91
|
+
let agents = meta.agents;
|
|
92
|
+
// AC: @trait-filterable-list ac-1 - automation status filter
|
|
93
|
+
if (opts.status) {
|
|
94
|
+
agents = agents.filter((a) => a.automation === opts.status);
|
|
95
|
+
}
|
|
96
|
+
// AC: @trait-filterable-list ac-2 - tag filter
|
|
97
|
+
const tags = opts.tag ?? [];
|
|
98
|
+
if (tags.length > 0) {
|
|
99
|
+
agents = agents.filter((a) => {
|
|
100
|
+
const agentTags = a.tags ?? [];
|
|
101
|
+
return tags.every((t) => agentTags.includes(t));
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
const total = agents.length;
|
|
105
|
+
// AC: @trait-filterable-list ac-8 - count mode
|
|
106
|
+
if (opts.count) {
|
|
107
|
+
output({ count: total }, () => {
|
|
108
|
+
console.log(total);
|
|
109
|
+
});
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
// AC: @trait-filterable-list ac-3, ac-4 - pagination
|
|
113
|
+
// AC: @trait-semantic-exit-codes ac-2 - invalid numeric input exits with validation error
|
|
114
|
+
let limit = total;
|
|
115
|
+
if (opts.limit !== undefined) {
|
|
116
|
+
const parsed = parseIntOption(opts.limit, {
|
|
117
|
+
min: 0,
|
|
118
|
+
max: Number.MAX_SAFE_INTEGER,
|
|
119
|
+
name: "Limit",
|
|
120
|
+
});
|
|
121
|
+
if (!parsed.ok) {
|
|
122
|
+
error(`Invalid --limit value: ${parsed.error}`, { suggestion: "Example: --limit 10" });
|
|
123
|
+
process.exit(EXIT_CODES.VALIDATION_FAILED);
|
|
124
|
+
}
|
|
125
|
+
limit = parsed.value;
|
|
126
|
+
}
|
|
127
|
+
let offset = 0;
|
|
128
|
+
if (opts.offset !== undefined) {
|
|
129
|
+
const parsed = parseIntOption(opts.offset, {
|
|
130
|
+
min: 0,
|
|
131
|
+
max: Number.MAX_SAFE_INTEGER,
|
|
132
|
+
name: "Offset",
|
|
133
|
+
});
|
|
134
|
+
if (!parsed.ok) {
|
|
135
|
+
error(`Invalid --offset value: ${parsed.error}`, { suggestion: "Example: --offset 5" });
|
|
136
|
+
process.exit(EXIT_CODES.VALIDATION_FAILED);
|
|
137
|
+
}
|
|
138
|
+
offset = parsed.value;
|
|
139
|
+
}
|
|
140
|
+
const paginated = agents.slice(offset, offset + limit);
|
|
141
|
+
// AC: @trait-semantic-exit-codes ac-5 - empty result set exits 0
|
|
142
|
+
// AC: @trait-filterable-list ac-6 - empty list with informative message
|
|
143
|
+
if (paginated.length === 0) {
|
|
144
|
+
output({ items: [], total, offset, limit }, () => {
|
|
145
|
+
if (opts.status || tags.length > 0) {
|
|
146
|
+
console.log("No agents match the specified filters.");
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
console.log("No agent definitions found.");
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
// AC: @trait-json-output ac-1 through ac-5
|
|
155
|
+
output({
|
|
156
|
+
items: paginated.map((a) => ({
|
|
157
|
+
id: a.id,
|
|
158
|
+
name: a.name,
|
|
159
|
+
adapter: a.adapter ?? "claude-agent-acp",
|
|
160
|
+
dispatch: a.dispatch ?? [],
|
|
161
|
+
concurrency: a.concurrency ?? { max_concurrent: 1 },
|
|
162
|
+
})),
|
|
163
|
+
total,
|
|
164
|
+
offset,
|
|
165
|
+
limit,
|
|
166
|
+
}, () => {
|
|
167
|
+
// AC: @trait-filterable-list ac-7 - summary with total and filter state
|
|
168
|
+
const filterDesc = [
|
|
169
|
+
opts.status ? `status=${opts.status}` : "",
|
|
170
|
+
tags.length > 0 ? `tags=${tags.join(",")}` : "",
|
|
171
|
+
].filter(Boolean).join(", ");
|
|
172
|
+
const summaryStr = filterDesc ? ` (filtered: ${filterDesc})` : "";
|
|
173
|
+
console.log(chalk.bold(`Agents${summaryStr}: ${paginated.length} of ${total}`));
|
|
174
|
+
console.log();
|
|
175
|
+
for (const a of paginated) {
|
|
176
|
+
console.log(` ${chalk.cyan(a.id)} ${chalk.gray(a.adapter ?? "claude-agent-acp")}`);
|
|
177
|
+
console.log(` ${chalk.gray("dispatch:")} ${formatDispatchRules(a)}`);
|
|
178
|
+
console.log(` ${chalk.gray("concurrency:")} max ${a.concurrency?.max_concurrent ?? 1}`);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
error("Failed to list agents", err);
|
|
184
|
+
process.exit(EXIT_CODES.ERROR);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
// ─── kspec agent run ──────────────────────────────────────────────────────
|
|
188
|
+
// AC: @cli-agent-commands ac-2, ac-3, ac-7, ac-8
|
|
189
|
+
// AC: @trait-dry-run ac-1 through ac-6
|
|
190
|
+
agent
|
|
191
|
+
.command("run <agent-id> [prompt]")
|
|
192
|
+
.description("Run a one-shot agent invocation")
|
|
193
|
+
.option("--task <ref>", "Task reference to target")
|
|
194
|
+
.option("--timeout <minutes>", "Timeout in minutes (overrides agent default)")
|
|
195
|
+
.option("--budget <n>", "Budget override (max tasks)")
|
|
196
|
+
.option("--adapter <id>", "Adapter override")
|
|
197
|
+
.option("--dry-run", "Show prompt without spawning agent")
|
|
198
|
+
.option("--json", "Output as JSON")
|
|
199
|
+
.action(async (agentId, prompt, opts) => {
|
|
200
|
+
try {
|
|
201
|
+
const ctx = await initContext();
|
|
202
|
+
if (!ctx.manifestPath) {
|
|
203
|
+
error(errors.project.noKspecProject);
|
|
204
|
+
process.exit(EXIT_CODES.VALIDATION_FAILED);
|
|
205
|
+
}
|
|
206
|
+
const meta = await loadMetaContext(ctx);
|
|
207
|
+
const agentDef = meta.agents.find((a) => a.id === agentId);
|
|
208
|
+
// AC: @trait-error-guidance ac-3 - not found error with suggestion
|
|
209
|
+
if (!agentDef) {
|
|
210
|
+
error(`Agent "${agentId}" not found.`, {
|
|
211
|
+
suggestion: `Check available agents with: kspec agent list`,
|
|
212
|
+
});
|
|
213
|
+
process.exit(EXIT_CODES.VALIDATION_FAILED);
|
|
214
|
+
}
|
|
215
|
+
// Resolve the effective adapter
|
|
216
|
+
const adapterId = opts.adapter ?? agentDef.adapter ?? "claude-agent-acp";
|
|
217
|
+
const adapter = resolveAdapter(adapterId);
|
|
218
|
+
// Build the prompt
|
|
219
|
+
const taskRef = opts.task;
|
|
220
|
+
const basePrompt = prompt ?? (taskRef
|
|
221
|
+
? `Work on task ${taskRef} according to your configuration and skills.`
|
|
222
|
+
: `Run as requested.`);
|
|
223
|
+
// Note: buildPromptWithSkills is called here for the dry-run preview path.
|
|
224
|
+
// runInvocation also calls buildPromptWithSkills internally, so we pass basePrompt
|
|
225
|
+
// to runInvocation (not fullPrompt) to avoid double-expansion.
|
|
226
|
+
const fullPromptForPreview = await buildPromptWithSkills({
|
|
227
|
+
basePrompt,
|
|
228
|
+
skillIds: agentDef.skills ?? [],
|
|
229
|
+
specDir: ctx.specDir,
|
|
230
|
+
adapterId,
|
|
231
|
+
});
|
|
232
|
+
// AC: @trait-dry-run ac-1, ac-2, ac-3 - dry run shows prompt, no changes
|
|
233
|
+
if (opts.dryRun) {
|
|
234
|
+
// Pre-compute overrides so dry-run reflects what the actual invocation would use
|
|
235
|
+
// AC: @cli-agent-commands ac-7 - overrides are visible in dry-run output
|
|
236
|
+
// AC: @trait-semantic-exit-codes ac-2 - validate numeric inputs even in dry-run
|
|
237
|
+
let dryTimeoutOverride;
|
|
238
|
+
if (opts.timeout) {
|
|
239
|
+
const parsed = parseIntOption(opts.timeout, { min: 1, max: 10080, name: "Timeout" });
|
|
240
|
+
if (!parsed.ok) {
|
|
241
|
+
error(`Invalid --timeout value: ${parsed.error}`, { suggestion: "Example: --timeout 30" });
|
|
242
|
+
process.exit(EXIT_CODES.VALIDATION_FAILED);
|
|
243
|
+
}
|
|
244
|
+
dryTimeoutOverride = parsed.value;
|
|
245
|
+
}
|
|
246
|
+
let dryBudgetOverride;
|
|
247
|
+
if (opts.budget) {
|
|
248
|
+
const parsed = parseIntOption(opts.budget, { min: 1, max: 99999, name: "Budget" });
|
|
249
|
+
if (!parsed.ok) {
|
|
250
|
+
error(`Invalid --budget value: ${parsed.error}`, { suggestion: "Example: --budget 10" });
|
|
251
|
+
process.exit(EXIT_CODES.VALIDATION_FAILED);
|
|
252
|
+
}
|
|
253
|
+
dryBudgetOverride = parsed.value;
|
|
254
|
+
}
|
|
255
|
+
const effectiveTimeoutMinutes = dryTimeoutOverride ?? agentDef.budget?.timeout_minutes;
|
|
256
|
+
const effectiveMaxTasks = dryBudgetOverride ?? agentDef.budget?.max_tasks;
|
|
257
|
+
// AC: @trait-dry-run ac-6 - JSON output includes dry_run field
|
|
258
|
+
output({
|
|
259
|
+
dry_run: true,
|
|
260
|
+
agent_id: agentId,
|
|
261
|
+
adapter: adapterId,
|
|
262
|
+
task_ref: taskRef ?? null,
|
|
263
|
+
timeout_minutes: effectiveTimeoutMinutes ?? null,
|
|
264
|
+
max_tasks: effectiveMaxTasks ?? null,
|
|
265
|
+
prompt: fullPromptForPreview,
|
|
266
|
+
}, () => {
|
|
267
|
+
console.log(chalk.yellow("DRY RUN - No agent will be spawned"));
|
|
268
|
+
console.log();
|
|
269
|
+
console.log(chalk.gray(`Agent: ${agentId}`));
|
|
270
|
+
console.log(chalk.gray(`Adapter: ${adapterId}`));
|
|
271
|
+
if (taskRef) {
|
|
272
|
+
console.log(chalk.gray(`Task: ${taskRef}`));
|
|
273
|
+
}
|
|
274
|
+
if (effectiveTimeoutMinutes !== undefined) {
|
|
275
|
+
console.log(chalk.gray(`Timeout: ${effectiveTimeoutMinutes} min`));
|
|
276
|
+
}
|
|
277
|
+
console.log();
|
|
278
|
+
console.log(chalk.gray("--- Prompt that would be sent ---"));
|
|
279
|
+
console.log(fullPromptForPreview);
|
|
280
|
+
console.log(chalk.gray("--- End prompt ---"));
|
|
281
|
+
});
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
// AC: @cli-agent-commands ac-2 - one-shot invocation with task binding
|
|
285
|
+
// AC: @cli-agent-commands ac-3 - one-shot invocation with custom prompt (no task binding)
|
|
286
|
+
// AC: @cli-agent-commands ac-7 - CLI overrides agent defaults
|
|
287
|
+
let timeoutOverride;
|
|
288
|
+
if (opts.timeout) {
|
|
289
|
+
const parsed = parseIntOption(opts.timeout, { min: 1, max: 10080, name: "Timeout" });
|
|
290
|
+
if (!parsed.ok) {
|
|
291
|
+
error(`Invalid --timeout value: ${parsed.error}`, { suggestion: "Example: --timeout 30" });
|
|
292
|
+
process.exit(EXIT_CODES.VALIDATION_FAILED);
|
|
293
|
+
}
|
|
294
|
+
timeoutOverride = parsed.value;
|
|
295
|
+
}
|
|
296
|
+
let budgetOverride;
|
|
297
|
+
if (opts.budget) {
|
|
298
|
+
const parsed = parseIntOption(opts.budget, { min: 1, max: 99999, name: "Budget" });
|
|
299
|
+
if (!parsed.ok) {
|
|
300
|
+
error(`Invalid --budget value: ${parsed.error}`, { suggestion: "Example: --budget 10" });
|
|
301
|
+
process.exit(EXIT_CODES.VALIDATION_FAILED);
|
|
302
|
+
}
|
|
303
|
+
budgetOverride = parsed.value;
|
|
304
|
+
}
|
|
305
|
+
const effectiveAgent = {
|
|
306
|
+
...agentDef,
|
|
307
|
+
adapter: adapterId,
|
|
308
|
+
budget: timeoutOverride !== undefined || budgetOverride !== undefined
|
|
309
|
+
? {
|
|
310
|
+
...agentDef.budget,
|
|
311
|
+
timeout_minutes: timeoutOverride ?? agentDef.budget?.timeout_minutes,
|
|
312
|
+
max_tasks: budgetOverride ?? agentDef.budget?.max_tasks,
|
|
313
|
+
}
|
|
314
|
+
: agentDef.budget,
|
|
315
|
+
};
|
|
316
|
+
console.log(chalk.gray(`Running agent "${agentId}"...`));
|
|
317
|
+
// AC: @cli-agent-commands ac-12 — stream text to stdout in interactive mode
|
|
318
|
+
// AC: @cli-agent-commands ac-11 — suppress streaming in --json mode
|
|
319
|
+
let didStream = false;
|
|
320
|
+
const onUpdate = isJsonMode()
|
|
321
|
+
? undefined
|
|
322
|
+
: (update) => {
|
|
323
|
+
if (update.sessionUpdate === "agent_message_chunk" &&
|
|
324
|
+
update.content.type === "text") {
|
|
325
|
+
process.stdout.write(update.content.text);
|
|
326
|
+
didStream = true;
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
// AC: @cli-agent-commands ac-3 - no task binding when --task not provided
|
|
330
|
+
// Pass basePrompt (not fullPromptForPreview) — runInvocation expands skills internally
|
|
331
|
+
const result = await runInvocation({
|
|
332
|
+
agent: effectiveAgent,
|
|
333
|
+
specDir: ctx.specDir,
|
|
334
|
+
cwd: ctx.rootDir,
|
|
335
|
+
taskRef: taskRef ?? undefined,
|
|
336
|
+
prompt: basePrompt,
|
|
337
|
+
trigger: "manual",
|
|
338
|
+
onUpdate,
|
|
339
|
+
});
|
|
340
|
+
// Ensure summary starts on its own line after streamed content
|
|
341
|
+
if (didStream)
|
|
342
|
+
process.stdout.write("\n");
|
|
343
|
+
output({
|
|
344
|
+
outcome: result.outcome,
|
|
345
|
+
session_id: result.session.id,
|
|
346
|
+
duration_ms: result.durationMs,
|
|
347
|
+
stop_reason: result.stopReason,
|
|
348
|
+
}, () => {
|
|
349
|
+
if (result.outcome === "success") {
|
|
350
|
+
success(`Agent invocation completed`, {
|
|
351
|
+
session: result.session.id,
|
|
352
|
+
duration: `${Math.round(result.durationMs / 1000)}s`,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
else {
|
|
356
|
+
error(`Agent invocation ${result.outcome}: ${result.error ?? "unknown"}`);
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
if (result.outcome !== "success") {
|
|
360
|
+
process.exit(EXIT_CODES.ERROR);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
catch (err) {
|
|
364
|
+
error("Failed to run agent", err);
|
|
365
|
+
process.exit(EXIT_CODES.ERROR);
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
// ─── kspec agent status ───────────────────────────────────────────────────
|
|
369
|
+
// AC: @cli-agent-commands ac-6
|
|
370
|
+
agent
|
|
371
|
+
.command("status")
|
|
372
|
+
.description("Show active and queued agent invocations")
|
|
373
|
+
.option("--json", "Output as JSON")
|
|
374
|
+
.action(async (opts) => {
|
|
375
|
+
try {
|
|
376
|
+
const daemonConn = getDaemonUrl();
|
|
377
|
+
// AC: @trait-error-guidance ac-1, ac-2
|
|
378
|
+
if (!daemonConn) {
|
|
379
|
+
error("Daemon is not running. Cannot retrieve agent status.", { suggestion: "Start the daemon with: kspec serve" });
|
|
380
|
+
process.exit(EXIT_CODES.ERROR);
|
|
381
|
+
}
|
|
382
|
+
const ctx = await initContext();
|
|
383
|
+
const headers = {};
|
|
384
|
+
if (ctx.rootDir) {
|
|
385
|
+
headers["X-Kspec-Dir"] = ctx.rootDir;
|
|
386
|
+
}
|
|
387
|
+
const response = await fetch(`${daemonConn.url}/api/agent/dispatch/status`, { headers });
|
|
388
|
+
if (!response.ok) {
|
|
389
|
+
error(`Daemon returned error: ${response.status} ${response.statusText}`);
|
|
390
|
+
process.exit(EXIT_CODES.ERROR);
|
|
391
|
+
}
|
|
392
|
+
// AC: @cli-agent-commands ac-6
|
|
393
|
+
const data = await response.json();
|
|
394
|
+
output(data, () => {
|
|
395
|
+
console.log(chalk.bold("Agent Status"));
|
|
396
|
+
console.log();
|
|
397
|
+
console.log(` Dispatch engine: ${data.running ? chalk.green("running") : chalk.gray("stopped")}`);
|
|
398
|
+
console.log(` Active invocations: ${chalk.cyan(String(data.activeInvocations))}`);
|
|
399
|
+
console.log(` Queued invocations: ${chalk.cyan(String(data.queuedInvocations))}`);
|
|
400
|
+
const invocations = data.invocations ?? [];
|
|
401
|
+
if (invocations.length > 0) {
|
|
402
|
+
console.log();
|
|
403
|
+
console.log(chalk.bold("Active:"));
|
|
404
|
+
for (const inv of invocations) {
|
|
405
|
+
const elapsed = Math.round(inv.elapsedMs / 1000);
|
|
406
|
+
const taskStr = inv.taskRef ? ` task: ${chalk.yellow(inv.taskRef)}` : "";
|
|
407
|
+
console.log(` ${chalk.cyan(inv.agentId)} ${chalk.gray(inv.agentName)}`);
|
|
408
|
+
console.log(` session: ${chalk.gray(inv.sessionId)} elapsed: ${elapsed}s${taskStr}`);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
const queuedItems = data.queued ?? [];
|
|
412
|
+
if (queuedItems.length > 0) {
|
|
413
|
+
console.log();
|
|
414
|
+
console.log(chalk.bold("Queued:"));
|
|
415
|
+
for (const q of queuedItems) {
|
|
416
|
+
const wait = Math.round(q.waitMs / 1000);
|
|
417
|
+
const taskStr = q.taskRef ? ` task: ${chalk.yellow(q.taskRef)}` : "";
|
|
418
|
+
console.log(` ${chalk.cyan(q.agentId)} ${chalk.gray(q.agentName)}`);
|
|
419
|
+
console.log(` waiting: ${wait}s${taskStr}`);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
catch (err) {
|
|
425
|
+
error("Failed to get agent status", err);
|
|
426
|
+
process.exit(EXIT_CODES.ERROR);
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
// ─── kspec agent dispatch ─────────────────────────────────────────────────
|
|
430
|
+
const dispatch = agent
|
|
431
|
+
.command("dispatch")
|
|
432
|
+
.description("Manage the agent dispatch engine");
|
|
433
|
+
// AC: @cli-agent-commands ac-4
|
|
434
|
+
// AC: @cli-agent-commands ac-10
|
|
435
|
+
dispatch
|
|
436
|
+
.command("start")
|
|
437
|
+
.description("Start the dispatch engine (daemon must be running)")
|
|
438
|
+
.option("--json", "Output as JSON")
|
|
439
|
+
.action(async (opts) => {
|
|
440
|
+
try {
|
|
441
|
+
const daemonConn = getDaemonUrl();
|
|
442
|
+
// AC: @cli-agent-commands ac-10 - error when daemon not running
|
|
443
|
+
if (!daemonConn) {
|
|
444
|
+
error("Daemon is not running. The dispatch engine requires the daemon.", { suggestion: "Start the daemon first with: kspec serve" });
|
|
445
|
+
process.exit(EXIT_CODES.ERROR);
|
|
446
|
+
}
|
|
447
|
+
const ctx = await initContext();
|
|
448
|
+
const headers = {
|
|
449
|
+
"Content-Type": "application/json",
|
|
450
|
+
};
|
|
451
|
+
if (ctx.rootDir) {
|
|
452
|
+
headers["X-Kspec-Dir"] = ctx.rootDir;
|
|
453
|
+
}
|
|
454
|
+
const response = await fetch(`${daemonConn.url}/api/agent/dispatch/start`, {
|
|
455
|
+
method: "POST",
|
|
456
|
+
headers,
|
|
457
|
+
});
|
|
458
|
+
if (!response.ok) {
|
|
459
|
+
const body = await response.text();
|
|
460
|
+
error(`Failed to start dispatch engine: ${response.status} - ${body}`);
|
|
461
|
+
process.exit(EXIT_CODES.ERROR);
|
|
462
|
+
}
|
|
463
|
+
const data = await response.json();
|
|
464
|
+
output(data, () => {
|
|
465
|
+
if (data.started) {
|
|
466
|
+
success("Dispatch engine started");
|
|
467
|
+
}
|
|
468
|
+
else {
|
|
469
|
+
console.log(chalk.yellow(`Dispatch engine: ${data.reason ?? "already running"}`));
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
catch (err) {
|
|
474
|
+
error("Failed to start dispatch engine", err);
|
|
475
|
+
process.exit(EXIT_CODES.ERROR);
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
// AC: @cli-agent-commands ac-5
|
|
479
|
+
dispatch
|
|
480
|
+
.command("stop")
|
|
481
|
+
.description("Stop the dispatch engine gracefully")
|
|
482
|
+
.option("--json", "Output as JSON")
|
|
483
|
+
.action(async (opts) => {
|
|
484
|
+
try {
|
|
485
|
+
const daemonConn = getDaemonUrl();
|
|
486
|
+
if (!daemonConn) {
|
|
487
|
+
error("Daemon is not running.", { suggestion: "Start the daemon with: kspec serve" });
|
|
488
|
+
process.exit(EXIT_CODES.ERROR);
|
|
489
|
+
}
|
|
490
|
+
const ctx = await initContext();
|
|
491
|
+
const headers = {
|
|
492
|
+
"Content-Type": "application/json",
|
|
493
|
+
};
|
|
494
|
+
if (ctx.rootDir) {
|
|
495
|
+
headers["X-Kspec-Dir"] = ctx.rootDir;
|
|
496
|
+
}
|
|
497
|
+
const response = await fetch(`${daemonConn.url}/api/agent/dispatch/stop`, {
|
|
498
|
+
method: "POST",
|
|
499
|
+
headers,
|
|
500
|
+
});
|
|
501
|
+
if (!response.ok) {
|
|
502
|
+
const body = await response.text();
|
|
503
|
+
error(`Failed to stop dispatch engine: ${response.status} - ${body}`);
|
|
504
|
+
process.exit(EXIT_CODES.ERROR);
|
|
505
|
+
}
|
|
506
|
+
const data = await response.json();
|
|
507
|
+
output(data, () => {
|
|
508
|
+
if (data.stopped) {
|
|
509
|
+
success("Dispatch engine stopped");
|
|
510
|
+
}
|
|
511
|
+
else {
|
|
512
|
+
console.log(chalk.yellow(`Dispatch engine: ${data.reason ?? "not running"}`));
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
catch (err) {
|
|
517
|
+
error("Failed to stop dispatch engine", err);
|
|
518
|
+
process.exit(EXIT_CODES.ERROR);
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
// AC: @cli-agent-commands ac-9
|
|
522
|
+
dispatch
|
|
523
|
+
.command("status")
|
|
524
|
+
.description("Show dispatch engine status and loaded agents")
|
|
525
|
+
.option("--json", "Output as JSON")
|
|
526
|
+
.action(async (opts) => {
|
|
527
|
+
try {
|
|
528
|
+
const daemonConn = getDaemonUrl();
|
|
529
|
+
if (!daemonConn) {
|
|
530
|
+
// Daemon not running — show as disabled
|
|
531
|
+
output({ running: false, activeInvocations: 0, queuedInvocations: 0, agents: [] }, () => {
|
|
532
|
+
console.log(chalk.bold("Dispatch Status"));
|
|
533
|
+
console.log();
|
|
534
|
+
console.log(` Dispatch engine: ${chalk.gray("not available (daemon offline)")}`);
|
|
535
|
+
console.log(chalk.gray(" Start daemon with: kspec serve"));
|
|
536
|
+
});
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
const ctx = await initContext();
|
|
540
|
+
const headers = {};
|
|
541
|
+
if (ctx.rootDir) {
|
|
542
|
+
headers["X-Kspec-Dir"] = ctx.rootDir;
|
|
543
|
+
}
|
|
544
|
+
// Get dispatch status
|
|
545
|
+
const statusResponse = await fetch(`${daemonConn.url}/api/agent/dispatch/status`, { headers });
|
|
546
|
+
if (!statusResponse.ok) {
|
|
547
|
+
error(`Daemon returned error: ${statusResponse.status}`);
|
|
548
|
+
process.exit(EXIT_CODES.ERROR);
|
|
549
|
+
}
|
|
550
|
+
const statusData = await statusResponse.json();
|
|
551
|
+
// Get loaded agents
|
|
552
|
+
let agents = [];
|
|
553
|
+
try {
|
|
554
|
+
const meta = await loadMetaContext(ctx);
|
|
555
|
+
agents = meta.agents.map((a) => ({ id: a.id, name: a.name }));
|
|
556
|
+
}
|
|
557
|
+
catch {
|
|
558
|
+
// Meta may not be available
|
|
559
|
+
}
|
|
560
|
+
const fullData = { ...statusData, agents };
|
|
561
|
+
output(fullData, () => {
|
|
562
|
+
console.log(chalk.bold("Dispatch Status"));
|
|
563
|
+
console.log();
|
|
564
|
+
console.log(` Engine: ${statusData.running ? chalk.green("enabled") : chalk.yellow("disabled")}`);
|
|
565
|
+
console.log(` Active invocations: ${chalk.cyan(String(statusData.activeInvocations))}`);
|
|
566
|
+
console.log(` Queued invocations: ${chalk.cyan(String(statusData.queuedInvocations))}`);
|
|
567
|
+
console.log();
|
|
568
|
+
console.log(chalk.bold("Loaded Agents:"));
|
|
569
|
+
if (agents.length === 0) {
|
|
570
|
+
console.log(chalk.gray(" (none defined)"));
|
|
571
|
+
}
|
|
572
|
+
else {
|
|
573
|
+
for (const a of agents) {
|
|
574
|
+
console.log(` ${chalk.cyan(a.id)} ${chalk.gray(a.name)}`);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
catch (err) {
|
|
580
|
+
error("Failed to get dispatch status", err);
|
|
581
|
+
process.exit(EXIT_CODES.ERROR);
|
|
582
|
+
}
|
|
583
|
+
});
|
|
584
|
+
// ─── kspec agent dispatch watch ───────────────────────────────────────────
|
|
585
|
+
// AC: @cli-agent-commands ac-13 through ac-18
|
|
586
|
+
dispatch
|
|
587
|
+
.command("watch")
|
|
588
|
+
.description("Stream agent text output from the dispatch engine in real time")
|
|
589
|
+
.option("--agent <name>", "Only show output from this agent")
|
|
590
|
+
.option("--session <id>", "Only show output from this session")
|
|
591
|
+
.option("--retries <n>", "Number of reconnect attempts on disconnect (default 5)", "5")
|
|
592
|
+
.action(async (opts) => {
|
|
593
|
+
const DEFAULT_RETRIES = 5;
|
|
594
|
+
const RETRY_BASE_MS = 1000;
|
|
595
|
+
const MAX_RETRY_MS = 30_000;
|
|
596
|
+
// AC: @cli-agent-commands ac-15 — error when daemon not running
|
|
597
|
+
const daemonConn = getDaemonUrl();
|
|
598
|
+
if (!daemonConn) {
|
|
599
|
+
error("Daemon is not running. The watch command requires the daemon.");
|
|
600
|
+
info("Suggestion: Start the daemon with: kspec serve");
|
|
601
|
+
// AC: @cli-agent-commands ac-15 — exit code 3
|
|
602
|
+
process.exit(EXIT_CODES.NOT_FOUND);
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
const parsedRetries = parseIntOption(opts.retries, {
|
|
606
|
+
min: 0,
|
|
607
|
+
max: Number.MAX_SAFE_INTEGER,
|
|
608
|
+
name: "Retries",
|
|
609
|
+
});
|
|
610
|
+
if (!parsedRetries.ok) {
|
|
611
|
+
error(`Invalid --retries value: ${parsedRetries.error}`, {
|
|
612
|
+
suggestion: "Example: --retries 5",
|
|
613
|
+
});
|
|
614
|
+
process.exit(EXIT_CODES.VALIDATION_FAILED);
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
const retryLimit = parsedRetries.value;
|
|
618
|
+
const agentFilter = opts.agent;
|
|
619
|
+
const sessionFilter = opts.session;
|
|
620
|
+
const streamStates = new Map();
|
|
621
|
+
let activeStreamKey = null;
|
|
622
|
+
let outputAtLineStart = true;
|
|
623
|
+
function getStreamState(streamKey) {
|
|
624
|
+
let state = streamStates.get(streamKey);
|
|
625
|
+
if (!state) {
|
|
626
|
+
state = { hasRenderedBody: false, spacerPending: false };
|
|
627
|
+
streamStates.set(streamKey, state);
|
|
628
|
+
}
|
|
629
|
+
return state;
|
|
630
|
+
}
|
|
631
|
+
function writeRaw(text) {
|
|
632
|
+
if (!text)
|
|
633
|
+
return;
|
|
634
|
+
process.stdout.write(text);
|
|
635
|
+
outputAtLineStart = text.endsWith("\n");
|
|
636
|
+
}
|
|
637
|
+
function writeSpeakerText(text) {
|
|
638
|
+
if (!text)
|
|
639
|
+
return;
|
|
640
|
+
writeRaw(text);
|
|
641
|
+
}
|
|
642
|
+
function ensureLineBreak() {
|
|
643
|
+
if (!outputAtLineStart) {
|
|
644
|
+
writeRaw("\n");
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
function startSpeakerSection(streamKey, prefix) {
|
|
648
|
+
if (activeStreamKey === streamKey)
|
|
649
|
+
return;
|
|
650
|
+
if (activeStreamKey) {
|
|
651
|
+
ensureLineBreak();
|
|
652
|
+
}
|
|
653
|
+
writeRaw(`${prefix}\n`);
|
|
654
|
+
activeStreamKey = streamKey;
|
|
655
|
+
}
|
|
656
|
+
function queuePrefixedChunk(streamKey, prefix, text) {
|
|
657
|
+
if (!text)
|
|
658
|
+
return;
|
|
659
|
+
const switchingSpeaker = activeStreamKey !== null && activeStreamKey !== streamKey;
|
|
660
|
+
startSpeakerSection(streamKey, prefix);
|
|
661
|
+
const state = getStreamState(streamKey);
|
|
662
|
+
if (switchingSpeaker) {
|
|
663
|
+
// Marker change already separates context; don't carry stale spacer
|
|
664
|
+
// into the top of a newly active speaker section.
|
|
665
|
+
state.spacerPending = false;
|
|
666
|
+
}
|
|
667
|
+
else if (state.spacerPending && state.hasRenderedBody) {
|
|
668
|
+
ensureLineBreak();
|
|
669
|
+
writeRaw("\n");
|
|
670
|
+
state.spacerPending = false;
|
|
671
|
+
}
|
|
672
|
+
writeSpeakerText(text);
|
|
673
|
+
state.hasRenderedBody = true;
|
|
674
|
+
}
|
|
675
|
+
function markMessageBoundary(streamKey) {
|
|
676
|
+
// Boundary signals are stream-local; ignore inactive streams.
|
|
677
|
+
if (activeStreamKey !== streamKey)
|
|
678
|
+
return;
|
|
679
|
+
const state = getStreamState(streamKey);
|
|
680
|
+
ensureLineBreak();
|
|
681
|
+
// Coalesce repeated boundary events into one pending spacer.
|
|
682
|
+
if (state.hasRenderedBody) {
|
|
683
|
+
state.spacerPending = true;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
function formatSessionIdForDisplay(sessionId) {
|
|
687
|
+
// AC: @cli-agent-commands ac-17 — shorten session ULID in watch prefix.
|
|
688
|
+
return sessionId ? sessionId.slice(0, 8) : "";
|
|
689
|
+
}
|
|
690
|
+
// Resolve project dir for WebSocket project binding
|
|
691
|
+
let projectDir;
|
|
692
|
+
try {
|
|
693
|
+
const ctx = await initContext();
|
|
694
|
+
projectDir = ctx.rootDir;
|
|
695
|
+
}
|
|
696
|
+
catch {
|
|
697
|
+
// non-fatal: WebSocket will use daemon default
|
|
698
|
+
}
|
|
699
|
+
let retryCount = 0;
|
|
700
|
+
let shouldReconnect = true;
|
|
701
|
+
function connect() {
|
|
702
|
+
const wsUrl = new URL(`ws://localhost:${daemonConn.port}/ws`);
|
|
703
|
+
if (projectDir) {
|
|
704
|
+
wsUrl.searchParams.set("project", projectDir);
|
|
705
|
+
}
|
|
706
|
+
// AC: @cli-agent-commands ac-15 — Node 22+ has global WebSocket
|
|
707
|
+
const ws = new WebSocket(wsUrl.toString());
|
|
708
|
+
ws.onopen = () => {
|
|
709
|
+
retryCount = 0;
|
|
710
|
+
// Subscribe to agents topic
|
|
711
|
+
ws.send(JSON.stringify({
|
|
712
|
+
action: "subscribe",
|
|
713
|
+
request_id: "watch-subscribe",
|
|
714
|
+
payload: { topics: ["agents"] },
|
|
715
|
+
}));
|
|
716
|
+
};
|
|
717
|
+
ws.onmessage = (event) => {
|
|
718
|
+
let msg;
|
|
719
|
+
try {
|
|
720
|
+
msg = JSON.parse(event.data);
|
|
721
|
+
}
|
|
722
|
+
catch {
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
// AC: @cli-agent-commands ac-18 — fail fast on subscribe handshake rejection.
|
|
726
|
+
if (msg.ack === true && msg.request_id === "watch-subscribe") {
|
|
727
|
+
if (msg.success === false) {
|
|
728
|
+
shouldReconnect = false;
|
|
729
|
+
const reasonParts = [msg.error, msg.details]
|
|
730
|
+
.filter((value) => typeof value === "string" && value.length > 0);
|
|
731
|
+
const reason = reasonParts.length > 0 ? ` (${reasonParts.join(": ")})` : "";
|
|
732
|
+
if (activeStreamKey) {
|
|
733
|
+
ensureLineBreak();
|
|
734
|
+
}
|
|
735
|
+
error(`Failed to subscribe to daemon agent output stream${reason}.`);
|
|
736
|
+
info("Suggestion: Verify daemon logs for subscribe errors, then restart with: kspec serve");
|
|
737
|
+
process.exit(EXIT_CODES.NOT_FOUND);
|
|
738
|
+
}
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
// AC: @cli-agent-commands ac-13 — stream text with per-line [agent-id session-id] prefixes
|
|
742
|
+
if (msg.event === "agent_text_chunk" && msg.data) {
|
|
743
|
+
const data = msg.data;
|
|
744
|
+
const sessionId = data.session_id ?? "";
|
|
745
|
+
const agentId = data.agent_id ?? "";
|
|
746
|
+
const text = data.text ?? "";
|
|
747
|
+
// AC: @cli-agent-commands ac-16 — filter by agent/session if specified
|
|
748
|
+
if (agentFilter && agentId !== agentFilter)
|
|
749
|
+
return;
|
|
750
|
+
if (sessionFilter && sessionId !== sessionFilter)
|
|
751
|
+
return;
|
|
752
|
+
const streamKey = `${agentId}\u0000${sessionId}`;
|
|
753
|
+
const displaySessionId = formatSessionIdForDisplay(sessionId);
|
|
754
|
+
const prefix = `[${agentId} ${displaySessionId}]`;
|
|
755
|
+
// Match old Ralph rendering semantics: empty chunk marks message end.
|
|
756
|
+
if (text.length === 0) {
|
|
757
|
+
markMessageBoundary(streamKey);
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
queuePrefixedChunk(streamKey, prefix, text);
|
|
761
|
+
}
|
|
762
|
+
};
|
|
763
|
+
ws.onerror = () => {
|
|
764
|
+
// error event fires before close, handled in onclose
|
|
765
|
+
};
|
|
766
|
+
ws.onclose = () => {
|
|
767
|
+
if (!shouldReconnect)
|
|
768
|
+
return;
|
|
769
|
+
if (activeStreamKey) {
|
|
770
|
+
ensureLineBreak();
|
|
771
|
+
}
|
|
772
|
+
if (retryCount >= retryLimit) {
|
|
773
|
+
// AC: @cli-agent-commands ac-14 — exit code 3 when retries exhausted
|
|
774
|
+
error(`WebSocket connection lost and reconnection failed after ${retryLimit} attempt(s).`);
|
|
775
|
+
info("Suggestion: Verify the daemon is still running with: kspec serve");
|
|
776
|
+
process.exit(EXIT_CODES.NOT_FOUND);
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
retryCount++;
|
|
780
|
+
const backoffMs = Math.min(RETRY_BASE_MS * Math.pow(2, retryCount - 1), MAX_RETRY_MS);
|
|
781
|
+
// AC: @cli-agent-commands ac-14 — print reconnecting message
|
|
782
|
+
process.stderr.write(`[watch] Connection lost. Reconnecting in ${Math.round(backoffMs / 1000)}s (attempt ${retryCount}/${retryLimit})...\n`);
|
|
783
|
+
setTimeout(connect, backoffMs);
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
connect();
|
|
787
|
+
// Keep process alive (WebSocket is non-blocking in Node)
|
|
788
|
+
// Users interrupt with Ctrl+C
|
|
789
|
+
await new Promise(() => { });
|
|
790
|
+
});
|
|
791
|
+
// ─── kspec agent end-loop ─────────────────────────────────────────────────
|
|
792
|
+
// AC: @ralph-replacement ac-1 — equivalent to kspec ralph end-loop
|
|
793
|
+
// AC: @session-end-loop-signal ac-signal
|
|
794
|
+
agent
|
|
795
|
+
.command("end-loop")
|
|
796
|
+
.description("Signal the agent dispatch engine to stop after current iteration")
|
|
797
|
+
.option("--reason <reason>", "Reason for ending the loop")
|
|
798
|
+
.action(async (options) => {
|
|
799
|
+
try {
|
|
800
|
+
const ctx = await initContext();
|
|
801
|
+
const sessionId = process.env.KSPEC_SESSION_ID;
|
|
802
|
+
if (!sessionId) {
|
|
803
|
+
// AC: @trait-error-guidance ac-1, ac-2
|
|
804
|
+
warn("No active agent session detected (KSPEC_SESSION_ID not set).");
|
|
805
|
+
info("This command requires an active session. It is designed to be called by agents during a dispatch invocation.");
|
|
806
|
+
info("Suggestion: Ensure KSPEC_SESSION_ID is set, or start a session with: kspec session create");
|
|
807
|
+
process.exit(EXIT_CODES.VALIDATION_FAILED);
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
// Write end-loop state to session
|
|
811
|
+
const updated = await requestEndLoop(ctx.specDir, sessionId, options.reason);
|
|
812
|
+
if (!updated) {
|
|
813
|
+
// AC: @trait-error-guidance ac-1, ac-2
|
|
814
|
+
error(`Session not found: ${sessionId}`);
|
|
815
|
+
info("Suggestion: Check session ID with: kspec session log list");
|
|
816
|
+
process.exit(EXIT_CODES.NOT_FOUND);
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
success("Loop end signal sent");
|
|
820
|
+
if (options.reason) {
|
|
821
|
+
info(`Reason: ${options.reason}`);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
catch (err) {
|
|
825
|
+
// AC: @trait-error-guidance ac-1
|
|
826
|
+
error("Failed to signal end-loop", err);
|
|
827
|
+
process.exit(EXIT_CODES.ERROR);
|
|
828
|
+
}
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
//# sourceMappingURL=agent.js.map
|