Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/atomic-project-state-migration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@sapiom/harness": patch
---

Migrate persisted project maps atomically to immutable version histories and role-neutral proposal attribution, with storage for shared build plans.
5 changes: 5 additions & 0 deletions .changeset/bootstrap-coordinator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@sapiom/harness": patch
---

Stage the complete bootstrap and user-input coordinator with recovery, FIFO delivery, and shutdown handling. Automatic bootstrap remains inactive until server integration.
5 changes: 5 additions & 0 deletions .changeset/bootstrap-storage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@sapiom/harness": patch
---

Stage internal durable bootstrap state, first-session claims, and project outbox storage. Automatic bootstrap remains inactive until the coordinator is connected.
5 changes: 5 additions & 0 deletions .changeset/durable-delegation-state.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@sapiom/harness": patch
---

Add internal durable delegation storage for reservations, exact child bindings, request receipts and release history. Retain unfinished private cleanup proof until cleanup completes.
5 changes: 5 additions & 0 deletions .changeset/durable-project-root-compatibility.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@sapiom/harness": patch
---

Resolve and retain durable project roots consistently for Studio and System Graph, including descendant sessions after a recent directory is evicted. Compare Windows drive and UNC roots without case sensitivity and migrate legacy case aliases while preserving durable project identities.
5 changes: 5 additions & 0 deletions .changeset/immutable-project-version-contracts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@sapiom/harness": patch
---

Add immutable Agent Map, build-plan and brief contracts with strict codecs and canonical digest helpers. These additive contracts prepare versioned project state for later activation.
6 changes: 6 additions & 0 deletions .changeset/one-studio-dialog-shell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@sapiom/harness": patch
"@sapiom/harness-desktop": patch
---

Studio's dialogs now behave the same as each other. Add existing agents, Add a project, Create an agent, Use a template, Remove project, End session and Clone agent share one shell, so Tab stays inside the open dialog instead of walking into the rail behind it, the page behind the dialog stops taking clicks and screen-reader attention while it is up, and focus opens on the dialog's subject rather than its close button. Closing a dialog hands focus back to the control that opened it — through the close button, the backdrop and Cancel, not only Escape — wherever that control is still on screen to receive it. Enter submits from a single-line field and Cmd/Ctrl+Enter from a text area, one rule everywhere, and the three confirmation dialogs take neither, so a removal is never one stray Return away. Dialog titles are one size instead of the two they had drifted into.
21 changes: 21 additions & 0 deletions .changeset/ordinary-project-session-identity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
"@sapiom/harness": minor
---

Give every Studio project session the same writable coding-agent prompt and
map capabilities, with durable project ownership revalidated before launch
and resume.

**Breaking for embedders** (minor while `@sapiom/harness` is pre-1.0):
`HarnessSession.agentMapIdentity` now exposes only
`ProjectAgentSession { projectId, userId, sessionId }`. Replace branches on
`role` and `assignment` with neutral project identity. Optional
`projectBootstrap` carries startup status without granting authority. Valid
persisted legacy metadata is normalized while session/provider IDs, cwd,
title, transcript, and Canvas are preserved. Malformed or conflicting
authority fails closed; unavailable project scope prevents resume until the
current owner and root binding are valid again.

Persisted bootstrap failures with `scope_unavailable` are recognized on
restart, so valid conversation metadata is retained and can resume after
scope is restored.
5 changes: 5 additions & 0 deletions .changeset/ordinary-session-input.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@sapiom/harness": patch
---

Track ordinary session input delivery and runtime ownership so partial input, preemption, stale ingest events, and shutdown are handled consistently.
5 changes: 5 additions & 0 deletions .changeset/owned-child-session-lifecycle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@sapiom/harness": patch
---

Add trusted child-session creation, recovery and closure with exact binding checks, plus exclusive Codex rollout attribution for simultaneous runtimes. This internal lifecycle support prepares project delegation.
5 changes: 5 additions & 0 deletions .changeset/project-bootstrap-activation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@sapiom/harness": patch
---

