@huynle/hub 0.7.3 → 0.8.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/CHANGELOG.md +229 -0
- package/Dockerfile +7 -2
- package/README.md +153 -24
- package/package.json +5 -3
- package/packages/claude-code/package.json +1 -1
- package/packages/claude-code/src/daemon.ts +8 -1
- package/packages/claude-code/src/launcher.ts +29 -9
- package/packages/claude-code/src/profile.ts +173 -0
- package/packages/claude-code/src/sessions.ts +22 -4
- package/packages/cli/dist/hub-cli.js +6474 -3470
- package/packages/cli/package.json +1 -1
- package/packages/cli/src/artifact-jobs-mcp.ts +226 -0
- package/packages/cli/src/artifacts-mcp.ts +160 -0
- package/packages/cli/src/bootstrap/enrollment/enroll.ts +20 -8
- package/packages/cli/src/bootstrap/enrollment/policy.ts +125 -12
- package/packages/cli/src/bootstrap/enrollment/status.ts +3 -2
- package/packages/cli/src/cli.ts +95 -13
- package/packages/cli/src/format.ts +48 -10
- package/packages/cli/src/http.ts +1 -0
- package/packages/cli/src/intents.ts +23 -0
- package/packages/cli/src/login.ts +20 -6
- package/packages/cli/src/mcp.ts +380 -9
- package/packages/cli/src/messaging.ts +3 -1
- package/packages/cli/src/setup/commands.ts +3 -2
- package/packages/cli/src/setup/runtime.ts +3 -2
- package/packages/cli/src/team-intake-mcp.ts +158 -0
- package/packages/cli/src/teams-mcp.ts +403 -0
- package/packages/cli/src/watchlist.ts +135 -0
- package/packages/codex/package.json +1 -1
- package/packages/codex/src/profile.ts +163 -0
- package/packages/codex/src/sessions.ts +12 -0
- package/packages/codex/src/spoke.ts +19 -1
- package/packages/node/package.json +1 -1
- package/packages/node/src/config.ts +96 -2
- package/packages/node/src/opencode-warm.ts +325 -0
- package/packages/node/src/run.ts +27 -1
- package/packages/preset/package.json +1 -1
- package/packages/preset/src/profile.ts +34 -0
- package/packages/preset/src/spoke.ts +4 -0
- package/packages/protocol/package.json +1 -1
- package/packages/protocol/src/artifacts.ts +142 -0
- package/packages/protocol/src/concierge.ts +366 -0
- package/packages/protocol/src/contracts.ts +104 -0
- package/packages/protocol/src/graph.ts +70 -0
- package/packages/protocol/src/hub-workspace.ts +201 -0
- package/packages/protocol/src/index.ts +169 -6
- package/packages/protocol/src/launch.ts +22 -0
- package/packages/protocol/src/profiles.ts +301 -0
- package/packages/protocol/src/teams.ts +204 -0
- package/packages/runtime-openshell/package.json +1 -1
- package/packages/runtime-sdk/package.json +1 -1
- package/packages/sdk/package.json +1 -1
- package/packages/sdk/src/address.ts +50 -2
- package/packages/sdk/src/artifact-jobs.ts +194 -0
- package/packages/sdk/src/artifacts.ts +187 -0
- package/packages/sdk/src/client.ts +226 -5
- package/packages/sdk/src/concierge.ts +88 -0
- package/packages/sdk/src/errors.ts +3 -1
- package/packages/sdk/src/index.ts +5 -0
- package/packages/sdk/src/team-intake.ts +122 -0
- package/packages/sdk/src/teams.ts +346 -0
- package/packages/sdk/src/types.ts +548 -1
- package/packages/spoke-sdk/package.json +1 -1
- package/packages/spoke-sdk/src/index.ts +1 -0
- package/packages/spoke-sdk/src/profile-apply.ts +245 -0
- package/src/central/actions.ts +7 -0
- package/src/central/agent/acting-routes.ts +2 -1
- package/src/central/agent/acting-switch.ts +2 -1
- package/src/central/agent/acting.ts +215 -10
- package/src/central/agent/app-tokens.ts +3 -2
- package/src/central/agent/audit.ts +17 -0
- package/src/central/agent/body.ts +163 -0
- package/src/central/agent/decisions.ts +69 -2
- package/src/central/agent/delivery.ts +95 -15
- package/src/central/agent/directory-routes.ts +37 -7
- package/src/central/agent/directory.ts +70 -14
- package/src/central/agent/errors.ts +5 -0
- package/src/central/agent/events-routes.ts +21 -4
- package/src/central/agent/events.ts +11 -3
- package/src/central/agent/form-check.ts +85 -0
- package/src/central/agent/grant-requests.ts +2 -1
- package/src/central/agent/grant-routes.ts +4 -3
- package/src/central/agent/grants.ts +29 -2
- package/src/central/agent/idempotency.ts +133 -0
- package/src/central/agent/intent-routes.ts +116 -0
- package/src/central/agent/intents.ts +505 -0
- package/src/central/agent/interaction-graph-routes.ts +212 -0
- package/src/central/agent/interaction-graph.ts +865 -0
- package/src/central/agent/launch-engine.ts +16 -2
- package/src/central/agent/launch-routes.ts +6 -1
- package/src/central/agent/launch-spec.ts +14 -1
- package/src/central/agent/message-routes.ts +2 -1
- package/src/central/agent/messages.ts +27 -2
- package/src/central/agent/names.ts +12 -2
- package/src/central/agent/read-routes.ts +86 -19
- package/src/central/agent/routes.ts +176 -5
- package/src/central/agent/webhook-routes.ts +2 -1
- package/src/central/agent-data.ts +4 -0
- package/src/central/artifacts/access.ts +44 -0
- package/src/central/artifacts/blob-store.ts +113 -0
- package/src/central/artifacts/blob-support.ts +98 -0
- package/src/central/artifacts/events.ts +27 -0
- package/src/central/artifacts/host-caps.ts +162 -0
- package/src/central/artifacts/host.ts +315 -0
- package/src/central/artifacts/idempotency.ts +109 -0
- package/src/central/artifacts/jobs-routes.ts +773 -0
- package/src/central/artifacts/jobs.ts +344 -0
- package/src/central/artifacts/maintenance.ts +103 -0
- package/src/central/artifacts/routes.ts +360 -0
- package/src/central/artifacts/store.ts +776 -0
- package/src/central/artifacts/workspaces.ts +31 -0
- package/src/central/backend/file-blobs.ts +377 -0
- package/src/central/backend/index.ts +12 -3
- package/src/central/backend/memory-blobs.ts +116 -0
- package/src/central/backend/postgres-blobs.ts +513 -0
- package/src/central/backend/postgres-migrate.ts +33 -0
- package/src/central/backend/postgres-state.ts +19 -0
- package/src/central/backend/types.ts +8 -0
- package/src/central/concierge/activity.ts +142 -0
- package/src/central/concierge/arm.ts +63 -0
- package/src/central/concierge/board-render.ts +190 -0
- package/src/central/concierge/board-results.ts +169 -0
- package/src/central/concierge/board-routes.ts +339 -0
- package/src/central/concierge/board.ts +249 -0
- package/src/central/concierge/context.ts +221 -0
- package/src/central/concierge/entitlements.ts +13 -0
- package/src/central/concierge/guard.ts +120 -0
- package/src/central/concierge/inbound.ts +89 -0
- package/src/central/concierge/index.ts +84 -0
- package/src/central/concierge/notes.ts +41 -0
- package/src/central/concierge/notifier.ts +986 -0
- package/src/central/concierge/prompt.ts +130 -0
- package/src/central/concierge/proposals.ts +262 -0
- package/src/central/concierge/push-remap.ts +49 -0
- package/src/central/concierge/routes.ts +62 -0
- package/src/central/concierge/schedule.ts +113 -0
- package/src/central/concierge/service.ts +243 -0
- package/src/central/concierge/session-routes.ts +69 -0
- package/src/central/concierge/settings.ts +217 -0
- package/src/central/concierge/supervisor.ts +740 -0
- package/src/central/concierge/tiers.ts +260 -0
- package/src/central/concierge/watchlist-routes.ts +277 -0
- package/src/central/concierge/watchlist.ts +273 -0
- package/src/central/config.ts +7 -1
- package/src/central/http.ts +633 -50
- package/src/central/mcp-http.ts +10 -21
- package/src/central/profiles/apply.ts +333 -0
- package/src/central/profiles/content.ts +174 -0
- package/src/central/profiles/events.ts +40 -0
- package/src/central/profiles/idempotency.ts +14 -0
- package/src/central/profiles/import.ts +139 -0
- package/src/central/profiles/routes.ts +356 -0
- package/src/central/profiles/store.ts +597 -0
- package/src/central/profiles/usage-ports.ts +120 -0
- package/src/central/profiles/usage.ts +364 -0
- package/src/central/push.ts +7 -0
- package/src/central/registry.ts +43 -6
- package/src/central/store.ts +4 -0
- package/src/central/teams/collections.ts +34 -0
- package/src/central/teams/discovery.ts +346 -0
- package/src/central/teams/draft-edits.ts +46 -0
- package/src/central/teams/events.ts +46 -0
- package/src/central/teams/intake-routes.ts +100 -0
- package/src/central/teams/intake.ts +772 -0
- package/src/central/teams/policy.ts +231 -0
- package/src/central/teams/routes.ts +516 -0
- package/src/central/teams/seats.ts +272 -0
- package/src/central/teams/staffing.ts +888 -0
- package/src/central/teams/store.ts +667 -0
- package/src/central/teams/validation.ts +256 -0
- package/src/central/workspaces/board.ts +408 -0
- package/src/central/workspaces/checkpoints.ts +316 -0
- package/src/central/workspaces/coordinator.ts +841 -0
- package/src/central/workspaces/events.ts +85 -0
- package/src/central/workspaces/lifecycle.ts +93 -0
- package/src/central/workspaces/master.ts +490 -0
- package/src/central/workspaces/members.ts +632 -0
- package/src/central/workspaces/routes.ts +354 -0
- package/src/central/workspaces/sidechats-routes.ts +410 -0
- package/src/central/workspaces/sidechats.ts +482 -0
- package/src/central/workspaces/store.ts +571 -0
- package/src/central/workspaces/supervision.ts +355 -0
- package/src/index.ts +97 -15
- package/src/spoke/concierge-agent.ts +33 -0
- package/src/spoke/deliver.ts +22 -4
- package/src/spoke/fork-revert.ts +142 -0
- package/src/spoke/hub-rpc.ts +41 -5
- package/src/spoke/hub-tool.ts +383 -18
- package/src/spoke/listing.ts +47 -4
- package/src/spoke/profile-apply.ts +298 -0
- package/src/spoke/session-context.ts +176 -0
- package/src/spoke/spoke.ts +319 -20
- package/src/spoke/timeline.ts +6 -1
- package/src/spoke/tracker.ts +23 -1
- package/src/spoke/who-teams.ts +81 -0
- package/src/tui-messaging.ts +32 -18
- package/web/dist/assets/ArtifactPage-BS0gVkfu.js +1 -0
- package/web/dist/assets/ArtifactPane-PLYHNs6s.js +1 -0
- package/web/dist/assets/ArtifactWorkbench-9eTj1CeW.css +1 -0
- package/web/dist/assets/ArtifactWorkbench-LkDyJ5Dv.js +6 -0
- package/web/dist/assets/Library-CtxRqTjo.js +2 -0
- package/web/dist/assets/Library-DYsFj_5c.css +1 -0
- package/web/dist/assets/ProfilesDestination-BYiHKLmc.js +1 -0
- package/web/dist/assets/TeamsPage-BwjsAAUC.js +1 -0
- package/web/dist/assets/Watchtower-C4swrWVs.js +2 -0
- package/web/dist/assets/Watchtower-CTgqHk5g.css +1 -0
- package/web/dist/assets/index-DaH5GUdV.js +31 -0
- package/web/dist/assets/index-WWuKfWxL.css +1 -0
- package/web/dist/assets/live-BUofsQpz.css +1 -0
- package/web/dist/assets/live-YyTjE47g.js +5 -0
- package/web/dist/assets/pane-layout-CKC4I00k.js +1 -0
- package/web/dist/assets/plural-Bspvoqfq.js +1 -0
- package/web/dist/assets/renderer2d-CX5j3NcD.js +1 -0
- package/web/dist/assets/renderer3d-BXIVOvfi.js +5393 -0
- package/web/dist/assets/safe-area-DfPsOdWR.js +1 -0
- package/web/dist/assets/source-CvUHTc3O.js +1 -0
- package/web/dist/assets/ui-C9Igxzjm.js +1 -0
- package/web/dist/index.html +5 -2
- package/web/dist/sw.js +22 -2
- package/web/dist/assets/index-Dm8OmkaO.css +0 -1
- package/web/dist/assets/index-FUJbj_Du.js +0 -20
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// Profile application for Claude Code sessions (HWA-17; contract packages/protocol/src/profiles.ts, SDK
|
|
2
|
+
// spoke-sdk/src/profile-apply.ts). Only the native mechanisms this spoke already uses, nothing written to the
|
|
3
|
+
// user's Claude Code settings, no plugin installed, no permission changed, no session restarted:
|
|
4
|
+
//
|
|
5
|
+
// instructions model skills / commands
|
|
6
|
+
// hub-launched hook context, next prompt set_model control request skipped (note)
|
|
7
|
+
// terminal hook context, next prompt ignored (note: the user's) skipped (note)
|
|
8
|
+
//
|
|
9
|
+
// - Instructions: kept in the daemon's memory for the session only and handed to Claude Code as hook
|
|
10
|
+
// additionalContext at the session's next UserPromptSubmit (and again at SessionStart after a compaction, which
|
|
11
|
+
// drops earlier context). They come after the local CLAUDE.md files, framed by profileInstructionsBlock (local
|
|
12
|
+
// instructions take precedence). A newer version replaces them; one without instructions says the earlier ones
|
|
13
|
+
// no longer apply.
|
|
14
|
+
// - Model: a hub-launched session runs headless under the daemon (launcher.ts), which changes its model with the
|
|
15
|
+
// stream-json control request set_model and waits for Claude Code's answer (anthropic models only). A terminal
|
|
16
|
+
// session's model belongs to the user at the terminal (/model): ignored with a note.
|
|
17
|
+
// - Skills and commands: Claude Code loads them only from plugin and settings directories when a session starts;
|
|
18
|
+
// adding them would mean installing a plugin, writing settings or restarting the session, none of which the hub
|
|
19
|
+
// does on its own. Skipped with a note (partial beside other parts; alone the answer is unsupported). So no
|
|
20
|
+
// command is ever rendered here and `{{secret:NAME}}` references are never resolved.
|
|
21
|
+
// - Unknown and subagent sessions are rejected (a subagent shares its parent's context). Applies to one session run
|
|
22
|
+
// one at a time, in arrival order. A repeat of the applied digest answers the same without re-applying (SDK).
|
|
23
|
+
import type { ProfileApplyRequest, ProfileApplyResult } from "../../protocol/src/index.ts"
|
|
24
|
+
import { profileApplyResult, ProfileApplyHandler, type ProfileApplyOutcome, type ProfileCapabilities, type ProfilePlan } from "../../spoke-sdk/src/index.ts"
|
|
25
|
+
import { launchModel } from "./launcher.ts"
|
|
26
|
+
|
|
27
|
+
/** How the daemon runs a session: started by the hub (headless under the daemon) or by a user in a terminal. */
|
|
28
|
+
export type ClaudeSessionKind = "launched" | "terminal"
|
|
29
|
+
|
|
30
|
+
/** What profile application needs of the daemon. */
|
|
31
|
+
export interface ClaudeProfilePort {
|
|
32
|
+
/** The kind of a root session the daemon reports; undefined for unknown and subagent sessions. */
|
|
33
|
+
kind(sessionID: string): ClaudeSessionKind | undefined
|
|
34
|
+
/** Changes a hub-launched session's model (a Claude Code model ID); throws with Claude Code's reason. */
|
|
35
|
+
setModel(sessionID: string, model: string): Promise<void>
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ClaudeProfilesOptions {
|
|
39
|
+
/** Sessions whose profile state is kept (default 1000; the oldest are forgotten first). */
|
|
40
|
+
readonly sessionsMax?: number
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const BASE: Omit<ProfileCapabilities, "model"> = { instructions: true, skills: false, commands: false, live: true, renderAtInvocation: false }
|
|
44
|
+
|
|
45
|
+
/** What Claude Code sessions take, by kind. */
|
|
46
|
+
export const CLAUDE_PROFILE_CAPABILITIES: Readonly<Record<ClaudeSessionKind, ProfileCapabilities>> = {
|
|
47
|
+
launched: { ...BASE, model: true },
|
|
48
|
+
terminal: { ...BASE, model: false },
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const SESSIONS_MAX = 1000
|
|
52
|
+
const SKILLS_NOTE = "Claude Code loads skills and commands only from plugin or settings directories at session start; the hub installs no plugin, writes no settings and restarts no session"
|
|
53
|
+
const NEXT_PROMPT_NOTE = "instructions reach the model with the session's next prompt (hook context)"
|
|
54
|
+
const TERMINAL_MODEL_NOTE = "model ignored: a terminal session's model is the user's to change (/model)"
|
|
55
|
+
|
|
56
|
+
/** The Claude Code model ID of a profile model "provider/model"; throws unless Claude Code can run it. */
|
|
57
|
+
export function profileModelID(model: string): string {
|
|
58
|
+
const at = model.indexOf("/")
|
|
59
|
+
if (at <= 0 || at === model.length - 1) throw new Error(`model ${JSON.stringify(model)} is not provider/model`)
|
|
60
|
+
return launchModel({ providerID: model.slice(0, at), modelID: model.slice(at + 1) })!
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface Staged {
|
|
64
|
+
readonly profileID: string
|
|
65
|
+
readonly version: number
|
|
66
|
+
/** The effective profile's framed instructions (re-sent after a compaction). */
|
|
67
|
+
readonly instructions?: string
|
|
68
|
+
/** What the next prompt carries (the instructions, or a notice that earlier ones no longer apply). */
|
|
69
|
+
pending?: string
|
|
70
|
+
/** Some profile text reached the model already. */
|
|
71
|
+
delivered: boolean
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const header = (req: Pick<ProfileApplyRequest, "profileID" | "version">) => `<!-- hub profile ${req.profileID} v${req.version} (session-scoped) -->`
|
|
75
|
+
|
|
76
|
+
export class ClaudeProfiles {
|
|
77
|
+
readonly #port: ClaudeProfilePort
|
|
78
|
+
readonly #max: number
|
|
79
|
+
readonly #staged = new Map<string, Staged>()
|
|
80
|
+
readonly #queues = new Map<string, Promise<unknown>>()
|
|
81
|
+
readonly #handlers: Readonly<Record<ClaudeSessionKind, ProfileApplyHandler>>
|
|
82
|
+
|
|
83
|
+
constructor(port: ClaudeProfilePort, options: ClaudeProfilesOptions = {}) {
|
|
84
|
+
this.#port = port
|
|
85
|
+
this.#max = options.sessionsMax ?? SESSIONS_MAX
|
|
86
|
+
const handler = (kind: ClaudeSessionKind) =>
|
|
87
|
+
new ProfileApplyHandler({ capabilities: CLAUDE_PROFILE_CAPABILITIES[kind], apply: (plan, req) => this.#apply(kind, plan, req), remembered: this.#max })
|
|
88
|
+
this.#handlers = { launched: handler("launched"), terminal: handler("terminal") }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The `profileApply` op handler (HarnessSpoke handlers: announces the feature). */
|
|
92
|
+
readonly handle = (req: ProfileApplyRequest): Promise<ProfileApplyResult> => {
|
|
93
|
+
const previous = this.#queues.get(req.sessionID) ?? Promise.resolve()
|
|
94
|
+
const run = previous.then(() => this.#handle(req))
|
|
95
|
+
const tail = run.catch(() => undefined)
|
|
96
|
+
this.#queues.set(req.sessionID, tail)
|
|
97
|
+
void tail.then(() => {
|
|
98
|
+
if (this.#queues.get(req.sessionID) === tail) this.#queues.delete(req.sessionID)
|
|
99
|
+
})
|
|
100
|
+
return run
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The profile context a hook hands the model now, if any: at UserPromptSubmit what is pending (once); at
|
|
105
|
+
* SessionStart after a compaction (source "compact") the effective instructions again.
|
|
106
|
+
*/
|
|
107
|
+
context(sessionID: string, event: "UserPromptSubmit" | "SessionStart", source?: string): string | undefined {
|
|
108
|
+
const s = this.#staged.get(sessionID)
|
|
109
|
+
if (!s) return undefined
|
|
110
|
+
let text: string | undefined
|
|
111
|
+
if (event === "UserPromptSubmit") text = s.pending
|
|
112
|
+
else if (event === "SessionStart" && source === "compact") text = s.instructions
|
|
113
|
+
if (text === undefined) return undefined
|
|
114
|
+
s.pending = undefined
|
|
115
|
+
s.delivered = true
|
|
116
|
+
return text
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** The session ended: its profile state goes with it. */
|
|
120
|
+
ended(sessionID: string): void {
|
|
121
|
+
this.#staged.delete(sessionID)
|
|
122
|
+
this.#handlers.launched.forget(sessionID)
|
|
123
|
+
this.#handlers.terminal.forget(sessionID)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async #handle(req: ProfileApplyRequest): Promise<ProfileApplyResult> {
|
|
127
|
+
const kind = this.#port.kind(req.sessionID)
|
|
128
|
+
if (!kind) return profileApplyResult(req, "rejected", [`${req.sessionID} is not a Claude Code session this spoke reports (a subagent session follows its parent's context)`])
|
|
129
|
+
return this.#handlers[kind].handle(req)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async #apply(kind: ClaudeSessionKind, plan: ProfilePlan, req: ProfileApplyRequest): Promise<ProfileApplyOutcome> {
|
|
133
|
+
const notes: string[] = []
|
|
134
|
+
let modelFailed = false
|
|
135
|
+
if (req.content.skills.length || req.content.commands.length) notes.push(SKILLS_NOTE)
|
|
136
|
+
if (kind === "terminal" && req.content.model !== undefined) notes.push(TERMINAL_MODEL_NOTE)
|
|
137
|
+
if (plan.model !== undefined) {
|
|
138
|
+
let id: string | undefined
|
|
139
|
+
try {
|
|
140
|
+
id = profileModelID(plan.model)
|
|
141
|
+
} catch (cause) {
|
|
142
|
+
modelFailed = true
|
|
143
|
+
notes.push(`model ignored: ${message(cause)}`)
|
|
144
|
+
}
|
|
145
|
+
if (id !== undefined) {
|
|
146
|
+
try {
|
|
147
|
+
await this.#port.setModel(req.sessionID, id)
|
|
148
|
+
} catch (cause) {
|
|
149
|
+
modelFailed = true
|
|
150
|
+
notes.push(`model not changed: ${message(cause)}`)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (modelFailed && plan.instructions === undefined) return { status: "rejected", notes }
|
|
155
|
+
this.#stage(req, plan.instructions)
|
|
156
|
+
if (plan.instructions !== undefined) notes.push(NEXT_PROMPT_NOTE)
|
|
157
|
+
return { ...(modelFailed ? { status: "partial" as const } : {}), notes }
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Keeps the version's instructions for the session's next prompt (replacing an earlier version's). */
|
|
161
|
+
#stage(req: ProfileApplyRequest, instructions: string | undefined): void {
|
|
162
|
+
const before = this.#staged.get(req.sessionID)
|
|
163
|
+
const delivered = before?.delivered === true
|
|
164
|
+
let pending: string | undefined
|
|
165
|
+
if (instructions !== undefined) pending = delivered ? instructions.replace(header(req), `${header(req)}\nThis replaces earlier hub profile instructions in this session.`) : instructions
|
|
166
|
+
else if (delivered) pending = `${header(req)}\nHub agent profile ${req.profileID} version ${req.version} now applies to this session without instructions: earlier hub profile instructions in this session no longer apply.`
|
|
167
|
+
this.#staged.delete(req.sessionID)
|
|
168
|
+
this.#staged.set(req.sessionID, { profileID: req.profileID, version: req.version, ...(instructions !== undefined ? { instructions } : {}), ...(pending !== undefined ? { pending } : {}), delivered })
|
|
169
|
+
while (this.#staged.size > this.#max) this.#staged.delete(this.#staged.keys().next().value!)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const message = (cause: unknown) => (cause instanceof Error ? cause.message : String(cause))
|
|
@@ -28,6 +28,9 @@
|
|
|
28
28
|
// Hub-launched sessions (launched(): the daemon runs their claude headless, launcher.ts) are reported at create,
|
|
29
29
|
// before any hook; a prompt the hub writes to one (hubPrompt) is delivered with origin "hub", and the turn's result
|
|
30
30
|
// line ends a turn the hooks left running (turnEnded: an interrupt fires no Stop hook).
|
|
31
|
+
//
|
|
32
|
+
// Profiles (HWA-17, profile.ts): a session's pending profile instructions ride on its next UserPromptSubmit context
|
|
33
|
+
// (before hub messages), and again on SessionStart after a compaction; a session's end forgets its profile.
|
|
31
34
|
import { existsSync } from "node:fs"
|
|
32
35
|
import path from "node:path"
|
|
33
36
|
import { randomBytes } from "node:crypto"
|
|
@@ -65,6 +68,12 @@ export interface HookReply {
|
|
|
65
68
|
readonly env?: Record<string, string>
|
|
66
69
|
}
|
|
67
70
|
|
|
71
|
+
/** The session-scoped profile state (ClaudeProfiles). */
|
|
72
|
+
export interface ProfileContextPort {
|
|
73
|
+
context(sessionID: string, event: "UserPromptSubmit" | "SessionStart", source?: string): string | undefined
|
|
74
|
+
ended(sessionID: string): void
|
|
75
|
+
}
|
|
76
|
+
|
|
68
77
|
export interface SessionsOptions {
|
|
69
78
|
readonly spoke: SpokePort
|
|
70
79
|
/** How long SessionStart waits for the hub's session token (default 3 s). */
|
|
@@ -76,6 +85,8 @@ export interface SessionsOptions {
|
|
|
76
85
|
/** Context for the hub addresses in a user's prompt (design §2.4); undefined when there is none. */
|
|
77
86
|
readonly references?: (prompt: string, sessionID: string) => Promise<string | undefined>
|
|
78
87
|
readonly now?: () => number
|
|
88
|
+
/** Profile instructions for the hooks to carry (HWA-17). */
|
|
89
|
+
readonly profiles?: ProfileContextPort
|
|
79
90
|
}
|
|
80
91
|
|
|
81
92
|
const DEFAULT_TOKEN_WAIT_MS = 3000
|
|
@@ -143,6 +154,7 @@ export class ClaudeSessions {
|
|
|
143
154
|
readonly #projectOf: (cwd: string) => string | undefined
|
|
144
155
|
readonly #references?: SessionsOptions["references"]
|
|
145
156
|
readonly #now: () => number
|
|
157
|
+
readonly #profiles?: ProfileContextPort
|
|
146
158
|
/** By native session ID. */
|
|
147
159
|
readonly #sessions = new Map<string, Session>()
|
|
148
160
|
/** Hub session ID (root or child) → native root session ID. */
|
|
@@ -157,6 +169,7 @@ export class ClaudeSessions {
|
|
|
157
169
|
this.#projectOf = options.projectOf ?? gitRoot
|
|
158
170
|
this.#references = options.references
|
|
159
171
|
this.#now = options.now ?? Date.now
|
|
172
|
+
this.#profiles = options.profiles
|
|
160
173
|
}
|
|
161
174
|
|
|
162
175
|
/** Handles one hook input; never throws for a valid input. */
|
|
@@ -175,7 +188,7 @@ export class ClaudeSessions {
|
|
|
175
188
|
if (ctx.rewake) return this.#rewake(s, ctx)
|
|
176
189
|
switch (event) {
|
|
177
190
|
case "SessionStart":
|
|
178
|
-
return this.#sessionStart(s, created)
|
|
191
|
+
return this.#sessionStart(s, created, input.source)
|
|
179
192
|
case "UserPromptSubmit":
|
|
180
193
|
return this.#prompt(s, input)
|
|
181
194
|
case "PreToolUse":
|
|
@@ -330,15 +343,17 @@ export class ClaudeSessions {
|
|
|
330
343
|
return { id, title, directory: cwd, updated: this.#now(), owned: true, ...(parentID ? { parentID } : {}), ...(project ? { project } : {}) }
|
|
331
344
|
}
|
|
332
345
|
|
|
333
|
-
async #sessionStart(s: Session, _created: boolean): Promise<HookReply> {
|
|
346
|
+
async #sessionStart(s: Session, _created: boolean, source: string | undefined): Promise<HookReply> {
|
|
347
|
+
const profile = this.#profiles?.context(s.hubID, "SessionStart", source)
|
|
348
|
+
const output = profile ? { output: { hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: profile } } } : {}
|
|
334
349
|
const deadline = this.#now() + this.#tokenWaitMs
|
|
335
350
|
let token = this.#spoke.sessionToken(s.hubID)
|
|
336
351
|
while (!token && this.#now() < deadline) {
|
|
337
352
|
await new Promise((resolve) => setTimeout(resolve, TOKEN_POLL_MS))
|
|
338
353
|
token = this.#spoke.sessionToken(s.hubID)
|
|
339
354
|
}
|
|
340
|
-
if (!token) return
|
|
341
|
-
return { env: { ...(token.url ? { HUB_URL: token.url } : {}), HUB_SESSION_TOKEN: token.token } }
|
|
355
|
+
if (!token) return output
|
|
356
|
+
return { env: { ...(token.url ? { HUB_URL: token.url } : {}), HUB_SESSION_TOKEN: token.token }, ...output }
|
|
342
357
|
}
|
|
343
358
|
|
|
344
359
|
async #prompt(s: Session, input: HookInput): Promise<HookReply> {
|
|
@@ -361,6 +376,8 @@ export class ClaudeSessions {
|
|
|
361
376
|
if (fromUser) this.#emit({ type: "session.inbox.delivered", data: { sessionID: s.hubID, inboxID: `prompt_${turnID}`, origin: "user" } })
|
|
362
377
|
this.#release(s)
|
|
363
378
|
const parts: string[] = []
|
|
379
|
+
const profile = this.#profiles?.context(s.hubID, "UserPromptSubmit")
|
|
380
|
+
if (profile) parts.push(profile)
|
|
364
381
|
if (fromUser && this.#references && prompt.includes("@")) {
|
|
365
382
|
const note = await this.#references(prompt, s.hubID).catch(() => undefined)
|
|
366
383
|
if (note) parts.push(note)
|
|
@@ -517,6 +534,7 @@ export class ClaudeSessions {
|
|
|
517
534
|
this.#sessions.delete(native)
|
|
518
535
|
if (s.launch && this.#launches.get(s.launch) === s.hubID) this.#launches.delete(s.launch)
|
|
519
536
|
this.#spoke.endSession(s.hubID)
|
|
537
|
+
this.#profiles?.ended(s.hubID)
|
|
520
538
|
}
|
|
521
539
|
|
|
522
540
|
// ---------- permissions ----------
|