diff --git a/.changeset/unified-project-agents.md b/.changeset/unified-project-agents.md new file mode 100644 index 000000000..9b892e914 --- /dev/null +++ b/.changeset/unified-project-agents.md @@ -0,0 +1,26 @@ +--- +"@sapiom/harness": minor +"@sapiom/harness-desktop": patch +--- + +Unify Agent Studio project sessions around one ordinary coding-agent identity, make the project name open the shared Agent Map, and seed new projects through a durable, retry-safe bootstrap in the first `Plan Agents` session. + +**Breaking for embedders** (minor while `@sapiom/harness` is pre-1.0): +`HarnessSession.agentMapIdentity` is now the role-neutral +`ProjectAgentSession { projectId, userId, sessionId }`; `role` and `assignment` +are no longer present. Valid persisted `planning` metadata is migrated into the +optional `projectBootstrap` lifecycle field and then removed. The deprecated +planner-message alias now returns `ProjectBootstrapMetadata | null`, with +`projectId`, `userId`, `targetSessionId`, and `bootstrap` replacing the former +nested `identity` and `greeting` fields. + +**Migration:** stop branching on `agentMapIdentity.role` or `.assignment`, read +optional `projectBootstrap` only for bootstrap status, and handle `metadata: +null` from the compatibility alias—or move to the generic session routes. An +embedder that already owns a new session's first prompt should send +`initialUserInputPending: true` in the same `CreateSessionRequest`, so automatic +bootstrap yields before launch. New telemetry consumers should recognize the +neutral `project_agent.*` and `project_bootstrap.*` events. Valid legacy state +keeps its session/provider IDs, cwd, title, transcript, and Canvas; malformed or +conflicting authority is retained and fails closed. Downgrading does not restore +the former planner coordinator semantics. diff --git a/packages/harness-desktop/scripts/smoke-agent-stub.cjs b/packages/harness-desktop/scripts/smoke-agent-stub.cjs new file mode 100644 index 000000000..9c4facb66 --- /dev/null +++ b/packages/harness-desktop/scripts/smoke-agent-stub.cjs @@ -0,0 +1,60 @@ +const fs = require("node:fs"); + +function environmentCapturePath(base, sessionId) { + return `${base}.${encodeURIComponent(sessionId)}.json`; +} + +function captureAgentEnvironment(env = process.env) { + const base = env.SAPIOM_SMOKE_AGENT_ENV; + const sessionId = env.SAPIOM_HARNESS_SESSION_ID; + if (!base || !sessionId) return null; + + const file = environmentCapturePath(base, sessionId); + const snapshot = { + schemaVersion: 1, + sessionId, + variableCount: Object.keys(env).length, + hasEsbuildBinaryPath: Object.prototype.hasOwnProperty.call( + env, + "ESBUILD_BINARY_PATH", + ), + hasPath: typeof env.PATH === "string" && env.PATH.length > 0, + }; + fs.writeFileSync(file, `${JSON.stringify(snapshot)}\n`, { + encoding: "utf8", + mode: 0o600, + }); + return file; +} + +function runSessionStartHook() { + try { + const settingsIndex = process.argv.indexOf("--settings"); + const settingsPath = + settingsIndex > -1 ? process.argv[settingsIndex + 1] : null; + if (!settingsPath) return; + const settings = JSON.parse(fs.readFileSync(settingsPath, "utf8")); + const command = settings.hooks.SessionStart[0].hooks[0].command; + const { execFileSync, execSync } = require("node:child_process"); + if (process.platform === "win32") { + const bash = "C:\\Program Files\\Git\\bin\\bash.exe"; + if (fs.existsSync(bash)) { + execFileSync(bash, ["-c", command], { stdio: "ignore" }); + } else { + execSync(command, { stdio: "ignore" }); + } + } else { + execFileSync("/bin/sh", ["-c", command], { stdio: "ignore" }); + } + } catch { + // A failed hook is exactly what checkSessionCreate's ready poll reports. + } +} + +module.exports = { captureAgentEnvironment, environmentCapturePath }; + +if (require.main === module) { + captureAgentEnvironment(); + runSessionStartHook(); + setTimeout(() => process.exit(0), 3000); +} diff --git a/packages/harness-desktop/scripts/smoke.sh b/packages/harness-desktop/scripts/smoke.sh index 8ce6e15f6..2a5a6c790 100755 --- a/packages/harness-desktop/scripts/smoke.sh +++ b/packages/harness-desktop/scripts/smoke.sh @@ -75,52 +75,31 @@ export SAPIOM_SMOKE_OUT="$(native "$report_file")" # check can assert the WHOLE readiness chain (settings → hook command → node # resolution under the hook shell → POST → ready), the exact seam that broke # silently on Windows and dropped every held first prompt. -cat > "$smoke_home/stub-agent.js" <<'STUBJS' -const fs = require("fs"); -const envFile = process.env.SAPIOM_SMOKE_AGENT_ENV; -if (envFile) fs.writeFileSync(envFile, Object.entries(process.env).map(([k, v]) => k + "=" + v).join("\n") + "\n"); -try { - const i = process.argv.indexOf("--settings"); - const settingsPath = i > -1 ? process.argv[i + 1] : null; - if (settingsPath) { - const settings = JSON.parse(fs.readFileSync(settingsPath, "utf8")); - const command = settings.hooks.SessionStart[0].hooks[0].command; - const { execFileSync, execSync } = require("child_process"); - if (process.platform === "win32") { - const bash = "C:\\Program Files\\Git\\bin\\bash.exe"; - if (fs.existsSync(bash)) execFileSync(bash, ["-c", command], { stdio: "ignore" }); - else execSync(command, { stdio: "ignore" }); - } else { - execFileSync("/bin/sh", ["-c", command], { stdio: "ignore" }); - } - } -} catch { - // A failed hook is exactly what the ready-poll in checkSessionCreate reports. -} -setTimeout(() => process.exit(0), 3000); -STUBJS +cp "$here/smoke-agent-stub.cjs" "$smoke_home/stub-agent.cjs" if [ "$(uname -s)" != "Linux" ] && [ "$(uname -s)" != "Darwin" ]; then # Shaped like an npm shim on purpose — a `.cmd` that runs `node