Activate one recoverable first-session map bootstrap for newly opened Studio projects. Durable project intent, input receipts, readiness, preemption, restart recovery, and shutdown now share the ordinary session lifecycle. Retire planner-session routes and metadata after migrating their persisted input queues; embedders should use the ordinary session create, resume, and input routes.
8 changes: 8 additions & 0 deletions .changeset/studio-created-agent-membership.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@sapiom/harness": patch
---

Register successfully scaffolded agents immediately under their creating Studio
project, including folders beside its root. Preserve the project's existing
conversation when selecting those agents, and restore membership from recorded
scaffold completions on restart without moving files or creating extra sessions.
12 changes: 12 additions & 0 deletions .changeset/studio-onboarding-followups.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"@sapiom/harness": patch
---

Deliver a new-agent request directly to Claude Code or Codex at startup after
scaffolding and attachment preparation, without requiring a second Enter or
placing internal authoring instructions in the user's prompt. Preserve project
scope during launch and retain the composer draft when preparation fails.

Strengthen shared Agent Map, build-plan, and writable subsession guidance while
keeping authoring and runtime capabilities primary. Document project-tool
contracts and replace the known stale Studio orientation at prompt delivery.
4 changes: 3 additions & 1 deletion .changeset/writable-project-subsessions.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,6 @@ atomically create one new binding and real session for the same delegation key.
Dormant eviction emits content-free release telemetry when it commits. If later
private-marker cleanup fails, the result remains truthfully `released`, includes
the bounded cleanup error, and retains exact proof for idempotent cleanup after
the indicated recovery.
the indicated recovery. Unfinished private cleanup remains discoverable by a
fresh bounded dormant sweep even after the original request receipt expires;
its proof becomes eligible for compaction only after the exact close succeeds.
47 changes: 45 additions & 2 deletions packages/harness/docs/shared-build-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,47 @@ map and build-plan MCP tools. Trusted `{ projectId, userId, sessionId }` scope
comes only from the private session capability; tool input cannot select or
override it.

## Coding-agent guidance and discovery

Agent delivery remains the priority: scaffold/check/local testing and runtime
capability guidance are not replaced by project coordination. The shared
`profiles/project-agent.ts` appendix teaches all nine project tools to every
ordinary writable project session, including focused children. MCP descriptions
and schema field descriptions carry the detailed request/reference contracts so
the startup prompt need not reproduce full schemas.

The Agent Map is distinct from the automatic per-agent Canvas. During
implementation, establish a real initial map once boundaries are clear and keep
meaningful agents, contracts, artifacts, and data flow current. Saving a new
`summary.md`, for example, warrants an artifact and writes/reads relationships
even without adding an agent. Read, validate, propose, and verify the saved map
before claiming it is current; validation alone does not persist anything.
Read-only requests still authorize no project-state mutation.

Use a build plan when coordination needs assignments, sequencing, or acceptance
criteria, not as a prerequisite for a small edit. Obtain exact references from
`build_plan_read({kind: "current"})`, not the compatibility map snapshot. Plan
tool input references omit `projectId`; delegation focus references include it.
After a map-version change, rebase an existing plan before applying plan edits.
Canonical brief refresh is already attempted after apply/rebase and can be
retried independently of the accepted plan.

Delegated sessions are writable execution contexts sharing the parent's cwd,
not automatically isolated worktrees. Specify disjoint ownership and written
deliverables. A ready session or acknowledged kickoff is not proof the task is
finished; inspect/test its deliverables. Creating a map node, plan, or brief
does not automatically delegate work.

Startup materialization upgrades only the exact known legacy base-prompt
revision pinned by `profiles/fixtures/legacy-system-prompt.md`; unknown/custom
remote prompts remain untouched. This prevents the old two-MCP, sample-project,
and stop-for-an-invitation orientation from returning while a backend still
serves that revision. The common project appendix also states current
orientation precedence. Keep the current fallback digest and backend served
copy synchronized when publishing; the fixture's historical digest must not
move with the current prompt pin. Local delivery tests do not prove a backend
deployment or that a model consistently follows the instructions.

