mirror of
https://github.com/affaan-m/ECC.git
synced 2026-08-17 21:15:40 +02:00
fix(skill): harden ito basket comparison lifecycle (#2712)
This commit is contained in:
@@ -7,57 +7,222 @@ metadata:
|
||||
|
||||
# Itô Basket Compare
|
||||
|
||||
Use this skill to compare a basket, theme, or market set against a user's
|
||||
knowledge base, portfolio notes, research memo, CRM context, or stated thesis.
|
||||
Use this skill for requests such as “compare this basket with my research,”
|
||||
“basket vs watchlist,” “run a gap analysis,” or “find conflicts and stale
|
||||
assumptions.” It compares a basket, theme, or market set with user-provided or
|
||||
explicitly selected context. It is read-only and never recommends or executes a
|
||||
trade.
|
||||
|
||||
This skill is read-only. It does not recommend trades. It helps a user inspect
|
||||
fit, exposure, assumptions, and missing context before they decide what to do.
|
||||
## Non-negotiable boundaries
|
||||
|
||||
## Guardrails
|
||||
- Do not advise the user to buy, sell, hold, hedge, lever, allocate, or size.
|
||||
- Do not prepare or submit an order, trade, purchase, reservation, or RFQ.
|
||||
- Do not run `ecc ito find`: despite its name, it submits an authenticated RFQ.
|
||||
- Do not claim that `ecc ito status` returns basket data; it reads RFQ and
|
||||
procurement status. Do not use `ecc ito evals` for basket comparison.
|
||||
- Do not use private documents, financial context, memory, or account data
|
||||
unless the user explicitly identifies the source for this comparison.
|
||||
- Never print, echo, log, persist, or expose an API key, device token, session
|
||||
token, or secret. Never put credentials in arguments, files, or chat.
|
||||
- If an operation could change external state, stop with `UNSUPPORTED_OPERATION`.
|
||||
A later confirmation cannot turn this read-only skill into an execution skill.
|
||||
|
||||
- Do not provide investment advice or tell the user to buy, sell, hold, hedge,
|
||||
lever, or size a trade.
|
||||
- Do not execute, prepare, or submit orders.
|
||||
- Do not use private documents unless the user explicitly points to them.
|
||||
- Use `ITO_API_KEY` only for read-only Itô basket/market data after explicit
|
||||
user request.
|
||||
- If comparing against financials, preserve privacy and summarize only the
|
||||
fields needed for the comparison.
|
||||
## Inputs and access
|
||||
|
||||
## Comparison Modes
|
||||
Accept either a pasted basket or an explicitly authorized read-only source. The
|
||||
minimum basket input is a stable `basket_id` or basket label plus one or more
|
||||
underliers. Each underlier should contain `underlier_id`, label, event or claim,
|
||||
and any weight/probability supplied by the source. The comparison target must be
|
||||
user-provided or explicitly selected; request missing material instead of
|
||||
searching private stores broadly.
|
||||
|
||||
### Basket vs Knowledge Base
|
||||
Record provenance for every input:
|
||||
|
||||
1. Identify the basket theme and underliers.
|
||||
2. Retrieve the user's relevant notes, docs, or memory snippets.
|
||||
3. Map each underlier to claims, sources, uncertainties, and stale assumptions.
|
||||
4. Return aligned signals, conflicting signals, and missing research.
|
||||
- `source_type`: `user_provided`, `public`, or `ito_authenticated`
|
||||
- `source_uri`: a non-secret URL/identifier, or `null` for pasted material
|
||||
- `retrieved_at`: UTC RFC 3339 time at retrieval
|
||||
- `as_of`: source observation/publication time, or `null` when unknown
|
||||
- `freshness_status`: `fresh`, `stale`, or `unknown`
|
||||
|
||||
### Basket vs Portfolio Notes
|
||||
Never label anonymous product data `ito_authenticated`; use `public`. ECC's real
|
||||
CLI/MCP surface does not expose a
|
||||
basket-read command: the CLI supports `login`, validation-only `auth`, `find`,
|
||||
`status`, and `evals`; MCP exposes `ito_auth`, `ito_find`, and `ito_status`.
|
||||
Therefore authentication success proves identity only, not basket-data
|
||||
availability. Prefer the documented public product-data routes when they satisfy
|
||||
the comparison; otherwise ask the user to paste/export the basket or use a
|
||||
documented keyed read with the minimum scope.
|
||||
|
||||
1. Parse the user's watchlist, holdings summary, or exposure notes.
|
||||
2. Compare themes, geographies, time horizons, and event outcomes.
|
||||
3. Flag concentration, correlation, and duplicated narrative exposure.
|
||||
4. Avoid recommendations; phrase output as inspection and questions.
|
||||
The canonical product-data surfaces are:
|
||||
|
||||
### Basket vs Financial Context
|
||||
- Anonymous, rate-limited GET routes at `https://itomarkets.com`, including
|
||||
`/api/baskets/bootstrap`, `/api/baskets/{basket_id}/bootstrap`, and
|
||||
`/api/markets/hot`. These are valid live product reads without a private key.
|
||||
- The keyed developer API at `https://itomarkets.com/api/v1`. Send a configured
|
||||
public API key only as `Authorization: Bearer <key>` to that
|
||||
exact HTTPS origin. Basket reads use `GET /baskets`,
|
||||
`GET /baskets/{basket_id}`, and their documented GET-only child routes and
|
||||
require `baskets:read`. Market lookup uses `GET /markets/search`,
|
||||
`GET /markets/{market_id}`, and documented GET-only market-data child routes
|
||||
and requires `markets:read`. Never use a write scope, dashboard automation
|
||||
key, cookie, or compute device credential as
|
||||
a substitute.
|
||||
- The official Python SDK package `ito-markets`, imported as `ito`, for typed
|
||||
basket and market reads. Before using it, record the installed version and
|
||||
verify the requested method, response type, origin, and required scope. Do
|
||||
not install or upgrade it without confirmation.
|
||||
|
||||
1. Accept only user-provided or explicitly selected financial context.
|
||||
2. Identify liquidity, drawdown, time-horizon, and constraint mismatches.
|
||||
3. Ask for missing constraints instead of guessing.
|
||||
Use an anonymous route when it supplies the basket, underliers, and current
|
||||
quote fields needed by the comparison. Use the SDK or keyed API only for a
|
||||
documented field absent from public data. Validate the response contract before
|
||||
comparison and record the endpoint, response `Date`, source observation
|
||||
timestamp, access mode, SDK version when applicable, and cache headers.
|
||||
|
||||
## Output Contract
|
||||
The verified anonymous catalog source is the GET-only endpoint
|
||||
`https://itomarkets.com/api/baskets/bootstrap?stream=1`. Basket detail uses
|
||||
`https://itomarkets.com/api/baskets/{basket_id}/bootstrap?stream=1`. Require
|
||||
HTTP 200, `contractVersion: ito.public_basket_read.v1`, and a parseable
|
||||
`generated_at`. Require a `baskets` array for catalog responses; require
|
||||
`basket`, `underlyers`, `charts`, `metrics`, and `commentary` objects for detail
|
||||
responses. Record the URL, response `Date`, `generated_at`, `Cache-Control`,
|
||||
`Age`, `Last-Modified`, and any `x-ito-edge-cache` value. Treat an edge `stale`
|
||||
marker as stale provenance even when `generated_at` is recent. Do not send
|
||||
credentials to this public endpoint, follow cross-origin redirects, or silently
|
||||
accept a changed contract version.
|
||||
|
||||
Use this structure:
|
||||
## First-run authentication handoff
|
||||
|
||||
1. Basket summary
|
||||
2. Comparison target
|
||||
3. Matches
|
||||
4. Conflicts or stale assumptions
|
||||
5. Missing context
|
||||
6. User-action checklist
|
||||
Resolve a concrete basket-read source and its authentication contract before
|
||||
requesting authentication. The public catalog/detail endpoints require no login
|
||||
and are sufficient for comparisons whose required fields they contain. If no
|
||||
authenticated basket-read source/tool is configured, use public or pasted input
|
||||
and do not request compute credentials.
|
||||
|
||||
End with:
|
||||
`ecc ito auth --json` is an optional, validation-only compute identity probe. It
|
||||
does not start login and cannot unlock basket reads. Use it only when the user
|
||||
explicitly requests compute-account identity validation in addition to the
|
||||
basket comparison; never present it as basket-source authentication.
|
||||
|
||||
For a concrete authenticated basket source whose documented contract explicitly
|
||||
uses the canonical Itô device credential (the public `/api/v1` does not):
|
||||
|
||||
1. Run `ecc ito auth --json` only if that source contract requires the same
|
||||
identity. This is validation-only and never starts login.
|
||||
2. On missing, expired, or confirmed revoked credentials, pause and return
|
||||
`AUTH_REQUIRED` or `AUTH_REVOKED`. Tell the user to run `ecc ito login`; it
|
||||
performs device authorization, opens the verification page by default, and
|
||||
stores the device token in macOS Keychain. `ecc ito login --no-browser`
|
||||
suppresses the browser handoff. ECC itself performs no browser automation.
|
||||
3. Preserve a secret-free resume summary containing the originating task/agent,
|
||||
user request, selected input identifiers, and completed read-only steps.
|
||||
4. After the user reports completion, return to the originating agent and run
|
||||
`ecc ito auth --json` once more. Resume only the original read-only request;
|
||||
never broaden scope because login succeeded.
|
||||
|
||||
`ITO_API_KEY` may be forwarded by compute `auth` only when already configured. Do not
|
||||
read or display its value. The canonical Itô client is a separately installed,
|
||||
currently unpublished dependency configured by an explicit absolute
|
||||
`ECC_ITO_CLI_EXECUTABLE`; ECC does not discover it through `PATH`. If absent,
|
||||
return `AUTH_REQUIRED` with installation guidance from `ito-compute`, without
|
||||
inventing a successful auth result.
|
||||
|
||||
## Deterministic normalization and comparison
|
||||
|
||||
For the same normalized input and the same explicit comparison time, produce
|
||||
the same output.
|
||||
|
||||
1. Copy inputs; never mutate source objects. Normalize text with Unicode NFKC,
|
||||
trim it, collapse internal whitespace, and use case-folded text only for
|
||||
matching. Preserve display text.
|
||||
2. Convert timestamps to UTC RFC 3339. Treat missing/unparseable `as_of` as
|
||||
`null` with `freshness_status: unknown`; never substitute the current time. Reject non-finite numbers and
|
||||
probabilities outside `[0,1]`. Do not infer missing weights.
|
||||
3. Deduplicate only exact normalized `underlier_id` values. If duplicate records
|
||||
disagree, retain the first record after provenance ordering and add a
|
||||
conflict; do not silently merge facts. Sort underliers by normalized
|
||||
`underlier_id`, then label. Sort sources by `source_type`, `source_uri`,
|
||||
`as_of`, and `retrieved_at`, with `null` last.
|
||||
4. Use the user's freshness threshold when supplied. Otherwise use 24 hours for
|
||||
market/basket observations and 30 days for notes/research. Compare `as_of`
|
||||
with the explicit comparison time: older is `stale`, within threshold is
|
||||
`fresh`, and absent/unparseable is `unknown`. State the freshness threshold.
|
||||
5. Match by exact stable ID first, then exact normalized claim/event text. Do
|
||||
not use fuzzy similarity as proof. Classify an item as:
|
||||
- `match`: same claim/direction and compatible horizon;
|
||||
- `conflict`: opposing claim, incompatible horizon, or duplicate ID with
|
||||
inconsistent facts;
|
||||
- `missing`: no target evidence for that underlier;
|
||||
- `stale`: otherwise relevant target evidence outside its threshold.
|
||||
6. Keep mixed-source disagreement visible. Sort every result array by
|
||||
`underlier_id`, then evidence `source_uri`. Use explicit `null` for unknown
|
||||
scalar fields and empty arrays for no findings.
|
||||
|
||||
## Recovery and safe failure
|
||||
|
||||
- Missing/invalid fields: `INVALID_INPUT`; identify fields without echoing
|
||||
sensitive content.
|
||||
- Missing/expired credentials required by a concrete basket source:
|
||||
`AUTH_REQUIRED`; provide that source's documented handoff. Use
|
||||
`AUTH_REVOKED` only when the source confirms revocation. A generic 401 is not
|
||||
proof of revocation. A 403/insufficient read scope is `AUTH_FORBIDDEN`; do not
|
||||
retry or broaden scope.
|
||||
- Timeout/network/5xx/malformed response: `SOURCE_TIMEOUT`; make at most one
|
||||
read-only retry when the user-specified deadline permits. Never replace a
|
||||
failed live read with mock or stale data while calling it live.
|
||||
- 429: honor a valid `Retry-After` within the user deadline; otherwise stop as
|
||||
`SOURCE_TIMEOUT`. Do not loop indefinitely.
|
||||
- Required stale data: return `STALE_SOURCE` as blocked unless the user
|
||||
explicitly accepts the displayed timestamps for informational comparison.
|
||||
Even then, preserve `freshness_status: stale`.
|
||||
- Unsupported CLI/tool or any state-changing request: `UNSUPPORTED_OPERATION`.
|
||||
|
||||
Partial results use `status: blocked`, retain only source-backed partial arrays,
|
||||
and include `incomplete: true` plus the applicable error. They must never be
|
||||
presented as a successful complete comparison.
|
||||
|
||||
## Output contract
|
||||
|
||||
Default to concise Markdown in this order: basket summary, comparison target,
|
||||
provenance/freshness, matches, conflicts or stale assumptions, missing context,
|
||||
and a user-action checklist containing research questions only. When structured
|
||||
output is requested, emit JSON with stable key order and no extra keys:
|
||||
|
||||
```json
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"status": "ok",
|
||||
"comparison_time": "2026-01-01T00:00:00Z",
|
||||
"basket": {"basket_id": "example", "label": "Example", "underliers": []},
|
||||
"target": {"label": "Research notes", "source_type": "user_provided"},
|
||||
"sources": [],
|
||||
"freshness_thresholds": {"market_hours": 24, "research_days": 30},
|
||||
"matches": [],
|
||||
"conflicts": [],
|
||||
"stale_assumptions": [],
|
||||
"missing_context": [],
|
||||
"checklist": [],
|
||||
"disclaimer": "This comparison is informational and not investment or trading advice."
|
||||
}
|
||||
```
|
||||
|
||||
Blocked output uses the same leading key order and contains no fabricated data:
|
||||
|
||||
```json
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"status": "blocked",
|
||||
"incomplete": true,
|
||||
"error": {"code": "AUTH_REQUIRED", "message": "Read-only Itô authentication is required.", "retryable": true},
|
||||
"resume": {"originating_agent": "current", "completed_steps": []},
|
||||
"disclaimer": "This comparison is informational and not investment or trading advice."
|
||||
}
|
||||
```
|
||||
|
||||
Allowed error codes are `AUTH_REQUIRED`, `AUTH_REVOKED`, `AUTH_FORBIDDEN`,
|
||||
`SOURCE_TIMEOUT`, `STALE_SOURCE`, `INVALID_INPUT`, and
|
||||
`UNSUPPORTED_OPERATION`.
|
||||
|
||||
Always end human-readable output with exactly:
|
||||
|
||||
```text
|
||||
This comparison is informational and not investment or trading advice.
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* Contract and lifecycle tests for the Itô basket comparison skill.
|
||||
* No test contacts Itô, opens a browser, or submits an RFQ/order.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const assert = require("assert");
|
||||
const fs = require("fs");
|
||||
const os = require("os");
|
||||
const path = require("path");
|
||||
const { spawnSync } = require("child_process");
|
||||
|
||||
const REPO_ROOT = path.join(__dirname, "..", "..");
|
||||
const SKILL_PATH = path.join(REPO_ROOT, "skills", "ito-basket-compare", "SKILL.md");
|
||||
|
||||
function run(name, test) {
|
||||
try {
|
||||
test();
|
||||
console.log(` ✓ ${name}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.log(` ✗ ${name}`);
|
||||
console.error(` ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function install(args, home, cwd) {
|
||||
return spawnSync(process.execPath, [path.join(REPO_ROOT, "scripts", "install-apply.js"), ...args], {
|
||||
cwd,
|
||||
encoding: "utf8",
|
||||
env: { ...process.env, HOME: home },
|
||||
});
|
||||
}
|
||||
|
||||
function uninstall(home, cwd) {
|
||||
return spawnSync(process.execPath, [path.join(REPO_ROOT, "scripts", "uninstall.js"), "--target", "claude", "--json"], {
|
||||
cwd,
|
||||
encoding: "utf8",
|
||||
env: { ...process.env, HOME: home },
|
||||
});
|
||||
}
|
||||
|
||||
function main() {
|
||||
const skill = fs.readFileSync(SKILL_PATH, "utf8");
|
||||
const tests = [
|
||||
["has valid discoverable frontmatter and representative trigger phrases", () => {
|
||||
assert.match(skill, /^---\nname: ito-basket-compare\ndescription: [^\n]+\nmetadata:\n origin: ECC\n---\n/);
|
||||
for (const phrase of ["compare this basket", "basket vs", "gap analysis", "stale assumptions", "watchlist"]) {
|
||||
assert.match(skill.toLowerCase(), new RegExp(phrase));
|
||||
}
|
||||
}],
|
||||
["documents the real auth handoff and return to the originating agent", () => {
|
||||
assert.match(skill, /ecc ito login/);
|
||||
assert.match(skill, /ecc ito login --no-browser/);
|
||||
assert.match(skill, /ecc ito auth --json/);
|
||||
assert.match(skill, /validation-only/i);
|
||||
assert.match(skill, /cannot unlock basket reads/i);
|
||||
assert.match(skill, /public catalog\/detail endpoints require no login/i);
|
||||
assert.match(skill, /macOS Keychain/i);
|
||||
assert.match(skill, /return to the originating agent/i);
|
||||
assert.match(skill, /never.*(?:print|echo|expose).*secret/is);
|
||||
}],
|
||||
["fails closed around unsupported or state-changing CLI and API behavior", () => {
|
||||
assert.match(skill, /does not expose a\s+basket-read command/i);
|
||||
assert.match(skill, /do not run `ecc ito find`/i);
|
||||
assert.match(skill, /RFQ/i);
|
||||
assert.match(skill, /do not.*(?:order|purchase|trade|reserve)/is);
|
||||
assert.match(skill, /explicitly authorized read-only/i);
|
||||
}],
|
||||
["aligns public, keyed, and SDK reads with the canonical product contract", () => {
|
||||
assert.match(skill, /Anonymous, rate-limited GET routes/i);
|
||||
assert.match(skill, /\/api\/baskets\/\{basket_id\}\/bootstrap/);
|
||||
assert.match(skill, /\/api\/markets\/hot/);
|
||||
assert.match(skill, /valid live product reads without a private key/i);
|
||||
assert.match(skill, /https:\/\/itomarkets\.com\/api\/v1/);
|
||||
assert.match(skill, /Authorization: Bearer/);
|
||||
assert.match(skill, /ito-markets/);
|
||||
assert.match(skill, /imported as `ito`/);
|
||||
assert.match(skill, /GET \/baskets/);
|
||||
assert.match(skill, /GET \/markets\/search/);
|
||||
assert.match(skill, /baskets:read/);
|
||||
assert.match(skill, /markets:read/);
|
||||
assert.match(skill, /Never use a\s+write scope/i);
|
||||
}],
|
||||
["defines deterministic normalization, provenance, freshness, and comparison", () => {
|
||||
for (const token of ["basket_id", "underlier_id", "retrieved_at", "as_of", "source_uri", "source_type", "freshness_status"]) {
|
||||
assert.match(skill, new RegExp(`\\b${token}\\b`));
|
||||
}
|
||||
assert.match(skill, /Unicode NFKC/i);
|
||||
assert.match(skill, /sort.*underlier_id/is);
|
||||
assert.match(skill, /duplicate.*underlier_id/is);
|
||||
assert.match(skill, /freshness threshold/i);
|
||||
assert.match(skill, /same normalized input[\s\S]*same output/i);
|
||||
}],
|
||||
["defines structured success and error output without advice", () => {
|
||||
assert.match(skill, /schema_version/);
|
||||
assert.match(skill, /"status": "ok"/);
|
||||
assert.match(skill, /"status": "blocked"/);
|
||||
for (const code of ["AUTH_REQUIRED", "AUTH_REVOKED", "AUTH_FORBIDDEN", "SOURCE_TIMEOUT", "STALE_SOURCE", "INVALID_INPUT", "UNSUPPORTED_OPERATION"]) {
|
||||
assert.match(skill, new RegExp(code));
|
||||
}
|
||||
assert.match(skill, /"incomplete": true/);
|
||||
assert.match(skill, /informational and not investment or trading advice/i);
|
||||
}],
|
||||
["installs, uninstalls, and reinstalls only the selected skill in a clean home", () => {
|
||||
const home = fs.mkdtempSync(path.join(os.tmpdir(), "ecc-basket-home-"));
|
||||
const project = fs.mkdtempSync(path.join(os.tmpdir(), "ecc-basket-project-"));
|
||||
const installed = path.join(home, ".claude", "skills", "ito-basket-compare", "SKILL.md");
|
||||
try {
|
||||
const first = install(["--skills", "ito-basket-compare"], home, project);
|
||||
assert.strictEqual(first.status, 0, first.stderr);
|
||||
assert.ok(fs.existsSync(installed));
|
||||
assert.strictEqual(fs.readFileSync(installed, "utf8"), skill);
|
||||
|
||||
const removed = uninstall(home, project);
|
||||
assert.strictEqual(removed.status, 0, removed.stderr);
|
||||
assert.ok(!fs.existsSync(installed));
|
||||
|
||||
const second = install(["--skills", "ito-basket-compare"], home, project);
|
||||
assert.strictEqual(second.status, 0, second.stderr);
|
||||
assert.strictEqual(fs.readFileSync(installed, "utf8"), skill);
|
||||
} finally {
|
||||
fs.rmSync(home, { recursive: true, force: true });
|
||||
fs.rmSync(project, { recursive: true, force: true });
|
||||
}
|
||||
}],
|
||||
];
|
||||
|
||||
let passed = 0;
|
||||
for (const [name, test] of tests) passed += run(name, test) ? 1 : 0;
|
||||
const failed = tests.length - passed;
|
||||
console.log(`\nPassed: ${passed}`);
|
||||
console.log(`Failed: ${failed}`);
|
||||
process.exitCode = failed === 0 ? 0 : 1;
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user