@staix/agent-hub 0.22.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +2 -2
- package/docs/agent-notes/adapters.md +2 -1
- package/docs/agent-notes/budget.md +1 -1
- package/docs/agent-notes/bus.md +4 -1
- package/docs/agent-notes/daemon.md +3 -2
- package/docs/agent-notes/local-worker.md +1 -1
- package/docs/agent-notes/models.md +2 -1
- package/docs/agent-notes/tasks.md +1 -0
- package/docs/agent-notes/tests.md +1 -1
- package/docs/dogfood.md +7 -1
- package/docs/operations.md +147 -6
- package/docs/profiles.md +28 -3
- package/docs/quickstart.md +5 -1
- package/docs/security.md +7 -1
- package/docs/specs/2026-09-19-agent-hub-design.md +66 -4
- package/docs/verification/2026-10-30-0.23.0.md +70 -0
- package/docs/verification/2026-10-30-adapter-operational-settings.md +261 -0
- package/docs/verification/2026-10-30-model-operational-settings.md +211 -0
- package/docs/verification/2026-10-30-operational-settings-closure.md +1217 -0
- package/docs/verification/2026-10-30-ui-console-operational-settings.md +1845 -0
- package/docs/verified.json +15 -15
- package/package.json +1 -1
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/ROLE-NOTICE.md +1 -1
- package/plugins/agent-hub/server.js +559 -115
- package/src/adapters/acp.ts +48 -2
- package/src/adapters/claude-channel.ts +6 -4
- package/src/adapters/codex-appserver.ts +19 -0
- package/src/adapters/jcode.ts +1 -1
- package/src/adapters/local-worker.ts +3 -3
- package/src/adapters/native-capabilities.ts +50 -0
- package/src/adapters/registry-types.ts +3 -10
- package/src/cli/capabilities-output.ts +30 -0
- package/src/cli/console-state.ts +8 -5
- package/src/cli/console.ts +27 -4
- package/src/cli/help.ts +2 -1
- package/src/cli/identity.ts +1 -0
- package/src/cli/main.ts +8 -0
- package/src/hub/board.ts +8 -2
- package/src/hub/bus.ts +100 -8
- package/src/hub/capabilities.ts +215 -0
- package/src/hub/config-apply.ts +6 -3
- package/src/hub/config-trust.ts +1 -0
- package/src/hub/control-client.ts +41 -8
- package/src/hub/daemon.ts +376 -20
- package/src/hub/deferred-hub.ts +2 -2
- package/src/hub/delivery-hints.ts +2 -2
- package/src/hub/delivery-journal.ts +2 -4
- package/src/hub/envelope.ts +12 -2
- package/src/hub/events.ts +2 -0
- package/src/hub/hub-tools.ts +16 -1
- package/src/hub/inference.ts +10 -5
- package/src/hub/invocations.ts +316 -0
- package/src/hub/operational-settings.ts +102 -1
- package/src/hub/peers.ts +3 -0
- package/src/hub/restart.ts +2 -2
- package/src/hub/room-invocations.ts +145 -0
- package/src/hub/settings.ts +1 -1
- package/src/hub/shipped-packs.ts +5 -2
- package/src/hub/shipped-roles.ts +13 -0
- package/src/hub/tasks.ts +14 -0
- package/src/memory/capture.ts +2 -2
- package/src/models/relay.ts +20 -9
- package/src/rooms/application.ts +67 -2
- package/src/rooms/boundary.ts +43 -11
- package/src/rooms/service.ts +40 -2
- package/templates/checks/analysis.mjs +59 -0
- package/templates/checks/fixtures/analysis-expected.json +1 -0
- package/templates/checks/fixtures/analysis-input.json +1 -0
- package/templates/checks/fixtures/analysis.mjs +7 -0
- package/templates/checks/fixtures/ops.json +4 -0
- package/templates/checks/fixtures/writing-rules.json +4 -0
- package/templates/checks/fixtures/writing.json +5 -0
- package/templates/checks/ops.mjs +40 -0
- package/templates/checks/writing.mjs +40 -0
- package/templates/packs/analysis.pack.json +14 -0
- package/templates/packs/ops.pack.json +14 -0
- package/templates/packs/writing-pack.pack.json +14 -0
- package/templates/roles/NOTICE.md +1 -1
- package/templates/roles/ops/meeting-actions-owner.md +26 -0
- package/templates/roles/ops/project-manager.md +26 -0
- package/templates/roles/writing/editor.md +26 -0
- package/templates/roles/writing/style-checker.md +26 -0
- package/templates/roles/writing/terminology-checker.md +26 -0
- package/templates/roles/writing/translator.md +26 -0
package/src/rooms/boundary.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { HUB, DIGEST, MAX_HOP, USER, sanitize, type Envelope } from "../hub/enve
|
|
|
4
4
|
import { mentionsPrivatePath } from "../hub/memory-policy.ts";
|
|
5
5
|
import { detectSignals, type Routing } from "../hub/routing.ts";
|
|
6
6
|
import type { PiiVerdict } from "../hub/inference.ts";
|
|
7
|
-
import { encodeRoomMessage, isRoomLocalPeerId, parseRemotePeerId, type RoomChatEnvelope } from "./application.ts";
|
|
7
|
+
import { encodeRoomMessage, isRoomLocalPeerId, parseRemotePeerId, type RoomChatEnvelope, type RoomInvocationPayload, type RoomInvocationKind, encodeRoomInvocationBody, decodeRoomInvocationBody } from "./application.ts";
|
|
8
8
|
|
|
9
9
|
export interface RoomBoundaryContext {
|
|
10
10
|
cwd: string; target: string; current: () => boolean;
|
|
@@ -15,6 +15,8 @@ export interface RoomBoundaryContext {
|
|
|
15
15
|
modelScreenOn: () => boolean;
|
|
16
16
|
/** The existing host-bound screenPii port owns device/campus admission, deadlines and fixed models. */
|
|
17
17
|
screen?: (text: string) => Promise<PiiVerdict>;
|
|
18
|
+
/** Host ledger provenance, never a serializable request/peer capability. */
|
|
19
|
+
invocation?: RoomInvocationAuthorization;
|
|
18
20
|
}
|
|
19
21
|
export type RoomBoundaryCode = "invalid_message" | "not_exposed" | "boundary_refused" | "hop";
|
|
20
22
|
declare const proofBrand: unique symbol;
|
|
@@ -25,6 +27,25 @@ const ENVELOPE_FIELDS = new Set(["id", "trace", "hop", "from", "to", "kind", "pr
|
|
|
25
27
|
const encoder = new TextEncoder();
|
|
26
28
|
const refused = (code: RoomBoundaryCode): RoomBoundaryResult => ({ status: "refused", code });
|
|
27
29
|
|
|
30
|
+
declare const invocationBrand: unique symbol;
|
|
31
|
+
export interface RoomInvocationAuthorization { readonly [invocationBrand]: true }
|
|
32
|
+
interface AuthorizedInvocation { binding: string; sender: string; kind: RoomInvocationKind; body: string; current: () => boolean }
|
|
33
|
+
const invocations = new WeakMap<RoomInvocationAuthorization, AuthorizedInvocation>();
|
|
34
|
+
/** Host-only construction: the ledger owner supplies the actual caller/callee and exact request/result. */
|
|
35
|
+
export function authorizeRoomInvocation(envelope: Envelope, sender: string, payload: RoomInvocationPayload, current: () => boolean): RoomInvocationAuthorization {
|
|
36
|
+
if (envelope.from !== HUB || envelope.kind !== "invocation" && envelope.kind !== "invocation_result" || !isRoomLocalPeerId(sender) || sender === USER) throw new Error("room: invocation provenance unavailable");
|
|
37
|
+
const body = encodeRoomInvocationBody(envelope.kind, payload);
|
|
38
|
+
const token = Object.freeze({}) as RoomInvocationAuthorization;
|
|
39
|
+
invocations.set(token, { binding: JSON.stringify(envelope), sender, kind: envelope.kind, body, current });
|
|
40
|
+
return token;
|
|
41
|
+
}
|
|
42
|
+
function invocationOf(token: RoomInvocationAuthorization | undefined, envelope: Envelope): AuthorizedInvocation | undefined {
|
|
43
|
+
if (!token || typeof token !== "object") return undefined;
|
|
44
|
+
const bound = invocations.get(token);
|
|
45
|
+
try { return bound && bound.kind === envelope.kind && bound.binding === JSON.stringify(envelope) && bound.current() === true ? bound : undefined; }
|
|
46
|
+
catch { return undefined; }
|
|
47
|
+
}
|
|
48
|
+
|
|
28
49
|
interface BodyPolicy { routing: Routing; paths: string[]; model: boolean; signature: string }
|
|
29
50
|
function bodyPolicy(context: RoomBoundaryContext): BodyPolicy | undefined {
|
|
30
51
|
try {
|
|
@@ -47,10 +68,12 @@ export async function roomBoundary(input: Envelope, ctx: RoomBoundaryContext): P
|
|
|
47
68
|
const env = structuredClone(input), context: RoomBoundaryContext = { ...ctx };
|
|
48
69
|
const target = parseRemotePeerId(context.target);
|
|
49
70
|
if (!target || !env || typeof env !== "object" || Array.isArray(env) || Object.keys(env).some(key => !ENVELOPE_FIELDS.has(key))) return refused("invalid_message");
|
|
50
|
-
|
|
51
|
-
if (env.
|
|
71
|
+
const invocation = invocationOf(context.invocation, env), sender = invocation?.sender ?? env.from;
|
|
72
|
+
if (env.from === HUB && !invocation || env.from === DIGEST || !isRoomLocalPeerId(sender)) return refused("boundary_refused");
|
|
73
|
+
if (!invocation && env.kind !== "chat" && env.kind !== "status" || env.private === true) return refused("boundary_refused");
|
|
52
74
|
if (env.private !== undefined && typeof env.private !== "boolean") return refused("invalid_message");
|
|
53
75
|
if (!Array.isArray(env.to) || !env.to.includes(context.target)) return refused("boundary_refused");
|
|
76
|
+
if (invocation && (env.to.length !== 1 || env.to[0] !== context.target)) return refused("boundary_refused");
|
|
54
77
|
if (env.to.length > 64 || env.to.some(id => !isRoomLocalPeerId(id) && !parseRemotePeerId(id))) return refused("invalid_message");
|
|
55
78
|
if (!Number.isSafeInteger(env.hop) || env.hop < 0 || env.hop > MAX_HOP) return refused("hop");
|
|
56
79
|
if (env.refs !== undefined && (!env.refs || typeof env.refs !== "object" || Array.isArray(env.refs))) return refused("invalid_message");
|
|
@@ -62,32 +85,41 @@ export async function roomBoundary(input: Envelope, ctx: RoomBoundaryContext): P
|
|
|
62
85
|
const authorized = (): RoomBoundaryCode | undefined => {
|
|
63
86
|
try {
|
|
64
87
|
if (context.current() !== true) return "boundary_refused";
|
|
65
|
-
if (
|
|
66
|
-
if (
|
|
88
|
+
if (context.invocation && invocationOf(context.invocation, env) !== invocation) return "boundary_refused";
|
|
89
|
+
if (sender !== USER && context.senderExposed(sender) !== true) return "not_exposed";
|
|
90
|
+
if (context.senderPrivate(sender) !== false || taskId !== undefined && context.taskPrivate(taskId) !== false) return "boundary_refused";
|
|
67
91
|
return context.current() === true ? undefined : "boundary_refused";
|
|
68
92
|
} catch { return "boundary_refused"; }
|
|
69
93
|
};
|
|
70
94
|
const initial = authorized(); if (initial) return refused(initial);
|
|
71
|
-
const envelope: RoomChatEnvelope = { id: env.id, trace: env.trace, hop: env.hop, from:
|
|
95
|
+
const envelope: RoomChatEnvelope = { id: env.id, trace: env.trace, hop: env.hop, from: sender, to: target.peer, kind: env.kind as RoomChatEnvelope["kind"], priority: invocation ? "status" : env.priority === "important" ? "status" : env.priority, body: invocation?.body ?? env.body, ts: env.ts };
|
|
72
96
|
encodeRoomMessage({ v: 1, type: "chat", envelope }); // Validate metadata/body budget before any model sees it.
|
|
73
97
|
const policy = bodyPolicy(context);
|
|
74
|
-
|
|
98
|
+
const body = envelope.body;
|
|
99
|
+
let withheld = !clearBody(body, paths, context, policy);
|
|
75
100
|
if (!withheld && policy?.model) {
|
|
76
101
|
const before = authorized(); if (before) return refused(before);
|
|
77
102
|
let verdict: PiiVerdict | undefined;
|
|
78
|
-
try { verdict = await context.screen?.(
|
|
103
|
+
try { verdict = await context.screen?.(body); } catch { /* unknown fails closed to the fixed stub */ }
|
|
79
104
|
const after = authorized(); if (after) return refused(after);
|
|
80
105
|
withheld = verdict?.label !== "clear";
|
|
81
106
|
const fresh = bodyPolicy(context);
|
|
82
107
|
if (!fresh || fresh.signature !== policy.signature) return refused("boundary_refused");
|
|
83
|
-
withheld ||= !clearBody(
|
|
108
|
+
withheld ||= !clearBody(body, paths, context, fresh);
|
|
84
109
|
}
|
|
85
110
|
const final = authorized(); if (final) return refused(final);
|
|
86
|
-
|
|
111
|
+
if (invocation?.kind === "invocation" && withheld) return refused("boundary_refused");
|
|
112
|
+
let outgoingBody = withheld ? `[room message ${envelope.id} withheld]` : sanitize(envelope.body);
|
|
113
|
+
if (invocation?.kind === "invocation_result") {
|
|
114
|
+
const result = decodeRoomInvocationBody("invocation_result", body);
|
|
115
|
+
const hiddenOutcome = result.outcome === "needs_review" || result.outcome === "timeout" ? "needs_review" : result.outcome === "refused" ? "refused" : "withheld";
|
|
116
|
+
outgoingBody = encodeRoomInvocationBody("invocation_result", withheld ? { ...result, outcome: hiddenOutcome, body: "" } : { ...result, body: sanitize(result.body) });
|
|
117
|
+
} else if (invocation) outgoingBody = body; // Canonical structured args, sanitized only when the target renders them.
|
|
118
|
+
const outgoing = Object.freeze({ ...envelope, body: outgoingBody });
|
|
87
119
|
encodeRoomMessage({ v: 1, type: "chat", envelope: outgoing });
|
|
88
120
|
const current = () => authorized() === undefined && (withheld || (() => {
|
|
89
121
|
const latest = bodyPolicy(context);
|
|
90
|
-
return latest !== undefined && latest.signature === policy?.signature && clearBody(
|
|
122
|
+
return latest !== undefined && latest.signature === policy?.signature && clearBody(body, paths, context, latest);
|
|
91
123
|
})());
|
|
92
124
|
const proof = Object.freeze({}) as RoomOutboundProof;
|
|
93
125
|
proofs.set(proof, { envelope: outgoing, current });
|
package/src/rooms/service.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/** Host integration for X4. Remote identities carry chat, never local control authority. */
|
|
2
2
|
import type { Bus } from "../hub/bus.ts";
|
|
3
3
|
import { USER, MAX_HOP, type Envelope, type PeerState } from "../hub/envelope.ts";
|
|
4
|
+
import type { RoomInvocationAuthorization } from "./boundary.ts";
|
|
5
|
+
import { roomInvocationHash } from "../hub/room-invocations.ts";
|
|
4
6
|
import { Limiter, PROJECT_LIMITS, type LimitsConfig } from "../hub/limits.ts";
|
|
5
7
|
import type { PiiVerdict } from "../hub/inference.ts";
|
|
6
8
|
import type { Routing } from "../hub/routing.ts";
|
|
@@ -8,9 +10,14 @@ import { roomMembers } from "./wire.ts";
|
|
|
8
10
|
import { RoomStore, RoomStoreError, RoomApplicationLimitError, type RoomOutgoing } from "./store.ts";
|
|
9
11
|
import { RoomPolicy } from "./policy.ts";
|
|
10
12
|
import { RemotePeer } from "./remote-peer.ts";
|
|
11
|
-
import { parseRemotePeerId, roomReceiptReasonText, type RoomMessage, type RoomReceiptReason } from "./application.ts";
|
|
13
|
+
import { parseRemotePeerId, roomReceiptReasonText, type RoomMessage, type RoomReceiptReason, type RoomChatEnvelope } from "./application.ts";
|
|
12
14
|
import { RoomApplicationDeferredError, type RoomLink, type RoomLinkApplication, type RoomLinkContext, type RoomLinkState, type RoomPresencePeer } from "./link.ts";
|
|
13
15
|
|
|
16
|
+
export interface RoomInvocationIdentity { room: string; member: string; epoch: string; current: () => boolean }
|
|
17
|
+
export interface RoomInvocationIngress {
|
|
18
|
+
envelope: RoomChatEnvelope; room: string; member: string; epoch: string; remotePeer: string; hash: string;
|
|
19
|
+
current: () => boolean; claim: () => boolean;
|
|
20
|
+
}
|
|
14
21
|
export interface RoomChatOptions {
|
|
15
22
|
bus: Bus; cwd: string; now?: () => number;
|
|
16
23
|
routing: () => Routing; piiPaths: () => readonly string[];
|
|
@@ -18,6 +25,8 @@ export interface RoomChatOptions {
|
|
|
18
25
|
taskPrivate: (id: number) => boolean | undefined;
|
|
19
26
|
modelScreenOn: () => boolean; screen: (text: string) => Promise<PiiVerdict>;
|
|
20
27
|
policyVersion: () => string | number;
|
|
28
|
+
invocationAuthorization?: (env: Envelope, identity: RoomInvocationIdentity) => RoomInvocationAuthorization | undefined;
|
|
29
|
+
invocation?: (input: RoomInvocationIngress) => Promise<"admitted" | "refused">;
|
|
21
30
|
roster?: (ids: readonly string[]) => void;
|
|
22
31
|
changed?: () => void;
|
|
23
32
|
limits?: () => LimitsConfig;
|
|
@@ -109,6 +118,15 @@ export class RoomChat {
|
|
|
109
118
|
this.bindings.set(name, binding);
|
|
110
119
|
this.refreshTargets(); this.refreshOutgoing(binding); this.queueChanged();
|
|
111
120
|
}
|
|
121
|
+
invocationIdentity(id: string, sender?: string): RoomInvocationIdentity | undefined {
|
|
122
|
+
const target = this.targets.get(id), state = target?.binding.store.get(target.binding.name);
|
|
123
|
+
if (!target || !state || !this.valid(target.binding) || sender !== undefined && !this.exposed(target.binding, sender)) return undefined;
|
|
124
|
+
const epoch = state.current.digest, room = state.current.room, member = target.member;
|
|
125
|
+
return { room, member, epoch, current: () => {
|
|
126
|
+
const next = this.targets.get(id), current = target.binding.store.get(target.binding.name);
|
|
127
|
+
return (sender === undefined || this.exposed(target.binding, sender)) && this.valid(target.binding) && next?.binding === target.binding && next.member === member && current?.current.room === room && current.current.digest === epoch;
|
|
128
|
+
} };
|
|
129
|
+
}
|
|
112
130
|
private refreshTargets(): void {
|
|
113
131
|
const candidates = new Map<string, Target[]>();
|
|
114
132
|
for (const binding of this.bindings.values()) {
|
|
@@ -146,7 +164,9 @@ export class RoomChat {
|
|
|
146
164
|
const digest = state?.current.digest;
|
|
147
165
|
const current = () => this.valid(binding) && this.targets.get(id)?.binding === binding && this.targets.get(id)?.member === target.member && this.options.policyVersion() === version
|
|
148
166
|
&& binding.store.get(binding.name)?.current.digest === digest;
|
|
149
|
-
|
|
167
|
+
const identity = this.invocationIdentity(id);
|
|
168
|
+
const invocation = identity ? this.options.invocationAuthorization?.(env, identity) : undefined;
|
|
169
|
+
return { cwd: this.options.cwd, target: id, current, ...(invocation ? { invocation } : {}),
|
|
150
170
|
senderExposed: peer => this.exposed(binding, peer), senderPrivate: this.options.senderPrivate,
|
|
151
171
|
taskPrivate: this.options.taskPrivate, routing: this.options.routing, piiPaths: this.options.piiPaths,
|
|
152
172
|
modelScreenOn: this.options.modelScreenOn, screen: this.options.screen };
|
|
@@ -299,6 +319,24 @@ export class RoomChat {
|
|
|
299
319
|
else if (chat.to !== USER && this.options.bus.hushed(env).some(row => row.peer === chat.to)) refusal = "boundary_refused";
|
|
300
320
|
else if (!this.rate(from, incoming.sender, chat.to, chat.body)) refusal = "rate_limited";
|
|
301
321
|
if (refusal) context.store.finishApplication(context.name, input.digest, "failed_safe", refusal, context.current);
|
|
322
|
+
else if (chat.kind === "invocation" || chat.kind === "invocation_result") {
|
|
323
|
+
if (!this.options.invocation) context.store.finishApplication(context.name, input.digest, "failed_safe", "boundary_refused", context.current);
|
|
324
|
+
else {
|
|
325
|
+
const epoch = state.current.digest;
|
|
326
|
+
const current = () => context.current() && this.valid(binding) && binding.store.get(binding.name)?.current.digest === epoch
|
|
327
|
+
&& this.sourceUnambiguous(binding, incoming.sender, member!.label) && member!.exposedPeers.includes(chat.from) && this.exposed(binding, chat.to)
|
|
328
|
+
&& !this.options.bus.hushed(env).some(row => row.peer === chat.to);
|
|
329
|
+
try {
|
|
330
|
+
const admitted = await this.options.invocation({ envelope: chat, room: state.current.room, member: incoming.sender, epoch, remotePeer: from,
|
|
331
|
+
hash: roomInvocationHash(chat), current, claim: () => current() && context.store.claimApplication(context.name, input.digest, current) });
|
|
332
|
+
if (!current()) throw new Error("room: state_changed");
|
|
333
|
+
context.store.finishApplication(context.name, input.digest, admitted === "admitted" ? "completed" : "failed_safe", admitted === "admitted" ? "admitted" : "boundary_refused", current);
|
|
334
|
+
if (admitted === "admitted") this.count(binding, "inbound");
|
|
335
|
+
} catch (error) {
|
|
336
|
+
context.store.quarantineApplication(context.name, input.digest); this.capacityChanged(); this.options.changed?.(); throw error;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
302
340
|
else if (chat.to === USER) {
|
|
303
341
|
if (!context.current() || !this.valid(binding)) throw new Error("room: state_changed");
|
|
304
342
|
const admission = context.store.admitPersonInbox(context.name, input.digest, context.current);
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Runs only the explicitly selected program. No shell and no automatic invocation.
|
|
3
|
+
import { constants, closeSync, fstatSync, lstatSync, openSync, readSync } from "node:fs";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
|
+
|
|
7
|
+
const MAX_BYTES = 1024 * 1024, TIMEOUT_MS = 5000;
|
|
8
|
+
function boundedFile(file) {
|
|
9
|
+
const before = lstatSync(file);
|
|
10
|
+
if (!before.isFile() || before.isSymbolicLink() || before.size > MAX_BYTES) throw new Error("input refused");
|
|
11
|
+
const fd = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
12
|
+
try {
|
|
13
|
+
const opened = fstatSync(fd);
|
|
14
|
+
if (!opened.isFile() || opened.dev !== before.dev || opened.ino !== before.ino || opened.size > MAX_BYTES) throw new Error("input changed");
|
|
15
|
+
const bytes = Buffer.alloc(opened.size + 1); let size = 0;
|
|
16
|
+
while (size < bytes.length) { const count = readSync(fd, bytes, size, bytes.length - size, size); if (!count) break; size += count; }
|
|
17
|
+
const after = fstatSync(fd);
|
|
18
|
+
if (size !== opened.size || after.size !== opened.size || after.mtimeMs !== opened.mtimeMs || after.ctimeMs !== opened.ctimeMs) throw new Error("input changed");
|
|
19
|
+
return bytes.subarray(0, size);
|
|
20
|
+
} finally { closeSync(fd); }
|
|
21
|
+
}
|
|
22
|
+
function run(program, input) {
|
|
23
|
+
return new Promise(resolveRun => {
|
|
24
|
+
const group = process.platform !== "win32";
|
|
25
|
+
const child = spawn(process.execPath, [program, input], { shell: false, detached: group, stdio: ["ignore", "pipe", "ignore"] });
|
|
26
|
+
const chunks = []; let size = 0, refused = false, settled = false;
|
|
27
|
+
const kill = () => { refused = true; try { if (group && child.pid) process.kill(-child.pid, "SIGKILL"); else child.kill("SIGKILL"); } catch { /* Already gone. */ } };
|
|
28
|
+
const timer = setTimeout(kill, TIMEOUT_MS);
|
|
29
|
+
const finish = (code) => {
|
|
30
|
+
if (settled) return; settled = true; clearTimeout(timer);
|
|
31
|
+
// A program may have started descendants; release only this invocation's process group.
|
|
32
|
+
if (group && child.pid) try { process.kill(-child.pid, "SIGKILL"); } catch { /* Already gone. */ }
|
|
33
|
+
resolveRun(!refused && code === 0 ? Buffer.concat(chunks, size) : undefined);
|
|
34
|
+
};
|
|
35
|
+
child.on("error", () => { kill(); finish(null); });
|
|
36
|
+
child.stdout.on("data", bytes => {
|
|
37
|
+
if (refused) return;
|
|
38
|
+
if (bytes.length > MAX_BYTES - size) { chunks.length = 0; kill(); return; }
|
|
39
|
+
size += bytes.length; chunks.push(Buffer.from(bytes));
|
|
40
|
+
});
|
|
41
|
+
child.on("close", finish);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
if (process.argv.length !== 5) {
|
|
45
|
+
console.error("usage: analysis.mjs <program.mjs> <input.json> <expected-output>"); console.log("analysis: FAIL"); process.exitCode = 2;
|
|
46
|
+
} else {
|
|
47
|
+
let ok = false;
|
|
48
|
+
try {
|
|
49
|
+
const program = resolve(process.argv[2]), input = resolve(process.argv[3]), expectedFile = resolve(process.argv[4]);
|
|
50
|
+
const programBytes = boundedFile(program), inputBytes = boundedFile(input), expected = boundedFile(expectedFile);
|
|
51
|
+
JSON.parse(new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(inputBytes));
|
|
52
|
+
const unchanged = () => boundedFile(program).equals(programBytes) && boundedFile(input).equals(inputBytes) && boundedFile(expectedFile).equals(expected);
|
|
53
|
+
const first = await run(program, input);
|
|
54
|
+
if (first === undefined || !unchanged()) throw new Error("first run refused or declared files changed");
|
|
55
|
+
const second = await run(program, input);
|
|
56
|
+
ok = unchanged() && first !== undefined && second !== undefined && first.equals(expected) && second.equals(expected);
|
|
57
|
+
} catch { /* Refuse without echoing program stderr, private paths, inputs or expected bytes. */ }
|
|
58
|
+
console.log(`analysis-reproduce: ${ok ? "OK" : "FAIL"}`); console.log(`analysis: ${ok ? "OK" : "FAIL"}`); process.exitCode = ok ? 0 : 1;
|
|
59
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"count":4,"sum":20}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[2, 4, 6, 8]
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Explicit fixture program: node analysis.mjs <input.json>.
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
const values = JSON.parse(readFileSync(process.argv[2], "utf8"));
|
|
4
|
+
if (!Array.isArray(values) || !values.every(value => typeof value === "number" && Number.isFinite(value))) process.exit(1);
|
|
5
|
+
const sum = values.reduce((total, value) => total + value, 0);
|
|
6
|
+
if (!Number.isFinite(sum)) process.exit(1);
|
|
7
|
+
process.stdout.write(JSON.stringify({ count: values.length, sum }) + "\n");
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Structure only. An empty array does not establish that all required actions were recorded.
|
|
3
|
+
import { constants, closeSync, fstatSync, lstatSync, openSync, readSync } from "node:fs";
|
|
4
|
+
|
|
5
|
+
const MAX_BYTES = 1024 * 1024;
|
|
6
|
+
function actions(file) {
|
|
7
|
+
const before = lstatSync(file);
|
|
8
|
+
if (!before.isFile() || before.isSymbolicLink() || before.size > MAX_BYTES) throw new Error("input refused");
|
|
9
|
+
const fd = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
10
|
+
try {
|
|
11
|
+
const opened = fstatSync(fd);
|
|
12
|
+
if (!opened.isFile() || opened.dev !== before.dev || opened.ino !== before.ino || opened.size > MAX_BYTES) throw new Error("input changed");
|
|
13
|
+
const bytes = Buffer.alloc(opened.size + 1); let size = 0;
|
|
14
|
+
while (size < bytes.length) { const count = readSync(fd, bytes, size, bytes.length - size, size); if (!count) break; size += count; }
|
|
15
|
+
const after = fstatSync(fd);
|
|
16
|
+
if (size !== opened.size || after.size !== opened.size || after.mtimeMs !== opened.mtimeMs || after.ctimeMs !== opened.ctimeMs) throw new Error("input changed");
|
|
17
|
+
return JSON.parse(new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes.subarray(0, size)));
|
|
18
|
+
} finally { closeSync(fd); }
|
|
19
|
+
}
|
|
20
|
+
function date(value) {
|
|
21
|
+
if (typeof value !== "string" || !/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/.test(value)) return false;
|
|
22
|
+
const parsed = new Date(`${value}T00:00:00.000Z`);
|
|
23
|
+
return Number.isFinite(parsed.getTime()) && parsed.toISOString().slice(0, 10) === value;
|
|
24
|
+
}
|
|
25
|
+
function valid(value) {
|
|
26
|
+
if (!Array.isArray(value)) return false;
|
|
27
|
+
const seen = new Set(), keys = ["id", "owner", "due", "status"], statuses = new Set(["open", "in_progress", "blocked", "done"]);
|
|
28
|
+
return value.every(action => {
|
|
29
|
+
if (!action || typeof action !== "object" || Array.isArray(action) || Object.keys(action).length !== keys.length || !keys.every(key => Object.hasOwn(action, key))) return false;
|
|
30
|
+
if (typeof action.id !== "string" || !action.id.trim() || seen.has(action.id) || typeof action.owner !== "string" || !action.owner.trim() || !date(action.due) || !statuses.has(action.status)) return false;
|
|
31
|
+
seen.add(action.id); return true;
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
if (process.argv.length !== 3) {
|
|
35
|
+
console.error("usage: ops.mjs <actions.json>"); console.log("ops: FAIL"); process.exitCode = 2;
|
|
36
|
+
} else {
|
|
37
|
+
let ok = false;
|
|
38
|
+
try { ok = valid(actions(process.argv[2])); } catch { /* Never echo action content, identifiers or paths. */ }
|
|
39
|
+
console.log(`ops-records: ${ok ? "OK" : "FAIL"}`); console.log(`ops: ${ok ? "OK" : "FAIL"}`); process.exitCode = ok ? 0 : 1;
|
|
40
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Explicit invocation only: node writing.mjs <document.json> <rules.json>.
|
|
3
|
+
import { constants, closeSync, fstatSync, lstatSync, openSync, readSync } from "node:fs";
|
|
4
|
+
|
|
5
|
+
const MAX_BYTES = 1024 * 1024;
|
|
6
|
+
function readJSON(file) {
|
|
7
|
+
const before = lstatSync(file);
|
|
8
|
+
if (!before.isFile() || before.isSymbolicLink() || before.size > MAX_BYTES) throw new Error("input refused");
|
|
9
|
+
const fd = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
10
|
+
try {
|
|
11
|
+
const opened = fstatSync(fd);
|
|
12
|
+
if (!opened.isFile() || opened.dev !== before.dev || opened.ino !== before.ino || opened.size > MAX_BYTES) throw new Error("input changed");
|
|
13
|
+
const bytes = Buffer.alloc(opened.size + 1); let size = 0;
|
|
14
|
+
while (size < bytes.length) { const count = readSync(fd, bytes, size, bytes.length - size, size); if (!count) break; size += count; }
|
|
15
|
+
const after = fstatSync(fd);
|
|
16
|
+
if (size !== opened.size || after.size !== opened.size || after.mtimeMs !== opened.mtimeMs || after.ctimeMs !== opened.ctimeMs) throw new Error("input changed");
|
|
17
|
+
return JSON.parse(new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes.subarray(0, size)));
|
|
18
|
+
} finally { closeSync(fd); }
|
|
19
|
+
}
|
|
20
|
+
const object = value => !!value && typeof value === "object" && !Array.isArray(value);
|
|
21
|
+
const nonempty = value => typeof value === "string" && value.trim().length > 0;
|
|
22
|
+
const terms = value => Array.isArray(value) && value.every(nonempty);
|
|
23
|
+
function report(results) {
|
|
24
|
+
for (const [id, ok] of results) console.log(`${id}: ${ok ? "OK" : "FAIL"}`);
|
|
25
|
+
const ok = results.every(([, passed]) => passed);
|
|
26
|
+
console.log(`writing: ${ok ? "OK" : "FAIL"}`); process.exitCode = ok ? 0 : 1;
|
|
27
|
+
}
|
|
28
|
+
if (process.argv.length !== 4) {
|
|
29
|
+
console.error("usage: writing.mjs <document.json> <rules.json>"); console.log("writing: FAIL"); process.exitCode = 2;
|
|
30
|
+
} else {
|
|
31
|
+
let document, rules;
|
|
32
|
+
try { document = readJSON(process.argv[2]); } catch { /* Refuse without echoing document bytes or paths. */ }
|
|
33
|
+
try { rules = readJSON(process.argv[3]); } catch { /* Rule values never become diagnostics or regular expressions. */ }
|
|
34
|
+
const metadata = object(document) && [document.title, document.language, document.text].every(nonempty);
|
|
35
|
+
const body = metadata ? `${document.title}\n${document.text}` : "";
|
|
36
|
+
const literals = metadata && object(rules) && terms(rules.requiredTerms) && terms(rules.bannedPhrases)
|
|
37
|
+
&& rules.requiredTerms.every(term => document.text.includes(term)) && rules.bannedPhrases.every(term => !body.includes(term));
|
|
38
|
+
const style = metadata && !/\b(?:TODO|TBD)\b/i.test(body) && body.split(/\r\n|\r|\n/).every(line => line === line.trimEnd());
|
|
39
|
+
report([["writing-metadata", metadata], ["writing-terms", literals], ["writing-style", style]]);
|
|
40
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "analysis",
|
|
3
|
+
"profile": "research",
|
|
4
|
+
"summary": "Reproducible analysis of declared inputs, with separate person review of provenance and method.",
|
|
5
|
+
"roles": ["analyst"],
|
|
6
|
+
"rubric": [
|
|
7
|
+
{ "id": "analysis-reproduce", "text": "The local reproduction check compares the declared program and input against the expected output and records its observed result.", "required": true },
|
|
8
|
+
{ "id": "analysis-inputs", "text": "Person review confirms input provenance, selection and limits, with unknowns and missing data stated explicitly.", "required": true },
|
|
9
|
+
{ "id": "analysis-method", "text": "Person review assesses method suitability, assumptions and interpretation; output equality alone does not establish validity.", "required": true }
|
|
10
|
+
],
|
|
11
|
+
"checks": [
|
|
12
|
+
{ "id": "analysis-reproduce", "note": "Manual, advisory example only: bun templates/checks/analysis.mjs <program.mjs> <input.json> <expected-output>. A match proves reproduction, not method validity." }
|
|
13
|
+
]
|
|
14
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ops",
|
|
3
|
+
"profile": "general",
|
|
4
|
+
"summary": "Traceable project and meeting action records with explicit ownership, status and human follow-up review.",
|
|
5
|
+
"roles": ["project-manager", "meeting-actions-owner"],
|
|
6
|
+
"rubric": [
|
|
7
|
+
{ "id": "ops-records", "text": "Action records pass the local schema, identity, ownership and status checks, with observed results retained.", "required": true },
|
|
8
|
+
{ "id": "ops-coverage", "text": "Person review checks coverage against the source meeting or project record and identifies missing or duplicated actions.", "required": true },
|
|
9
|
+
{ "id": "ops-followup", "text": "Person review confirms assignments and follow-up dates were agreed, or marks them proposed or unknown; completion requires evidence.", "required": true }
|
|
10
|
+
],
|
|
11
|
+
"checks": [
|
|
12
|
+
{ "id": "ops-records", "note": "Manual, advisory example only: bun templates/checks/ops.mjs <actions.json>. A schema pass does not prove meeting coverage or agreement." }
|
|
13
|
+
]
|
|
14
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "writing-pack",
|
|
3
|
+
"profile": "writing",
|
|
4
|
+
"summary": "Scoped editing and translation with explicit metadata, terminology and style evidence.",
|
|
5
|
+
"roles": ["editor", "translator", "style-checker", "terminology-checker"],
|
|
6
|
+
"rubric": [
|
|
7
|
+
{ "id": "writing-metadata", "text": "Required document metadata is present and agrees with the supplied rules.", "required": true },
|
|
8
|
+
{ "id": "writing-terms", "text": "The document passes the supplied terminology rules; unresolved usage decisions are identified for person review.", "required": true },
|
|
9
|
+
{ "id": "writing-style", "text": "The document passes the supplied mechanical style rules; readability and audience fit have a separate reviewer judgment.", "required": true }
|
|
10
|
+
],
|
|
11
|
+
"checks": [
|
|
12
|
+
{ "id": "writing-check", "note": "Manual, advisory example only: bun templates/checks/writing.mjs <document.json> <rules.json>. Output is evidence, not publication approval." }
|
|
13
|
+
]
|
|
14
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Role catalog attribution
|
|
2
2
|
|
|
3
|
-
Selected role guidance is adapted from msitarzewski/agency-agents at f99f6aa910a442b0197b768ce0ea7751e35e2060. Each adapted definition names its upstream path. The other three research definitions are original agent-hub guidance. Upstream persona, code samples and autonomy claims are excluded.
|
|
3
|
+
Selected role guidance is adapted from msitarzewski/agency-agents at f99f6aa910a442b0197b768ce0ea7751e35e2060. Each adapted definition names its upstream path. The other three research definitions and the writing and ops definitions are original agent-hub guidance. Upstream persona, code samples and autonomy claims are excluded.
|
|
4
4
|
|
|
5
5
|
MIT License
|
|
6
6
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: meeting-actions-owner
|
|
3
|
+
name: Meeting actions owner
|
|
4
|
+
group: ops
|
|
5
|
+
summary: Extract and maintain traceable action records from a bounded meeting or project record.
|
|
6
|
+
base: implementer
|
|
7
|
+
classes: implement,summarize
|
|
8
|
+
source: original agent-hub guidance
|
|
9
|
+
---
|
|
10
|
+
## Scope
|
|
11
|
+
Create action records from the supplied meeting or project sources. Keep decisions, proposals, assignments, deadlines and unknowns separate, with stable action IDs and source references.
|
|
12
|
+
|
|
13
|
+
## Not in scope
|
|
14
|
+
Do not infer attendance, acceptance, completion or dates from silence. External follow-up, calendar changes and commitments by others require explicit authorization.
|
|
15
|
+
|
|
16
|
+
## Deliverables
|
|
17
|
+
Action records with the required fields, source locations, stated owner and date status, plus unresolved questions and a concise change account.
|
|
18
|
+
|
|
19
|
+
## Hub use
|
|
20
|
+
Use the existing task flow for the assigned base role. Name source and output paths in the plan; report missing records and proposed follow-up before treating it as agreed work.
|
|
21
|
+
|
|
22
|
+
## Review duties
|
|
23
|
+
Check coverage against the source, duplicate actions, ownership and status consistency. Verify that a completion claim has an actual receipt rather than a planned date.
|
|
24
|
+
|
|
25
|
+
## Evidence
|
|
26
|
+
Meeting excerpts or decision references and the observed record-check result. Person review establishes source coverage and confirms proposed assignments or follow-up dates.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: project-manager
|
|
3
|
+
name: Project manager
|
|
4
|
+
group: ops
|
|
5
|
+
summary: Define a bounded delivery plan with explicit ownership, dependencies and acceptance evidence.
|
|
6
|
+
base: planner
|
|
7
|
+
classes: plan
|
|
8
|
+
source: original agent-hub guidance
|
|
9
|
+
---
|
|
10
|
+
## Scope
|
|
11
|
+
Turn the authorized objective into scoped tasks, dependencies, proposed owners, milestones and acceptance evidence. Distinguish recorded commitments from planning assumptions.
|
|
12
|
+
|
|
13
|
+
## Not in scope
|
|
14
|
+
Do not invent progress, assign authority through prose or commit other people to dates. Budget, staffing, external messages and release decisions remain with their authorized actors.
|
|
15
|
+
|
|
16
|
+
## Deliverables
|
|
17
|
+
Reviewable task plan, dependency and risk notes, owner and date proposals, acceptance criteria, and decisions or missing inputs requiring a person.
|
|
18
|
+
|
|
19
|
+
## Hub use
|
|
20
|
+
Use the existing planning flow and task board. Reference the source request and affected paths; surface overlap, blockers and scope changes through the established coordination tools.
|
|
21
|
+
|
|
22
|
+
## Review duties
|
|
23
|
+
Check that each proposed outcome has an owner, a feasible dependency order and observable completion evidence. Keep unknown capacity and unaccepted dates explicit.
|
|
24
|
+
|
|
25
|
+
## Evidence
|
|
26
|
+
Source requests, recorded decisions, task references and verified progress receipts. A plan or inventory count is not evidence of completed work or available capacity.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: editor
|
|
3
|
+
name: Editor
|
|
4
|
+
group: writing
|
|
5
|
+
summary: Edit a scoped document for structure, clarity and fidelity to its source material.
|
|
6
|
+
base: implementer
|
|
7
|
+
classes: implement,bulk_edit
|
|
8
|
+
source: original agent-hub guidance
|
|
9
|
+
---
|
|
10
|
+
## Scope
|
|
11
|
+
Edit the assigned document against its audience, purpose, source material and declared style rules. Keep supported meaning and attribution intact.
|
|
12
|
+
|
|
13
|
+
## Not in scope
|
|
14
|
+
Do not invent facts, quotes or approvals. Publication, external submission and changes to project policy require their own authorized actors.
|
|
15
|
+
|
|
16
|
+
## Deliverables
|
|
17
|
+
Revised document, a concise account of substantive changes, unresolved questions and references to the exact inputs and rules used.
|
|
18
|
+
|
|
19
|
+
## Hub use
|
|
20
|
+
Use the existing task flow for the assigned base role. Identify the document paths and proposed edits in the plan; report blockers and changed scope before extending the work.
|
|
21
|
+
|
|
22
|
+
## Review duties
|
|
23
|
+
Check organization, unsupported claims, ambiguous wording and consistency. Separate a mechanical check result from a person's editorial judgment.
|
|
24
|
+
|
|
25
|
+
## Evidence
|
|
26
|
+
Versioned source and document references, recorded rule checks, and a clear list of unverified claims or editorial decisions requiring review.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: style-checker
|
|
3
|
+
name: Style checker
|
|
4
|
+
group: writing
|
|
5
|
+
summary: Check a document against supplied style rules and separate mechanical findings from editorial judgment.
|
|
6
|
+
base: verifier,reviewer
|
|
7
|
+
classes: test,review
|
|
8
|
+
source: original agent-hub guidance
|
|
9
|
+
---
|
|
10
|
+
## Scope
|
|
11
|
+
Check the assigned document against the exact supplied style rules. Distinguish deterministic constraints from readability or tone judgments that need human review.
|
|
12
|
+
|
|
13
|
+
## Not in scope
|
|
14
|
+
Do not invent a style policy, rewrite the whole document or treat a mechanical pass as approval to publish. Verification grants no additional permissions.
|
|
15
|
+
|
|
16
|
+
## Deliverables
|
|
17
|
+
Findings with rule IDs, document locations, observed text and suggested corrections. Record passed, failed, unrun and human-review items separately.
|
|
18
|
+
|
|
19
|
+
## Hub use
|
|
20
|
+
Use the existing task or review flow for the assigned base role. Cite the document and rule versions, and bind each finding to a reproducible observation.
|
|
21
|
+
|
|
22
|
+
## Review duties
|
|
23
|
+
Check declared formatting, consistency and language constraints without changing their thresholds. Identify conflicts or ambiguous rules instead of silently choosing a new standard.
|
|
24
|
+
|
|
25
|
+
## Evidence
|
|
26
|
+
Observed local check results and source locations support mechanical findings. Readability, audience fit and tone require a stated reviewer judgment.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: terminology-checker
|
|
3
|
+
name: Terminology checker
|
|
4
|
+
group: writing
|
|
5
|
+
summary: Verify declared terms, names and translations against a versioned glossary.
|
|
6
|
+
base: verifier,reviewer
|
|
7
|
+
classes: test,review
|
|
8
|
+
source: original agent-hub guidance
|
|
9
|
+
---
|
|
10
|
+
## Scope
|
|
11
|
+
Check the assigned document for the supplied preferred, restricted and language-specific terms. Keep exact glossary rules distinct from context-dependent usage judgments.
|
|
12
|
+
|
|
13
|
+
## Not in scope
|
|
14
|
+
Do not create institutional names, expand unknown acronyms by guesswork or change the authoritative glossary. A literal match does not prove factual or translation correctness.
|
|
15
|
+
|
|
16
|
+
## Deliverables
|
|
17
|
+
Term findings with rule IDs, source locations and glossary references; explicit ambiguity and exception requests where context prevents a firm verdict.
|
|
18
|
+
|
|
19
|
+
## Hub use
|
|
20
|
+
Use the existing task or review flow for the assigned base role. Identify document and glossary paths and versions; report a missing or conflicting glossary before treating terms as verified.
|
|
21
|
+
|
|
22
|
+
## Review duties
|
|
23
|
+
Check proper nouns, abbreviations and consistent translations. Preserve quoted source text and distinguish a declared exception from an unnoticed mismatch.
|
|
24
|
+
|
|
25
|
+
## Evidence
|
|
26
|
+
Versioned glossary entries, exact document occurrences and observed checker output. Domain usage and proposed exceptions need a recorded person review.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: translator
|
|
3
|
+
name: Translator
|
|
4
|
+
group: writing
|
|
5
|
+
summary: Translate a bounded document while preserving meaning, attribution and declared terminology.
|
|
6
|
+
base: implementer
|
|
7
|
+
classes: implement,bulk_edit
|
|
8
|
+
source: original agent-hub guidance
|
|
9
|
+
---
|
|
10
|
+
## Scope
|
|
11
|
+
Translate the assigned source into the requested language and register. Preserve names, quantities, qualifiers, citations and the distinction between completed and proposed activity.
|
|
12
|
+
|
|
13
|
+
## Not in scope
|
|
14
|
+
Do not supply missing facts, certify a translation or imply publication approval. Leave legal or specialist interpretation and external delivery to their authorized actors.
|
|
15
|
+
|
|
16
|
+
## Deliverables
|
|
17
|
+
Translated document, source-to-output references, terminology decisions and unresolved ambiguities with their source locations.
|
|
18
|
+
|
|
19
|
+
## Hub use
|
|
20
|
+
Use the existing task flow for the assigned base role. Name source, output and glossary paths in the plan and completion summary; report unavailable inputs explicitly.
|
|
21
|
+
|
|
22
|
+
## Review duties
|
|
23
|
+
Compare meaning and coverage against the source. Check omissions, changed certainty, numbers and proper nouns; identify wording that needs a fluent or domain reviewer.
|
|
24
|
+
|
|
25
|
+
## Evidence
|
|
26
|
+
Exact source and glossary versions, check results and examples of reviewed ambiguities. A terminology match alone does not establish translation fidelity.
|