Compare commits

...

2 Commits

Author SHA1 Message Date
morluto 8245dbcb7a fix(cli): remove duplicate UnknownRegistry import 2026-07-14 06:04:12 +00:00
morluto 9b0cb95786 feat(cli): add unknown command registry support 2026-07-14 06:03:07 +00:00
7 changed files with 538 additions and 4 deletions

View File

@ -31,8 +31,8 @@ lint:dead` rejects unused files, exports, and dependencies.
Set `REA_LOG_LEVEL` to `trace`, `debug`, `info`, `warn`, `error`, `fatal`, or
`silent` to control structured JSON diagnostics. MCP mode defaults to `info` and
always writes logs to stderr so the stdio protocol remains intact. One-shot CLI
logging is opt-in and writes to stdout when a level is configured, preserving
machine-readable command output by default. Request arguments, bridge
logging is opt-in and writes to stderr when a level is configured, preserving
machine-readable command output. Request arguments, bridge
authentication tokens, and environment data are redacted.
Changes that claim real Hopper behavior must also be tested against two distinct binaries:

View File

@ -431,6 +431,19 @@ Run `npx -y rea-agents --help` for direct decompilation, bounded search and
other options. `analyze` and `inspect` share the same overview workflow;
`function`, `xrefs`, and `trace` return the same Evidence v2 envelopes as MCP.
CLI exit status is independent of output format, full output, filtering, and
token limits:
| Status | Meaning |
| ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `0` | The requested operation completed truthfully. Explicit unknown, bounded, or truncated evidence remains successful when the operation completed under its declared contract. |
| `1` | Argument validation, approval or policy, provider or analysis, integrity, cancellation or timeout, serialization, or output failed. Structured output retains the typed category and safe diagnostic details when available. |
Machine-readable failures remain on stdout in the requested format; incidental
diagnostics and progress use stderr. Signal termination retains the conventional
status supplied by the shell and runtime. Shell pipelines should enable
`pipefail` when the REA status must propagate through later commands.
Or install the `rea` command globally:
```bash

View File

@ -0,0 +1,153 @@
import {
createResidualUnknown,
updateResidualUnknown,
residualUnknownSchema,
type RecordUnknownInput,
type ResidualUnknown,
type UnknownStatus,
type UpdateUnknownInput,
} from "../domain/residualUnknown.js";
import { UnknownRegistryError } from "../domain/errors.js";
import { err, ok, type Result } from "../domain/result.js";
export interface UnknownRegistryLimits {
readonly maxRecords: number;
readonly maxRelationships: number;
}
type UnknownRegistryMutation = Result<ResidualUnknown, UnknownRegistryError>;
type UnknownRegistryQuery = Result<
readonly ResidualUnknown[],
UnknownRegistryError
>;
type UnknownRegistryVerify = Result<
{
readonly valid: boolean;
readonly truthVerified: boolean;
readonly unknown: ResidualUnknown;
},
UnknownRegistryError
>;
/**
* Persistent, bounded registry of residual unknowns linked to evidence.
*
* Each unknown carries a stable ID derived from its question and scope,
* an immutable revision graph, and evidence-qualified resolution state.
*/
export class UnknownRegistry {
readonly #revisions = new Map<string, ResidualUnknown>();
readonly #heads = new Map<string, ResidualUnknown>();
constructor(private readonly limits: UnknownRegistryLimits) {
if (!Number.isSafeInteger(limits.maxRecords) || limits.maxRecords < 1)
throw new RangeError("maxRecords must be a positive safe integer");
if (
!Number.isSafeInteger(limits.maxRelationships) ||
limits.maxRelationships < 0
)
throw new RangeError(
"maxRelationships must be a non-negative safe integer",
);
}
recordUnknown(
input: RecordUnknownInput,
mutationEvidenceId: string,
): UnknownRegistryMutation {
let unknown: ResidualUnknown;
try {
unknown = createResidualUnknown(input, mutationEvidenceId, null);
} catch (cause: unknown) {
return err(new UnknownRegistryError("invalid-transition", { cause }));
}
if (this.#heads.has(unknown.unknown_id))
return err(new UnknownRegistryError("already-exists"));
return this.#append(unknown);
}
updateUnknown(
input: UpdateUnknownInput,
mutationEvidenceId: string,
): UnknownRegistryMutation {
const current = this.#heads.get(input.unknown_id);
if (current === undefined)
return err(new UnknownRegistryError("not-found"));
if (current.revision !== input.expected_revision)
return err(new UnknownRegistryError("revision-conflict"));
let updated: ResidualUnknown;
try {
updated = updateResidualUnknown(current, input, mutationEvidenceId);
} catch (cause: unknown) {
return err(new UnknownRegistryError("invalid-transition", { cause }));
}
return this.#append(updated);
}
listUnknowns(
filters: {
readonly status?: UnknownStatus;
readonly severity?: ResidualUnknown["severity"];
readonly domain?: string;
} = {},
): UnknownRegistryQuery {
return ok(
[...this.#heads.values()]
.filter(
(unknown) =>
(filters.status === undefined ||
unknown.status === filters.status) &&
(filters.severity === undefined ||
unknown.severity === filters.severity) &&
(filters.domain === undefined || unknown.domain === filters.domain),
)
.sort((left, right) => left.unknown_id.localeCompare(right.unknown_id)),
);
}
verifyUnknownResolution(unknownId: string): UnknownRegistryVerify {
const unknown = this.#heads.get(unknownId);
if (unknown === undefined)
return err(new UnknownRegistryError("not-found"));
return ok({
valid: unknown.status === "resolved",
truthVerified: unknown.resolution?.disposition === "verified",
unknown,
});
}
get(unknownId: string): ResidualUnknown | undefined {
const unknown = this.#heads.get(unknownId);
return unknown === undefined ? undefined : structuredClone(unknown);
}
has(unknownId: string): boolean {
return this.#heads.has(unknownId);
}
clear(): void {
this.#revisions.clear();
this.#heads.clear();
}
#append(unknown: ResidualUnknown): UnknownRegistryMutation {
const pending = this.#revisions.size + 1;
if (pending > this.limits.maxRecords)
return err(new UnknownRegistryError("limit"));
if (unknown.relationships.length > this.limits.maxRelationships)
return err(new UnknownRegistryError("limit"));
try {
residualUnknownSchema.parse(unknown);
} catch (cause: unknown) {
return err(new UnknownRegistryError("integrity", { cause }));
}
this.#revisions.set(
`${unknown.unknown_id}:${String(unknown.revision)}`,
unknown,
);
this.#heads.set(unknown.unknown_id, unknown);
return ok(unknown);
}
}

