Skip to content
Merged
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
14 changes: 14 additions & 0 deletions .cursor/rules/agent-trust.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
description: Terminal49/API paved path and verification gate
alwaysApply: true
---

# Agent trust gate

- Extend OpenAPI endpoints, npm packages, and MCP tools through
`skills/agent-trust/references/paved-path.md`.
- Keep `skills/agent-trust/feature-map.json` synchronized with MCP tool
registrations, SDK package exports, and non-generated SDK/MCP docs routes.
- Run `npm run agent-verify` before handing off code changes. Do not claim a
passing result from `agent-verify:quick`.
- Keep generated files generated and credentials out of this public repository.
14 changes: 14 additions & 0 deletions .cursor/rules/pstack-onboarding.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
description: Repository-local P-Stack routing
alwaysApply: true
---

# P-Stack

For non-trivial Terminal49/API work, use the installed P-Stack plugin's
`/poteto-mode` workflow when available. Use `/how` for unfamiliar subsystems
and `/reflect` to turn durable lessons into executable guards. The agent-trust
rule owns the repository verification gate.

Model and reasoning-budget choices belong in the user-local file created by
`/setup-pstack`; never commit personal model slugs.
9 changes: 2 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ on:
push:
branches: [main, master, feature/**]
pull_request:
branches: [main, master, feature/**, cursor/vite-plus-toolchain-cdbd]

permissions:
contents: read
Expand All @@ -24,12 +23,8 @@ jobs:
cache-dependency-path: package-lock.json
- name: Install workspace dependencies
run: npm ci
- name: Check root and every workspace
run: npx vp run check
- name: Test every workspace
run: npx vp run test
- name: Build every workspace
run: npx vp run build
- name: Verify agent paved path
run: npm run agent-verify

# Consumer compatibility: the standalone SDK lockfile on every supported
# Node version, with no workspace tooling involved.
Expand Down
8 changes: 8 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,14 @@ For substantive documentation writing, use the repo-local skill at `skills/termi
- CI (`.github/workflows/ci.yml`) runs build + test + lint for both packages.
- Running the MCP server locally + testing tool calls with Claude Desktop (stdio and gateway paths): [packages/mcp/LOCAL_DEV.md](packages/mcp/LOCAL_DEV.md). Gateway env template: `.env.local.example`.

### Agent paved path

- Read [`skills/agent-trust/SKILL.md`](skills/agent-trust/SKILL.md) before adding or changing an OpenAPI endpoint, npm package, MCP tool, SDK entrypoint, or SDK/MCP docs route.
- Extend the repository through [`skills/agent-trust/references/paved-path.md`](skills/agent-trust/references/paved-path.md); do not create alternate registration, generation, or verification flows.
- Keep [`skills/agent-trust/feature-map.json`](skills/agent-trust/feature-map.json) synchronized in the same commit. CI rejects MCP tool, SDK export, and docs-route drift.
- Run `npm run agent-verify` before handoff. `npm run agent-verify:quick` is for iteration and is not the completion gate.
- P-Stack is optional user tooling, not a repository dependency. Install it with `/add-plugin pstack`, then run `/setup-pstack` to choose a user-local model budget. Repository wiring is in [`skills/agent-trust/references/pstack.md`](skills/agent-trust/references/pstack.md).

---

## MCP Auth Gateway notes
Expand Down
917 changes: 554 additions & 363 deletions Terminal49-API.postman_collection.json

Large diffs are not rendered by default.

93 changes: 93 additions & 0 deletions bin/agent-verify.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
#!/usr/bin/env node

import { spawnSync } from 'node:child_process';
import { resolve } from 'node:path';
import { fileURLToPath } from 'node:url';

const root = resolve(fileURLToPath(new URL('..', import.meta.url)));
const mode = process.argv[2];
if (mode && mode !== '--quick') {
console.error('Usage: bin/agent-verify.mjs [--quick]');
process.exit(2);
}

const steps = [
{
name: 'Node runtime',
run: () => {
const [major, minor] = process.versions.node.split('.').map(Number);
if (major !== 24 || minor < 11) {
throw new Error(
`Node 24.11+ is required; received ${process.versions.node}`,
);
}
Comment on lines +18 to +23

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Supported Node versions fail verification

The repository declares support for Node 24.x, but this required completion gate rejects Node 24.0–24.10. Contributors using those supported versions cannot pass npm run agent-verify. Align the verifier’s minimum with the declared engine range.

Prompt To Fix With AI
This is a comment left during a code review.
Path: bin/agent-verify.mjs
Line: 18-23

Comment:
**Supported Node versions fail verification**

The repository declares support for Node `24.x`, but this required completion gate rejects Node 24.0–24.10. Contributors using those supported versions cannot pass `npm run agent-verify`. Align the verifier’s minimum with the declared engine range.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Codex Fix in Claude Code

},
},
{
name: 'Agent trust guards',
command: ['node', 'bin/check-agent-trust.mjs'],
},
{
name: 'Vite+ and TypeScript checks',
command: ['npx', '--no-install', 'vp', 'run', 'check'],
},
{
name: 'Workspace builds',
command: ['npx', '--no-install', 'vp', 'run', 'build'],
},
...(mode === '--quick'
? []
: [
{
name: 'Workspace tests',
command: ['npx', '--no-install', 'vp', 'run', 'test'],
},
]),
{
name: 'OpenAPI SDK type generation',
command: ['node', 'bin/check-openapi-types.mjs'],
},
{
name: 'MCP initialize and tools/list smoke',
command: ['npm', 'run', 'test:protocol', '--workspace', '@terminal49/mcp'],
env: { MCP_PROTOCOL_VERSION: '2025-06-18' },
},
];

function runStep(step) {
const startedAt = performance.now();
process.stdout.write(`\n[RUN ] ${step.name}\n`);
if (step.run) {
step.run();
} else {
const [command, ...args] = step.command;
const result = spawnSync(command, args, {
cwd: root,
env: { ...process.env, ...step.env },
stdio: 'inherit',
});
if (result.error) {
throw result.error;
}
if (result.status !== 0) {
throw new Error(`command exited ${result.status ?? 'without a status'}`);
}
}
const duration = Math.round(performance.now() - startedAt);
process.stdout.write(`[PASS] ${step.name} (${duration} ms)\n`);
}

for (const step of steps) {
try {
runStep(step);
} catch (error) {
console.error(
`[FAIL] ${step.name}: ${error instanceof Error ? error.message : String(error)}`,
);
process.exit(1);
}
}

console.log(
`\nPASS agent-verify${mode === '--quick' ? ' --quick' : ''}: ${steps.length} steps`,
);
171 changes: 171 additions & 0 deletions bin/check-agent-trust.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
#!/usr/bin/env node

import { existsSync, readFileSync, readdirSync } from 'node:fs';
import { join, relative, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';

const root = resolve(fileURLToPath(new URL('..', import.meta.url)));

function readJson(path) {
return JSON.parse(readFileSync(resolve(root, path), 'utf8'));
}

function sorted(values) {
return [...values].sort((left, right) => left.localeCompare(right));
}

function normalizedVersion(version) {
return version.replace(/^=/, '');
}

function assertEqual(label, actual, expected) {
const actualJson = JSON.stringify(sorted(actual));
const expectedJson = JSON.stringify(sorted(expected));
if (actualJson !== expectedJson) {
throw new Error(
`${label} drifted.\nExpected: ${expectedJson}\nActual: ${actualJson}`,
);
}
}

function collectFiles(directory) {
const files = [];
for (const entry of readdirSync(directory, { withFileTypes: true })) {
const path = join(directory, entry.name);
if (entry.isDirectory()) {
files.push(...collectFiles(path));
} else {
files.push(path);
}
}
Comment on lines +33 to +40

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Workspace scan includes generated files

To find Vite config files, this function walks every file under packages and sdks without excluding generated output or nested dependencies. That adds unnecessary filesystem work to both verification modes and could mistake a dependency’s config for a workspace-owned one. Limit the scan to workspace source and config locations.

Prompt To Fix With AI
This is a comment left during a code review.
Path: bin/check-agent-trust.mjs
Line: 33-40

Comment:
**Workspace scan includes generated files**

To find Vite config files, this function walks every file under `packages` and `sdks` without excluding generated output or nested dependencies. That adds unnecessary filesystem work to both verification modes and could mistake a dependency’s config for a workspace-owned one. Limit the scan to workspace source and config locations.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Codex Fix in Claude Code

return files;
}

function collectStrings(value) {
if (typeof value === 'string') {
return [value];
}
if (Array.isArray(value)) {
return value.flatMap(collectStrings);
}
if (value && typeof value === 'object') {
return Object.values(value).flatMap(collectStrings);
}
return [];
}

function checkFeatureMap() {
const featureMap = readJson('skills/agent-trust/feature-map.json');
const serverSource = readFileSync(
resolve(root, 'packages/mcp/src/server.ts'),
'utf8',
);
const registeredTools = [
...serverSource.matchAll(/server\.registerTool\(\s*'([^']+)'/g),
].map((match) => match[1]);
Comment on lines +59 to +65

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Tool registrations can escape guard

The guard reads only server.ts and matches single-quoted server.registerTool(...) calls. An otherwise valid registration using double quotes or another module would not be counted, so verification could pass while the feature map omits a public tool. Make the check account for equivalent registration forms and locations.

Prompt To Fix With AI
This is a comment left during a code review.
Path: bin/check-agent-trust.mjs
Line: 59-65

Comment:
**Tool registrations can escape guard**

The guard reads only `server.ts` and matches single-quoted `server.registerTool(...)` calls. An otherwise valid registration using double quotes or another module would not be counted, so verification could pass while the feature map omits a public tool. Make the check account for equivalent registration forms and locations.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Codex Fix in Claude Code

Comment on lines +63 to +65

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Parse all valid MCP tool registrations

When a new tool is registered through a constant such as server.registerTool(getFooTool.name, ...), this regex ignores the registration; if the feature map is also left unchanged, both arrays still compare equal and the advertised drift guard passes. This syntax is valid, survives formatting, and is especially plausible because tool modules already export metadata objects, so derive registrations structurally or otherwise reject unrecognized registerTool calls rather than silently omitting them.

AGENTS.md reference: AGENTS.md:L78-L81

Useful? React with 👍 / 👎.

assertEqual(
'MCP feature map',
featureMap.mcpTools.map((tool) => tool.name),
registeredTools,
);

for (const tool of featureMap.mcpTools) {
if (!existsSync(resolve(root, tool.implementation))) {
throw new Error(`Missing MCP implementation: ${tool.implementation}`);
}
}

const sdkPackage = readJson('sdks/typescript-sdk/package.json');
assertEqual(
'SDK entrypoint map',
featureMap.sdkEntrypoints.map((entry) => entry.export),
Object.keys(sdkPackage.exports),
);
for (const entry of featureMap.sdkEntrypoints) {
if (!existsSync(resolve(root, entry.source))) {
throw new Error(`Missing SDK entrypoint source: ${entry.source}`);
}
}

const docsConfig = readJson('docs/docs.json');
const docsRoutes = collectStrings(docsConfig.navigation).filter(
(route) =>
route === 'api-docs/in-depth-guides/mcp' ||
route.startsWith('mcp/') ||
(route.startsWith('sdk/') && !route.startsWith('sdk/reference/')),
);
assertEqual('Docs route map', featureMap.docsRoutes, docsRoutes);
for (const route of featureMap.docsRoutes) {
if (!existsSync(resolve(root, `docs/${route}.mdx`))) {
throw new Error(`Missing docs route source: docs/${route}.mdx`);
}
}
}

function checkToolchain() {
const rootPackage = readJson('package.json');
const vitePlusPackage = readJson('node_modules/vite-plus/package.json');
const expected = vitePlusPackage.dependencies;
const workspaces = [
'packages/mcp/package.json',
'sdks/typescript-sdk/package.json',
'sdks/typescript-sdk-cli/package.json',
];
Comment on lines +109 to +113

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Discover workspaces before validating toolchain pins

When another package is added under the root packages/* or sdks/* workspace globs, this hard-coded list never checks its vite-plus or @vitest/coverage-v8 versions. Consequently agent-verify can pass while the new workspace violates the repository's lockstep toolchain invariant; enumerate package manifests from the declared workspace patterns so future packages are covered automatically.

AGENTS.md reference: AGENTS.md:L69-L72

Useful? React with 👍 / 👎.


if (rootPackage.devDependencies['vite-plus'] !== vitePlusPackage.version) {
throw new Error(
'Root vite-plus version does not match the installed tool.',
);
}
if (
rootPackage.devDependencies['@oxlint/plugins'] !==
normalizedVersion(expected['@oxlint/plugins'])
) {
throw new Error('@oxlint/plugins is not coupled to vite-plus.');
}
if (rootPackage.overrides.vitest !== expected.vitest) {
throw new Error('Vitest override is not coupled to vite-plus.');
}
if (
rootPackage.overrides.vite !==
`npm:@voidzero-dev/vite-plus-core@${vitePlusPackage.version}`
) {
throw new Error('Vite core override is not coupled to vite-plus.');
}

for (const workspacePath of workspaces) {
const workspace = readJson(workspacePath);
if (workspace.devDependencies['vite-plus'] !== vitePlusPackage.version) {
throw new Error(`${workspacePath} has a mismatched vite-plus version.`);
}
if (workspace.devDependencies['@vitest/coverage-v8'] !== expected.vitest) {
throw new Error(
`${workspacePath} has a mismatched Vitest coverage version.`,
);
}
}
}

function checkConfigOwnership() {
const nestedConfigs = ['packages', 'sdks']
.flatMap((directory) => collectFiles(resolve(root, directory)))
.filter((path) => /(?:^|\/)vite\.config\.[cm]?[jt]s$/.test(path))
.map((path) => relative(root, path));
if (nestedConfigs.length > 0) {
throw new Error(
`Workspace Vite configs are not allowed: ${nestedConfigs.join(', ')}`,
);
}
}

try {
checkFeatureMap();
checkToolchain();
checkConfigOwnership();
console.log('PASS agent trust guards');
} catch (error) {
console.error(
`FAIL agent trust guards: ${error instanceof Error ? error.message : String(error)}`,
);
process.exitCode = 1;
}
52 changes: 52 additions & 0 deletions bin/check-openapi-types.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#!/usr/bin/env node

import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
import { spawnSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';

const root = resolve(fileURLToPath(new URL('..', import.meta.url)));
const temporaryDirectory = mkdtempSync(join(tmpdir(), 't49-openapi-'));
const generatedPath = join(temporaryDirectory, 'terminal49.ts');

try {
const result = spawnSync(
'npx',
[
'--no-install',
'openapi-typescript',
'docs/openapi.json',
'-o',
generatedPath,
],
{
cwd: root,
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
},
);
if (result.status !== 0) {
throw new Error(result.stderr.trim() || 'openapi-typescript failed');
}

const expected = readFileSync(
resolve(root, 'sdks/typescript-sdk/src/generated/terminal49.ts'),
'utf8',
);
const actual = readFileSync(generatedPath, 'utf8');
if (actual !== expected) {
throw new Error(
'OpenAPI-generated SDK types are stale. Run `npm run generate:types --workspace @terminal49/sdk`.',
);
}

console.log('PASS OpenAPI SDK type generation');
} catch (error) {
console.error(
`FAIL OpenAPI SDK type generation: ${error instanceof Error ? error.message : String(error)}`,
);
process.exitCode = 1;
} finally {
rmSync(temporaryDirectory, { recursive: true, force: true });
}
Loading
Loading