## Digests and exact references

`GraphContentDigest` identifies canonical graph semantics without project,
Expand Down Expand Up @@ -147,8 +188,10 @@ capacity errors expose the explicit `release_dormant` recovery code; an
all-active live cap continues to require session inspection instead of suggesting
an inapplicable dormant cleanup. A bounded private-marker cleanup error may
accompany an already-`released` result because eviction is durable first. Exact
cleanup proof remains available so the same sweep can finish after the indicated
recovery or inspection without changing the release outcome.
cleanup proof remains available until the private close completes. After the
original sweep receipt expires, a fresh bounded `release-dormant` request also
retries unfinished cleanup without changing the release outcome or emitting a
second release event.

The coordinator waits for canonical adapter readiness and exact transcript
identity, then uses fenced spawn and delivery epochs to submit one kickoff.
Expand Down
13 changes: 13 additions & 0 deletions packages/harness/src/core/adapters/claude-code.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ describe("ClaudeCodeAdapter", () => {
adapter.detectBlockingPrompt("\x1b[1mDo you trust the files in this folder?\x1b[0m"),
).toBe(true);
expect(adapter.detectBlockingPrompt("Do you trust the files in this directory?")).toBe(true);
expect(adapter.detectBlockingPrompt("Quick safety check: Is this a project you created or one you trust?")).toBe(true);
expect(adapter.detectBlockingPrompt("Yes, I trust this folder")).toBe(true);
expect(adapter.detectBlockingPrompt("Choose the text style that looks best")).toBe(true);
expect(adapter.detectBlockingPrompt("Select login method:")).toBe(true);
expect(adapter.detectBlockingPrompt("> welcome, composer is ready")).toBe(false);
Expand Down Expand Up @@ -66,6 +68,17 @@ describe("ClaudeCodeAdapter", () => {
});

describe("launch/resume", () => {
it("hands the initial user task to the CLI once, separately from startup instructions", () => {
const adapter = new ClaudeCodeAdapter({ binary: "fake-claude" });
const opts = {
harnessSessionId: "first-task",
cwd: "/tmp/proj",
initialPrompt: "--help\nBuild my ticket triage agent",
};
expect(adapter.launch(opts).args.slice(-2)).toEqual(["--", opts.initialPrompt]);
expect(adapter.resume("native-id", opts).args).not.toContain(opts.initialPrompt);
});

it("builds a launch SpawnSpec with settings/mcp-config/system-prompt flags and unsets CLAUDECODE", async () => {
const promptDir = await mkdtemp(join(tmpdir(), "harness-claude-test-"));
const promptFile = join(promptDir, "prompt.txt");
Expand Down
4 changes: 4 additions & 0 deletions packages/harness/src/core/adapters/claude-code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,7 @@ function buildInteractiveConfigArgs(opts: LaunchOpts): string[] {
export const CLAUDE_BLOCKING_PROMPT_PATTERNS: readonly RegExp[] = [
// First-run / new-directory trust dialog.
/do\s+you\s+trust\s+the\s+files\s+in\s+this\s+(folder|directory)/i,
/quick\s*safety\s*check|yes,?\s*i\s*trust\s*this\s*folder/i,
// First-run theme picker.
/choose\s+the\s+text\s+style/i,
// Signed-out login flow.
Expand Down Expand Up @@ -475,6 +476,9 @@ export class ClaudeCodeAdapter implements HarnessAdapter {
if (opts.systemPromptFile) {
args.push("--append-system-prompt", readPromptFile(opts.systemPromptFile));
}
// A positional initial task is consumed by Claude after onboarding/trust.
// Do not paste it into the PTY or interpret leading dashes as CLI options.
if (opts.initialPrompt) args.push("--", opts.initialPrompt);
return {
command: this.binary,
args,
Expand Down
7 changes: 7 additions & 0 deletions packages/harness/src/core/adapters/codex.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ function userMessageLine(message: string): string {

describe("CodexAdapter", () => {
describe("launch/resume", () => {
it("passes the initial user task as one positional argument on fresh launch only", () => {
const adapter = new CodexAdapter({ binary: "fake-codex" });
const opts = { harnessSessionId: "first-task", cwd: "/tmp/proj", initialPrompt: "--help\nBuild a ticket triage agent" };
expect(adapter.launch(opts).args.slice(-2)).toEqual(["--", opts.initialPrompt]);
expect(adapter.resume("native-id", opts).args).not.toContain(opts.initialPrompt);
});

it("builds a launch SpawnSpec with update check off, never-ask approvals, workspace-write sandbox, and no env overrides", () => {
const adapter = new CodexAdapter({ binary: "fake-codex" });
const spec = adapter.launch({ harnessSessionId: "h1", cwd: "/tmp/proj" });
Expand Down
4 changes: 3 additions & 1 deletion packages/harness/src/core/adapters/codex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,9 +300,11 @@ export class CodexAdapter implements HarnessAdapter {
}

launch(opts: LaunchOpts): SpawnSpec {
const args = buildConfigArgs(opts);
if (opts.initialPrompt) args.push("--", opts.initialPrompt);
return {
command: this.binary,
args: buildConfigArgs(opts),
args,
// Codex has no analog to Claude's CLAUDECODE nested-agent guard; no env
// overrides are needed for a fresh launch.
env: opts.agentMapMcp
Expand Down
12 changes: 6 additions & 6 deletions packages/harness/src/core/agent-map-proposal-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ const stripUndefinedProperties = <T extends Record<string, unknown>>(
export const nodeRefSchema = z.union([
z.object({ nodeId: planNodeIdSchema }).strict(),
z.object({ draftRef: draftRefSchema }).strict(),
]);
]).describe("Use nodeId from a prior read/propose for existing nodes; draftRef points to a node added in this same atomic batch.");

const nodeChangesSchema = z
.object({
Expand All @@ -79,14 +79,14 @@ const relationshipChangesSchema = z
const addNodeSchema = z
.object({
kind: z.literal("add-node"),
draftRef: draftRefSchema,
draftRef: draftRefSchema.describe("Caller-chosen local name for this new node; use the returned allocated node ID in later batches."),
node: z
.object({
kind: z.enum(PLAN_NODE_KINDS),
kind: z.enum(PLAN_NODE_KINDS).describe("Architectural role: deployable agent, meaningful subagent, shared resource, connector, or input/output artifact. Not every internal function or file is a node."),
name: boundedText(160),
purpose: boundedText(2_000),
ownerAgent: nodeRefSchema.nullable(),
contractRefs: contractRefsSchema,
ownerAgent: nodeRefSchema.nullable().describe("Semantic owning agent when applicable; this does not grant coding authority or bind a runtime session."),
contractRefs: contractRefsSchema.describe("Relevant input/output/interface contracts; [] when none are established."),
})
.strict(),
})
Expand All @@ -113,7 +113,7 @@ const addRelationshipSchema = z
from: nodeRefSchema,
to: nodeRefSchema,
kind: z.enum(RELATIONSHIP_KINDS),
executionMode: z.enum(EXECUTION_MODES).nullable(),
executionMode: z.enum(EXECUTION_MODES).nullable().describe("Actual sequencing when applicable; use human-triggered for a manual handoff, not an invented automatic pipeline. null for non-execution relationships."),
contractRef: contractRefSchema.nullable(),
description: boundedText(2_000, true),
})
Expand Down
25 changes: 13 additions & 12 deletions packages/harness/src/core/build-plan-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,12 @@ const identityKey = (value: string | { clientRef: string }) =>
export const toolMapVersionRefSchema = z.object({
versionId: generatedId("mapv"),
contentDigest: digest,
}).strict();
}).strict().describe("Exact map version from build_plan_read: copy versionId and contentDigest only, omitting projectId. Never derive a digest from the numeric proposal version.");
export const toolPlanVersionRefSchema = z.object({
planId: generatedId("plan"),
versionId: generatedId("planv"),
semanticDigest: digest,
}).strict();
}).strict().describe("Exact plan reference from build_plan_read: copy planId, versionId, and semanticDigest only, omitting projectId.");

const focusedBriefSelectionSchema = z.object({
focusScope: z.object({
Expand All @@ -52,7 +52,7 @@ const focusedBriefSelectionSchema = z.object({

export const agentBriefRefreshRequestSchema = z.object({
schemaVersion: z.literal(1),
requestId: opaque,
requestId: opaque.describe("Caller-chosen identity: identical retries reuse this ID; changed request content needs a fresh ID."),
expectedMap: toolMapVersionRefSchema,
expectedPlan: toolPlanVersionRefSchema,
focus: z.discriminatedUnion("mode", [
Expand All @@ -61,7 +61,7 @@ export const agentBriefRefreshRequestSchema = z.object({
selections: unique(focusedBriefSelectionSchema,
(selection) => `${selection.focusScope.delegationKey}\0${selection.focusScope.parentScopeKey ?? ""}`),
}).strict(),
]),
]).describe("canonical compiles workstream briefs; focused compiles explicit ad-hoc selections. Does not inject a new prompt into a running child."),
}).strict();

const milestone = z.object({
Expand Down Expand Up @@ -129,14 +129,14 @@ export const buildPlanContentInputSchema = z.object({

const replaceContentOperation = z.object({
op: z.literal("replace-content"),
content: buildPlanContentInputSchema,
content: buildPlanContentInputSchema.describe("Complete replacement, not a patch: supply every collection, preserve unrelated intent, reuse existing IDs, and use {clientRef:'local-name'} for new plan-owned IDs."),
}).strict();

export const buildPlanApplyRequestSchema = z.object({
schemaVersion: z.literal(1),
requestId: opaque,
requestId: opaque.describe("Reuse for the same validate/apply request and identical retries; changed content needs a new ID."),
expectedMap: toolMapVersionRefSchema,
expectedPlan: toolPlanVersionRefSchema.nullable(),
expectedPlan: toolPlanVersionRefSchema.nullable().describe("Current buildPlan reference from build_plan_read, omitting projectId; null only if creating the first plan."),
operations: z.tuple([replaceContentOperation]),
}).strict();

Expand All @@ -149,11 +149,12 @@ const rebaseResolution = z.discriminatedUnion("kind", [

export const buildPlanRebaseRequestSchema = z.object({
schemaVersion: z.literal(1),
requestId: opaque,
requestId: opaque.describe("Stable ID for identical rebase retries; changed resolutions or source expectations need a new ID."),
expectedPlan: toolPlanVersionRefSchema,
fromMap: toolMapVersionRefSchema,
toMap: toolMapVersionRefSchema,
resolutions: unique(rebaseResolution, (resolution) => JSON.stringify(resolution)),
fromMap: toolMapVersionRefSchema.describe("The current plan's bound map: read plan.map and omit projectId."),
toMap: toolMapVersionRefSchema.describe("The latest map: read current.map and omit projectId, even when its content digest is unchanged."),
resolutions: unique(rebaseResolution, (resolution) => JSON.stringify(resolution))
.describe("Use [] when references remain valid; explicitly resolve invalidated node/assignment/dependency/repository references, preserving unrelated intent."),
}).strict();

export const buildPlanReadRequestSchema = z.discriminatedUnion("kind", [
Expand All @@ -168,7 +169,7 @@ export const buildPlanReadRequestSchema = z.discriminatedUnion("kind", [
* the request through `buildPlanReadRequestSchema` again.
*/
export const buildPlanReadToolInputSchema = z.object({
kind: z.enum(["current", "exact"]),
kind: z.enum(["current", "exact"]).describe("current: send only kind. exact: also supply planId, versionId, and semanticDigest for one immutable historical version."),
planId: generatedId("plan").optional(),
versionId: generatedId("planv").optional(),
semanticDigest: digest.optional(),
Expand Down
Loading
Loading