@suwujs/king-ai 0.2.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 +96 -0
- package/dist/src/agent-config-validation.d.ts +9 -0
- package/dist/src/agent-config-validation.js +30 -0
- package/dist/src/api.d.ts +4 -0
- package/dist/src/api.js +48 -0
- package/dist/src/attachments.d.ts +45 -0
- package/dist/src/attachments.js +322 -0
- package/dist/src/cli.d.ts +20 -0
- package/dist/src/cli.js +1697 -0
- package/dist/src/config.d.ts +3 -0
- package/dist/src/config.js +20 -0
- package/dist/src/cron.d.ts +11 -0
- package/dist/src/cron.js +65 -0
- package/dist/src/daemon.d.ts +36 -0
- package/dist/src/daemon.js +373 -0
- package/dist/src/engine.d.ts +32 -0
- package/dist/src/engine.js +1014 -0
- package/dist/src/heartbeat.d.ts +18 -0
- package/dist/src/heartbeat.js +28 -0
- package/dist/src/host-api.d.ts +40 -0
- package/dist/src/host-api.js +59 -0
- package/dist/src/host-control.d.ts +48 -0
- package/dist/src/host-control.js +1279 -0
- package/dist/src/host-export.d.ts +50 -0
- package/dist/src/host-export.js +187 -0
- package/dist/src/host-feedback.d.ts +78 -0
- package/dist/src/host-feedback.js +178 -0
- package/dist/src/host-home.d.ts +13 -0
- package/dist/src/host-home.js +54 -0
- package/dist/src/host-ledger.d.ts +261 -0
- package/dist/src/host-ledger.js +554 -0
- package/dist/src/host-loop-events.d.ts +69 -0
- package/dist/src/host-loop-events.js +288 -0
- package/dist/src/host-permission.d.ts +36 -0
- package/dist/src/host-permission.js +180 -0
- package/dist/src/host-policy.d.ts +15 -0
- package/dist/src/host-policy.js +36 -0
- package/dist/src/host-run-executor.d.ts +13 -0
- package/dist/src/host-run-executor.js +221 -0
- package/dist/src/host-run-heartbeat.d.ts +40 -0
- package/dist/src/host-run-heartbeat.js +103 -0
- package/dist/src/host-run-layout.d.ts +17 -0
- package/dist/src/host-run-layout.js +387 -0
- package/dist/src/host-run-meta.d.ts +41 -0
- package/dist/src/host-run-meta.js +115 -0
- package/dist/src/host-run-spec.d.ts +149 -0
- package/dist/src/host-run-spec.js +465 -0
- package/dist/src/host-runs.d.ts +77 -0
- package/dist/src/host-runs.js +195 -0
- package/dist/src/host-sdk.d.ts +412 -0
- package/dist/src/host-sdk.js +628 -0
- package/dist/src/host-server.d.ts +26 -0
- package/dist/src/host-server.js +921 -0
- package/dist/src/host-timeline.d.ts +24 -0
- package/dist/src/host-timeline.js +161 -0
- package/dist/src/jsonl.d.ts +13 -0
- package/dist/src/jsonl.js +47 -0
- package/dist/src/lifecycle.d.ts +5 -0
- package/dist/src/lifecycle.js +18 -0
- package/dist/src/message-routing.d.ts +32 -0
- package/dist/src/message-routing.js +119 -0
- package/dist/src/paths.d.ts +19 -0
- package/dist/src/paths.js +26 -0
- package/dist/src/project-profile.d.ts +49 -0
- package/dist/src/project-profile.js +356 -0
- package/dist/src/remediation.d.ts +14 -0
- package/dist/src/remediation.js +114 -0
- package/dist/src/remote-devices.d.ts +41 -0
- package/dist/src/remote-devices.js +156 -0
- package/dist/src/remote-diagnostics.d.ts +39 -0
- package/dist/src/remote-diagnostics.js +199 -0
- package/dist/src/remote-ssh.d.ts +39 -0
- package/dist/src/remote-ssh.js +129 -0
- package/dist/src/run-stream.d.ts +57 -0
- package/dist/src/run-stream.js +119 -0
- package/dist/src/runner.d.ts +131 -0
- package/dist/src/runner.js +1161 -0
- package/dist/src/runtime-data.d.ts +68 -0
- package/dist/src/runtime-data.js +172 -0
- package/dist/src/service.d.ts +114 -0
- package/dist/src/service.js +631 -0
- package/dist/src/shared-skills.d.ts +26 -0
- package/dist/src/shared-skills.js +85 -0
- package/dist/src/shim.d.ts +1 -0
- package/dist/src/shim.js +64 -0
- package/dist/src/skill-check.d.ts +17 -0
- package/dist/src/skill-check.js +158 -0
- package/dist/src/sse.d.ts +9 -0
- package/dist/src/sse.js +36 -0
- package/dist/src/team-routing.d.ts +55 -0
- package/dist/src/team-routing.js +131 -0
- package/dist/src/team-workflow.d.ts +78 -0
- package/dist/src/team-workflow.js +253 -0
- package/dist/src/text.d.ts +7 -0
- package/dist/src/text.js +27 -0
- package/dist/src/types.d.ts +98 -0
- package/dist/src/types.js +1 -0
- package/dist/src/usage.d.ts +116 -0
- package/dist/src/usage.js +350 -0
- package/dist/src/workspace.d.ts +9 -0
- package/dist/src/workspace.js +56 -0
- package/dist/src/worktree.d.ts +47 -0
- package/dist/src/worktree.js +201 -0
- package/package.json +63 -0
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { cp, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { HOST_LOOP_RESULTS_HEADER } from "./host-loop-events.js";
|
|
5
|
+
import { writeHostRunHeartbeat } from "./host-run-heartbeat.js";
|
|
6
|
+
import { defaultTeamSpec } from "./team-workflow.js";
|
|
7
|
+
import { createHostLaunchPlan, formatHostLaunchPlanSummary, toJsonSafeHostLaunchPlan } from "./host-run-spec.js";
|
|
8
|
+
import { commandText, planAgentWorktrees } from "./worktree.js";
|
|
9
|
+
export async function prepareHostRunLayout(input, options = {}) {
|
|
10
|
+
const launchPlan = createHostLaunchPlan(input, options.env ?? process.env, options.availableEngines);
|
|
11
|
+
if (!launchPlan.ready) {
|
|
12
|
+
throw new Error("host run layout is not ready; run preflight and resolve errors first");
|
|
13
|
+
}
|
|
14
|
+
if (launchPlan.layout.exists && !input.force) {
|
|
15
|
+
throw new Error(`host run layout already exists: ${launchPlan.layout.baseDir}`);
|
|
16
|
+
}
|
|
17
|
+
if (launchPlan.layout.exists && input.force) {
|
|
18
|
+
await rm(launchPlan.layout.baseDir, { recursive: true, force: true });
|
|
19
|
+
}
|
|
20
|
+
const writtenFiles = [];
|
|
21
|
+
const copiedDirectories = [];
|
|
22
|
+
await mkdir(launchPlan.layout.baseDir, { recursive: true });
|
|
23
|
+
await mkdir(dirname(launchPlan.layout.configPath), { recursive: true });
|
|
24
|
+
await mkdir(launchPlan.layout.workspaceRoot, { recursive: true });
|
|
25
|
+
await mkdir(launchPlan.layout.sharedSkillsDir, { recursive: true });
|
|
26
|
+
await mkdir(launchPlan.layout.outputDir, { recursive: true });
|
|
27
|
+
await writeFile(launchPlan.layout.configPath, await buildLocalizedAgentConfig(launchPlan), "utf8");
|
|
28
|
+
writtenFiles.push(launchPlan.layout.configPath);
|
|
29
|
+
await writeFile(launchPlan.layout.collaborationPath, await buildCollaborationManifest(launchPlan), "utf8");
|
|
30
|
+
writtenFiles.push(launchPlan.layout.collaborationPath);
|
|
31
|
+
if (launchPlan.spec.projectDir) {
|
|
32
|
+
const projectAgents = join(launchPlan.spec.projectDir, "agents");
|
|
33
|
+
if (existsSync(projectAgents)) {
|
|
34
|
+
await copyDirectoryContents(projectAgents, launchPlan.layout.workspaceRoot);
|
|
35
|
+
copiedDirectories.push(projectAgents);
|
|
36
|
+
}
|
|
37
|
+
const projectSkills = join(launchPlan.spec.projectDir, "skills");
|
|
38
|
+
if (existsSync(projectSkills)) {
|
|
39
|
+
await copyDirectoryContents(projectSkills, launchPlan.layout.sharedSkillsDir);
|
|
40
|
+
copiedDirectories.push(projectSkills);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
for (const file of await writeMissingAgentGuides(launchPlan))
|
|
44
|
+
writtenFiles.push(file);
|
|
45
|
+
for (const file of await writeRunObservationFiles(launchPlan, input.force === true))
|
|
46
|
+
writtenFiles.push(file);
|
|
47
|
+
return {
|
|
48
|
+
launchPlan: toJsonSafeHostLaunchPlan({
|
|
49
|
+
...launchPlan,
|
|
50
|
+
layout: {
|
|
51
|
+
...launchPlan.layout,
|
|
52
|
+
exists: true
|
|
53
|
+
},
|
|
54
|
+
launchSummary: formatHostLaunchPlanSummary({
|
|
55
|
+
...launchPlan,
|
|
56
|
+
layout: {
|
|
57
|
+
...launchPlan.layout,
|
|
58
|
+
exists: true
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
}),
|
|
62
|
+
writtenFiles,
|
|
63
|
+
copiedDirectories,
|
|
64
|
+
summary: formatHostRunLayoutResult(launchPlan, writtenFiles, copiedDirectories)
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
export function formatHostRunLayoutResult(plan, writtenFiles, copiedDirectories) {
|
|
68
|
+
return [
|
|
69
|
+
`prepared host run layout: ${plan.runId}`,
|
|
70
|
+
`base: ${plan.layout.baseDir}`,
|
|
71
|
+
`config: ${plan.layout.configPath}`,
|
|
72
|
+
`workspace: ${plan.layout.workspaceRoot}`,
|
|
73
|
+
`shared skills: ${plan.layout.sharedSkillsDir}`,
|
|
74
|
+
`collaboration: ${plan.layout.collaborationPath}`,
|
|
75
|
+
`git root: ${plan.layout.gitRoot}`,
|
|
76
|
+
`written files: ${writtenFiles.length ? writtenFiles.join(", ") : "(none)"}`,
|
|
77
|
+
copiedDirectories.length ? `copied directories: ${copiedDirectories.join(", ")}` : "copied directories: (none)"
|
|
78
|
+
].join("\n");
|
|
79
|
+
}
|
|
80
|
+
async function buildLocalizedAgentConfig(plan) {
|
|
81
|
+
const raw = plan.config.path && plan.config.exists
|
|
82
|
+
? await readFile(plan.config.path, "utf8")
|
|
83
|
+
: createDefaultHostRunConfigText(plan);
|
|
84
|
+
let config;
|
|
85
|
+
try {
|
|
86
|
+
config = JSON.parse(raw);
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
throw new Error(`host run config must be valid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
90
|
+
}
|
|
91
|
+
config.workspaceRoot = plan.layout.workspaceRoot;
|
|
92
|
+
config.gitRoot = plan.layout.gitRoot;
|
|
93
|
+
config.outputDir = plan.layout.outputDir;
|
|
94
|
+
config.layoutSchema = "king-ai.host-run-layout.v1";
|
|
95
|
+
redactConfigSecrets(config);
|
|
96
|
+
return `${JSON.stringify(config, null, 2)}\n`;
|
|
97
|
+
}
|
|
98
|
+
async function writeRunObservationFiles(plan, force) {
|
|
99
|
+
const written = [];
|
|
100
|
+
if (force || !existsSync(plan.layout.loopEventsPath)) {
|
|
101
|
+
await writeFile(plan.layout.loopEventsPath, "", "utf8");
|
|
102
|
+
written.push(plan.layout.loopEventsPath);
|
|
103
|
+
}
|
|
104
|
+
if (force || !existsSync(plan.layout.resultsPath)) {
|
|
105
|
+
await writeFile(plan.layout.resultsPath, HOST_LOOP_RESULTS_HEADER, "utf8");
|
|
106
|
+
written.push(plan.layout.resultsPath);
|
|
107
|
+
}
|
|
108
|
+
if (force || !existsSync(plan.layout.heartbeatPath)) {
|
|
109
|
+
await writeHostRunHeartbeat({
|
|
110
|
+
path: plan.layout.heartbeatPath,
|
|
111
|
+
runId: plan.runId,
|
|
112
|
+
status: "prepared",
|
|
113
|
+
outputDir: plan.layout.outputDir
|
|
114
|
+
});
|
|
115
|
+
written.push(plan.layout.heartbeatPath);
|
|
116
|
+
}
|
|
117
|
+
if (force || !existsSync(plan.layout.metaPath)) {
|
|
118
|
+
await writeFile(plan.layout.metaPath, `${JSON.stringify(createPreparedRunMeta(plan), null, 2)}\n`, "utf8");
|
|
119
|
+
written.push(plan.layout.metaPath);
|
|
120
|
+
}
|
|
121
|
+
if (force || !existsSync(plan.layout.tasksPath)) {
|
|
122
|
+
await writeFile(plan.layout.tasksPath, "", "utf8");
|
|
123
|
+
written.push(plan.layout.tasksPath);
|
|
124
|
+
}
|
|
125
|
+
if (force || !existsSync(plan.layout.capsulesPath)) {
|
|
126
|
+
await writeFile(plan.layout.capsulesPath, "", "utf8");
|
|
127
|
+
written.push(plan.layout.capsulesPath);
|
|
128
|
+
}
|
|
129
|
+
if (force || !existsSync(plan.layout.workflowPath)) {
|
|
130
|
+
await writeFile(plan.layout.workflowPath, "", "utf8");
|
|
131
|
+
written.push(plan.layout.workflowPath);
|
|
132
|
+
}
|
|
133
|
+
if (force || !existsSync(plan.layout.feedbackPath)) {
|
|
134
|
+
await writeFile(plan.layout.feedbackPath, "", "utf8");
|
|
135
|
+
written.push(plan.layout.feedbackPath);
|
|
136
|
+
}
|
|
137
|
+
return written;
|
|
138
|
+
}
|
|
139
|
+
function createPreparedRunMeta(plan) {
|
|
140
|
+
return {
|
|
141
|
+
schema: "king-ai.host-run-meta.v1",
|
|
142
|
+
status: "prepared",
|
|
143
|
+
runId: plan.runId,
|
|
144
|
+
goal: plan.spec.goal,
|
|
145
|
+
preparedAt: new Date().toISOString(),
|
|
146
|
+
maxLoops: plan.options.loopMode === "infinite" ? "infinite" : plan.options.loops,
|
|
147
|
+
actualLoops: 0,
|
|
148
|
+
session: plan.session,
|
|
149
|
+
paths: {
|
|
150
|
+
baseDir: plan.layout.baseDir,
|
|
151
|
+
configPath: plan.layout.configPath,
|
|
152
|
+
workspaceRoot: plan.layout.workspaceRoot,
|
|
153
|
+
sharedSkillsDir: plan.layout.sharedSkillsDir,
|
|
154
|
+
gitRoot: plan.layout.gitRoot,
|
|
155
|
+
outputDir: plan.layout.outputDir,
|
|
156
|
+
loopEventsPath: plan.layout.loopEventsPath,
|
|
157
|
+
resultsPath: plan.layout.resultsPath,
|
|
158
|
+
heartbeatPath: plan.layout.heartbeatPath,
|
|
159
|
+
metaPath: plan.layout.metaPath,
|
|
160
|
+
collaborationPath: plan.layout.collaborationPath,
|
|
161
|
+
tasksPath: plan.layout.tasksPath,
|
|
162
|
+
capsulesPath: plan.layout.capsulesPath,
|
|
163
|
+
workflowPath: plan.layout.workflowPath,
|
|
164
|
+
feedbackPath: plan.layout.feedbackPath
|
|
165
|
+
},
|
|
166
|
+
config: {
|
|
167
|
+
source: plan.config.source,
|
|
168
|
+
label: plan.config.label
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
async function buildCollaborationManifest(plan) {
|
|
173
|
+
const config = JSON.parse(await buildLocalizedAgentConfig(plan));
|
|
174
|
+
const agents = (config.agents ?? []).map((agent) => ({
|
|
175
|
+
id: agent.id,
|
|
176
|
+
name: agent.name,
|
|
177
|
+
role: agent.role,
|
|
178
|
+
roleTemplate: agent.roleTemplate,
|
|
179
|
+
responsibility: agent.responsibility,
|
|
180
|
+
handoffPolicy: agent.handoffPolicy,
|
|
181
|
+
lifecycle: agent.lifecycle,
|
|
182
|
+
tier: agent.tier,
|
|
183
|
+
engine: agent.engine
|
|
184
|
+
}));
|
|
185
|
+
const team = defaultTeamSpec(`${plan.runId}-team`, "Host Run Team");
|
|
186
|
+
const worktreePlans = agents.map((agent) => ({
|
|
187
|
+
agentId: agent.id,
|
|
188
|
+
plans: agent.id
|
|
189
|
+
? planAgentWorktrees({
|
|
190
|
+
agentId: agent.id,
|
|
191
|
+
workspaces: [plan.layout.gitRoot],
|
|
192
|
+
baseRoot: join(plan.layout.workspaceRoot, agent.id, "worktrees")
|
|
193
|
+
}).map((worktree) => ({
|
|
194
|
+
repoRoot: worktree.repoRoot,
|
|
195
|
+
repoName: worktree.repoName,
|
|
196
|
+
repoUrl: worktree.repoUrl,
|
|
197
|
+
branch: worktree.branch,
|
|
198
|
+
worktreePath: worktree.worktreePath,
|
|
199
|
+
command: commandText(worktree.command)
|
|
200
|
+
}))
|
|
201
|
+
: []
|
|
202
|
+
}));
|
|
203
|
+
return `${JSON.stringify({
|
|
204
|
+
schema: "king-ai.host-run-collaboration.v1",
|
|
205
|
+
runId: plan.runId,
|
|
206
|
+
goal: plan.spec.goal,
|
|
207
|
+
team,
|
|
208
|
+
agents,
|
|
209
|
+
governance: {
|
|
210
|
+
mode: "opt-in",
|
|
211
|
+
securityBoundary: false,
|
|
212
|
+
appliesWhen: "actorRole or KING_AI_TEAM_ROLE is supplied",
|
|
213
|
+
purpose: "route work, record decisions, audit role intent, and keep trusted local automation moving",
|
|
214
|
+
securityBoundaryNote: "Use OS account isolation, workspace boundaries, runtime tokens, command allowlists, destructive confirmations, and per-agent homes for security."
|
|
215
|
+
},
|
|
216
|
+
taskRules: {
|
|
217
|
+
dependencyField: "dependsOn",
|
|
218
|
+
blockedUntil: "all referenced tasks are done",
|
|
219
|
+
localTaskCommands: ["king-ai host task-create", "king-ai host task-list", "king-ai host task-update", "king-ai host agenda"],
|
|
220
|
+
handoffCommand: "king-ai send <agent> \"<message>\"",
|
|
221
|
+
humanDecisionCommand: "king-ai host decision-create",
|
|
222
|
+
routingModes: ["one-of-us", "each", "review-required", "human-decision"],
|
|
223
|
+
permissionRules: team.permissionPolicy.rules
|
|
224
|
+
},
|
|
225
|
+
capsuleRules: {
|
|
226
|
+
requiredFields: ["owner", "branchOrWorktree", "allowedPaths", "acceptance", "reviewer", "verificationCommands"],
|
|
227
|
+
defaultReviewer: "reviewer",
|
|
228
|
+
localCapsuleCommands: ["king-ai host capsule-create", "king-ai host capsule-list", "king-ai host capsule-update"],
|
|
229
|
+
repoChangePolicy: "keep repository edits inside the agreed capsule scope and record acceptance evidence before marking work done"
|
|
230
|
+
},
|
|
231
|
+
workflowObjects: ["initiative", "task", "handoff", "review", "decision", "artifact"],
|
|
232
|
+
worktreePlans,
|
|
233
|
+
paths: {
|
|
234
|
+
configPath: plan.layout.configPath,
|
|
235
|
+
workspaceRoot: plan.layout.workspaceRoot,
|
|
236
|
+
sharedSkillsDir: plan.layout.sharedSkillsDir,
|
|
237
|
+
gitRoot: plan.layout.gitRoot,
|
|
238
|
+
outputDir: plan.layout.outputDir,
|
|
239
|
+
loopEventsPath: plan.layout.loopEventsPath,
|
|
240
|
+
resultsPath: plan.layout.resultsPath,
|
|
241
|
+
heartbeatPath: plan.layout.heartbeatPath,
|
|
242
|
+
metaPath: plan.layout.metaPath,
|
|
243
|
+
tasksPath: plan.layout.tasksPath,
|
|
244
|
+
capsulesPath: plan.layout.capsulesPath,
|
|
245
|
+
workflowPath: plan.layout.workflowPath,
|
|
246
|
+
feedbackPath: plan.layout.feedbackPath
|
|
247
|
+
}
|
|
248
|
+
}, null, 2)}\n`;
|
|
249
|
+
}
|
|
250
|
+
export function createDefaultHostRunConfigText(plan) {
|
|
251
|
+
const engine = plan.effectiveEngine ?? plan.options.engine;
|
|
252
|
+
const agents = defaultHostRunAgents(plan).map((agent) => ({
|
|
253
|
+
...agent,
|
|
254
|
+
...(engine ? { engine } : {}),
|
|
255
|
+
...(plan.options.model && agent.id === "ceo" ? { model: plan.options.model } : {}),
|
|
256
|
+
...(plan.options.fastModel && agent.id !== "ceo" ? { model: plan.options.fastModel } : {})
|
|
257
|
+
}));
|
|
258
|
+
return JSON.stringify({
|
|
259
|
+
agents,
|
|
260
|
+
workspaceRoot: plan.layout.workspaceRoot,
|
|
261
|
+
gitRoot: plan.layout.gitRoot,
|
|
262
|
+
outputDir: plan.layout.outputDir,
|
|
263
|
+
layoutSchema: "king-ai.host-run-layout.v1"
|
|
264
|
+
}, null, 2);
|
|
265
|
+
}
|
|
266
|
+
function defaultHostRunAgents(plan) {
|
|
267
|
+
const role = (template, responsibility, reviewerRole) => ({
|
|
268
|
+
roleTemplate: template,
|
|
269
|
+
responsibility,
|
|
270
|
+
handoffPolicy: {
|
|
271
|
+
mode: reviewerRole ? "review-required" : "one-of-us",
|
|
272
|
+
reviewerRole,
|
|
273
|
+
escalation: "coordinator",
|
|
274
|
+
acceptanceRequired: Boolean(reviewerRole)
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
const definitions = {
|
|
278
|
+
ceo: defaultHostRunAgent(plan, "ceo", "Planner", "24/7", "high", "Turn the run goal into a small backlog, assign work, review progress, and produce a concise final deliverable.", role("planner", "Plan the run, route work to the right role, and close the loop with the human.")),
|
|
279
|
+
cto: defaultHostRunAgent(plan, "cto", "Reviewer", "on-demand", "high", "Review architecture-sensitive changes, enforce capsule acceptance criteria, and decide whether work is ready to merge.", role("reviewer", "Review scope, correctness, acceptance evidence, and merge readiness.")),
|
|
280
|
+
dev: defaultHostRunAgent(plan, "dev", "Builder", "on-demand", "standard", "Implement assigned code, docs, or analysis tasks end-to-end and report exact files or outputs changed.", role("builder", "Build assigned work inside the agreed scope and hand off for review.", "reviewer")),
|
|
281
|
+
devops: defaultHostRunAgent(plan, "devops", "Ops", "on-demand", "standard", "Keep build, test, CI, release, and local environment tasks moving with reproducible commands.", role("ops", "Handle environment, queue, release, audit, and operational safety tasks.", "reviewer")),
|
|
282
|
+
feedback: defaultHostRunAgent(plan, "feedback", "Researcher", "on-demand", "standard", "Review outputs, identify gaps, and convert useful observations into concrete follow-up tasks.", role("researcher", "Collect evidence, find gaps, and turn observations into structured follow-up tasks.")),
|
|
283
|
+
marketing: defaultHostRunAgent(plan, "marketing", "Doc Writer", "on-demand", "standard", "Keep user-facing docs, release notes, and product narrative aligned with shipped changes.", role("doc-writer", "Write verified user-facing documentation, release notes, and briefs.", "reviewer")),
|
|
284
|
+
tester: defaultHostRunAgent(plan, "tester", "Tester", "on-demand", "standard", "Design and run verification, regression, and release-readiness checks with reproducible commands.", role("tester", "Verify assigned work and record commands, evidence, and residual risk.", "reviewer"))
|
|
285
|
+
};
|
|
286
|
+
if (plan.spec.roleProfile === "small")
|
|
287
|
+
return [definitions.ceo, definitions.dev, definitions.cto];
|
|
288
|
+
if (plan.spec.roleProfile === "engineering")
|
|
289
|
+
return [definitions.ceo, definitions.cto, definitions.dev, definitions.tester, definitions.devops, definitions.feedback];
|
|
290
|
+
if (plan.spec.roleProfile === "product")
|
|
291
|
+
return [definitions.ceo, definitions.feedback, definitions.marketing, definitions.cto];
|
|
292
|
+
return [definitions.ceo, definitions.dev, definitions.cto, definitions.tester, definitions.devops, definitions.feedback, definitions.marketing];
|
|
293
|
+
}
|
|
294
|
+
function defaultHostRunAgent(plan, id, name, lifecycle, tier, role, workflow) {
|
|
295
|
+
return {
|
|
296
|
+
id,
|
|
297
|
+
name,
|
|
298
|
+
role,
|
|
299
|
+
...workflow,
|
|
300
|
+
lifecycle,
|
|
301
|
+
tier,
|
|
302
|
+
systemPrompt: `${role}\n\n${defaultHostRunCliInstructions(plan)}`
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
function defaultHostRunCliInstructions(plan) {
|
|
306
|
+
return [
|
|
307
|
+
"Use `king-ai recv` for messages, `king-ai task list` for work state, and `king-ai send <agent> \"<message>\"` to coordinate.",
|
|
308
|
+
"Use `king-ai host decision-create` or `king-ai send human --type decision \"<question>\"` when a human choice is required.",
|
|
309
|
+
"For repository changes, prefer a capsule-shaped handoff: owner, branch/worktree, allowed paths, acceptance criteria, reviewer, and verification commands.",
|
|
310
|
+
`Keep outputs under ${plan.layout.outputDir} unless the task explicitly asks for repository changes.`
|
|
311
|
+
].join("\n");
|
|
312
|
+
}
|
|
313
|
+
async function writeMissingAgentGuides(plan) {
|
|
314
|
+
let agents = [];
|
|
315
|
+
try {
|
|
316
|
+
const config = JSON.parse(await readFile(plan.layout.configPath, "utf8"));
|
|
317
|
+
if (Array.isArray(config.agents))
|
|
318
|
+
agents = config.agents;
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
return [];
|
|
322
|
+
}
|
|
323
|
+
const written = [];
|
|
324
|
+
for (const agent of agents) {
|
|
325
|
+
const id = typeof agent.id === "string" && agent.id.trim()
|
|
326
|
+
? agent.id.trim()
|
|
327
|
+
: typeof agent.name === "string" && agent.name.trim()
|
|
328
|
+
? agent.name.trim()
|
|
329
|
+
: undefined;
|
|
330
|
+
if (!id)
|
|
331
|
+
continue;
|
|
332
|
+
const agentDir = join(plan.layout.workspaceRoot, id);
|
|
333
|
+
const guidePath = join(agentDir, "AGENT.md");
|
|
334
|
+
if (existsSync(guidePath))
|
|
335
|
+
continue;
|
|
336
|
+
await mkdir(agentDir, { recursive: true });
|
|
337
|
+
await writeFile(guidePath, defaultAgentGuideText(agent, plan), "utf8");
|
|
338
|
+
written.push(guidePath);
|
|
339
|
+
}
|
|
340
|
+
return written;
|
|
341
|
+
}
|
|
342
|
+
function defaultAgentGuideText(agent, plan) {
|
|
343
|
+
const name = typeof agent.name === "string" && agent.name.trim()
|
|
344
|
+
? agent.name.trim()
|
|
345
|
+
: typeof agent.id === "string" && agent.id.trim()
|
|
346
|
+
? agent.id.trim()
|
|
347
|
+
: "Agent";
|
|
348
|
+
const role = typeof agent.systemPrompt === "string" && agent.systemPrompt.trim()
|
|
349
|
+
? agent.systemPrompt.trim()
|
|
350
|
+
: typeof agent.role === "string" && agent.role.trim()
|
|
351
|
+
? agent.role.trim()
|
|
352
|
+
: "Work through assigned local run tasks and report concrete results.";
|
|
353
|
+
return [
|
|
354
|
+
`# ${name}`,
|
|
355
|
+
"",
|
|
356
|
+
role,
|
|
357
|
+
"",
|
|
358
|
+
"## Operating Rules",
|
|
359
|
+
"",
|
|
360
|
+
"- Read current messages with `king-ai recv` before starting new work.",
|
|
361
|
+
"- Check assigned work with `king-ai task list` and update the team through `king-ai send`.",
|
|
362
|
+
"- For repository edits, work from a private branch or worktree and keep the changed paths inside the agreed capsule scope.",
|
|
363
|
+
"- Before marking work done, record the acceptance evidence: commands run, files changed, reviewer needed, and any export path.",
|
|
364
|
+
`- Put files you create under ${plan.layout.outputDir} unless the task explicitly asks for repository changes.`
|
|
365
|
+
].join("\n") + "\n";
|
|
366
|
+
}
|
|
367
|
+
async function copyDirectoryContents(sourceDir, targetDir) {
|
|
368
|
+
await mkdir(targetDir, { recursive: true });
|
|
369
|
+
for (const entry of await readdir(sourceDir)) {
|
|
370
|
+
await cp(join(sourceDir, entry), join(targetDir, entry), {
|
|
371
|
+
recursive: true,
|
|
372
|
+
force: true,
|
|
373
|
+
dereference: false
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
function redactConfigSecrets(value) {
|
|
378
|
+
if (!value || typeof value !== "object")
|
|
379
|
+
return;
|
|
380
|
+
for (const key of Object.keys(value)) {
|
|
381
|
+
if (/^(apiKey|api_key|token|secret|password)$/i.test(key)) {
|
|
382
|
+
delete value[key];
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
redactConfigSecrets(value[key]);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export interface HostRunMetaData {
|
|
2
|
+
schema: "king-ai.host-run-meta.v1";
|
|
3
|
+
status: string;
|
|
4
|
+
runId: string;
|
|
5
|
+
goal?: string;
|
|
6
|
+
preparedAt?: string;
|
|
7
|
+
updatedAt?: string;
|
|
8
|
+
completedAt?: string;
|
|
9
|
+
maxLoops?: number | "infinite";
|
|
10
|
+
actualLoops?: number;
|
|
11
|
+
detail?: string;
|
|
12
|
+
command?: string;
|
|
13
|
+
exitCode?: number;
|
|
14
|
+
session?: Record<string, unknown>;
|
|
15
|
+
paths?: Record<string, unknown>;
|
|
16
|
+
config?: Record<string, unknown>;
|
|
17
|
+
[key: string]: unknown;
|
|
18
|
+
}
|
|
19
|
+
export interface HostRunMetaReadInput {
|
|
20
|
+
file?: string;
|
|
21
|
+
outputDir?: string;
|
|
22
|
+
}
|
|
23
|
+
export interface HostRunMetaReadResult {
|
|
24
|
+
file: string;
|
|
25
|
+
meta: HostRunMetaData | null;
|
|
26
|
+
exists: boolean;
|
|
27
|
+
}
|
|
28
|
+
export interface HostRunMetaUpdateInput extends HostRunMetaReadInput {
|
|
29
|
+
runId: string;
|
|
30
|
+
status: string;
|
|
31
|
+
actualLoops?: number;
|
|
32
|
+
detail?: string;
|
|
33
|
+
command?: string;
|
|
34
|
+
exitCode?: number;
|
|
35
|
+
now?: () => Date;
|
|
36
|
+
}
|
|
37
|
+
export declare function hostRunMetaPathForOutputDir(outputDir: string): string;
|
|
38
|
+
export declare function resolveHostRunMetaPath(input?: HostRunMetaReadInput): string;
|
|
39
|
+
export declare function readHostRunMeta(input?: HostRunMetaReadInput): Promise<HostRunMetaReadResult>;
|
|
40
|
+
export declare function formatHostRunMeta(result: HostRunMetaReadResult): string;
|
|
41
|
+
export declare function updateHostRunMeta(input: HostRunMetaUpdateInput): Promise<HostRunMetaReadResult>;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
export function hostRunMetaPathForOutputDir(outputDir) {
|
|
4
|
+
return join(resolve(outputDir), "meta.json");
|
|
5
|
+
}
|
|
6
|
+
export function resolveHostRunMetaPath(input = {}) {
|
|
7
|
+
if (input.file && input.file.trim())
|
|
8
|
+
return resolve(input.file);
|
|
9
|
+
const outputDir = input.outputDir && input.outputDir.trim() ? input.outputDir : "deliverables";
|
|
10
|
+
return hostRunMetaPathForOutputDir(outputDir);
|
|
11
|
+
}
|
|
12
|
+
export async function readHostRunMeta(input = {}) {
|
|
13
|
+
const file = resolveHostRunMetaPath(input);
|
|
14
|
+
const text = await readFile(file, "utf8").catch((err) => {
|
|
15
|
+
if (err && typeof err === "object" && "code" in err && err.code === "ENOENT")
|
|
16
|
+
return undefined;
|
|
17
|
+
throw err;
|
|
18
|
+
});
|
|
19
|
+
if (text === undefined) {
|
|
20
|
+
return {
|
|
21
|
+
file,
|
|
22
|
+
meta: null,
|
|
23
|
+
exists: false
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
file,
|
|
28
|
+
meta: parseHostRunMeta(text),
|
|
29
|
+
exists: true
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export function formatHostRunMeta(result) {
|
|
33
|
+
if (!result.meta)
|
|
34
|
+
return `host run meta: ${result.file}\nnot found`;
|
|
35
|
+
return [
|
|
36
|
+
`host run meta: ${result.file}`,
|
|
37
|
+
`run: ${result.meta.runId}`,
|
|
38
|
+
`status: ${result.meta.status}`,
|
|
39
|
+
result.meta.goal ? `goal: ${result.meta.goal}` : "",
|
|
40
|
+
result.meta.preparedAt ? `prepared: ${result.meta.preparedAt}` : "",
|
|
41
|
+
result.meta.maxLoops !== undefined ? `loops: ${result.meta.actualLoops ?? 0}/${result.meta.maxLoops}` : ""
|
|
42
|
+
].filter(Boolean).join("\n");
|
|
43
|
+
}
|
|
44
|
+
export async function updateHostRunMeta(input) {
|
|
45
|
+
const file = resolveHostRunMetaPath(input);
|
|
46
|
+
const existing = await readHostRunMeta({ file });
|
|
47
|
+
const now = (input.now ?? (() => new Date()))().toISOString();
|
|
48
|
+
const base = existing.meta ?? {
|
|
49
|
+
schema: "king-ai.host-run-meta.v1",
|
|
50
|
+
status: input.status,
|
|
51
|
+
runId: input.runId
|
|
52
|
+
};
|
|
53
|
+
const next = {
|
|
54
|
+
...base,
|
|
55
|
+
schema: "king-ai.host-run-meta.v1",
|
|
56
|
+
runId: base.runId || input.runId,
|
|
57
|
+
status: cleanString(input.status) ?? base.status,
|
|
58
|
+
updatedAt: now,
|
|
59
|
+
actualLoops: input.actualLoops !== undefined ? Math.max(0, Math.floor(input.actualLoops)) : base.actualLoops,
|
|
60
|
+
detail: cleanString(input.detail) ?? base.detail,
|
|
61
|
+
command: cleanString(input.command) ?? base.command,
|
|
62
|
+
exitCode: normalizeExitCode(input.exitCode) ?? base.exitCode,
|
|
63
|
+
completedAt: isTerminalStatus(input.status) ? now : base.completedAt
|
|
64
|
+
};
|
|
65
|
+
await mkdir(dirname(file), { recursive: true });
|
|
66
|
+
await writeFile(file, `${JSON.stringify(dropUndefined({ ...next }), null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
67
|
+
return {
|
|
68
|
+
file,
|
|
69
|
+
meta: next,
|
|
70
|
+
exists: true
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function parseHostRunMeta(text) {
|
|
74
|
+
try {
|
|
75
|
+
const parsed = JSON.parse(text);
|
|
76
|
+
if (!parsed || parsed.schema !== "king-ai.host-run-meta.v1" || typeof parsed.runId !== "string")
|
|
77
|
+
return null;
|
|
78
|
+
return {
|
|
79
|
+
...parsed,
|
|
80
|
+
schema: "king-ai.host-run-meta.v1",
|
|
81
|
+
status: typeof parsed.status === "string" ? parsed.status : "",
|
|
82
|
+
runId: parsed.runId,
|
|
83
|
+
goal: typeof parsed.goal === "string" ? parsed.goal : undefined,
|
|
84
|
+
preparedAt: typeof parsed.preparedAt === "string" ? parsed.preparedAt : undefined,
|
|
85
|
+
updatedAt: typeof parsed.updatedAt === "string" ? parsed.updatedAt : undefined,
|
|
86
|
+
completedAt: typeof parsed.completedAt === "string" ? parsed.completedAt : undefined,
|
|
87
|
+
maxLoops: parsed.maxLoops === "infinite" || typeof parsed.maxLoops === "number" ? parsed.maxLoops : undefined,
|
|
88
|
+
actualLoops: typeof parsed.actualLoops === "number" ? parsed.actualLoops : undefined,
|
|
89
|
+
detail: typeof parsed.detail === "string" ? parsed.detail : undefined,
|
|
90
|
+
command: typeof parsed.command === "string" ? parsed.command : undefined,
|
|
91
|
+
exitCode: typeof parsed.exitCode === "number" ? parsed.exitCode : undefined,
|
|
92
|
+
session: parsed.session && typeof parsed.session === "object" ? parsed.session : undefined,
|
|
93
|
+
paths: parsed.paths && typeof parsed.paths === "object" ? parsed.paths : undefined,
|
|
94
|
+
config: parsed.config && typeof parsed.config === "object" ? parsed.config : undefined
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function cleanString(value) {
|
|
102
|
+
return typeof value === "string" && value.trim() ? value.trim().slice(0, 1000) : undefined;
|
|
103
|
+
}
|
|
104
|
+
function normalizeExitCode(value) {
|
|
105
|
+
if (value === undefined)
|
|
106
|
+
return undefined;
|
|
107
|
+
const parsed = Number(value);
|
|
108
|
+
return Number.isFinite(parsed) ? Math.floor(parsed) : undefined;
|
|
109
|
+
}
|
|
110
|
+
function isTerminalStatus(value) {
|
|
111
|
+
return value === "completed" || value === "failed" || value === "cancelled";
|
|
112
|
+
}
|
|
113
|
+
function dropUndefined(value) {
|
|
114
|
+
return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
|
|
115
|
+
}
|