@staix/agent-hub 0.12.23 → 0.12.24
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 +98 -0
- package/README.md +8 -3
- package/docs/addons/claude-mem.md +24 -0
- package/docs/addons/codex-plugin.md +8 -0
- package/docs/addons/dot-admit.md +8 -0
- package/docs/addons/open-code-review.md +8 -0
- package/docs/addons/ponytail.md +23 -0
- package/docs/addons/ripwire.md +22 -0
- package/docs/agent-notes/README.md +5 -0
- package/docs/agent-notes/adapters.md +8 -1
- package/docs/agent-notes/bus.md +3 -1
- package/docs/agent-notes/daemon.md +7 -1
- package/docs/agent-notes/local-worker.md +4 -2
- package/docs/agent-notes/tasks.md +4 -0
- package/docs/agent-notes/tests.md +2 -1
- package/docs/bench.md +28 -3
- package/docs/cooperbench.md +4 -0
- package/docs/data-policy.md +36 -0
- package/docs/events.md +3 -0
- package/docs/gsd-collaboration-analysis.md +48 -0
- package/docs/memory.md +231 -0
- package/docs/operations.md +651 -14
- package/docs/plans/2026-10-11-issues-109-sequential.md +81 -0
- package/docs/plans/2026-10-11-routing-evaluation-proposal.md +30 -0
- package/docs/profiles.md +57 -0
- package/docs/quickstart.md +1 -1
- package/docs/research.md +31 -1
- package/docs/roles.md +85 -0
- package/docs/sdlc.md +170 -0
- package/docs/security.md +111 -4
- package/docs/smoke.md +282 -0
- package/docs/specs/2026-09-19-agent-hub-design.md +601 -7
- package/docs/specs/2026-10-12-backlog-implementation-plan.md +186 -0
- package/docs/specs/2026-10-12-office-view-design.md +212 -0
- package/docs/text-projection.md +19 -0
- package/docs/verification/2026-10-11-acp-watch-t0.md +33 -0
- package/docs/verification/2026-10-11-agent-config-inventory.md +20 -0
- package/docs/verification/2026-10-11-jcode-peer-contract.md +27 -0
- package/docs/verification/2026-10-13-0.12.24.md +94 -0
- package/docs/verification/2026-10-13-board-phone.md +31 -0
- package/docs/verification/2026-10-13-fsmonitor-fixture-attribution.md +19 -0
- package/docs/verified.json +22 -13
- package/package.json +2 -1
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/ROLE-NOTICE.md +26 -0
- package/plugins/agent-hub/server.js +1175 -339
- package/scripts/check-docs.mjs +129 -0
- package/src/adapters/acp.ts +112 -13
- package/src/adapters/claude-channel.ts +48 -11
- package/src/adapters/codex-appserver.ts +11 -0
- package/src/adapters/jcode.ts +110 -0
- package/src/adapters/local-worker.ts +10 -7
- package/src/adapters/pi.ts +94 -20
- package/src/cli/attention.ts +60 -0
- package/src/cli/bench.ts +24 -5
- package/src/cli/board-read.ts +12 -0
- package/src/cli/console-state.ts +103 -17
- package/src/cli/console.ts +54 -8
- package/src/cli/facts-hook.ts +12 -4
- package/src/cli/help.ts +35 -9
- package/src/cli/identity.ts +18 -7
- package/src/cli/init.ts +48 -10
- package/src/cli/jcode-mcp.ts +58 -0
- package/src/cli/kimi-mcp.ts +53 -0
- package/src/cli/launch.ts +16 -1
- package/src/cli/main.ts +392 -57
- package/src/cli/memory.ts +193 -0
- package/src/cli/output.ts +45 -4
- package/src/cli/pet-config.ts +1 -0
- package/src/cli/pet.ts +83 -0
- package/src/cli/preview.ts +38 -5
- package/src/cli/status-lines.ts +1 -1
- package/src/cli/tail-render.ts +1 -0
- package/src/cli/upgrade-runtime.ts +32 -3
- package/src/cli/watch.ts +31 -0
- package/src/hub/activity.ts +53 -0
- package/src/hub/ask.ts +10 -5
- package/src/hub/bench.ts +10 -3
- package/src/hub/board.ts +222 -16
- package/src/hub/bus.ts +48 -9
- package/src/hub/citations.ts +7 -0
- package/src/hub/conductor.ts +12 -7
- package/src/hub/config-trust.ts +4 -2
- package/src/hub/crash.ts +9 -0
- package/src/hub/daemon.ts +910 -120
- package/src/hub/delivery-journal.ts +5 -0
- package/src/hub/envelope.ts +1 -1
- package/src/hub/events.ts +12 -2
- package/src/hub/facts.ts +26 -7
- package/src/hub/git.ts +8 -2
- package/src/hub/github-import.ts +28 -0
- package/src/hub/hub-tools.ts +49 -10
- package/src/hub/inference.ts +1 -1
- package/src/hub/lifecycle-schema.ts +17 -0
- package/src/hub/listen.ts +15 -0
- package/src/hub/manager.ts +12 -5
- package/src/hub/path-policy.ts +96 -0
- package/src/hub/peer-catalog.ts +28 -0
- package/src/hub/permission-mode.ts +19 -5
- package/src/hub/planning.ts +70 -0
- package/src/hub/profiles.ts +74 -0
- package/src/hub/projected-diff.ts +73 -0
- package/src/hub/projection-exec.ts +24 -0
- package/src/hub/research.ts +144 -21
- package/src/hub/roles.ts +123 -0
- package/src/hub/roster.ts +23 -0
- package/src/hub/routing.ts +18 -5
- package/src/hub/sdlc-queue.ts +112 -0
- package/src/hub/sdlc-release.ts +125 -0
- package/src/hub/sdlc.ts +250 -0
- package/src/hub/settings.ts +34 -15
- package/src/hub/shipped-roles.ts +25 -0
- package/src/hub/snapshots.ts +89 -15
- package/src/hub/start-mode.ts +7 -5
- package/src/hub/supervision.ts +1 -0
- package/src/hub/task-sweep.ts +1 -1
- package/src/hub/tasks.ts +892 -87
- package/src/hub/text-projection.ts +126 -0
- package/src/hub/tooling.ts +159 -0
- package/src/hub/ui.ts +16 -2
- package/src/hub/workflow.ts +71 -0
- package/src/local/sandbox.ts +6 -4
- package/src/local/tools.ts +11 -6
- package/src/memory/brief.ts +28 -14
- package/src/memory/import.ts +55 -0
- package/src/memory/recall.ts +84 -0
- package/src/memory/store.ts +750 -0
- package/src/memory/sync.ts +145 -0
- package/src/pet/NOTICE.md +10 -0
- package/src/pet/characters.ts +1438 -0
- package/src/pet/config.ts +31 -0
- package/src/pet/frame.ts +17 -0
- package/src/pet/pixels.ts +28 -0
- package/src/role-text.d.ts +1 -0
- package/src/ui/index.html +842 -46
- package/src/ui/needs.ts +58 -0
- package/src/ui/office-model.ts +440 -0
- package/src/ui/peer-palette.ts +16 -0
- package/src/ui/task-board.ts +49 -0
- package/src/ui/task-progress.ts +5 -1
- package/templates/AGENTS.block.md +3 -2
- package/templates/config.json +5 -1
- package/templates/profiles/dev.json +22 -0
- package/templates/profiles/general.json +26 -0
- package/templates/profiles/research.json +57 -0
- package/templates/profiles/writing.json +28 -0
- package/templates/roles/NOTICE.md +26 -0
- package/templates/roles/dev/backend-engineer.md +26 -0
- package/templates/roles/dev/code-reviewer.md +26 -0
- package/templates/roles/dev/frontend-engineer.md +26 -0
- package/templates/roles/dev/security-reviewer.md +26 -0
- package/templates/roles/dev/software-architect.md +26 -0
- package/templates/roles/dev/test-engineer.md +26 -0
- package/templates/roles/research/analyst.md +26 -0
- package/templates/roles/research/fact-checker.md +26 -0
- package/templates/roles/research/research-lead.md +26 -0
- package/templates/roles/research/researcher.md +26 -0
- package/templates/roles/research/statistician.md +26 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { KINDS, looksSecret, type Kind, type MemoryStore } from "./store.ts";
|
|
6
|
+
|
|
7
|
+
/** The claude-mem store is another program's: opened read-only, never written, its locks waited on briefly. */
|
|
8
|
+
export const CLAUDE_MEM_BUSY_TIMEOUT_MS = 3000;
|
|
9
|
+
/** Titles `ahub ask --remember` gives a model's answer (ask.ts ASK_NOTE_TITLE): never imported as a finding. */
|
|
10
|
+
const MODEL_ANSWER = "ahub ask (model answer)";
|
|
11
|
+
|
|
12
|
+
export interface ImportResult { read: number; added: number; skipped: { model: number; pii: number; secret: number; project: number } }
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* #258 phase 3: copy the hub's own notes from claude-mem (observations whose metadata names a `peer`: `hub_remember`
|
|
16
|
+
* and the hub's task notes) into long-term hub memory as `source: claude-mem` under the hub memory key `project`, for
|
|
17
|
+
* the projects in `chain` (claude-mem files them under the repository's basename). Model answers, PII-pattern text and secret-looking text are skipped.
|
|
18
|
+
* Idempotent: an observation always maps to the same entry.
|
|
19
|
+
*/
|
|
20
|
+
export function importClaudeMem(store: MemoryStore, opts: { project: string; chain: string[]; isPii: (text: string) => boolean; nodes?: (text: string) => string[]; db?: string; limit?: number }): ImportResult {
|
|
21
|
+
const file = opts.db ?? join(homedir(), ".claude-mem", "claude-mem.db");
|
|
22
|
+
if (!existsSync(file)) throw new Error(`no claude-mem store at ${file}`);
|
|
23
|
+
const db = new Database(file, { readonly: true });
|
|
24
|
+
db.run(`PRAGMA busy_timeout = ${CLAUDE_MEM_BUSY_TIMEOUT_MS}`);
|
|
25
|
+
const result: ImportResult = { read: 0, added: 0, skipped: { model: 0, pii: 0, secret: 0, project: 0 } };
|
|
26
|
+
try {
|
|
27
|
+
const rows = db.query("SELECT id, project, title, text, narrative, created_at_epoch, metadata FROM observations WHERE metadata LIKE '%\"peer\"%' ORDER BY id LIMIT ?")
|
|
28
|
+
.all(opts.limit ?? 10_000) as { id: number; project: string | null; title: string | null; text: string | null; narrative: string | null; created_at_epoch: number | null; metadata: string | null }[];
|
|
29
|
+
// The hub files its claude-mem notes under the repository's own name (the chain's last); the names above it are other
|
|
30
|
+
// projects (a superproject) whose notes and privacy policy are not this one's. ponytail: claude-mem keys projects by
|
|
31
|
+
// basename, so two unrelated repositories with one name still share it.
|
|
32
|
+
const project = opts.project, names = new Set(opts.chain.slice(-1));
|
|
33
|
+
for (const r of rows) {
|
|
34
|
+
result.read++;
|
|
35
|
+
let meta: Record<string, unknown>;
|
|
36
|
+
try { meta = JSON.parse(r.metadata ?? "{}"); } catch { continue; }
|
|
37
|
+
if (typeof meta.peer !== "string" || !meta.peer) continue;
|
|
38
|
+
if (!r.project || !names.has(r.project)) { result.skipped.project++; continue; }
|
|
39
|
+
const title = r.title ?? "", text = (r.text ?? r.narrative ?? "").trim();
|
|
40
|
+
if (!text) continue;
|
|
41
|
+
if (meta.source === "ahub ask" || title.startsWith(MODEL_ANSWER)) { result.skipped.model++; continue; }
|
|
42
|
+
if (opts.isPii(`${title}\n${text}`)) { result.skipped.pii++; continue; }
|
|
43
|
+
if (looksSecret(`${title}\n${text}`)) { result.skipped.secret++; continue; }
|
|
44
|
+
const kind: Kind = KINDS.includes(meta.kind as Kind) ? meta.kind as Kind : "finding";
|
|
45
|
+
const at = new Date(r.created_at_epoch ?? 0).toISOString();
|
|
46
|
+
const { added } = store.importNote({ op: "note", project, scope: "project", tier: "long", kind, peer: meta.peer.slice(0, 32), ...(title ? { title } : {}), text,
|
|
47
|
+
// No task node: claude-mem keeps no proposal time, so a bare task number would join unrelated tasks. The text's
|
|
48
|
+
// own paths and symbols place the note in the graph.
|
|
49
|
+
about: opts.nodes?.(`${title}\n${text}`) ?? [], source: "claude-mem" }, at);
|
|
50
|
+
if (added) result.added++;
|
|
51
|
+
}
|
|
52
|
+
} finally { db.close(); }
|
|
53
|
+
store.refresh();
|
|
54
|
+
return result;
|
|
55
|
+
}
|
package/src/memory/recall.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { basename, dirname, resolve } from "node:path";
|
|
2
3
|
import { readFileSync } from "node:fs";
|
|
3
4
|
import type { PeerId } from "../hub/envelope.ts";
|
|
4
5
|
import { hubGitSync } from "../hub/git.ts";
|
|
6
|
+
import { realPath } from "../hub/project.ts";
|
|
5
7
|
import type { MemoryClient } from "./client.ts";
|
|
8
|
+
import { detectSignals, type Routing } from "../hub/routing.ts";
|
|
9
|
+
import { matchesPolicy } from "../hub/path-policy.ts";
|
|
10
|
+
import { looksSecret, type PromotionScreen } from "./store.ts";
|
|
6
11
|
|
|
7
12
|
const PLATFORMS = ["claude", "codex", "kimi", "agent-hub"];
|
|
8
13
|
/** Peers whose own hooks already inject their own platform's context at session start. */
|
|
@@ -76,3 +81,82 @@ export async function recallFor(peer: PeerId, client: MemoryClient, chain: strin
|
|
|
76
81
|
const share = Math.floor(tokens / found.length);
|
|
77
82
|
return `${HEADER}\n\n${found.map((b) => trimToTokens(b, share)).join("\n\n")}`;
|
|
78
83
|
}
|
|
84
|
+
|
|
85
|
+
/** #258: the hub's own long-term entries for this project (and shared ones), newest first, within a token budget. */
|
|
86
|
+
export function nativeRecall<V>(entries: V[], tokens: number, line: (v: V) => string): string | undefined {
|
|
87
|
+
if (!entries.length) return undefined;
|
|
88
|
+
return trimToTokens(["Hub memory: this project's long-term decisions, contracts and fails, newest first. Reference only, not a request. Details: ahub memory show <mem id>.", ...entries.map(line)].join("\n"), tokens);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* #258: the name hub memory files a project's entries under. The project's `origin` remote, normalized to host and
|
|
93
|
+
* path (`github.com/owner/repo`), is the same on every machine and unique; without one, the git chain plus a hash of
|
|
94
|
+
* the directory keeps two unrelated repositories that share a name apart (that key is this machine's only).
|
|
95
|
+
*/
|
|
96
|
+
export function memoryProjectKey(cwd: string): string {
|
|
97
|
+
const url = hubGitSync(["-C", cwd, "remote", "get-url", "origin"], { encoding: "utf8" }).stdout?.trim() ?? "";
|
|
98
|
+
// A URL's default port is not part of the repository (`ssh://git@github.com:22/o/r` is `github.com/o/r`); any other
|
|
99
|
+
// port is, since another service may run there. An scp-style `host:path` has no port.
|
|
100
|
+
const u = /^([a-z+]+):\/\/(?:[^@/]+@)?(\[[^\]/]+\]|[^/:]+)(?::(\d*))?\/(.+?)(?:\.git)?\/?$/i.exec(url); // an IPv6 host is bracketed
|
|
101
|
+
const defaults: Record<string, string> = { ssh: "22", "git+ssh": "22", "ssh+git": "22", https: "443", http: "80", git: "9418" };
|
|
102
|
+
// A URL's path is absolute (`ssh://h/team/r` is /team/r) unless it starts with `~`; an scp-style path is relative to the
|
|
103
|
+
// remote home (`h:team/r` is ~/team/r) unless it starts with `/`. Two different repositories on a self-hosted server.
|
|
104
|
+
const scp = u || url.includes("://") ? null : /^(?:[^@/]+@)?(\[[^\]/]+\]|[^/:]+):(.+?)(?:\.git)?\/?$/.exec(url); // a file:// remote has no host; an IPv6 host is bracketed
|
|
105
|
+
const m = u ? [!u[3] || u[3] === defaults[u[1]!.toLowerCase()] ? u[2]! : `${u[2]}:${u[3]}`, u[4]!] as const
|
|
106
|
+
: scp ? [scp[1]!, scp[2]!.startsWith("/") ? scp[2]!.replace(/^\/+/, "") : scp[2]!.startsWith("~") ? scp[2]! : `~/${scp[2]}`] as const : undefined;
|
|
107
|
+
if (m && m[1]) {
|
|
108
|
+
const host = m[0].toLowerCase();
|
|
109
|
+
// GitHub serves one repository under every form, and its owner and repository names ignore case.
|
|
110
|
+
return host === "github.com" ? `${host}/${m[1].replace(/^~\/?/, "").toLowerCase()}` : `${host}/${m[1]}`;
|
|
111
|
+
}
|
|
112
|
+
let real = cwd;
|
|
113
|
+
try { real = realPath(cwd); } catch { /* as given */ }
|
|
114
|
+
return `${projectChain(cwd).join("/")}@${createHash("sha256").update(real).digest("hex").slice(0, 8)}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Every path-like word of a text (`dir/file`, `file.ext`), whether or not it exists or a denylist hides it from the
|
|
119
|
+
* graph: what a private-path policy is checked against.
|
|
120
|
+
*/
|
|
121
|
+
export const pathCandidates = (text: string): string[] => [...new Set(text.normalize("NFC").match(/\/?[\p{L}\p{M}\p{N}_.~-]+(?:\/[\p{L}\p{M}\p{N}_.~-]+)+|[\p{L}\p{M}\p{N}_-]+\.\p{L}[\p{L}\p{M}\p{N}_]{0,9}/gu) ?? [])]; // any script, combining marks included; an absolute path keeps its slash
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Whether a note is about a path the `pii.paths` policy makes private: a stored node's path, a path-like word of its
|
|
125
|
+
* text, or, for a policy entry with no glob that a word split cannot find (a space in it), that entry written out.
|
|
126
|
+
*/
|
|
127
|
+
export function mentionsPrivatePath(text: string, about: string[], policy: string[], cwd: string): boolean {
|
|
128
|
+
if (!policy.length) return false;
|
|
129
|
+
const nfc = (v: string) => v.normalize("NFC"), body = nfc(text), entries = policy.map(nfc);
|
|
130
|
+
const paths = [...about.map(nodePath), ...pathCandidates(body)].filter((p): p is string => p !== undefined).map(nfc);
|
|
131
|
+
if (paths.some((p) => matchesPolicy(cwd, p, entries, true))) return true;
|
|
132
|
+
// The entry's literal part (up to its first glob character) written out: catches a path a word split breaks (a space).
|
|
133
|
+
return entries.some((entry) => { const lit = entry.replace(/^\.\//, "").split(/[*?[\]{}]/)[0]!.replace(/\/+$/, ""); return /[/\s]/.test(lit) && body.includes(lit); });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** The file a graph node names: `path:<p>` and `symbol:<name>@<p>`; nothing for other nodes. */
|
|
137
|
+
export const nodePath = (n: string): string | undefined => n.startsWith("path:") ? n.slice(5) : n.startsWith("symbol:") && n.includes("@") ? n.slice(n.lastIndexOf("@") + 1) : undefined;
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Notes whose title, text, nodes, task key or project key match the PII patterns of `routing`, or that are about a path
|
|
141
|
+
* the project's `pii.paths` (`policy`) makes private, left out: what may go to a model.
|
|
142
|
+
*/
|
|
143
|
+
export function withoutPii<T extends { title: string | null; text: string; about?: string[]; task?: string | null; project?: string }>(notes: T[], routing: Routing, cwd: string, policy: string[] = []): T[] {
|
|
144
|
+
// A task key's `#<id>@<created ms>` is the hub's own numbers, not note content: a digit-run pattern must not withhold a
|
|
145
|
+
// clean note for it. The project part of the key stays screened.
|
|
146
|
+
const keyText = (k: string) => k.replace(/#\d+(?:@\d+)?$/, "");
|
|
147
|
+
return notes.filter((v) => !mentionsPrivatePath(`${v.title ?? ""}\n${v.text}`, v.about ?? [], policy, cwd) && !detectSignals({ title: v.title ?? "",
|
|
148
|
+
detail: [v.text, ...(v.about ?? []).map((n) => n.startsWith("task:") ? keyText(n) : n), keyText(v.task ?? ""), v.project ?? ""].join("\n"), refs: {} }, routing, cwd).includes("pii"));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* The promotion screen for one project: another project's notes are left to that project's own processes; this one's
|
|
153
|
+
* move only when no secret, PII pattern or private path (`policy()`, read when asked) is in them.
|
|
154
|
+
*/
|
|
155
|
+
export function promotionScreen(project: string, cwd: string, routing: () => Routing, policy: () => string[], taskPaths: (task: string) => string[] = () => []): PromotionScreen {
|
|
156
|
+
return (v) => {
|
|
157
|
+
if (v.project !== project) return "skip";
|
|
158
|
+
// The task's current paths too, with no denylist: a path the note's own nodes leave out is still checked.
|
|
159
|
+
const screened = { ...v, about: [...v.about, ...(v.task ? taskPaths(v.task) : []).map((p) => `path:${p}`)] };
|
|
160
|
+
return looksSecret(`${v.title ?? ""}\n${v.text}`) || !withoutPii([screened], routing(), cwd, policy()).length ? "keep" : "promote";
|
|
161
|
+
};
|
|
162
|
+
}
|