total-recall-brain 3.43.3 → 3.45.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/bin/total-recall.mjs +6 -4
- package/docs/ARCHITECTURE.md +10 -7
- package/docs/reference/cli-reference.md +16 -9
- package/frontend/dist/assets/index-DACh1dhx.js +5613 -0
- package/frontend/dist/index.html +1 -1
- package/frontend/src/App.tsx +2 -2
- package/frontend/src/pages/PluginsPage.tsx +1 -1
- package/package.json +3 -3
- package/scaffold/.agent/skills/plugin/SKILL.md +7 -2
- package/scaffold/.agent/skills/plugin/evals/README.md +5 -0
- package/scaffold/.agent/skills/plugin/evals/cases/01-fabricated-seed.mjs +1 -0
- package/scaffold/.agent/skills/plugin/evals/cases/02-reassuring-default.mjs +1 -0
- package/scaffold/.agent/skills/plugin/evals/cases/03-declared-template.mjs +1 -0
- package/scaffold/.agent/skills/plugin/evals/cases/04-card-collects-credential.mjs +1 -0
- package/scaffold/.agent/skills/plugin/evals/cases/05-host-leak-in-scaffold.mjs +1 -0
- package/scaffold/.agent/skills/plugin/evals/cases/06-correct-plugin.mjs +1 -0
- package/scaffold/.agent/skills/plugin/evals/cases/07-missing-reuse.mjs +11 -0
- package/scaffold/.agent/skills/plugin/evals/cases/08-unresolved-owner-command.mjs +12 -0
- package/scaffold/.agent/skills/plugin/evals/cases/09-missing-brick.mjs +12 -0
- package/scaffold/.agent/skills/plugin/prompts/01-function.md +4 -3
- package/scaffold/.agent/skills/plugin/prompts/08-verify.md +1 -0
- package/scaffold/.agent/skills/plugin/references/surfaces.md +12 -2
- package/scaffold/.agent/skills/plugin/scripts/record.mjs +59 -2
- package/scaffold/.agent/skills/total-recall/plugins/auth/README.md +8 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/package-lock.json +15 -14
- package/scaffold/.agent/skills/total-recall/plugins/auth/package.json +3 -3
- package/scaffold/.agent/skills/total-recall/plugins/auth/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/cli-gh-login.mjs +102 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/cli-secret.mjs +38 -2
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/login-projections.mjs +157 -13
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-store.mjs +12 -1
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-sync.mjs +161 -17
- package/scaffold/.agent/skills/total-recall/plugins/automations/BRICK.md +7 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/LICENSE +21 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/README.md +66 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/adapters.mjs +231 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/capability/index.mjs +16 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/cli.mjs +121 -0
- package/{src/core/plugin-cron.mjs → scaffold/.agent/skills/total-recall/plugins/automations/cron.mjs} +1 -3
- package/scaffold/.agent/skills/total-recall/plugins/automations/inventory.mjs +88 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/package-lock.json +7661 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/package.json +35 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/plugin.json +167 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/scheduler.mjs +246 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/ui/DESIGN.md +42 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/ui/automations.js +2 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/ui/card.js +273 -0
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/commands/run-core.mjs +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/check.mjs +57 -5
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/report-data.mjs +40 -14
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/report.mjs +15 -40
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +15 -14
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/package.json +3 -3
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +15 -14
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package.json +3 -3
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +15 -14
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/package.json +3 -3
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/git/README.md +112 -0
- package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/cli.mjs +135 -23
- package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/package-lock.json +17 -16
- package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/package.json +5 -4
- package/scaffold/.agent/skills/total-recall/plugins/git/plugin.json +161 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/lib/context.mjs +94 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/lib/phase3-spec-support.mjs +50 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/lib/runtime.mjs +40 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/lib/spec-support.mjs +51 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/_support.mjs +195 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/auth.mjs +201 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/bisect.mjs +40 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/cli-meta.mjs +159 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/commit-memory.mjs +30 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/commit.mjs +46 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/fleet.mjs +156 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/force-push.mjs +58 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/group.mjs +119 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/handoff.mjs +59 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/message.mjs +22 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/resolve.mjs +60 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/rules.mjs +81 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/secret-firewall.mjs +66 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/undo.mjs +132 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/why.mjs +16 -0
- package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/ui/card.js +2 -2
- package/scaffold/.agent/skills/total-recall/plugins/mesh/package-lock.json +15 -14
- package/scaffold/.agent/skills/total-recall/plugins/mesh/package.json +3 -3
- package/scaffold/.agent/skills/total-recall/plugins/mesh/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/README.md +59 -10
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/capability/BRICK.md +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/capability/index.mjs +18 -6
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/cli.mjs +160 -29
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package-lock.json +15 -14
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/plugin.json +231 -16
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/skills/meta-harness/SKILL.md +19 -4
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/adoption.mjs +104 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/alerts.mjs +3 -3
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/cascade.mjs +101 -16
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/control.mjs +145 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/coordinator.mjs +54 -21
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/dispatch-request.mjs +40 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/dispatch.mjs +68 -13
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/documentation.mjs +55 -11
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/executor.mjs +64 -15
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/hermes.mjs +29 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/index.mjs +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/included-usage.mjs +249 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/jobs.mjs +84 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/learner.mjs +6 -12
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/maintenance.mjs +17 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/mcp-server.mjs +228 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/pool.mjs +16 -8
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/process-manager.mjs +26 -2
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/reasoning.mjs +6 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/remote-operation.mjs +6 -2
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/review.mjs +34 -3
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/reviews.mjs +25 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/router.mjs +28 -12
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/runtime.mjs +28 -6
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session/claude.mjs +319 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session/codex.mjs +387 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session/result.mjs +67 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session/resume.mjs +373 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session/run.mjs +223 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session-jobs.mjs +72 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/setup.mjs +6 -3
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/termination.mjs +16 -12
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/threads.mjs +32 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/usage-queue.mjs +110 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/verify-ssss.mjs +3 -3
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worker.mjs +597 -70
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worktree.mjs +76 -9
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/DESIGN.md +5 -5
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/card.js +3 -3
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/cli.mjs +47 -18
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +15 -14
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/package.json +3 -3
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/refresh-diagnostics.mjs +44 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/refresh.mjs +120 -113
- package/scaffold/.agent/skills/total-recall/plugins/security/package-lock.json +15 -14
- package/scaffold/.agent/skills/total-recall/plugins/security/package.json +3 -3
- package/scaffold/.agent/skills/total-recall/plugins/security/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/ssss/assets/scaffold.json +5 -5
- package/scaffold/.agent/skills/total-recall/plugins/ssss/package-lock.json +6 -6
- package/scaffold/.agent/skills/total-recall/plugins/ssss/package.json +2 -2
- package/scaffold/.agent/skills/total-recall/plugins/ssss/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/references/operations.md +1 -1
- package/src/cli/agent.mjs +1 -1
- package/src/cli/command.mjs +41 -2
- package/src/cli/connect.mjs +2 -29
- package/src/cli/context.mjs +1 -0
- package/src/cli/dream.mjs +93 -69
- package/src/cli/harness.mjs +4 -4
- package/src/cli/help.mjs +1 -1
- package/src/cli/ingest-okf.mjs +4 -4
- package/src/cli/init.mjs +0 -25
- package/src/cli/lessons.mjs +51 -0
- package/src/cli/memory-plugin.mjs +1 -1
- package/src/cli/plugin/copies.mjs +64 -14
- package/src/cli/plugin/create.mjs +6 -7
- package/src/cli/plugin/index.mjs +11 -7
- package/src/cli/plugin/install.mjs +10 -15
- package/src/cli/plugin/remove.mjs +1 -1
- package/src/cli/plugin/search.mjs +2 -2
- package/src/cli/preflight.mjs +19 -6
- package/src/cli/proposals.mjs +1 -1
- package/src/cli/recall.mjs +66 -23
- package/src/cli/release.mjs +4 -1
- package/src/cli/remember.mjs +3 -14
- package/src/cli/server.mjs +18 -0
- package/src/cli/startup.mjs +1 -0
- package/src/cli/task.mjs +9 -2
- package/src/cli/update.mjs +10 -3
- package/src/core/access-ledger.mjs +78 -0
- package/src/core/agent-manager.mjs +2 -2
- package/src/core/automatic-brain-updates.mjs +11 -6
- package/src/core/brain-locate.mjs +15 -0
- package/src/core/brain-updates.mjs +42 -12
- package/src/core/claude-hooks.mjs +3 -1
- package/src/core/cleanup-task.mjs +15 -0
- package/src/core/cli-commands.mjs +2 -0
- package/src/core/cli-maintenance.mjs +4 -1
- package/src/core/cli-semantic-index.mjs +23 -3
- package/src/core/context-compactor.mjs +7 -2
- package/src/core/context-compiler.mjs +26 -1
- package/src/core/context-policy.mjs +7 -3
- package/src/core/daemon-control.mjs +22 -0
- package/src/core/daemon-loop.mjs +39 -62
- package/src/core/default-plugins.mjs +1 -1
- package/src/core/dream.mjs +33 -105
- package/src/core/ensure-dependencies.mjs +148 -0
- package/src/core/fast-recall.mjs +2 -2
- package/src/core/field-evaluation.mjs +23 -0
- package/src/core/gate-mode.mjs +1 -1
- package/src/core/global-brain-updates.mjs +1 -1
- package/src/core/harness-hooks.mjs +14 -2
- package/src/core/lesson-hooks.mjs +74 -0
- package/src/core/lesson-memory.mjs +24 -0
- package/src/core/lesson-sessions.mjs +52 -0
- package/src/core/lessons.mjs +96 -0
- package/src/core/mesh-brain-updates.mjs +2 -1
- package/src/core/meta-harness.mjs +2 -2
- package/src/core/optimizer.mjs +66 -165
- package/src/core/plugin-contracts/card.mjs +3 -0
- package/src/core/plugin-loader.mjs +12 -3
- package/src/core/plugin-runtime.mjs +40 -1
- package/src/core/plugin-store.mjs +35 -23
- package/src/core/post-mortem.mjs +52 -36
- package/src/core/preflight-gate.mjs +23 -2
- package/src/core/proposal-applier.mjs +31 -2
- package/src/core/repo-sync.mjs +4 -6
- package/src/core/required-plugins.mjs +26 -4
- package/src/core/rule-length.mjs +23 -0
- package/src/core/runtime.mjs +1 -1
- package/src/core/scheduler.mjs +35 -43
- package/src/core/schema.mjs +1 -2
- package/src/core/search.mjs +4 -3
- package/src/core/server-restart.mjs +128 -7
- package/src/core/server-service.mjs +148 -0
- package/src/core/session-watcher.mjs +19 -1
- package/src/core/skill-gap.mjs +16 -0
- package/src/core/ssss-kernel-bridge.mjs +33 -6
- package/src/core/startup-health.mjs +6 -1
- package/src/core/surface.mjs +68 -57
- package/src/core/task-context.mjs +9 -8
- package/src/core/task-executors.mjs +54 -106
- package/src/core/validated-write.mjs +5 -0
- package/src/core/vault-files.mjs +8 -2
- package/src/core/vault.mjs +1 -1
- package/src/core/vector-field.mjs +134 -192
- package/src/server/index.mjs +3 -1
- package/src/server/invoke-core-child.mjs +24 -0
- package/src/server/invoke-core-operations.mjs +102 -0
- package/src/server/route-manifest.json +0 -56
- package/src/server/routes/brains.mjs +0 -18
- package/src/server/routes/context.mjs +0 -41
- package/src/server/routes/invoke.mjs +26 -1
- package/src/server/routes/ssss.mjs +0 -169
- package/src/server/routes/system.mjs +0 -15
- package/src/server/routes/tasks.mjs +0 -26
- package/src/server/routes/vault.mjs +2 -23
- package/frontend/dist/assets/index-DD4qONvH.js +0 -5607
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/README.md +0 -53
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/plugin.json +0 -129
- package/src/core/append-log.mjs +0 -131
- package/src/core/plugin-tasks.mjs +0 -173
- package/templates/onboarding-interview.md +0 -82
- /package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/LICENSE +0 -0
- /package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/ui/DESIGN.md +0 -0
|
@@ -1,38 +1,93 @@
|
|
|
1
1
|
/** One path for every agent run: contain it if needed, dispatch it, classify the outcome, write its receipt. */
|
|
2
2
|
import { executeOnNode } from './executor.mjs';
|
|
3
|
-
import { classifyFailure, READ_ONLY_ADAPTERS } from './worker.mjs';
|
|
3
|
+
import { classifyFailure, INCLUDED_BILLING, READ_ONLY_ADAPTERS, cancellationResult } from './worker.mjs';
|
|
4
4
|
import { recordRunReceipt } from './learner.mjs';
|
|
5
|
-
import { isGitRepo, createWorktree, removeWorktree } from './worktree.mjs';
|
|
5
|
+
import { isGitRepo, createWorktree, removeWorktree, commitAll, deleteBranch } from './worktree.mjs';
|
|
6
|
+
import { randomUUID } from 'node:crypto';
|
|
7
|
+
import { includedAdmission, openRouterAdmission, readIncludedPolicy } from './included-usage.mjs';
|
|
8
|
+
import { queueIncludedJob } from './usage-queue.mjs';
|
|
6
9
|
|
|
7
|
-
export async function dispatchWithReceipt(request, { dispatchFn, node = request.node || 'local', role, complexityScore, escalatedFrom,
|
|
10
|
+
export async function dispatchWithReceipt(request, { dispatchFn, node = request.node || 'local', role, complexityScore, escalatedFrom,
|
|
11
|
+
record = recordRunReceipt, replay, retryId, manageWrites = false, usagePolicy = readIncludedPolicy, admit = includedAdmission, admitOpenRouter = openRouterAdmission } = {}) {
|
|
8
12
|
const started = Date.now();
|
|
13
|
+
if (cancellationResult()) return cancellationResult();
|
|
9
14
|
// A harness that cannot be told not to write does read-only work in a checkout that is thrown away.
|
|
10
15
|
const contain = node === 'local' && request.permissions?.write !== true && !READ_ONLY_ADAPTERS.includes(request.agent) && request.isolated !== true && isGitRepo(request.cwd);
|
|
11
|
-
|
|
16
|
+
const ownWrites = manageWrites && node === 'local' && request.permissions?.write === true && isGitRepo(request.cwd);
|
|
17
|
+
let scratch = null, branch = null, result, billingMode = null, launchPrevented = false;
|
|
12
18
|
try {
|
|
13
19
|
let effective = request;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
20
|
+
const policy = await usagePolicy();
|
|
21
|
+
if (policy.enabled && node === 'local') {
|
|
22
|
+
const openRouter = Object.hasOwn(policy.openRouter ?? {}, request.agent);
|
|
23
|
+
billingMode = openRouter ? 'openrouter' : 'included';
|
|
24
|
+
const admission = openRouter ? await admitOpenRouter(request.agent, policy) : await admit(request.agent, request.cwd, policy);
|
|
25
|
+
if (!admission.ok) {
|
|
26
|
+
result = { ...admission, agent: request.agent, exitCode: null };
|
|
27
|
+
launchPrevented = true;
|
|
28
|
+
} else effective = { ...request, [INCLUDED_BILLING]: { key: admission.key, claudePlan: admission.claudePlan, openRouter: admission.openRouter } };
|
|
29
|
+
}
|
|
30
|
+
result = cancellationResult() || result;
|
|
31
|
+
if (!result) {
|
|
32
|
+
if (contain || ownWrites) {
|
|
33
|
+
branch = ownWrites ? `meta-harness/dispatch-${randomUUID()}` : null;
|
|
34
|
+
scratch = createWorktree(request.cwd, { ...(branch ? { branch } : {}), prefix: ownWrites ? 'meta-harness-dispatch-' : 'meta-harness-readonly-' });
|
|
35
|
+
effective = { ...effective, ...(branch ? { branch } : {}), cwd: scratch, contextCwd: request.contextCwd || request.cwd, isolated: true };
|
|
36
|
+
}
|
|
37
|
+
result = dispatchFn ? await dispatchFn(effective) : await executeOnNode(node, { ...effective, operation: 'dispatch' });
|
|
38
|
+
if (cancellationResult()) result = { ...result, ...cancellationResult() };
|
|
39
|
+
if (contain) result = { ...result, readOnly: 'disposable-checkout' };
|
|
17
40
|
}
|
|
18
|
-
result = dispatchFn ? await dispatchFn(effective) : await executeOnNode(node, { ...effective, operation: 'dispatch' });
|
|
19
|
-
if (contain) result = { ...result, readOnly: 'disposable-checkout' };
|
|
20
41
|
} catch (error) {
|
|
21
42
|
const message = String(error?.message || error);
|
|
22
43
|
result = { ok: false, error: message, agent: request.agent, ...(error?.code === 'NOT_READY' ? { failureKind: 'not-ready' } : classifyFailure(message)) };
|
|
23
44
|
} finally {
|
|
24
|
-
if (scratch)
|
|
45
|
+
if (scratch) {
|
|
46
|
+
try {
|
|
47
|
+
if (branch) {
|
|
48
|
+
const commit = commitAll(scratch, result?.cancelled ? 'meta-harness: unfinished cancelled dispatch' : 'meta-harness: dispatch changes');
|
|
49
|
+
if (commit) result = { ...result, branch, ...(result?.ok ? { commit } : { wip: commit }) };
|
|
50
|
+
else if (result?.sessionId) result = { ...result, branch };
|
|
51
|
+
else deleteBranch(request.cwd, branch);
|
|
52
|
+
}
|
|
53
|
+
} finally { removeWorktree(request.cwd, scratch); }
|
|
54
|
+
if (branch && !result?.branch) deleteBranch(request.cwd, branch);
|
|
55
|
+
}
|
|
25
56
|
}
|
|
26
57
|
const durationMs = Date.now() - started;
|
|
58
|
+
if (result.execution?.receiptOwner === 'destination' && result.receiptId) return { ...result, durationMs };
|
|
59
|
+
if (result.execution?.remote && (result.queued || result.launchPrevented)) return { ...result, durationMs };
|
|
60
|
+
if (billingMode === 'included' && result.failureKind === 'quota') {
|
|
61
|
+
if (retryId) result = { ...result, needsReview: !launchPrevented && request.permissions?.write === true };
|
|
62
|
+
else {
|
|
63
|
+
try {
|
|
64
|
+
const job = await queueIncludedJob(request, result, { role, replay, launchPrevented });
|
|
65
|
+
result = { ...result, queued: true, queueId: job.id, needsReview: job.needsReview };
|
|
66
|
+
} catch (error) {
|
|
67
|
+
result = { ...result, queueError: error.message };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (launchPrevented) {
|
|
72
|
+
// A launch refused for quota is a quota wall too (A-124): record it, so routing sees the harness as
|
|
73
|
+
// exhausted until its reset and picks another included harness instead of choosing it again.
|
|
74
|
+
if (result.failureKind === 'quota') {
|
|
75
|
+
try { await record({ harness: request.agent, role, task: String(request.contextTask || request.prompt || '').slice(0, 200), success: false,
|
|
76
|
+
failureKind: 'quota', error: result.error, resetsAt: result.resetsAt || null }); } catch { /* bookkeeping must not hide the refusal */ }
|
|
77
|
+
}
|
|
78
|
+
return { ...result, launchPrevented, durationMs };
|
|
79
|
+
}
|
|
27
80
|
// A finished run keeps its result: the worktree and any cost are real even when bookkeeping fails.
|
|
28
|
-
let receiptError;
|
|
81
|
+
let receipt, receiptError;
|
|
29
82
|
try {
|
|
30
|
-
await record({
|
|
83
|
+
receipt = await record({
|
|
31
84
|
harness: request.agent, role, node, task: request.contextTask || request.prompt, complexityScore,
|
|
85
|
+
billingMode,
|
|
32
86
|
model: result.model ?? null, exitCode: result.exitCode ?? null, finishedReason: result.finishedReason ?? null,
|
|
87
|
+
tools: result.toolResults,
|
|
33
88
|
success: result.ok === true, durationMs, failureKind: result.ok ? null : result.failureKind || 'agent',
|
|
34
89
|
error: result.ok ? null : result.error, resetsAt: result.resetsAt, escalatedFrom,
|
|
35
90
|
});
|
|
36
91
|
} catch (error) { receiptError = String(error?.message || error); }
|
|
37
|
-
return { ...result, durationMs, ...(receiptError ? { receiptError } : {}) };
|
|
92
|
+
return { ...result, durationMs, ...(billingMode ? { billingMode } : {}), ...(receipt?.id ? { receiptId: receipt.id } : {}), ...(receiptError ? { receiptError } : {}) };
|
|
38
93
|
}
|
|
@@ -3,7 +3,11 @@ import fs from 'node:fs';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import os from 'node:os';
|
|
5
5
|
import crypto from 'node:crypto';
|
|
6
|
-
import {
|
|
6
|
+
import { execFile } from 'node:child_process';
|
|
7
|
+
import { promisify } from 'node:util';
|
|
8
|
+
import { agentEnvironment, loadCustomAdapters } from './worker.mjs';
|
|
9
|
+
|
|
10
|
+
const exec = promisify(execFile);
|
|
7
11
|
|
|
8
12
|
export const DOC_SOURCES = [
|
|
9
13
|
{ id: 'claude', index: 'https://code.claude.com/docs/llms.txt', prefix: 'https://code.claude.com/docs/', catalog:true },
|
|
@@ -69,7 +73,7 @@ async function download(url, fetchImpl, declaredOrigin) {
|
|
|
69
73
|
if (declaredOrigin) allowed.add(declaredOrigin);
|
|
70
74
|
for(let redirects=0;redirects<5;redirects++) {
|
|
71
75
|
if(target.protocol!=='https:' || target.username || target.password || !allowed.has(target.hostname)) throw new Error(`Unapproved official documentation origin: ${target.hostname}`);
|
|
72
|
-
const response=await fetchImpl(target,{redirect:'manual',signal:AbortSignal.timeout(20000),headers:{'User-Agent':'Total-Recall-
|
|
76
|
+
const response=await fetchImpl(target,{redirect:'manual',signal:AbortSignal.timeout(20000),headers:{'User-Agent':'Total-Recall-Metaharness-Docs','Accept':'*/*'}});
|
|
73
77
|
if(response.status>=300 && response.status<400){target=new URL(response.headers.get('location'),target);continue;}
|
|
74
78
|
// Some entries advertise Markdown exports which no longer exist while the
|
|
75
79
|
// same official HTML document remains available. Fetch that representation.
|
|
@@ -91,7 +95,27 @@ function links(text,base,prefix) {
|
|
|
91
95
|
return [...new Set(values.map(value=>{try{const url=new URL(value);url.hash='';url.search='';url.pathname=url.pathname.replace(/\/{2,}/g,'/').replace(/\/$/,'') || '/';return url.href;}catch{return '';}}))]
|
|
92
96
|
.filter(url=>url.startsWith(prefix) && !/%3[CE]|[<>"'`\s]/i.test(url) && !/\.(png|jpg|jpeg|svg|gif|css|js|woff2?|ico|pdf|json|ya?ml|xml|zip|gz|tgz|lock)$/i.test(url));
|
|
93
97
|
}
|
|
94
|
-
|
|
98
|
+
async function gitDocumentationPaths(source) {
|
|
99
|
+
const checkout = fs.mkdtempSync(path.join(os.tmpdir(), 'meta-harness-doc-tree-'));
|
|
100
|
+
const options = {
|
|
101
|
+
cwd: checkout, timeout: 30000, maxBuffer: 4 * 1024 * 1024,
|
|
102
|
+
env: { ...agentEnvironment(), GIT_TERMINAL_PROMPT: '0', GIT_CONFIG_NOSYSTEM: '1', GIT_CONFIG_GLOBAL: os.devNull },
|
|
103
|
+
};
|
|
104
|
+
try {
|
|
105
|
+
await exec('git', ['-c', 'http.followRedirects=false', 'clone', '--quiet', '--filter=blob:none',
|
|
106
|
+
'--depth=1', '--single-branch', '--branch', source.branch, '--no-checkout',
|
|
107
|
+
'--', `https://github.com/${source.repo}.git`, checkout], options);
|
|
108
|
+
const { stdout } = await exec('git', ['ls-tree', '-r', '-z', 'HEAD', '--', ...source.directories], options);
|
|
109
|
+
return stdout.split('\0').flatMap(entry => {
|
|
110
|
+
const match = /^100(?:644|755) blob [a-f0-9]+\t([\s\S]+)$/.exec(entry);
|
|
111
|
+
return match ? [match[1]] : [];
|
|
112
|
+
});
|
|
113
|
+
} catch {
|
|
114
|
+
throw new Error(`Could not read the official documentation tree through Git for ${source.repo}`);
|
|
115
|
+
} finally { fs.rmSync(checkout, { recursive: true, force: true }); }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function refreshDocumentation({ directory = documentationDirectory(), agent, fetchImpl = fetch, now = () => new Date(), maxPages = 1500, readGitPaths = gitDocumentationPaths } = {}) {
|
|
95
119
|
const sources=sourcesFor(agent);
|
|
96
120
|
if(!sources.length)throw new Error('Unknown documentation adapter');
|
|
97
121
|
const reports=[];
|
|
@@ -99,11 +123,27 @@ export async function refreshDocumentation({ directory = documentationDirectory(
|
|
|
99
123
|
const previous=metadata(directory,source.id), pages=[], queue=[source.index], seen=new Set(), queued=new Set(queue), errors=[], discovered=new Set(), skipped=[];
|
|
100
124
|
// `found` marks a link taken from HTML navigation. The vendor's catalog, the index and the repository tree are authoritative; a link found by scraping is not.
|
|
101
125
|
const enqueue=(url,found=false)=>{if(!queued.has(url)){queued.add(url);queue.push(url);if(found)discovered.add(url);}};
|
|
126
|
+
let treeSource;
|
|
102
127
|
try {
|
|
103
128
|
if(source.repo) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
129
|
+
let paths;
|
|
130
|
+
try {
|
|
131
|
+
const tree = JSON.parse(await download(`https://api.github.com/repos/${source.repo}/git/trees/${source.branch}?recursive=1`, fetchImpl));
|
|
132
|
+
if (tree.truncated) throw new Error('Official repository documentation tree is truncated');
|
|
133
|
+
if (!Array.isArray(tree.tree)) throw new Error('Invalid official repository documentation tree');
|
|
134
|
+
paths = tree.tree.filter(entry => entry.type === 'blob').map(entry => entry.path);
|
|
135
|
+
treeSource = 'github-rest';
|
|
136
|
+
} catch (error) {
|
|
137
|
+
if (!/^Documentation HTTP (403|429):|^Official repository documentation tree is truncated$/.test(error.message)) throw error;
|
|
138
|
+
paths = await readGitPaths(source);
|
|
139
|
+
treeSource = 'github-git';
|
|
140
|
+
}
|
|
141
|
+
for (const file of paths) {
|
|
142
|
+
if (!/\.(md|mdx)$/i.test(file) || !source.directories.some(dir => file.startsWith(dir))) continue;
|
|
143
|
+
const encoded = file.split('/').map(encodeURIComponent).join('/');
|
|
144
|
+
enqueue(`https://raw.githubusercontent.com/${source.repo}/${source.branch}/${encoded}`);
|
|
145
|
+
}
|
|
146
|
+
if (queue.length === 1) throw new Error('Official repository documentation tree contains no additional pages');
|
|
107
147
|
}
|
|
108
148
|
// Bounded batches; follow the official index/navigation to cover agent features.
|
|
109
149
|
while(queue.length) {
|
|
@@ -138,7 +178,7 @@ export async function refreshDocumentation({ directory = documentationDirectory(
|
|
|
138
178
|
reports.push({id:source.id,ok:false,error,pages:preserved.pages?.length || 0,downloadedPages:pages.length,failedCount:errors.length,failures:errors,...(skipped.length?{skippedCount:skipped.length,skipped}:{})});continue;
|
|
139
179
|
}
|
|
140
180
|
atomic(path.join(directory,source.id,'index.json'),JSON.stringify({id:source.id,complete:true,checkedAt,lastAttempt:checkedAt,pages,...(skipped.length?{skipped}:{})},null,2));
|
|
141
|
-
reports.push({id:source.id,ok:true,pages:pages.length,...(skipped.length?{skippedCount:skipped.length,skipped}:{})});
|
|
181
|
+
reports.push({id:source.id,ok:true,pages:pages.length,...(treeSource?{treeSource}:{}),...(skipped.length?{skippedCount:skipped.length,skipped}:{})});
|
|
142
182
|
} catch(error) {
|
|
143
183
|
atomic(path.join(directory,source.id,'index.json'),JSON.stringify({...previous,id:source.id,lastAttempt:now().toISOString(),error:error.message},null,2));
|
|
144
184
|
reports.push({id:source.id,ok:false,error:error.message,pages:previous?.pages?.length || 0});
|
|
@@ -156,11 +196,15 @@ export async function ensureDocumentation(options={}) {
|
|
|
156
196
|
|
|
157
197
|
export async function maintainDocumentation(options={}) {
|
|
158
198
|
const status=documentationStatus(options);
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
199
|
+
const pending=status.agents.filter(row=>!row.fresh).sort((a,b)=>String(a.lastAttempt || '').localeCompare(String(b.lastAttempt || '')));
|
|
200
|
+
const reports=[];
|
|
201
|
+
// Missing documentation for one harness must not prevent attempts for the others.
|
|
202
|
+
for(const row of pending) {
|
|
203
|
+
try { reports.push(...(await refreshDocumentation({...options,agent:row.id})).agents); }
|
|
204
|
+
catch(error) { reports.push({id:row.id,ok:false,error:error.message}); }
|
|
205
|
+
}
|
|
162
206
|
const current=documentationStatus(options);
|
|
163
|
-
return {...
|
|
207
|
+
return {...current,ok:current.ok && reports.every(row=>row.ok),scope:'all-agent-documentation',refresh:reports,coverage_complete:current.ok,pending:current.agents.filter(row=>!row.fresh).map(row=>row.id)};
|
|
164
208
|
}
|
|
165
209
|
|
|
166
210
|
export function readDocumentation({agent,url,directory=documentationDirectory()}) {
|
|
@@ -3,6 +3,10 @@ import path from 'node:path';
|
|
|
3
3
|
import { pathToFileURL } from 'node:url';
|
|
4
4
|
import { runWorker, runProcess, getActiveAdapters, notReady, agentEnvironment } from './worker.mjs';
|
|
5
5
|
import { dispatchWithReceipt } from './dispatch.mjs';
|
|
6
|
+
import { commandPermissions, destinationDispatchRequest } from './dispatch-request.mjs';
|
|
7
|
+
import { readIncludedPolicy } from './included-usage.mjs';
|
|
8
|
+
|
|
9
|
+
export { DEFAULT_PERMISSIONS, commandPermissions } from './dispatch-request.mjs';
|
|
6
10
|
|
|
7
11
|
async function hostModule(name) {
|
|
8
12
|
const selected = process.env.TR_PACKAGE_ROOT;
|
|
@@ -12,13 +16,6 @@ async function hostModule(name) {
|
|
|
12
16
|
return import(pathToFileURL(path.join(root, 'src/core', `${name}.mjs`)).href);
|
|
13
17
|
}
|
|
14
18
|
|
|
15
|
-
export const DEFAULT_PERMISSIONS = Object.freeze({ read: true, write: false, send: false, destructive: false });
|
|
16
|
-
export function commandPermissions(value = DEFAULT_PERMISSIONS) {
|
|
17
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('Command permissions must be boolean values');
|
|
18
|
-
for (const key of Object.keys(value)) if (!Object.hasOwn(DEFAULT_PERMISSIONS, key) || typeof value[key] !== 'boolean') throw new Error(`Invalid permission ${key}`);
|
|
19
|
-
return { ...DEFAULT_PERMISSIONS, ...value };
|
|
20
|
-
}
|
|
21
|
-
|
|
22
19
|
export async function listTargets() {
|
|
23
20
|
try {
|
|
24
21
|
const mesh = await hostModule('mesh');
|
|
@@ -27,32 +24,84 @@ export async function listTargets() {
|
|
|
27
24
|
nodes: nodes.length > 0 ? nodes : [{ id: 'local', name: 'Local Host', online: true, self: true, accessConfigured: true }],
|
|
28
25
|
adapters: getActiveAdapters().map(({ id, name, docs, custom }) => ({ id, name, docs, custom: Boolean(custom) }))
|
|
29
26
|
};
|
|
30
|
-
} catch {
|
|
27
|
+
} catch (error) {
|
|
31
28
|
return {
|
|
32
29
|
nodes: [{ id: 'local', name: 'Local Host', online: true, self: true, accessConfigured: true }],
|
|
33
|
-
adapters: getActiveAdapters().map(({ id, name, docs, custom }) => ({ id, name, docs, custom: Boolean(custom) }))
|
|
30
|
+
adapters: getActiveAdapters().map(({ id, name, docs, custom }) => ({ id, name, docs, custom: Boolean(custom) })),
|
|
31
|
+
meshError: String(error?.message || error).slice(0, 400),
|
|
34
32
|
};
|
|
35
33
|
}
|
|
36
34
|
}
|
|
37
35
|
|
|
38
36
|
export function workerProgram(request) {
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
// Materialize a portable module graph. The stdin launcher imports only Node
|
|
38
|
+
// built-ins. Relative imports resolve inside this owned bundle on the node.
|
|
39
|
+
const names = ['worker.mjs', 'control.mjs', 'jobs.mjs', 'paths.mjs', 'mcp-server.mjs',
|
|
40
|
+
...fs.readdirSync(new URL('./session/', import.meta.url)).filter(name => name.endsWith('.mjs')).map(name => `session/${name}`)];
|
|
41
|
+
const files = Object.fromEntries(names.map(name => [name, fs.readFileSync(new URL(name, import.meta.url), 'utf8')]));
|
|
42
|
+
return `import fs from 'node:fs';
|
|
43
|
+
import os from 'node:os';
|
|
44
|
+
import path from 'node:path';
|
|
45
|
+
import { pathToFileURL } from 'node:url';
|
|
46
|
+
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'metaharness-worker-'));
|
|
47
|
+
try {
|
|
48
|
+
for (const [name, source] of Object.entries(${JSON.stringify(files)})) {
|
|
49
|
+
const file = path.join(directory, name);
|
|
50
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
51
|
+
fs.writeFileSync(file, source, { mode: 0o600 });
|
|
52
|
+
}
|
|
53
|
+
const { runWorker } = await import(pathToFileURL(path.join(directory, 'worker.mjs')).href);
|
|
54
|
+
console.log(JSON.stringify(await runWorker(${JSON.stringify(request)})));
|
|
55
|
+
} catch (error) { console.error(String(error?.message || 'Selected agent worker failed')); process.exitCode = 1; }
|
|
56
|
+
finally { fs.rmSync(directory, { recursive: true, force: true }); }
|
|
57
|
+
`;
|
|
58
|
+
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function destinationDispatchResult(result, node, request) {
|
|
62
|
+
let payload;
|
|
63
|
+
try { payload = JSON.parse(result.output); } catch { /* No structured result from the installed owner. */ }
|
|
64
|
+
if (!payload || typeof payload.ok !== 'boolean' || (payload.ok && !result.ok)) {
|
|
65
|
+
throw notReady(`Installed Metaharness on ${node} returned no valid dispatch outcome (exit ${result.exitCode ?? 'unknown'})`);
|
|
66
|
+
}
|
|
67
|
+
return { ...payload, execution: { node, workspace: request.cwd, remote: true,
|
|
68
|
+
...(typeof payload.receiptId === 'string' ? { receiptOwner: 'destination' } : {}) } };
|
|
41
69
|
}
|
|
42
70
|
|
|
43
71
|
export async function executeOnNode(node, request, { worker = runWorker, processRunner = runProcess, mesh, access } = {}) {
|
|
44
72
|
if (!node || node === 'local') return worker(request);
|
|
45
|
-
mesh ||= await hostModule('mesh');
|
|
73
|
+
mesh ||= await hostModule('mesh');
|
|
46
74
|
const target = mesh.findMeshNode(node);
|
|
47
75
|
if (!target) throw new Error('Selected mesh node is not recorded');
|
|
48
|
-
if (target.self) return worker(request);
|
|
76
|
+
if (target.self && request.operation !== 'dispatch') return worker(request);
|
|
77
|
+
// Route simple dispatch through the installed owner, just like a routed, coordinated or managed job.
|
|
78
|
+
const dispatch = request.operation === 'dispatch';
|
|
79
|
+
let transport = request;
|
|
80
|
+
if (dispatch) {
|
|
81
|
+
const input = JSON.stringify(destinationDispatchRequest(request));
|
|
82
|
+
if (Buffer.byteLength(input) > 65536) throw new Error('Remote dispatch request is too large');
|
|
83
|
+
// The transport outlives the attempt it carries by a minute, so the destination reports its own timeout (A-101).
|
|
84
|
+
transport = { operation: 'direct', cwd: request.cwd, argv: ['total-recall', 'meta-harness', 'dispatch', '--stdin', '--json'], input,
|
|
85
|
+
timeoutMs: Number.isInteger(request.timeoutMs) && request.timeoutMs > 0 ? request.timeoutMs + 60_000 : 600000 };
|
|
86
|
+
}
|
|
87
|
+
const managed = dispatch || (transport.argv?.[1] === 'meta-harness' &&
|
|
88
|
+
['route', 'coordinate', 'review', 'process', 'run-command', 'usage-retry'].includes(transport.argv[2]));
|
|
89
|
+
if (managed && (await readIncludedPolicy()).enabled) transport = { ...transport, requireIncluded: true };
|
|
90
|
+
if (target.self) {
|
|
91
|
+
const result = destinationDispatchResult(await worker(transport), 'local', request);
|
|
92
|
+
result.execution.remote = false;
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
49
95
|
if (target.online === false) throw new Error('Selected mesh node is offline');
|
|
96
|
+
access ||= await hostModule('mesh-access');
|
|
50
97
|
const resolved = access.resolveNodeAccess(target);
|
|
51
98
|
const args = access.buildSshArgs(resolved, { command: 'export PATH="$HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin:$PATH"; exec node --input-type=module', extraOptions: ['BatchMode=yes', 'ConnectTimeout=10'] });
|
|
52
99
|
if (!args) throw new Error('Selected mesh node has no verified SSH access');
|
|
53
|
-
const result = await processRunner('ssh', args, { input: workerProgram(
|
|
100
|
+
const result = await processRunner('ssh', args, { input: workerProgram(transport), timeoutMs: transport.operation === 'inspect' ? 120000 : (transport.timeoutMs || 900000) + 30000, maxBytes: 17 * 1024 * 1024, env: { ...agentEnvironment(), ...(process.env.SSH_AUTH_SOCK ? { SSH_AUTH_SOCK: process.env.SSH_AUTH_SOCK } : {}) } });
|
|
54
101
|
if (result.reason || result.exitCode !== 0) throw notReady(`Worker on ${node} failed: ${String(result.stderr || result.reason || 'verify node access, workspace and agent installation').trim().slice(0, 400)}`);
|
|
55
|
-
|
|
102
|
+
let payload;
|
|
103
|
+
try { payload = JSON.parse(result.stdout); } catch { throw new Error('Selected mesh worker returned invalid data'); }
|
|
104
|
+
return dispatch ? destinationDispatchResult(payload, target.hostname || node, request) : payload;
|
|
56
105
|
}
|
|
57
106
|
|
|
58
107
|
export async function inspectNode(node = 'local', dependencies) {
|
package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/hermes.mjs
CHANGED
|
@@ -27,6 +27,32 @@ function withGate(text) {
|
|
|
27
27
|
return `${text.replace(/\n*$/, text ? '\n' : '')}\nhooks:\n pre_tool_call:\n${entry}`;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Remove every hook list item whose command runs the gate. Other tools rewrite config.yaml (the
|
|
32
|
+
* lesson hooks re-serialize it), so the item is found by indentation, not by its exact text: the
|
|
33
|
+
* item runs from its `- ` line to the next line indented no deeper than that dash.
|
|
34
|
+
*/
|
|
35
|
+
export function withoutGate(text) {
|
|
36
|
+
if (!text.includes(GATE)) return text;
|
|
37
|
+
const exact = text.replace(ENTRY_LINES, '');
|
|
38
|
+
if (!exact.includes(GATE)) return exact;
|
|
39
|
+
let lines = text.split('\n');
|
|
40
|
+
const indent = (line) => line.match(/^ */)[0].length;
|
|
41
|
+
for (let guard = 0; guard < 50; guard++) {
|
|
42
|
+
const hit = lines.findIndex((line) => line.includes(GATE));
|
|
43
|
+
if (hit < 0) break;
|
|
44
|
+
let start = hit;
|
|
45
|
+
while (start >= 0 && !/^ *- /.test(lines[start])) start--;
|
|
46
|
+
if (start < 0 || indent(lines[start]) > indent(lines[hit])) break;
|
|
47
|
+
const dash = indent(lines[start]);
|
|
48
|
+
let end = start + 1;
|
|
49
|
+
while (end < lines.length && (lines[end].trim() === '' || indent(lines[end]) > dash)) end++;
|
|
50
|
+
while (end > start + 1 && lines[end - 1].trim() === '') end--;
|
|
51
|
+
lines = [...lines.slice(0, start), ...lines.slice(end)];
|
|
52
|
+
}
|
|
53
|
+
return lines.join('\n');
|
|
54
|
+
}
|
|
55
|
+
|
|
30
56
|
export const hermesAdapter = {
|
|
31
57
|
id: 'hermes',
|
|
32
58
|
level: 'enforced',
|
|
@@ -118,7 +144,9 @@ export const hermesAdapter = {
|
|
|
118
144
|
const present = text.includes(GATE);
|
|
119
145
|
if (remove || off) {
|
|
120
146
|
if (present) {
|
|
121
|
-
|
|
147
|
+
const next = withoutGate(text);
|
|
148
|
+
if (next.includes(GATE)) throw new Error(`could not remove the gate from ${configPath}; remove the pre_tool_call entry that runs "${GATE}" by hand`);
|
|
149
|
+
if (!dryRun) fs.writeFileSync(configPath, next);
|
|
122
150
|
removed.push('managed pre_tool_call gate');
|
|
123
151
|
}
|
|
124
152
|
return { settings: configPath, added, removed };
|
package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Gate Adapters registry for
|
|
2
|
+
* Gate Adapters registry for Metaharness (tr-plugin-cli-agents).
|
|
3
3
|
* Provides harness-specific parsing, emission, installation and verification.
|
|
4
4
|
*/
|
|
5
5
|
import { claudeAdapter } from './claude.mjs';
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/** Admission for the operator's included-usage policy. No provider generation happens here. */
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
6
|
+
import { createInterface } from 'node:readline';
|
|
7
|
+
import { pathToFileURL } from 'node:url';
|
|
8
|
+
import { configDir } from './paths.mjs';
|
|
9
|
+
import { agentEnvironment, CODEX_PLAN_ARGS, findBinary, OPENROUTER_AGENTS, readClaudeMaxOrganization } from './worker.mjs';
|
|
10
|
+
import { activeQuotaWalls, readReceipts } from './learner.mjs';
|
|
11
|
+
|
|
12
|
+
export const INCLUDED_AGENTS = ['claude', 'codex'];
|
|
13
|
+
const VERSION = 1;
|
|
14
|
+
const DEFAULT = { version: VERSION, enabled: false, claude: null };
|
|
15
|
+
const sha256 = key => createHash('sha256').update(key).digest('hex');
|
|
16
|
+
const validKeyName = name => typeof name === 'string' && /^[A-Z][A-Z0-9_]{1,100}$/.test(name);
|
|
17
|
+
const validModel = model => typeof model === 'string' && /^[\w.-]+\/[\w./:-]+$/.test(model) && model.length <= 180;
|
|
18
|
+
const validOpenRouterBinding = (agent, binding) => OPENROUTER_AGENTS.includes(agent) &&
|
|
19
|
+
validKeyName(binding?.keyName) && validModel(binding?.model);
|
|
20
|
+
|
|
21
|
+
async function policyStore() {
|
|
22
|
+
const { PluginStateStore } = await import('./ssss-store.mjs');
|
|
23
|
+
return new PluginStateStore(path.join(configDir(), 'included-usage.json'), 'meta-harness');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function readIncludedPolicy(store) {
|
|
27
|
+
store ??= await policyStore();
|
|
28
|
+
const state = store.read(DEFAULT);
|
|
29
|
+
if (state?.version !== VERSION || typeof state.enabled !== 'boolean' ||
|
|
30
|
+
(state.claude !== null && (!/^[a-f0-9]{64}$/.test(state.claude?.fingerprint || '') ||
|
|
31
|
+
typeof state.claude.keyName !== 'string' || typeof state.claude.organization !== 'string' ||
|
|
32
|
+
!Number.isFinite(Date.parse(state.claude.expiresAt)))) ||
|
|
33
|
+
(state.claudePlan !== undefined && (!/^[\w-]{1,100}$/.test(state.claudePlan?.organization || '') ||
|
|
34
|
+
state.claudePlan.usageCreditsEnabled !== false || !Number.isFinite(Date.parse(state.claudePlan.verifiedAt)))) ||
|
|
35
|
+
(state.openRouter !== undefined && (!state.openRouter || typeof state.openRouter !== 'object' ||
|
|
36
|
+
Array.isArray(state.openRouter) || !Object.entries(state.openRouter).every(([agent, binding]) => validOpenRouterBinding(agent, binding))))) {
|
|
37
|
+
throw new Error('Included usage policy is invalid; dispatch stopped.');
|
|
38
|
+
}
|
|
39
|
+
return state;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function requireOperator() {
|
|
43
|
+
if (process.env.TR_META_HARNESS_RUN) throw new Error('Only the operator may change the included usage policy.');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function enforceIncludedUsage() {
|
|
47
|
+
requireOperator();
|
|
48
|
+
const store = await policyStore();
|
|
49
|
+
const state = await readIncludedPolicy(store);
|
|
50
|
+
if (!state.enabled) store.write({ ...state, enabled: true });
|
|
51
|
+
return includedUsageStatus();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function authRuntime() {
|
|
55
|
+
const root = process.env.TR_PACKAGE_ROOT;
|
|
56
|
+
if (!root || !path.isAbsolute(root) ||
|
|
57
|
+
JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')).name !== 'total-recall-brain') {
|
|
58
|
+
throw new Error('Verified Total Recall Auth runtime required.');
|
|
59
|
+
}
|
|
60
|
+
const { loadPluginModule } = await import(pathToFileURL(path.join(root, 'src/core/plugin-runtime.mjs')).href);
|
|
61
|
+
const { hostSecretsBrainDir } = await loadPluginModule('auth', 'host-secrets-store', { activeRequired: true });
|
|
62
|
+
return { brain: hostSecretsBrainDir(), load: module => loadPluginModule('auth', module, { activeRequired: true }) };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function authValue(keyName) {
|
|
66
|
+
const { brain, load } = await authRuntime();
|
|
67
|
+
const { getSecret } = await load('secrets-store');
|
|
68
|
+
const result = await getSecret(brain, keyName, { action: 'use', actor: 'meta-harness' });
|
|
69
|
+
return result.found && typeof result.value === 'string' ? result.value : '';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function authKey(keyName) {
|
|
73
|
+
const value = await authValue(keyName);
|
|
74
|
+
if (!/^sk-ant-api[0-9]+-[\w-]+$/.test(value)) {
|
|
75
|
+
throw new Error('The selected Anthropic API key is missing or invalid in Total Recall Auth.');
|
|
76
|
+
}
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function verifyOpenRouterKey(keyName) {
|
|
81
|
+
const { brain, load } = await authRuntime();
|
|
82
|
+
const { syncSecretAccount } = await load('provider-account-sync');
|
|
83
|
+
const result = await syncSecretAccount(brain, keyName);
|
|
84
|
+
return result.provider === 'openrouter' && result.key_valid === true && result.tracking_status === 'ok';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** OpenRouter opt-in is per harness and per destination, with no subscription fallback. */
|
|
88
|
+
export async function bindOpenRouter(agent, keyName, model, { verify = verifyOpenRouterKey } = {}) {
|
|
89
|
+
requireOperator();
|
|
90
|
+
const binding = { keyName, model };
|
|
91
|
+
if (!validOpenRouterBinding(agent, binding)) throw new Error('Select a supported OpenRouter harness, an Auth key name and a catalog model ID.');
|
|
92
|
+
if (!(await verify(keyName))) throw new Error('Total Recall Auth could not verify this OpenRouter key.');
|
|
93
|
+
const store = await policyStore();
|
|
94
|
+
const state = await readIncludedPolicy(store);
|
|
95
|
+
store.write({ ...state, enabled: true, openRouter: { ...state.openRouter, [agent]: binding } });
|
|
96
|
+
return { ok: true, agent, ...binding };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function openRouterAdmission(agent, policy, { readKey = authValue } = {}) {
|
|
100
|
+
const binding = policy.openRouter?.[agent];
|
|
101
|
+
if (!validOpenRouterBinding(agent, binding)) return { ok: false, failureKind: 'billing-policy', error: 'No approved OpenRouter binding for this harness.' };
|
|
102
|
+
const key = await readKey(binding.keyName);
|
|
103
|
+
if (!/^sk-or-v1-[\w-]+$/.test(key)) return { ok: false, failureKind: 'auth', error: 'The approved OpenRouter key is unavailable in Total Recall Auth.' };
|
|
104
|
+
return { ok: true, openRouter: { key, keyName: binding.keyName, model: binding.model } };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Bind the operator's Console observation to the actual secret without recording its value or hint. */
|
|
108
|
+
export async function bindClaudeIncludedKey(evidence, { readKey = authKey, now = Date.now() } = {}) {
|
|
109
|
+
requireOperator();
|
|
110
|
+
const { keyName, organization, keyHint, expiresAt, creditSource, autoReload, paymentMethod, invoiced, paidBalanceCents } = evidence || {};
|
|
111
|
+
const expiry = Date.parse(expiresAt);
|
|
112
|
+
const hint = typeof keyHint === 'string' && /^(sk-ant-api[0-9]+-[\w-]{2,20})(?:\.{3}|…)([\w-]{4,12})$/.exec(keyHint);
|
|
113
|
+
if (!/^[A-Z][A-Z0-9_]{1,100}$/.test(keyName || '') || !/^[\w-]{1,100}$/.test(organization || '') ||
|
|
114
|
+
!hint || !Number.isFinite(expiry) || expiry <= now || expiry > now + 35 * 86400000 ||
|
|
115
|
+
creditSource !== 'claude-max' || autoReload !== false || paymentMethod !== false || invoiced !== false || paidBalanceCents !== 0) {
|
|
116
|
+
throw new Error('Binding requires verified Max credit expiry, Console key hint, and a prepaid organization with no paid balance, auto-reload, or payment method.');
|
|
117
|
+
}
|
|
118
|
+
const key = await readKey(keyName);
|
|
119
|
+
if (!key.startsWith(hint[1]) || !key.endsWith(hint[2])) throw new Error('The Console key hint does not match the selected Total Recall Auth key.');
|
|
120
|
+
const store = await policyStore();
|
|
121
|
+
const state = await readIncludedPolicy(store);
|
|
122
|
+
store.write({ ...state, enabled: true, claude: { keyName, organization, expiresAt: new Date(expiry).toISOString(),
|
|
123
|
+
verifiedAt: new Date(now).toISOString(), fingerprint: sha256(key) } });
|
|
124
|
+
return includedUsageStatus();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export const claudeProofCurrent = (policy, now = Date.now()) => Date.parse(policy.claude?.expiresAt) > now;
|
|
128
|
+
|
|
129
|
+
/** Bind the observed account setting to this host's native Claude Max login. */
|
|
130
|
+
export async function bindClaudeMaxPlan(evidence, { readOrganization = readClaudeMaxOrganization } = {}) {
|
|
131
|
+
requireOperator();
|
|
132
|
+
const { organization, usageCreditsEnabled } = evidence || {};
|
|
133
|
+
if (!/^[\w-]{1,100}$/.test(organization || '') || usageCreditsEnabled !== false) {
|
|
134
|
+
throw new Error('Confirm Usage credits are off in the signed-in Max account before binding it.');
|
|
135
|
+
}
|
|
136
|
+
if (organization !== await readOrganization(process.cwd())) {
|
|
137
|
+
throw new Error('The native Claude Max login does not match the verified organization.');
|
|
138
|
+
}
|
|
139
|
+
const store = await policyStore();
|
|
140
|
+
const state = await readIncludedPolicy(store);
|
|
141
|
+
store.write({ ...state, enabled: true, claudePlan: { organization, usageCreditsEnabled, verifiedAt: new Date().toISOString() } });
|
|
142
|
+
return includedUsageStatus();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export async function includedUsageStatus({ probe = false } = {}) {
|
|
146
|
+
const policy = await readIncludedPolicy();
|
|
147
|
+
const claude = policy.claude ? { organization: policy.claude.organization, verifiedAt: policy.claude.verifiedAt,
|
|
148
|
+
expiresAt: policy.claude.expiresAt, current: claudeProofCurrent(policy) } : null;
|
|
149
|
+
return { ok: true, policyVersion: VERSION, enabled: policy.enabled, claude, claudePlan: policy.claudePlan ?? null,
|
|
150
|
+
openRouter: policy.openRouter ?? {},
|
|
151
|
+
...(probe ? { codex: await readCodexAllowance(process.cwd()) } : {}) };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Use only the documented account reads on stdio. No turn/start is sent. */
|
|
155
|
+
export function queryCodexAccount(cwd, { env = process.env, timeoutMs = 10000 } = {}) {
|
|
156
|
+
const binary = findBinary('codex', env);
|
|
157
|
+
if (!binary) throw new Error('Codex CLI is unavailable.');
|
|
158
|
+
return new Promise((resolve, reject) => {
|
|
159
|
+
const child = spawn(binary, [...CODEX_PLAN_ARGS, 'app-server', '--listen', 'stdio://'], {
|
|
160
|
+
cwd, env: agentEnvironment(env), stdio: ['pipe', 'pipe', 'ignore'],
|
|
161
|
+
});
|
|
162
|
+
const lines = createInterface({ input: child.stdout, crlfDelay: Infinity });
|
|
163
|
+
let account, limits, size = 0, outcome;
|
|
164
|
+
const send = message => child.stdin.write(JSON.stringify(message) + '\n');
|
|
165
|
+
const fail = error => {
|
|
166
|
+
outcome ||= { error };
|
|
167
|
+
child.kill('SIGTERM');
|
|
168
|
+
};
|
|
169
|
+
const timer = setTimeout(() => { outcome ||= { error: 'Codex account read timed out.' }; child.kill('SIGKILL'); }, timeoutMs);
|
|
170
|
+
const closed = () => {
|
|
171
|
+
clearTimeout(timer);
|
|
172
|
+
lines.close();
|
|
173
|
+
if (outcome?.result) resolve(outcome.result);
|
|
174
|
+
else reject(new Error(outcome?.error || 'Codex account read ended without a complete result.'));
|
|
175
|
+
};
|
|
176
|
+
child.once('error', () => { outcome = { error: 'Could not start Codex account read.' }; });
|
|
177
|
+
child.once('close', closed);
|
|
178
|
+
child.stdin.on('error', () => fail('Codex account read input closed.'));
|
|
179
|
+
child.stdout.on('data', chunk => { size += chunk.length; if (size > 256 * 1024) fail('Codex account response exceeded its bound.'); });
|
|
180
|
+
lines.on('line', line => {
|
|
181
|
+
if (outcome) return;
|
|
182
|
+
let frame;
|
|
183
|
+
try { frame = JSON.parse(line); } catch { fail('Codex returned invalid account data.'); return; }
|
|
184
|
+
if (![1, 2, 3].includes(frame?.id)) return;
|
|
185
|
+
if (!frame.result || frame.error) { fail('Codex could not read the signed-in account or its limits.'); return; }
|
|
186
|
+
if (frame.id === 1) {
|
|
187
|
+
send({ method: 'initialized', params: {} });
|
|
188
|
+
send({ id: 2, method: 'account/read', params: { refreshToken: false } });
|
|
189
|
+
send({ id: 3, method: 'account/rateLimits/read' });
|
|
190
|
+
} else if (frame.id === 2) account = frame.result;
|
|
191
|
+
else limits = frame.result;
|
|
192
|
+
if (account && limits) {
|
|
193
|
+
outcome = { result: { account, limits } };
|
|
194
|
+
child.stdin.end();
|
|
195
|
+
child.kill('SIGTERM');
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
send({ id: 1, method: 'initialize', params: { clientInfo: { name: 'meta_harness', title: 'Metaharness', version: '1' } } });
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function evaluateCodexAllowance({ account, limits }, now = Date.now()) {
|
|
203
|
+
const plan = account?.account?.planType;
|
|
204
|
+
if (account?.account?.type !== 'chatgpt' || !/^(plus|pro|prolite|go|free)(?:[_-]\w+)?$/i.test(plan || '')) {
|
|
205
|
+
return { ok: false, failureKind: 'auth', error: 'Codex requires a verified personal ChatGPT plan with included limits.' };
|
|
206
|
+
}
|
|
207
|
+
const rate = limits?.rateLimits;
|
|
208
|
+
const buckets = [rate, ...Object.values(limits?.rateLimitsByLimitId || {})].filter(Boolean);
|
|
209
|
+
const windows = [rate?.primary, rate?.secondary].filter(Boolean);
|
|
210
|
+
if (!windows.length || windows.some(window => typeof window.usedPercent !== 'number')) {
|
|
211
|
+
return { ok: false, plan, failureKind: 'billing-policy', error: 'Codex did not report current included usage.' };
|
|
212
|
+
}
|
|
213
|
+
const credits = buckets.map(bucket => bucket.credits).filter(Boolean);
|
|
214
|
+
if (!credits.length || credits.some(c => c.hasCredits !== false || c.unlimited !== false ||
|
|
215
|
+
(c.balance != null && !/^0(?:\.0+)?$/.test(String(c.balance))))) {
|
|
216
|
+
return { ok: false, plan, failureKind: 'billing-policy', error: 'Codex did not confirm that paid credits are unavailable.' };
|
|
217
|
+
}
|
|
218
|
+
const full = windows.filter(window => window.usedPercent >= 100);
|
|
219
|
+
if (full.length) {
|
|
220
|
+
const resets = full.map(window => window.resetsAt * 1000);
|
|
221
|
+
return { ok: false, plan, failureKind: 'quota', error: 'Codex has reached an included usage limit.',
|
|
222
|
+
resetsAt: resets.every(reset => Number.isFinite(reset) && reset > now) ? new Date(Math.max(...resets)).toISOString() : null };
|
|
223
|
+
}
|
|
224
|
+
return { ok: true, plan, usagePercent: Math.max(...windows.map(window => window.usedPercent)) };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export async function readCodexAllowance(cwd, { query = queryCodexAccount } = {}) {
|
|
228
|
+
try { return evaluateCodexAllowance(await query(cwd)); }
|
|
229
|
+
catch (error) { return { ok: false, failureKind: 'billing-policy', error: error.message }; }
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export async function includedAdmission(agent, cwd, policy, { readKey = authKey, codex = readCodexAllowance,
|
|
233
|
+
receipts = readReceipts } = {}) {
|
|
234
|
+
if (!INCLUDED_AGENTS.includes(agent)) return { ok: false, failureKind: 'billing-policy', error: 'This harness is outside the included usage policy.' };
|
|
235
|
+
if (agent === 'claude' && !policy.claudePlan && !claudeProofCurrent(policy)) {
|
|
236
|
+
return { ok: false, failureKind: 'billing-policy', error: 'Verify the native Claude Max login with Usage credits off, or bind a verified included grant.' };
|
|
237
|
+
}
|
|
238
|
+
const wall = activeQuotaWalls((await receipts()).filter(run => run.billingMode === 'included'))[agent];
|
|
239
|
+
if (wall) return { ok: false, failureKind: 'quota', error: 'This harness is waiting for included usage.', resetsAt: wall.resetReported ? wall.until : null };
|
|
240
|
+
if (agent === 'codex') return codex(cwd);
|
|
241
|
+
if (policy.claudePlan) return { ok: true, claudePlan: policy.claudePlan.organization };
|
|
242
|
+
try {
|
|
243
|
+
const key = await readKey(policy.claude.keyName);
|
|
244
|
+
if (sha256(key) !== policy.claude.fingerprint) throw new Error('The Claude key changed; verify the new key against the Max grant.');
|
|
245
|
+
return { ok: true, key };
|
|
246
|
+
} catch (error) {
|
|
247
|
+
return { ok: false, failureKind: 'auth', error: error.message };
|
|
248
|
+
}
|
|
249
|
+
}
|