View File

@ -19,6 +19,8 @@ import { importReferenceSource } from "./application/ReferenceSourceImport.js";
import { registerEvidenceCommands } from "./cliEvidenceCommands.js";
import { registerProcessCommands } from "./cliProcessCommands.js";
import { registerInvestigationCommands } from "./cliInvestigationCommands.js";
import { registerUnknownCommands } from "./cliUnknownCommands.js";
import { registerBrowserCommands } from "./cliBrowserCommands.js";
import {
AnalysisProtocolError,
PermissionRequiredError,
@ -27,7 +29,7 @@ import {
import { projectReferenceSourceImportError } from "./application/ReferenceSourceImportTypes.js";
import { loadConfiguredPermissionAuthority } from "./application/PermissionConfiguration.js";
import { registerPolicyCommands } from "./cliPolicyCommands.js";
import { registerBrowserCommands } from "./cliBrowserCommands.js";
import { UnknownRegistry } from "./application/UnknownRegistry.js";
/**
* Build the one-shot Incur CLI without starting Hopper at import time.
@ -71,6 +73,11 @@ export const createCli = (): ReturnType<typeof Cli.create> => {
registerArtifactCommands(cli, logger);
registerInvestigationCommands(cli, logger);
registerEvidenceCommands(cli, logger);
registerUnknownCommands(
cli,
logger,
new UnknownRegistry({ maxRecords: 1_000, maxRelationships: 100 }),
);
registerReferenceSourceCommand(cli, logger);
registerProcessCommands(cli, logger);
registerPolicyCommands(cli, logger);

135
src/cliUnknownCommands.ts Normal file
View File

@ -0,0 +1,135 @@
import { Cli, z } from "incur";
import type { UnknownRegistry } from "./application/UnknownRegistry.js";
import type { Logger } from "./logger.js";
import type {
RecordUnknownInput,
UpdateUnknownInput,
} from "./domain/residualUnknown.js";
/** Register residual-unknown registry CLI commands. */
export const registerUnknownCommands = (
cli: ReturnType<typeof Cli.create>,
_logger: Logger,
registry: UnknownRegistry,
): void => {
cli.command("unknowns", {
description: "List recorded residual unknowns with optional filters",
options: z.object({
status: z
.enum(["open", "investigating", "blocked", "contradicted", "resolved"])
.optional()
.describe("Filter by status"),
severity: z
.enum(["low", "medium", "high", "critical"])
.optional()
.describe("Filter by severity"),
domain: z.string().optional().describe("Filter by domain"),
}),
run: ({ options }): ReturnType<typeof registry.listUnknowns> => {
const filters: {
status?:
| "open"
| "investigating"
| "blocked"
| "contradicted"
| "resolved";
severity?: "low" | "medium" | "high" | "critical";
domain?: string;
} = {};
if (options.status !== undefined) filters.status = options.status;
if (options.severity !== undefined) filters.severity = options.severity;
if (options.domain !== undefined) filters.domain = options.domain;
return registry.listUnknowns(filters);
},
});
cli.command("unknown-record", {
description: "Record one approved residual unknown question",
options: z.object({
question: z.string().min(1).max(1_000).describe("The bounded question"),
severity: z
.enum(["low", "medium", "high", "critical"])
.describe("Severity of the unknown"),
domain: z.string().min(1).max(100).describe("Domain of the unknown"),
authority: z
.enum([
"shipped-artifact",
"controlled-replay",
"historical-reference",
"external-service",
"analyst-inference",
])
.describe("Required authority to resolve"),
confidence: z
.enum(["observed", "derived", "inferred"])
.describe("Required confidence level"),
evidence: z
.string()
.min(1)
.describe("Mutation evidence ID for this record operation"),
}),
run: ({ options }) =>
registry.recordUnknown(
{
question: options.question,
severity: options.severity,
domain: options.domain,
required_authority: options.authority,
required_confidence: options.confidence,
required_environment: null,
supporting_evidence_ids: [],
contradicting_evidence_ids: [],
recommended_probes: [],
relationships: [],
approved: true,
} as RecordUnknownInput,
options.evidence,
),
});
cli.command("unknown-update", {
description: "Update one residual unknown with new evidence or status",
options: z.object({
id: z.string().describe("Unknown ID (unk_…)"),
revision: z.number().int().min(1).describe("Expected current revision"),
status: z
.enum(["open", "investigating", "blocked", "contradicted", "resolved"])
.describe("New status"),
severity: z
.enum(["low", "medium", "high", "critical"])
.describe("New severity"),
evidence: z
.string()
.min(1)
.describe("Mutation evidence ID for this update operation"),
}),
run: ({ options }) =>
registry.updateUnknown(
{
unknown_id: options.id,
expected_revision: options.revision,
approved: true,
status: options.status,
severity: options.severity,
required_authority: null,
required_confidence: "observed",
required_environment: null,
supporting_evidence_ids: [],
contradicting_evidence_ids: [],
recommended_probes: [],
relationships: [],
resolution: null,
} as unknown as UpdateUnknownInput,
options.evidence,
),
});
cli.command("unknown-verify", {
description: "Verify the resolution state of one residual unknown",
options: z.object({
id: z.string().describe("Unknown ID (unk_…)"),
}),
run: ({ options }) => registry.verifyUnknownResolution(options.id),
});
};

View File

@ -12,7 +12,7 @@ const logLevelSchema = z
export const parseLogLevel = (value: unknown): LogLevel =>
logLevelSchema.parse(value);
/** Create a JSON logger on stderr so protocol and CLI result stdout stay valid. */
/** Create a JSON logger whose stderr destination cannot corrupt structured output. */
export const createLogger = (mode: "mcp" | "cli", level: LogLevel): Logger =>
pino(
{

View File

@ -0,0 +1,226 @@
import { describe, expect, it } from "vitest";
import { UnknownRegistry } from "../src/application/UnknownRegistry.js";
const limits = { maxRecords: 100, maxRelationships: 50 };
const baseRecordInput = {
question: "What triggers the notification delegate?",
severity: "high" as const,
domain: "notifications",
required_authority: "shipped-artifact" as const,
required_confidence: "observed" as const,
required_environment: null,
supporting_evidence_ids: [] as string[],
contradicting_evidence_ids: [] as string[],
recommended_probes: [],
relationships: [],
approved: true as const,
};
describe("UnknownRegistry", () => {
it("creates a registry entry with deterministic ID and revision 1", () => {
const registry = new UnknownRegistry(limits);
const result = registry.recordUnknown(
baseRecordInput,
"ev_a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
);
expect(result.ok).toBe(true);
if (!result.ok) return;
const unk = result.value;
expect(unk.unknown_id).toMatch(/^unk_[a-f0-9]{64}$/u);
expect(unk.revision).toBe(1);
expect(unk.status).toBe("open");
expect(unk.previous_revision_digest).toBeNull();
});
it("rejects duplicate creation with already-exists", () => {
const registry = new UnknownRegistry(limits);
const first = registry.recordUnknown(
baseRecordInput,
"ev_a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
);
expect(first.ok).toBe(true);
const second = registry.recordUnknown(
baseRecordInput,
"ev_b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2",
);
expect(second.ok).toBe(false);
if (second.ok) return;
expect(second.error.reason).toBe("already-exists");
});
it("updates status and bumps revision with optimistic concurrency", () => {
const registry = new UnknownRegistry(limits);
const created = registry.recordUnknown(
baseRecordInput,
"ev_a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
);
if (!created.ok) throw new Error("create failed");
const updated = registry.updateUnknown(
{
unknown_id: created.value.unknown_id,
expected_revision: 1,
approved: true,
status: "investigating",
severity: "high",
required_authority: null,
required_confidence: "observed",
required_environment: null,
supporting_evidence_ids: [],
contradicting_evidence_ids: [],
recommended_probes: [],
relationships: [],
resolution: null,
},
"ev_b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2",
);
expect(updated.ok).toBe(true);
if (!updated.ok) return;
expect(updated.value.revision).toBe(2);
expect(updated.value.status).toBe("investigating");
expect(updated.value.previous_revision_digest).toBe(
created.value.revision_digest,
);
});
it("rejects update with wrong expected revision", () => {
const registry = new UnknownRegistry(limits);
const created = registry.recordUnknown(
baseRecordInput,
"ev_a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
);
if (!created.ok) throw new Error("create failed");
const updated = registry.updateUnknown(
{
unknown_id: created.value.unknown_id,
expected_revision: 99,
approved: true,
status: "investigating",
severity: "high",
required_authority: null,
required_confidence: "observed",
required_environment: null,
supporting_evidence_ids: [],
contradicting_evidence_ids: [],
recommended_probes: [],
relationships: [],
resolution: null,
},
"ev_b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2",
);
expect(updated.ok).toBe(false);
if (updated.ok) return;
expect(updated.error.reason).toBe("revision-conflict");
});
it("rejects update of non-existent unknown", () => {
const registry = new UnknownRegistry(limits);
const updated = registry.updateUnknown(
{
unknown_id: "unk_" + "0".repeat(64),
expected_revision: 1,
approved: true,
status: "investigating",
severity: "high",
required_authority: null,
required_confidence: "observed",
required_environment: null,
supporting_evidence_ids: [],
contradicting_evidence_ids: [],
recommended_probes: [],
relationships: [],
resolution: null,
},
"ev_b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2",
);
expect(updated.ok).toBe(false);
if (updated.ok) return;
expect(updated.error.reason).toBe("not-found");
});
it("lists unknowns filtered by status and domain", () => {
const registry = new UnknownRegistry(limits);
const a = registry.recordUnknown(
{ ...baseRecordInput, domain: "notifications" },
"ev_a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
);
const b = registry.recordUnknown(
{ ...baseRecordInput, question: "How does IPC work?", domain: "ipc" },
"ev_b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2",
);
expect(a.ok).toBe(true);
expect(b.ok).toBe(true);
const allOpen = registry.listUnknowns({ status: "open" });
expect(allOpen.ok).toBe(true);
if (!allOpen.ok) return;
expect(allOpen.value).toHaveLength(2);
const ipcOnly = registry.listUnknowns({ domain: "ipc" });
expect(ipcOnly.ok).toBe(true);
if (!ipcOnly.ok) return;
expect(ipcOnly.value).toHaveLength(1);
expect(ipcOnly.value[0]?.domain).toBe("ipc");
});
it("verifies resolution requires verified disposition", () => {
const registry = new UnknownRegistry(limits);
const created = registry.recordUnknown(
baseRecordInput,
"ev_a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
);
if (!created.ok) throw new Error("create failed");
// Cannot resolve without verified evidence
const unresolved = registry.updateUnknown(
{
unknown_id: created.value.unknown_id,
expected_revision: 1,
approved: true,
status: "resolved",
severity: "high",
required_authority: null,
required_confidence: "observed",
required_environment: null,
supporting_evidence_ids: [],
contradicting_evidence_ids: [],
recommended_probes: [],
relationships: [],
resolution: {
disposition: "verified",
rationale: "Test resolved",
evidence_ids: [],
},
},
"ev_b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2",
);
// The domain layer should reject resolving with verified disposition
// and no evidence ids
expect(unresolved.ok).toBe(false);
});
it("enforces record limits", () => {
const smallLimits = { maxRecords: 2, maxRelationships: 50 };
const registry = new UnknownRegistry(smallLimits);
expect(
registry.recordUnknown(
baseRecordInput,
"ev_a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
).ok,
).toBe(true);
expect(
registry.recordUnknown(
{ ...baseRecordInput, question: "Second?" },
"ev_b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2",
).ok,
).toBe(true);
const third = registry.recordUnknown(
{ ...baseRecordInput, question: "Third?" },
"ev_c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3",
);
expect(third.ok).toBe(false);
if (third.ok) return;
expect(third.error.reason).toBe("limit");
});
});