From e5a913ca99a797fe74d58e718b777362d7ceae7e Mon Sep 17 00:00:00 2001 From: Petrus Pennanen Date: Wed, 20 May 2026 02:20:17 +0200 Subject: [PATCH 1/2] feat(uik): introduce User Intent Kit as an IAK feature - docs/UIK.md: design doc for the human-intent contract layer, the seven elements of an Intent, raw chat vs UIK example, composition with existing IAK MCP tools. - packages/uik: seed TypeScript package (@thinkoff/uik) with Intent / Constraint / ApprovalGate / Owner / Receipt / Escalation / DoneCriterion interfaces and a minimal validateIntent() helper. Mirrors packages/core conventions (tsconfig, exports, license). - packages/uik/README.md: pointer to docs/UIK.md plus a short example. - README.md: new "User Intent Kit" section and ToC entry; no other changes. Ships UIK through IAK rather than as a separate repo, per the room decision: IAK provides the substrate (rooms, sessions, tools, receipts, confirmations); UIK is the contract humans speak in. Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 7 ++ docs/UIK.md | 91 ++++++++++++++++++++ packages/uik/README.md | 50 +++++++++++ packages/uik/package.json | 32 +++++++ packages/uik/src/index.ts | 12 +++ packages/uik/src/intent.ts | 171 +++++++++++++++++++++++++++++++++++++ packages/uik/tsconfig.json | 14 +++ 7 files changed, 377 insertions(+) create mode 100644 docs/UIK.md create mode 100644 packages/uik/README.md create mode 100644 packages/uik/package.json create mode 100644 packages/uik/src/index.ts create mode 100644 packages/uik/src/intent.ts create mode 100644 packages/uik/tsconfig.json diff --git a/README.md b/README.md index fc9c011..629f406 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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: diff --git a/docs/UIK.md b/docs/UIK.md new file mode 100644 index 0000000..f568321 --- /dev/null +++ b/docs/UIK.md @@ -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. diff --git a/packages/uik/README.md b/packages/uik/README.md new file mode 100644 index 0000000..12ff897 --- /dev/null +++ b/packages/uik/README.md @@ -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. diff --git a/packages/uik/package.json b/packages/uik/package.json new file mode 100644 index 0000000..f034ffe --- /dev/null +++ b/packages/uik/package.json @@ -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" +} diff --git a/packages/uik/src/index.ts b/packages/uik/src/index.ts new file mode 100644 index 0000000..7ba7d73 --- /dev/null +++ b/packages/uik/src/index.ts @@ -0,0 +1,12 @@ +export type { + Intent, + Constraint, + ApprovalGate, + Owner, + Receipt, + Escalation, + DoneCriterion, + ValidationResult, +} from "./intent.js"; + +export { validateIntent } from "./intent.js"; diff --git a/packages/uik/src/intent.ts b/packages/uik/src/intent.ts new file mode 100644 index 0000000..7876d13 --- /dev/null +++ b/packages/uik/src/intent.ts @@ -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 }; +} diff --git a/packages/uik/tsconfig.json b/packages/uik/tsconfig.json new file mode 100644 index 0000000..93e7d5e --- /dev/null +++ b/packages/uik/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*.ts"] +} From 46c99a1f49f1e5926d3cdc748432d5d1dba5a5eb Mon Sep 17 00:00:00 2001 From: petrus Date: Wed, 20 May 2026 02:50:28 +0200 Subject: [PATCH 2/2] chore(uik): wire packages/* as npm workspaces and add build:uik / test:uik scripts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to PR #13. Per @ether's review note: building UIK from the repo root should Just Work, not require a cd into packages/uik. Adds: - "workspaces": ["packages/*"] in root package.json so npm install at root installs UIK's devDeps and links @thinkoff/uik into the workspace graph. - "build:uik" → npm run build -w @thinkoff/uik - "test:uik" → npm test -w @thinkoff/uik --if-present (no UIK tests yet; --if-present keeps this a no-op until tests are added). Verified locally: `npm install --ignore-scripts && npm run build:uik` from repo root produces packages/uik/dist/index.js cleanly. No other workspace side effects: only packages/uik exists under packages/ today. Co-Authored-By: Claude Opus 4.7 (1M context) --- package.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 0ebba0a..b9ee2ba 100644 --- a/package.json +++ b/package.json @@ -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",