Skip to content
Open
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
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ LAN URL to paste into CodeWatch — the upcoming mobile + watch companion app.
- [Key Integrations](#key-integrations)
- [How It Works](#how-it-works)
- [Features](#features)
- [User Intent Kit](#user-intent-kit)
- [Quick Start](#quick-start)
- [IDE-Specific Setup](#ide-specific-setup)
- [Claude Code CLI](#claude-code-cli)
Expand Down Expand Up @@ -112,6 +113,12 @@ Run allowlisted commands in a named tmux session, capture output + exit code.

No dependencies. Node.js ≥ 18 only.

## User Intent Kit

IAK now includes **User Intent Kit (UIK)** - the human-intent contract layer that sits above the existing tool primitives. Where IAK gives agents safe access to rooms, sessions, and tools, UIK gives humans a structured way to express goals, constraints, approval gates, ownership, receipts, escalation, and done criteria. A free-form chat request becomes a reviewable, replayable `Intent` the moment those seven slots are filled.

UIK ships as `@thinkoff/uik` inside this repo (`packages/uik/`) with TypeScript types and a `validateIntent()` helper. It composes with the existing MCP tools (`room_post`, `room_recent`, `room_ack`, `tmux_run`, the confirmation registry) rather than replacing them. See [`docs/UIK.md`](docs/UIK.md) for the design and a worked example.

## IDE-Specific Setup

Choose the guide for your AI environment:
Expand Down
91 changes: 91 additions & 0 deletions docs/UIK.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# User Intent Kit (UIK)

UIK is the human-intent contract layer shipped inside IDE Agent Kit. Where IAK
gives agents safe access to tools, rooms, and sessions, UIK gives humans a
structured way to express what they actually want done so an agent can execute
on it without guesswork.

## What UIK is

An Intent is a small JSON object that names a goal, the constraints around it,
who owns it, what counts as "done", which actions require human approval, what
to record as receipts, and how to escalate when things go wrong. Agents read
Intents the same way they read MCP tool calls: structured, validated, replayable.
A free-form chat request becomes a UIK Intent the moment a human (or an agent
on their behalf) fills in the seven required slots.

## Why it lives in IAK

IAK already owns the substrate: room I/O (`room_post`, `room_recent`,
`room_ack`), session control (`tmux_run`, `wake_ide`, `wake_remote`), the
confirmation registry, and the receipts log. UIK is the layer humans speak in;
IAK is the layer agents act in. Shipping UIK as a separate repo would orphan
both halves. Inside IAK, an Intent flows naturally into the existing primitives:
constraints become tool allowlists, approval gates become confirmation registry
entries, receipts append to the existing receipts JSONL.

## The seven elements of an Intent

1. **Goal** - one-sentence statement of the outcome ("ship a PR that fixes #214").
2. **Constraints** - hard limits: budget, allowed tools, time window, files
off-limits, "no force push", etc.
3. **Approval gates** - named decision points where the agent must stop and ask
a specific human (e.g. before merging, before sending an external email).
4. **Ownership** - the agent handle responsible for execution (`@claudemb`)
and the human accountable for outcome (`@petrus`).
5. **Receipts** - what evidence to record per step: PR URL, commit hash, room
message id, file diff. Appended to IAK's existing receipts log.
6. **Escalation** - who to wake and how when blocked: room handle, gate URL,
timeout before auto-escalation.
7. **Done criteria** - testable conditions for completion. Non-empty array.
"PR merged AND tests green AND owner acked in #thinkoff-development".

## Raw chat vs UIK Intent

Raw chat:

> @claudemb fix the flaky test in payments_test.go and ship it

Same task as UIK Intent:

```ts
{
goal: "Fix flaky payments_test.go and land a PR",
constraints: [
{ kind: "tool_allowlist", value: ["git", "go test", "gh pr"] },
{ kind: "no_force_push", value: true }
],
approvalGates: [
{ name: "before_merge", approver: "@petrus" }
],
owner: { agent: "@claudemb", human: "@petrus" },
receipts: [
{ kind: "pr_url" }, { kind: "commit_hash" }, { kind: "test_output" }
],
escalation: { room: "thinkoff-development", timeoutMinutes: 30 },
doneCriteria: [
{ check: "PR merged" },
{ check: "CI green on main" }
]
}
```

The Intent is reviewable, diffable, and replayable. The chat line is not.

## Composition with IAK MCP tools

Once an Intent is accepted, its lifecycle maps onto the tools IAK already
exposes. `room_post` announces the Intent and posts progress receipts.
`room_recent` plus `room_ack` confirm escalation reads. Approval gates register
with the existing confirmation registry (`POST /intent` on the iak-mcp-daemon)
and surface in the CodeWatch / GroupMind Approve/Deny UI. `tmux_run` executes
allowlisted commands the constraints permit. Each step appends to the receipts
JSONL keyed by Intent id.

UIK does not replace any IAK primitive. It is the schema that decides which
ones to call, in which order, with whose permission, and what to record.

See `packages/uik/` for the type definitions and `validateIntent()` helper.
A concrete reference implementation of approval-gate execution and the
`intent_create / intent_status / intent_close` MCP tools will follow once the
shape settles.
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,13 @@
"scripts": {
"test": "node --test test/*.test.mjs",
"start": "node bin/cli.mjs serve",
"mcp": "node bin/iak-mcp.mjs"
"mcp": "node bin/iak-mcp.mjs",
"build:uik": "npm run build -w @thinkoff/uik",
"test:uik": "npm test -w @thinkoff/uik --if-present"
},
"workspaces": [
"packages/*"
],
"keywords": [
"ide",
"agent",
Expand Down
50 changes: 50 additions & 0 deletions packages/uik/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# @thinkoff/uik

User Intent Kit - the human-intent contract layer of [IDE Agent Kit](../../README.md).

UIK turns free-form chat requests into structured `Intent` objects: goal,
constraints, approval gates, ownership, receipts, escalation, done criteria.
Agents executing inside IAK read Intents as the source of truth for what a
human actually asked for.

Full design doc: [`docs/UIK.md`](../../docs/UIK.md).

## Install

This package is part of the IAK repo and ships with it. Build it locally with:

```bash
cd packages/uik
npx tsc
```

## Example

```ts
import { validateIntent, type Intent } from "@thinkoff/uik";

const intent: Intent = {
goal: "Fix flaky payments_test.go and land a PR",
constraints: [
{ kind: "tool_allowlist", value: ["git", "go test", "gh pr"] },
],
approvalGates: [
{ name: "before_merge", approver: "@petrus" },
],
owner: { agent: "@claudemb", human: "@petrus" },
receipts: [{ kind: "pr_url" }, { kind: "commit_hash" }],
escalation: { room: "thinkoff-development", timeoutMinutes: 30 },
doneCriteria: [{ check: "PR merged and CI green on main" }],
};

const result = validateIntent(intent);
if (!result.ok) {
console.error("invalid intent:", result.errors);
}
```

## Status

Seed package: types and a minimal validator. The MCP tools
(`intent_create`, `intent_status`, `intent_close`) and the approval-gate
executor land in a follow-up once the shape stabilises.
32 changes: 32 additions & 0 deletions packages/uik/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"name": "@thinkoff/uik",
"version": "0.1.0",
"description": "User Intent Kit - human-intent contract layer for IDE Agent Kit",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./intent": {
"types": "./dist/intent.d.ts",
"import": "./dist/intent.js"
}
},
"scripts": {
"build": "tsc",
"prepare": "tsc",
"prepublishOnly": "tsc"
},
"files": [
"dist",
"src"
],
"devDependencies": {
"@types/node": "^25.3.2",
"typescript": "^5.9.3"
},
"license": "AGPL-3.0-only"
}
12 changes: 12 additions & 0 deletions packages/uik/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export type {
Intent,
Constraint,
ApprovalGate,
Owner,
Receipt,
Escalation,
DoneCriterion,
ValidationResult,
} from "./intent.js";

export { validateIntent } from "./intent.js";
171 changes: 171 additions & 0 deletions packages/uik/src/intent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
/**
* User Intent Kit - core type definitions.
*
* An Intent is the structured form of a human request: goal, constraints,
* approval gates, ownership, receipts, escalation, and done criteria.
*
* See docs/UIK.md in the repo root for the full design rationale.
*/

/** Hard limits the agent must respect while executing the Intent. */
export interface Constraint {
/**
* Constraint family. Common values:
* - `tool_allowlist` - value is string[] of permitted tool names.
* - `tool_denylist` - value is string[] of forbidden tool names.
* - `time_window` - value is { startIso: string; endIso: string }.
* - `budget_usd` - value is number, max spend.
* - `paths_off_limit` - value is string[] of glob patterns.
* - `no_force_push` - value is true.
* Custom kinds are allowed; consumers may ignore unknown kinds.
*/
kind: string;
value: unknown;
note?: string;
}

/** A named decision point where the agent must pause and ask a specific human. */
export interface ApprovalGate {
/** Stable name for this gate, used in receipts and the confirmation registry. */
name: string;
/** Handle of the human (or agent) whose approval is required, e.g. `@petrus`. */
approver: string;
/** Optional human-readable prompt shown in the approval UI. */
prompt?: string;
/** Optional auto-deny timeout in minutes. */
timeoutMinutes?: number;
}

/** Who runs the Intent and who is accountable for the outcome. */
export interface Owner {
/** Agent handle responsible for execution, e.g. `@claudemb`. */
agent: string;
/** Human handle accountable for the outcome, e.g. `@petrus`. */
human: string;
}

/** A piece of evidence to record as the Intent progresses. */
export interface Receipt {
/**
* Receipt kind. Common values: `pr_url`, `commit_hash`, `room_message_id`,
* `file_diff`, `test_output`, `tool_call`, `external_link`.
*/
kind: string;
/** Optional label shown in the receipts log. */
label?: string;
}

/** How to escalate when the Intent is blocked or stalled. */
export interface Escalation {
/** Room handle to ping, e.g. `thinkoff-development`. */
room: string;
/** Optional explicit handle(s) to mention. */
mention?: string[];
/** Minutes of inactivity before auto-escalation. */
timeoutMinutes?: number;
/** Optional URL to a gate / dashboard / runbook. */
gateUrl?: string;
}

/** A testable condition that, when true, means the Intent is complete. */
export interface DoneCriterion {
/** Plain-language description of the check. */
check: string;
/** Optional structured probe an agent can evaluate (URL, shell snippet, etc.). */
probe?: string;
}

/** The full Intent contract. */
export interface Intent {
/** Optional stable id; if absent, the executor assigns one. */
id?: string;
/** One-sentence statement of the outcome. */
goal: string;
/** Hard limits on execution. */
constraints: Constraint[];
/** Decision points requiring human approval. */
approvalGates: ApprovalGate[];
/** Execution and accountability. */
owner: Owner;
/** Evidence to record. */
receipts: Receipt[];
/** Escalation path. */
escalation: Escalation;
/** Non-empty list of completion checks. */
doneCriteria: DoneCriterion[];
/** Optional free-form context the agent may use. */
notes?: string;
}

export interface ValidationResult {
ok: boolean;
errors: string[];
}

/**
* Minimal validation: required fields present, ownership is a non-empty
* agent handle, doneCriteria is a non-empty array. Deeper semantic checks
* (constraint families, approver existence) are left to the executor.
*/
export function validateIntent(intent: Intent): ValidationResult {
const errors: string[] = [];

if (!intent || typeof intent !== "object") {
return { ok: false, errors: ["intent must be an object"] };
}

if (typeof intent.goal !== "string" || intent.goal.trim().length === 0) {
errors.push("goal must be a non-empty string");
}

if (!Array.isArray(intent.constraints)) {
errors.push("constraints must be an array");
}

if (!Array.isArray(intent.approvalGates)) {
errors.push("approvalGates must be an array");
}

if (!intent.owner || typeof intent.owner !== "object") {
errors.push("owner is required");
} else {
if (
typeof intent.owner.agent !== "string" ||
intent.owner.agent.trim().length === 0
) {
errors.push("owner.agent must be a non-empty agent handle");
}
if (
typeof intent.owner.human !== "string" ||
intent.owner.human.trim().length === 0
) {
errors.push("owner.human must be a non-empty handle");
}
}

if (!Array.isArray(intent.receipts)) {
errors.push("receipts must be an array");
}

if (!intent.escalation || typeof intent.escalation !== "object") {
errors.push("escalation is required");
} else if (
typeof intent.escalation.room !== "string" ||
intent.escalation.room.trim().length === 0
) {
errors.push("escalation.room must be a non-empty room handle");
}

if (!Array.isArray(intent.doneCriteria) || intent.doneCriteria.length === 0) {
errors.push("doneCriteria must be a non-empty array");
} else {
for (let i = 0; i < intent.doneCriteria.length; i++) {
const dc = intent.doneCriteria[i];
if (!dc || typeof dc.check !== "string" || dc.check.trim().length === 0) {
errors.push(`doneCriteria[${i}].check must be a non-empty string`);
}
}
}

return { ok: errors.length === 0, errors };
}
Loading
Loading