@kontextmind/kxm 0.6.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/.claude-plugin/marketplace.json +19 -0
- package/.kxm/README.md +14 -0
- package/.kxm/assets/README.md +5 -0
- package/.kxm/assets/retrospectives/README.md +5 -0
- package/.kxm/config/README.md +5 -0
- package/.kxm/config/agents.json +43 -0
- package/.kxm/config/env.example +56 -0
- package/.kxm/config/update.example.yaml +9 -0
- package/.kxm/config/workflows/fix.json +160 -0
- package/.kxm/config/workflows/jira-development.json +116 -0
- package/.kxm/config/workflows/provenance-quorum.json +150 -0
- package/.kxm/config/workflows/v04-dogfood.json +72 -0
- package/CHANGELOG.md +465 -0
- package/LICENSE +21 -0
- package/README.md +306 -0
- package/SECURITY.md +72 -0
- package/docs/README.md +48 -0
- package/docs/agent-communication-envelopes-and-gates.md +553 -0
- package/docs/architecture.md +242 -0
- package/docs/assignment-runner.md +241 -0
- package/docs/configuration.md +361 -0
- package/docs/continuous-improvement.md +114 -0
- package/docs/getting-started.md +253 -0
- package/docs/kxm-handbook.md +1090 -0
- package/docs/operations.md +205 -0
- package/docs/provenance-gates.md +291 -0
- package/docs/skills.md +45 -0
- package/docs/templates/README.md +95 -0
- package/docs/templates/adr.md +88 -0
- package/docs/templates/architecture.md +120 -0
- package/docs/templates/bug-fix.md +109 -0
- package/docs/templates/feature.md +108 -0
- package/docs/templates/handoff.md +72 -0
- package/docs/templates/postmortem.md +77 -0
- package/docs/templates/research.md +100 -0
- package/docs/templates/review.md +85 -0
- package/docs/templates/runbook.md +73 -0
- package/docs/templates/test-plan.md +87 -0
- package/docs/templates/test-report.md +72 -0
- package/docs/test-matrix.md +121 -0
- package/docs/troubleshooting.md +249 -0
- package/docs/vnext/README.md +62 -0
- package/docs/vnext/architecture.md +185 -0
- package/docs/vnext/effects-and-recovery.md +172 -0
- package/docs/vnext/lifecycles.md +235 -0
- package/docs/vnext/migration.md +220 -0
- package/docs/vnext/routing.md +184 -0
- package/docs/vnext/synchronization.md +172 -0
- package/docs/vnext/terminology.md +240 -0
- package/docs/vnext/validation.md +335 -0
- package/docs/webhook-workflows.md +240 -0
- package/docs/workflow-guide.md +1150 -0
- package/examples/README.md +102 -0
- package/examples/provenance-workflow.json +40 -0
- package/examples/requester.ts +30 -0
- package/examples/reviewer-agent.ts +29 -0
- package/examples/roundtrip.ts +46 -0
- package/examples/vnext/.kxm/agents/coordinator.yaml +16 -0
- package/examples/vnext/.kxm/agents/critic-1.yaml +16 -0
- package/examples/vnext/.kxm/agents/critic-2.yaml +15 -0
- package/examples/vnext/.kxm/agents/critic-3.yaml +15 -0
- package/examples/vnext/.kxm/agents/implementer.yaml +15 -0
- package/examples/vnext/.kxm/agents/planner.yaml +13 -0
- package/examples/vnext/.kxm/agents/reproducer.yaml +15 -0
- package/examples/vnext/.kxm/agents/reviewer.yaml +15 -0
- package/examples/vnext/.kxm/gates.yaml +8 -0
- package/examples/vnext/.kxm/models/critic-claude.yaml +11 -0
- package/examples/vnext/.kxm/models/critic-gemini.yaml +11 -0
- package/examples/vnext/.kxm/models/critic-grok.yaml +12 -0
- package/examples/vnext/.kxm/models/implementation.yaml +14 -0
- package/examples/vnext/.kxm/models/primary.yaml +17 -0
- package/examples/vnext/.kxm/prices.yaml +111 -0
- package/examples/vnext/.kxm/project/env.yaml +7 -0
- package/examples/vnext/.kxm/project.yaml +32 -0
- package/examples/vnext/.kxm/repo/repo.yaml +8 -0
- package/examples/vnext/.kxm/workflows/default.yaml +92 -0
- package/examples/vnext/.kxm/workflows/fix.yaml +376 -0
- package/examples/vnext/.kxm/workflows/improve.yaml +57 -0
- package/examples/vnext/README.md +53 -0
- package/examples/vnext/records/assignment-result-recorded.json +63 -0
- package/examples/vnext/records/assignment-result.json +46 -0
- package/examples/vnext/records/context-candidate.json +42 -0
- package/examples/vnext/records/delivery-manifest.json +66 -0
- package/examples/vnext/records/effect-uncertainty-resolved-sync.json +67 -0
- package/examples/vnext/records/effect-uncertainty-resolved.json +62 -0
- package/examples/vnext/records/run-created.json +54 -0
- package/examples/vnext/records/sync-event.json +65 -0
- package/examples/vnext/repositories/api/.kxm/repo/env.yaml +7 -0
- package/examples/vnext/repositories/api/.kxm/repo/repo.yaml +8 -0
- package/examples/vnext/repositories/web/.kxm/repo/repo.yaml +8 -0
- package/examples/workflow-signal.ts +63 -0
- package/package.json +129 -0
- package/plugins/kxm/.claude-plugin/plugin.json +73 -0
- package/plugins/kxm/.mcp.json +19 -0
- package/plugins/kxm/README.md +93 -0
- package/plugins/kxm/dist/cli.js +42853 -0
- package/plugins/kxm/dist/client.js +416 -0
- package/plugins/kxm/dist/core.js +1823 -0
- package/plugins/kxm/dist/extension.js +3797 -0
- package/plugins/kxm/dist/mcp-server.js +17104 -0
- package/plugins/kxm/dist/runtime.js +23361 -0
- package/plugins/kxm/dist/server.js +13640 -0
- package/plugins/kxm/dist/vnext-runtime-supervisor.js +21109 -0
- package/plugins/kxm/package.json +12 -0
- package/plugins/kxm/skills/kxm/SKILL.md +97 -0
- package/plugins/kxm/skills/kxm/references/protocol.md +103 -0
- package/plugins/kxm/skills/kxm-session/SKILL.md +53 -0
- package/plugins/kxm/src/arbiter.ts +355 -0
- package/plugins/kxm/src/artifacts-exist.ts +62 -0
- package/plugins/kxm/src/autocomplete.ts +236 -0
- package/plugins/kxm/src/cli.ts +3707 -0
- package/plugins/kxm/src/client.ts +614 -0
- package/plugins/kxm/src/commands.ts +1063 -0
- package/plugins/kxm/src/config.ts +290 -0
- package/plugins/kxm/src/context/providers.ts +101 -0
- package/plugins/kxm/src/context-packet.ts +332 -0
- package/plugins/kxm/src/context.ts +499 -0
- package/plugins/kxm/src/core.ts +6 -0
- package/plugins/kxm/src/database.ts +563 -0
- package/plugins/kxm/src/diagnostics.ts +184 -0
- package/plugins/kxm/src/envelope.ts +118 -0
- package/plugins/kxm/src/extension.ts +895 -0
- package/plugins/kxm/src/external-effects.ts +299 -0
- package/plugins/kxm/src/github-watch.ts +255 -0
- package/plugins/kxm/src/hub-binding.ts +160 -0
- package/plugins/kxm/src/hub.ts +2502 -0
- package/plugins/kxm/src/improve.ts +383 -0
- package/plugins/kxm/src/inbox.ts +10 -0
- package/plugins/kxm/src/kxm-install-kind.ts +113 -0
- package/plugins/kxm/src/kxm-update-config.ts +39 -0
- package/plugins/kxm/src/kxm-update.ts +238 -0
- package/plugins/kxm/src/local-snapshot.ts +406 -0
- package/plugins/kxm/src/logger.ts +198 -0
- package/plugins/kxm/src/mcp-server.ts +143 -0
- package/plugins/kxm/src/memory.ts +385 -0
- package/plugins/kxm/src/nous-pi.ts +287 -0
- package/plugins/kxm/src/nous-provider.ts +729 -0
- package/plugins/kxm/src/price-calc.ts +87 -0
- package/plugins/kxm/src/prices.ts +121 -0
- package/plugins/kxm/src/protocol.ts +172 -0
- package/plugins/kxm/src/recovery.ts +211 -0
- package/plugins/kxm/src/redact.ts +26 -0
- package/plugins/kxm/src/retrospective.ts +400 -0
- package/plugins/kxm/src/routing.ts +830 -0
- package/plugins/kxm/src/runtime.ts +9 -0
- package/plugins/kxm/src/server.ts +117 -0
- package/plugins/kxm/src/session-work.ts +571 -0
- package/plugins/kxm/src/session.ts +184 -0
- package/plugins/kxm/src/skills.ts +535 -0
- package/plugins/kxm/src/state.ts +326 -0
- package/plugins/kxm/src/store.ts +637 -0
- package/plugins/kxm/src/studio-layout.ts +268 -0
- package/plugins/kxm/src/suggest.ts +162 -0
- package/plugins/kxm/src/task-manager.ts +244 -0
- package/plugins/kxm/src/telemetry.ts +116 -0
- package/plugins/kxm/src/tui.ts +1046 -0
- package/plugins/kxm/src/vnext-bindings.ts +403 -0
- package/plugins/kxm/src/vnext-config.ts +1646 -0
- package/plugins/kxm/src/vnext-engine-artifacts.ts +86 -0
- package/plugins/kxm/src/vnext-engine-command.ts +533 -0
- package/plugins/kxm/src/vnext-engine-compile.ts +722 -0
- package/plugins/kxm/src/vnext-engine-evidence.ts +273 -0
- package/plugins/kxm/src/vnext-engine-fold.ts +1400 -0
- package/plugins/kxm/src/vnext-engine-gate-records.ts +583 -0
- package/plugins/kxm/src/vnext-engine-plan.ts +717 -0
- package/plugins/kxm/src/vnext-engine.ts +2458 -0
- package/plugins/kxm/src/vnext-gate-hash.ts +10 -0
- package/plugins/kxm/src/vnext-harness.ts +1142 -0
- package/plugins/kxm/src/vnext-init.ts +430 -0
- package/plugins/kxm/src/vnext-migrate.ts +1848 -0
- package/plugins/kxm/src/vnext-oneshot-producer.ts +424 -0
- package/plugins/kxm/src/vnext-permission.ts +936 -0
- package/plugins/kxm/src/vnext-pi-producer.ts +628 -0
- package/plugins/kxm/src/vnext-repair.ts +1094 -0
- package/plugins/kxm/src/vnext-runtime-owner.ts +320 -0
- package/plugins/kxm/src/vnext-runtime-store.ts +1560 -0
- package/plugins/kxm/src/vnext-runtime-supervisor.ts +586 -0
- package/plugins/kxm/src/vnext-runtime.ts +663 -0
- package/plugins/kxm/src/vnext-template.ts +247 -0
- package/plugins/kxm/src/wiki.ts +313 -0
- package/plugins/kxm/src/workflow.ts +1548 -0
- package/schemas/vnext/README.md +46 -0
- package/schemas/vnext/agent.schema.json +40 -0
- package/schemas/vnext/assignment-result.schema.json +66 -0
- package/schemas/vnext/backup-manifest.schema.json +89 -0
- package/schemas/vnext/candidate.schema.json +109 -0
- package/schemas/vnext/common.schema.json +422 -0
- package/schemas/vnext/context-candidate.schema.json +76 -0
- package/schemas/vnext/context-packet.schema.json +192 -0
- package/schemas/vnext/delivery-manifest.schema.json +159 -0
- package/schemas/vnext/environment.schema.json +66 -0
- package/schemas/vnext/gate-registry.schema.json +109 -0
- package/schemas/vnext/handoff-manifest.schema.json +146 -0
- package/schemas/vnext/init-operation.schema.json +61 -0
- package/schemas/vnext/local-repository-bindings.schema.json +30 -0
- package/schemas/vnext/memory-record.schema.json +45 -0
- package/schemas/vnext/migration-decision.schema.json +26 -0
- package/schemas/vnext/migration-plan.schema.json +123 -0
- package/schemas/vnext/migration-receipt.schema.json +52 -0
- package/schemas/vnext/model.schema.json +42 -0
- package/schemas/vnext/permission-diff.schema.json +57 -0
- package/schemas/vnext/prices.schema.json +115 -0
- package/schemas/vnext/project.schema.json +85 -0
- package/schemas/vnext/repository.schema.json +24 -0
- package/schemas/vnext/run-event.schema.json +460 -0
- package/schemas/vnext/session-brief.schema.json +153 -0
- package/schemas/vnext/sync-event.schema.json +234 -0
- package/schemas/vnext/template-provenance.schema.json +38 -0
- package/schemas/vnext/workflow.schema.json +248 -0
- package/scripts/assignment-run.d.mts +354 -0
- package/scripts/assignment-run.mjs +4451 -0
- package/scripts/build-runtime.mjs +56 -0
- package/scripts/check-generated.mjs +77 -0
- package/scripts/check-versions.mjs +34 -0
- package/scripts/emit-codex-artifacts.d.mts +9 -0
- package/scripts/emit-codex-artifacts.mjs +91 -0
- package/scripts/harness-run.d.mts +83 -0
- package/scripts/harness-run.mjs +2095 -0
- package/scripts/kxm-hub.mjs +105 -0
- package/scripts/kxm-publish-npm.mjs +327 -0
- package/scripts/kxm-release-github.mjs +472 -0
- package/scripts/kxm-runtime-supervisor.mjs +7 -0
- package/scripts/kxm-worker.mjs +1127 -0
- package/scripts/kxm.mjs +27 -0
- package/scripts/roster-policy.d.mts +20 -0
- package/scripts/roster-policy.mjs +161 -0
- package/scripts/smoke-multi-pi.mjs +479 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from "./vnext-runtime.ts";
|
|
2
|
+
export * from "./vnext-runtime-store.ts";
|
|
3
|
+
export * from "./vnext-runtime-supervisor.ts";
|
|
4
|
+
export * from "./vnext-pi-producer.ts";
|
|
5
|
+
export * from "./vnext-oneshot-producer.ts";
|
|
6
|
+
export * from "./vnext-harness.ts";
|
|
7
|
+
export * from "./database.ts";
|
|
8
|
+
export * from "./logger.ts";
|
|
9
|
+
export * from "./improve.ts";
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { createMeshHub } from "./hub.ts";
|
|
2
|
+
import { createWriteStream, mkdirSync, readFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_MESSAGE_RETENTION_MS,
|
|
6
|
+
DEFAULT_MESSAGE_TTL_MS,
|
|
7
|
+
DEFAULT_PORT,
|
|
8
|
+
DEFAULT_RATE_LIMIT_MAX,
|
|
9
|
+
DEFAULT_RATE_LIMIT_WINDOW_MS,
|
|
10
|
+
} from "./protocol.ts";
|
|
11
|
+
import { parseWorkflowDefinitions } from "./workflow.ts";
|
|
12
|
+
|
|
13
|
+
const host = process.env.KXM_HOST ?? "127.0.0.1";
|
|
14
|
+
const port = Number.parseInt(process.env.KXM_PORT ?? String(DEFAULT_PORT), 10);
|
|
15
|
+
const authToken = process.env.KXM_AUTH_TOKEN;
|
|
16
|
+
const workspaceDir = resolve(process.env.KXM_WORKSPACE_DIR?.trim() || ".kxm");
|
|
17
|
+
const configDir = resolve(process.env.KXM_CONFIG_DIR?.trim() || join(workspaceDir, "config"));
|
|
18
|
+
const logsDir = resolve(process.env.KXM_LOGS_DIR?.trim() || join(workspaceDir, "logs"));
|
|
19
|
+
const assetsDir = resolve(process.env.KXM_ASSETS_DIR?.trim() || join(workspaceDir, "assets"));
|
|
20
|
+
const stateDir = resolve(process.env.KXM_STATE_DIR?.trim() || join(workspaceDir, "state"));
|
|
21
|
+
const dataPathValue = process.env.KXM_DATA_PATH?.trim();
|
|
22
|
+
const dataPath = dataPathValue === ":memory:" ? dataPathValue : resolve(dataPathValue || join(stateDir, "kxm.db"));
|
|
23
|
+
const logPath = resolve(process.env.KXM_LOG_PATH?.trim() || join(logsDir, "kxm-hub.jsonl"));
|
|
24
|
+
const messageTtlMs = Number.parseInt(process.env.KXM_MESSAGE_TTL_MS ?? String(DEFAULT_MESSAGE_TTL_MS), 10);
|
|
25
|
+
const messageRetentionMs = Number.parseInt(
|
|
26
|
+
process.env.KXM_MESSAGE_RETENTION_MS ?? String(DEFAULT_MESSAGE_RETENTION_MS),
|
|
27
|
+
10,
|
|
28
|
+
);
|
|
29
|
+
const rateLimitMax = Number.parseInt(process.env.KXM_RATE_LIMIT_MAX ?? String(DEFAULT_RATE_LIMIT_MAX), 10);
|
|
30
|
+
import { createLogger } from "./logger.ts";
|
|
31
|
+
|
|
32
|
+
const rateLimitWindowMs = Number.parseInt(
|
|
33
|
+
process.env.KXM_RATE_LIMIT_WINDOW_MS ?? String(DEFAULT_RATE_LIMIT_WINDOW_MS),
|
|
34
|
+
10,
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
for (const directory of [configDir, logsDir, assetsDir, stateDir, dirname(logPath)]) {
|
|
38
|
+
mkdirSync(directory, { recursive: true });
|
|
39
|
+
}
|
|
40
|
+
const structuredLog = createLogger({
|
|
41
|
+
component: "hub",
|
|
42
|
+
path: logPath,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
function projectTokens(): Record<string, string> | undefined {
|
|
46
|
+
const raw = process.env.KXM_PROJECT_TOKENS?.trim();
|
|
47
|
+
if (!raw) return undefined;
|
|
48
|
+
const value = JSON.parse(raw) as unknown;
|
|
49
|
+
if (!value || Array.isArray(value) || typeof value !== "object") {
|
|
50
|
+
throw new Error("KXM_PROJECT_TOKENS must be a JSON object of project names to tokens");
|
|
51
|
+
}
|
|
52
|
+
const entries = Object.entries(value as Record<string, unknown>);
|
|
53
|
+
if (entries.some(([project, token]) => !project.trim() || typeof token !== "string" || !token.trim())) {
|
|
54
|
+
throw new Error("KXM_PROJECT_TOKENS must contain non-empty project names and token strings");
|
|
55
|
+
}
|
|
56
|
+
return Object.fromEntries(entries) as Record<string, string>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!Number.isInteger(port) || port < 0 || port > 65_535) {
|
|
60
|
+
throw new Error("KXM_PORT must be an integer between 0 and 65535");
|
|
61
|
+
}
|
|
62
|
+
if (![messageTtlMs, messageRetentionMs, rateLimitMax, rateLimitWindowMs].every(Number.isInteger)) {
|
|
63
|
+
throw new Error("message TTL and rate limit settings must be integers");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const configuredProjectTokens = projectTokens();
|
|
67
|
+
const inlineWorkflows = process.env.KXM_WEBHOOK_WORKFLOWS?.trim();
|
|
68
|
+
const workflowFile = process.env.KXM_WEBHOOK_WORKFLOWS_FILE?.trim();
|
|
69
|
+
if (inlineWorkflows && workflowFile) {
|
|
70
|
+
throw new Error("configure only one of KXM_WEBHOOK_WORKFLOWS or KXM_WEBHOOK_WORKFLOWS_FILE");
|
|
71
|
+
}
|
|
72
|
+
const webhookWorkflows = parseWorkflowDefinitions(
|
|
73
|
+
workflowFile ? readFileSync(resolve(workflowFile), "utf8") : inlineWorkflows,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const hub = createMeshHub({
|
|
77
|
+
host,
|
|
78
|
+
port,
|
|
79
|
+
dataPath,
|
|
80
|
+
assetsDir,
|
|
81
|
+
messageTtlMs,
|
|
82
|
+
messageRetentionMs,
|
|
83
|
+
rateLimit: { maxRequests: rateLimitMax, windowMs: rateLimitWindowMs },
|
|
84
|
+
...(authToken ? { authToken } : {}),
|
|
85
|
+
...(configuredProjectTokens ? { projectTokens: configuredProjectTokens } : {}),
|
|
86
|
+
...(webhookWorkflows.length ? { webhookWorkflows } : {}),
|
|
87
|
+
logger: structuredLog,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const address = await hub.start();
|
|
91
|
+
const auth = authToken ? "token" : "none";
|
|
92
|
+
if (auth === "none") {
|
|
93
|
+
process.stderr.write("kxm hub: auth=none; every loopback caller is trusted. Set KXM_AUTH_TOKEN before relying on this hub.\n");
|
|
94
|
+
}
|
|
95
|
+
structuredLog({ event: "hub_started", url: address.url, workspaceDir, configDir, logsDir, assetsDir, stateDir, dataPath, logPath, auth });
|
|
96
|
+
process.stdout.write(`kxm hub listening at ${address.url}; storage=${dataPath}; auth=${auth}\n`);
|
|
97
|
+
|
|
98
|
+
let shutdownPromise: Promise<void> | undefined;
|
|
99
|
+
function shutdown(signal: string): Promise<void> {
|
|
100
|
+
if (shutdownPromise) return shutdownPromise;
|
|
101
|
+
shutdownPromise = (async () => {
|
|
102
|
+
structuredLog({ event: "hub_stopping", signal });
|
|
103
|
+
await hub.close();
|
|
104
|
+
structuredLog.close();
|
|
105
|
+
process.exit(0);
|
|
106
|
+
})();
|
|
107
|
+
return shutdownPromise;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
process.once("SIGINT", () => void shutdown("SIGINT"));
|
|
111
|
+
process.once("SIGTERM", () => void shutdown("SIGTERM"));
|
|
112
|
+
process.once("message", (message: unknown) => {
|
|
113
|
+
if (message && typeof message === "object" && (message as { type?: string }).type === "shutdown") {
|
|
114
|
+
const signal = (message as { signal?: unknown }).signal;
|
|
115
|
+
void shutdown(typeof signal === "string" ? signal : "parent");
|
|
116
|
+
}
|
|
117
|
+
});
|
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
loadLocalMeshSnapshot,
|
|
7
|
+
resolveKxmSnapshotPaths,
|
|
8
|
+
type LocalMeshSnapshot,
|
|
9
|
+
type MeshTuiPlan,
|
|
10
|
+
type MeshTuiRun,
|
|
11
|
+
} from "./local-snapshot.ts";
|
|
12
|
+
import { readUpdateCache } from "./kxm-update.ts";
|
|
13
|
+
import { probeHubHealth, readHubBinding } from "./hub-binding.ts";
|
|
14
|
+
import { readRoutingRecords } from "./telemetry.ts";
|
|
15
|
+
|
|
16
|
+
export const SESSION_BRIEF_SKIP_LABEL = "Skip — start a fresh session";
|
|
17
|
+
export const MAX_SESSION_BRIEF_TASKS = 5;
|
|
18
|
+
export const MAX_SESSION_BRIEF_PLANS = 5;
|
|
19
|
+
export const SESSION_BRIEF_SCHEMA = "kxm.session-brief.v1" as const;
|
|
20
|
+
export const DEFAULT_SESSION_BRIEF_STALE_SECONDS = 5;
|
|
21
|
+
|
|
22
|
+
export interface SessionWorkItem {
|
|
23
|
+
kind: "task" | "plan";
|
|
24
|
+
id: string;
|
|
25
|
+
runId: string;
|
|
26
|
+
label: string;
|
|
27
|
+
detail: string;
|
|
28
|
+
prompt: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface SessionWorkStats {
|
|
32
|
+
activeTasks: number;
|
|
33
|
+
waitingTasks: number;
|
|
34
|
+
planCount: number;
|
|
35
|
+
inbox: number;
|
|
36
|
+
runTotal: number;
|
|
37
|
+
latestPlan?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface SessionHubStatus {
|
|
41
|
+
state: "on" | "off" | "unknown";
|
|
42
|
+
evidence: "probed" | "bound" | "cached" | "unconfigured" | "process" | "timeout";
|
|
43
|
+
online?: boolean;
|
|
44
|
+
url?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface SessionShipStatus {
|
|
48
|
+
dirty: boolean;
|
|
49
|
+
ahead: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface SessionBrief {
|
|
53
|
+
schema: "kxm.session-brief.v1";
|
|
54
|
+
generatedAt: string;
|
|
55
|
+
staleSeconds: number;
|
|
56
|
+
source: "legacy" | "vnext" | "both";
|
|
57
|
+
hub: SessionHubStatus;
|
|
58
|
+
stats: SessionWorkStats;
|
|
59
|
+
tasks: SessionWorkItem[];
|
|
60
|
+
plans: SessionWorkItem[];
|
|
61
|
+
statusLine: string;
|
|
62
|
+
widgetLines: string[];
|
|
63
|
+
cost?: string;
|
|
64
|
+
sessionToken?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function hubPrefix(hub?: Partial<SessionHubStatus>): string {
|
|
68
|
+
if (hub?.state === "on" || (hub?.state === undefined && hub?.online === true)) return "kxm hub:on";
|
|
69
|
+
if (hub?.state === "off" || (hub?.state === undefined && hub?.online === false)) return "kxm hub:off";
|
|
70
|
+
if (hub?.state === "unknown") return "kxm hub:unknown";
|
|
71
|
+
return "kxm";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function truncate(value: string, width: number): string {
|
|
75
|
+
const text = value.replace(/\s+/g, " ").trim();
|
|
76
|
+
if (text.length <= width) return text;
|
|
77
|
+
return `${text.slice(0, Math.max(0, width - 1))}…`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function uniqueLabels(items: SessionWorkItem[]): SessionWorkItem[] {
|
|
81
|
+
const seen = new Set<string>();
|
|
82
|
+
return items.map((item) => {
|
|
83
|
+
let label = item.label;
|
|
84
|
+
if (seen.has(label)) label = truncate(`${label} · ${item.id.slice(-6)}`, 72);
|
|
85
|
+
seen.add(label);
|
|
86
|
+
return label === item.label ? item : { ...item, label };
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function taskItem(run: MeshTuiRun): SessionWorkItem {
|
|
91
|
+
const stage = run.currentStage ?? "-";
|
|
92
|
+
const progress = run.progress ? ` ${run.progress.done}/${run.progress.total}` : "";
|
|
93
|
+
const detail = `${run.definitionId}/${stage}${progress}`.trim();
|
|
94
|
+
const label = truncate(`Task ${run.definitionId} ${run.status} ${stage}${progress}`, 72);
|
|
95
|
+
const prompt = `Continue KXM task \`${run.definitionId}\` (run ${run.id}) at stage ${stage}${run.progress ? ` (${run.progress.done}/${run.progress.total})` : ""}. Load the run, then proceed without repeating completed work.`;
|
|
96
|
+
return { kind: "task", id: run.id, runId: run.id, label, detail, prompt };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function planItem(plan: MeshTuiPlan): SessionWorkItem {
|
|
100
|
+
const label = truncate(`Plan ${plan.summary}`, 72);
|
|
101
|
+
const prompt = `Continue from KXM plan: ${plan.summary} (run ${plan.runId}). Load that run and proceed.`;
|
|
102
|
+
return { kind: "plan", id: plan.id, runId: plan.runId, label, detail: plan.summary, prompt };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function recentTasks(runs: MeshTuiRun[]): MeshTuiRun[] {
|
|
106
|
+
const active = runs.filter((run) =>
|
|
107
|
+
run.status === "running" || run.status === "waiting" || run.status === "created" || run.status === "preparing"
|
|
108
|
+
);
|
|
109
|
+
const rest = runs.filter((run) =>
|
|
110
|
+
run.status !== "running" && run.status !== "waiting" && run.status !== "created" && run.status !== "preparing"
|
|
111
|
+
);
|
|
112
|
+
return [...active, ...rest].slice(0, MAX_SESSION_BRIEF_TASKS);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function formatShipLine(ship?: SessionShipStatus): string {
|
|
116
|
+
if (!ship) return "ship verify · PR=CI";
|
|
117
|
+
if (ship.dirty) return "ship dirty · commit after verify";
|
|
118
|
+
if (ship.ahead > 0) return `ship ${ship.ahead} local · PR after CI`;
|
|
119
|
+
return "ship clean · PR after CI";
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function readGitShip(cwd: string): SessionShipStatus | undefined {
|
|
123
|
+
try {
|
|
124
|
+
const dirty = spawnSync("git", ["-C", cwd, "status", "--porcelain"], { encoding: "utf8", windowsHide: true });
|
|
125
|
+
if (dirty.status !== 0) return undefined;
|
|
126
|
+
const isDirty = dirty.stdout.trim().length > 0;
|
|
127
|
+
|
|
128
|
+
// First try configured upstream
|
|
129
|
+
const upstream = spawnSync("git", ["-C", cwd, "rev-list", "--count", "@{u}..HEAD"], { encoding: "utf8", windowsHide: true });
|
|
130
|
+
if (upstream.status === 0) {
|
|
131
|
+
return {
|
|
132
|
+
dirty: isDirty,
|
|
133
|
+
ahead: Number.parseInt(upstream.stdout.trim(), 10) || 0,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// When no upstream, count against merge base of default branch
|
|
138
|
+
for (const baseRef of ["origin/HEAD", "main", "origin/main", "master", "origin/master"]) {
|
|
139
|
+
const mb = spawnSync("git", ["-C", cwd, "merge-base", baseRef, "HEAD"], { encoding: "utf8", windowsHide: true });
|
|
140
|
+
if (mb.status === 0 && mb.stdout.trim()) {
|
|
141
|
+
const count = spawnSync("git", ["-C", cwd, "rev-list", "--count", `${mb.stdout.trim()}..HEAD`], { encoding: "utf8", windowsHide: true });
|
|
142
|
+
if (count.status === 0) {
|
|
143
|
+
return {
|
|
144
|
+
dirty: isDirty,
|
|
145
|
+
ahead: Number.parseInt(count.stdout.trim(), 10) || 0,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
dirty: isDirty,
|
|
153
|
+
ahead: 0,
|
|
154
|
+
};
|
|
155
|
+
} catch {
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function formatCostLine(input?: {
|
|
161
|
+
sessionCostUsd?: number;
|
|
162
|
+
runCostUsd?: number;
|
|
163
|
+
harness?: string;
|
|
164
|
+
model?: string;
|
|
165
|
+
}): string | undefined {
|
|
166
|
+
if (!input) return undefined;
|
|
167
|
+
if (input.sessionCostUsd === undefined && input.runCostUsd === undefined) return "unknown";
|
|
168
|
+
const sess = input.sessionCostUsd !== undefined ? `$${input.sessionCostUsd.toFixed(2)} sess` : undefined;
|
|
169
|
+
const run = input.runCostUsd !== undefined ? `$${input.runCostUsd.toFixed(2)} run` : undefined;
|
|
170
|
+
const route = input.harness && input.model ? `${input.harness}/${input.model}` : (input.model ?? undefined);
|
|
171
|
+
const parts = [sess, run, route].filter(Boolean);
|
|
172
|
+
return parts.length > 0 ? parts.join(" · ") : "unknown";
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function renderStatusLine(
|
|
176
|
+
statsOrBrief: SessionWorkStats | SessionBrief,
|
|
177
|
+
current?: SessionWorkItem,
|
|
178
|
+
hub?: Partial<SessionHubStatus>,
|
|
179
|
+
ship?: SessionShipStatus,
|
|
180
|
+
updateLatest?: string,
|
|
181
|
+
cost?: string,
|
|
182
|
+
): string {
|
|
183
|
+
if ("schema" in statsOrBrief && statsOrBrief.schema === "kxm.session-brief.v1") {
|
|
184
|
+
const brief = statsOrBrief;
|
|
185
|
+
if (!current && !hub && !ship && !updateLatest && !cost) {
|
|
186
|
+
return brief.statusLine;
|
|
187
|
+
}
|
|
188
|
+
return formatSessionStatusLine(
|
|
189
|
+
brief.stats,
|
|
190
|
+
current,
|
|
191
|
+
hub ?? brief.hub,
|
|
192
|
+
ship,
|
|
193
|
+
updateLatest,
|
|
194
|
+
cost ?? brief.cost,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
return formatSessionStatusLine(statsOrBrief as SessionWorkStats, current, hub, ship, updateLatest, cost);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function formatSessionStatusLine(
|
|
201
|
+
stats: SessionWorkStats,
|
|
202
|
+
current?: SessionWorkItem,
|
|
203
|
+
hub?: Partial<SessionHubStatus>,
|
|
204
|
+
ship?: SessionShipStatus,
|
|
205
|
+
updateLatest?: string,
|
|
206
|
+
cost?: string,
|
|
207
|
+
): string {
|
|
208
|
+
const head = hubPrefix(hub);
|
|
209
|
+
if (!current && stats.activeTasks === 0 && stats.planCount === 0 && stats.inbox === 0) {
|
|
210
|
+
const idle = hub?.online === undefined && hub?.state === undefined ? "kxm idle" : `${head} · idle`;
|
|
211
|
+
const withShip = ship?.dirty
|
|
212
|
+
? `${idle} · dirty`
|
|
213
|
+
: (ship && ship.ahead > 0 ? `${idle} · ${ship.ahead} local` : idle);
|
|
214
|
+
const withCost = cost ? `${withShip} · ${cost}` : withShip;
|
|
215
|
+
const finalLine = updateLatest ? `${withCost} · upd ${updateLatest}` : withCost;
|
|
216
|
+
return finalLine.length <= 80 ? finalLine : `${finalLine.slice(0, 79)}…`;
|
|
217
|
+
}
|
|
218
|
+
const parts: string[] = [];
|
|
219
|
+
if (current?.kind === "task") parts.push(`${head} ${current.detail}`);
|
|
220
|
+
else if (current?.kind === "plan") parts.push(`${head} plan ${truncate(current.detail, 36)}`);
|
|
221
|
+
else parts.push(head);
|
|
222
|
+
parts.push(`${stats.activeTasks} task${stats.activeTasks === 1 ? "" : "s"}`);
|
|
223
|
+
if (stats.waitingTasks > 0) parts.push(`${stats.waitingTasks} waiting`);
|
|
224
|
+
parts.push(`${stats.planCount} plan${stats.planCount === 1 ? "" : "s"}`);
|
|
225
|
+
if (stats.inbox > 0) parts.push(`inbox ${stats.inbox}`);
|
|
226
|
+
if (ship?.dirty) parts.push("dirty");
|
|
227
|
+
else if (ship && ship.ahead > 0) parts.push(`${ship.ahead} local`);
|
|
228
|
+
if (cost) parts.push(cost);
|
|
229
|
+
if (updateLatest) parts.push(`upd ${updateLatest}`);
|
|
230
|
+
const line = parts.join(" · ");
|
|
231
|
+
return line.length <= 80 ? line : `${line.slice(0, 79)}…`;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function formatSessionWidget(
|
|
235
|
+
stats: SessionWorkStats,
|
|
236
|
+
current?: SessionWorkItem,
|
|
237
|
+
hub?: Partial<SessionHubStatus>,
|
|
238
|
+
ship?: SessionShipStatus,
|
|
239
|
+
updateLatest?: string,
|
|
240
|
+
cost?: string,
|
|
241
|
+
): string[] {
|
|
242
|
+
const hubMark = hub?.state === "on" || hub?.online === true
|
|
243
|
+
? "hub:on "
|
|
244
|
+
: (hub?.state === "off" || hub?.online === false
|
|
245
|
+
? "hub:off "
|
|
246
|
+
: (hub?.state === "unknown" ? "hub:unknown " : ""));
|
|
247
|
+
const lines = [
|
|
248
|
+
`KXM ${hubMark}${stats.activeTasks} tasks ${stats.waitingTasks} waiting ${stats.planCount} plans inbox ${stats.inbox}`,
|
|
249
|
+
];
|
|
250
|
+
if (current) lines.push(`now ${current.kind} ${truncate(current.detail, 56)}`);
|
|
251
|
+
else if (stats.latestPlan) lines.push(`plan ${truncate(stats.latestPlan, 60)}`);
|
|
252
|
+
else lines.push("now no selected work");
|
|
253
|
+
lines.push(formatShipLine(ship));
|
|
254
|
+
if (cost) lines.push(`cost ${cost}`);
|
|
255
|
+
if (updateLatest) lines.push(`update ${updateLatest} available · kxm update --kxm`);
|
|
256
|
+
return lines;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function buildSessionBrief(
|
|
260
|
+
snapshot: Pick<LocalMeshSnapshot, "runs" | "plans" | "openMessageTotal" | "runTotal"> & { source?: "legacy" | "vnext" | "both" | undefined },
|
|
261
|
+
current?: SessionWorkItem,
|
|
262
|
+
hub?: SessionHubStatus,
|
|
263
|
+
ship?: SessionShipStatus,
|
|
264
|
+
updateLatest?: string,
|
|
265
|
+
cost?: string,
|
|
266
|
+
sessionToken?: string,
|
|
267
|
+
source?: "legacy" | "vnext" | "both" | undefined,
|
|
268
|
+
staleSeconds = DEFAULT_SESSION_BRIEF_STALE_SECONDS,
|
|
269
|
+
): SessionBrief {
|
|
270
|
+
const resolvedSource = source ?? snapshot.source ?? "legacy";
|
|
271
|
+
const active = snapshot.runs.filter((run) =>
|
|
272
|
+
run.status === "running" || run.status === "waiting" || run.status === "created" || run.status === "preparing"
|
|
273
|
+
);
|
|
274
|
+
const stats: SessionWorkStats = {
|
|
275
|
+
activeTasks: active.length,
|
|
276
|
+
waitingTasks: snapshot.runs.filter((run) => run.status === "waiting").length,
|
|
277
|
+
planCount: snapshot.plans.length,
|
|
278
|
+
inbox: snapshot.openMessageTotal,
|
|
279
|
+
runTotal: snapshot.runTotal,
|
|
280
|
+
...(snapshot.plans[0]?.summary ? { latestPlan: snapshot.plans[0].summary } : {}),
|
|
281
|
+
};
|
|
282
|
+
const tasks = uniqueLabels(recentTasks(snapshot.runs).map(taskItem));
|
|
283
|
+
const plans = uniqueLabels(snapshot.plans.slice(0, MAX_SESSION_BRIEF_PLANS).map(planItem));
|
|
284
|
+
const resolvedHub: SessionHubStatus = hub ?? { state: "off", evidence: "unconfigured", online: false };
|
|
285
|
+
const statusLine = formatSessionStatusLine(stats, current, resolvedHub, ship, updateLatest, cost);
|
|
286
|
+
const widgetLines = formatSessionWidget(stats, current, resolvedHub, ship, updateLatest, cost);
|
|
287
|
+
return {
|
|
288
|
+
schema: SESSION_BRIEF_SCHEMA,
|
|
289
|
+
generatedAt: new Date().toISOString(),
|
|
290
|
+
staleSeconds,
|
|
291
|
+
source: resolvedSource,
|
|
292
|
+
hub: resolvedHub,
|
|
293
|
+
stats,
|
|
294
|
+
tasks,
|
|
295
|
+
plans,
|
|
296
|
+
statusLine,
|
|
297
|
+
widgetLines,
|
|
298
|
+
...(cost ? { cost } : {}),
|
|
299
|
+
...(sessionToken ? { sessionToken } : {}),
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export function readCachedSessionBrief(stateDir: string): SessionBrief | undefined {
|
|
304
|
+
const file = join(stateDir, "session-brief.json");
|
|
305
|
+
try {
|
|
306
|
+
if (!existsSync(file)) return undefined;
|
|
307
|
+
const raw = readFileSync(file, "utf8");
|
|
308
|
+
const parsed = JSON.parse(raw) as SessionBrief;
|
|
309
|
+
if (parsed && parsed.schema === "kxm.session-brief.v1" && typeof parsed.generatedAt === "string") {
|
|
310
|
+
const ageMs = Date.now() - Date.parse(parsed.generatedAt);
|
|
311
|
+
const ttlMs = (parsed.staleSeconds ?? DEFAULT_SESSION_BRIEF_STALE_SECONDS) * 1000;
|
|
312
|
+
if (ageMs >= 0 && ageMs < ttlMs) {
|
|
313
|
+
return parsed;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
} catch {
|
|
317
|
+
/* cache miss or unreadable */
|
|
318
|
+
}
|
|
319
|
+
return undefined;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export function writeCachedSessionBrief(stateDir: string, brief: SessionBrief): void {
|
|
323
|
+
try {
|
|
324
|
+
mkdirSync(stateDir, { recursive: true, mode: 0o700 });
|
|
325
|
+
const file = join(stateDir, "session-brief.json");
|
|
326
|
+
const tmp = `${file}.tmp.${randomUUID().slice(0, 8)}`;
|
|
327
|
+
writeFileSync(tmp, JSON.stringify(brief, null, 2), { encoding: "utf8", mode: 0o600 });
|
|
328
|
+
renameSync(tmp, file);
|
|
329
|
+
} catch {
|
|
330
|
+
/* best effort */
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export function estimateSessionCost(stateDir: string): string | undefined {
|
|
335
|
+
try {
|
|
336
|
+
const telemetryFile = join(stateDir, "telemetry.jsonl");
|
|
337
|
+
if (!existsSync(telemetryFile)) return undefined;
|
|
338
|
+
const records = readRoutingRecords(telemetryFile);
|
|
339
|
+
if (records.length === 0) return undefined;
|
|
340
|
+
let totalCost = 0;
|
|
341
|
+
let hasMetered = false;
|
|
342
|
+
let latestModel: string | undefined;
|
|
343
|
+
let latestHarness: string | undefined;
|
|
344
|
+
for (const { routing } of records) {
|
|
345
|
+
const r = routing as unknown as Record<string, unknown>;
|
|
346
|
+
const costBasis = (r.costBasis as string | undefined) ?? (typeof r.costUsd === "number" ? "metered" : "unmetered");
|
|
347
|
+
if (costBasis === "metered" && typeof r.costUsd === "number") {
|
|
348
|
+
totalCost += r.costUsd;
|
|
349
|
+
hasMetered = true;
|
|
350
|
+
}
|
|
351
|
+
latestModel = (r.effectiveModel as string | undefined) ?? (r.requestedModel as string | undefined) ?? latestModel;
|
|
352
|
+
latestHarness = (r.harness as string | undefined) ?? latestHarness;
|
|
353
|
+
}
|
|
354
|
+
if (!hasMetered) return "unknown";
|
|
355
|
+
const route = latestHarness && latestModel ? `${latestHarness}/${latestModel}` : latestModel;
|
|
356
|
+
return `$${totalCost.toFixed(2)} sess${route ? ` · ${route}` : ""}`;
|
|
357
|
+
} catch {
|
|
358
|
+
return undefined;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export async function resolveSessionHubStatus(
|
|
363
|
+
url: string | undefined,
|
|
364
|
+
fetchImpl: typeof fetch = fetch,
|
|
365
|
+
timeoutMs = 300,
|
|
366
|
+
): Promise<SessionHubStatus> {
|
|
367
|
+
if (!url || !url.trim()) {
|
|
368
|
+
return { state: "off", evidence: "unconfigured", online: false };
|
|
369
|
+
}
|
|
370
|
+
try {
|
|
371
|
+
const { health } = await probeHubHealth(url.trim(), fetchImpl, timeoutMs);
|
|
372
|
+
if (health === "on") {
|
|
373
|
+
return { state: "on", evidence: "probed", online: true, url: url.trim() };
|
|
374
|
+
}
|
|
375
|
+
if (health === "off") {
|
|
376
|
+
return { state: "off", evidence: "probed", online: false, url: url.trim() };
|
|
377
|
+
}
|
|
378
|
+
return { state: "unknown", evidence: "timeout", online: false, url: url.trim() };
|
|
379
|
+
} catch {
|
|
380
|
+
return { state: "unknown", evidence: "timeout", online: false, url: url.trim() };
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export function loadSessionBrief(
|
|
385
|
+
cwd: string,
|
|
386
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
387
|
+
current?: SessionWorkItem,
|
|
388
|
+
hub?: SessionHubStatus,
|
|
389
|
+
options: {
|
|
390
|
+
force?: boolean;
|
|
391
|
+
updateLatest?: string;
|
|
392
|
+
cost?: string;
|
|
393
|
+
sessionToken?: string;
|
|
394
|
+
ship?: SessionShipStatus;
|
|
395
|
+
} = {},
|
|
396
|
+
): SessionBrief {
|
|
397
|
+
const paths = resolveKxmSnapshotPaths(cwd, env);
|
|
398
|
+
if (!options.force) {
|
|
399
|
+
const cached = readCachedSessionBrief(paths.stateDir);
|
|
400
|
+
if (cached) {
|
|
401
|
+
if (current || hub || options.cost) {
|
|
402
|
+
const effectiveHub = hub ?? cached.hub;
|
|
403
|
+
const effectiveCost = options.cost ?? cached.cost;
|
|
404
|
+
const statusLine = formatSessionStatusLine(
|
|
405
|
+
cached.stats,
|
|
406
|
+
current,
|
|
407
|
+
effectiveHub,
|
|
408
|
+
options.ship,
|
|
409
|
+
options.updateLatest,
|
|
410
|
+
effectiveCost,
|
|
411
|
+
);
|
|
412
|
+
const widgetLines = formatSessionWidget(
|
|
413
|
+
cached.stats,
|
|
414
|
+
current,
|
|
415
|
+
effectiveHub,
|
|
416
|
+
options.ship,
|
|
417
|
+
options.updateLatest,
|
|
418
|
+
effectiveCost,
|
|
419
|
+
);
|
|
420
|
+
return {
|
|
421
|
+
...cached,
|
|
422
|
+
hub: effectiveHub,
|
|
423
|
+
...(effectiveCost ? { cost: effectiveCost } : {}),
|
|
424
|
+
statusLine,
|
|
425
|
+
widgetLines,
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
return cached;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const ship = options.ship ?? readGitShip(cwd);
|
|
433
|
+
let brief: SessionBrief;
|
|
434
|
+
try {
|
|
435
|
+
const cachedUpdate = readUpdateCache(paths.stateDir);
|
|
436
|
+
const updateLatest = options.updateLatest ?? (cachedUpdate?.available ? cachedUpdate.latest : undefined);
|
|
437
|
+
const cost = options.cost ?? estimateSessionCost(paths.stateDir);
|
|
438
|
+
const snapshot = loadLocalMeshSnapshot(paths.dataPath, paths.stateDir, { env });
|
|
439
|
+
brief = buildSessionBrief(
|
|
440
|
+
snapshot,
|
|
441
|
+
current,
|
|
442
|
+
hub,
|
|
443
|
+
ship,
|
|
444
|
+
updateLatest,
|
|
445
|
+
cost,
|
|
446
|
+
options.sessionToken,
|
|
447
|
+
snapshot.source,
|
|
448
|
+
);
|
|
449
|
+
} catch {
|
|
450
|
+
brief = buildSessionBrief(
|
|
451
|
+
{ runs: [], plans: [], openMessageTotal: 0, runTotal: 0 },
|
|
452
|
+
current,
|
|
453
|
+
hub,
|
|
454
|
+
ship,
|
|
455
|
+
options.updateLatest,
|
|
456
|
+
options.cost,
|
|
457
|
+
options.sessionToken,
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
writeCachedSessionBrief(paths.stateDir, brief);
|
|
462
|
+
return brief;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
export async function loadSessionBriefAsync(
|
|
466
|
+
cwd: string,
|
|
467
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
468
|
+
current?: SessionWorkItem,
|
|
469
|
+
hub?: SessionHubStatus,
|
|
470
|
+
options: {
|
|
471
|
+
force?: boolean;
|
|
472
|
+
fetchImpl?: typeof fetch;
|
|
473
|
+
cost?: string;
|
|
474
|
+
sessionToken?: string;
|
|
475
|
+
ship?: SessionShipStatus;
|
|
476
|
+
} = {},
|
|
477
|
+
): Promise<SessionBrief> {
|
|
478
|
+
const paths = resolveKxmSnapshotPaths(cwd, env);
|
|
479
|
+
if (!options.force) {
|
|
480
|
+
const cached = readCachedSessionBrief(paths.stateDir);
|
|
481
|
+
if (cached) {
|
|
482
|
+
if (current || hub || options.cost) {
|
|
483
|
+
const effectiveHub = hub ?? cached.hub;
|
|
484
|
+
const effectiveCost = options.cost ?? cached.cost;
|
|
485
|
+
const statusLine = formatSessionStatusLine(
|
|
486
|
+
cached.stats,
|
|
487
|
+
current,
|
|
488
|
+
effectiveHub,
|
|
489
|
+
options.ship,
|
|
490
|
+
undefined,
|
|
491
|
+
effectiveCost,
|
|
492
|
+
);
|
|
493
|
+
const widgetLines = formatSessionWidget(
|
|
494
|
+
cached.stats,
|
|
495
|
+
current,
|
|
496
|
+
effectiveHub,
|
|
497
|
+
options.ship,
|
|
498
|
+
undefined,
|
|
499
|
+
effectiveCost,
|
|
500
|
+
);
|
|
501
|
+
return {
|
|
502
|
+
...cached,
|
|
503
|
+
hub: effectiveHub,
|
|
504
|
+
...(effectiveCost ? { cost: effectiveCost } : {}),
|
|
505
|
+
statusLine,
|
|
506
|
+
widgetLines,
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
return cached;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
let resolvedHub = hub;
|
|
514
|
+
if (!resolvedHub) {
|
|
515
|
+
const serverUrl = env.KXM_SERVER_URL?.trim() || readHubBinding(env)?.url;
|
|
516
|
+
resolvedHub = await resolveSessionHubStatus(serverUrl, options.fetchImpl, 300);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
return loadSessionBrief(cwd, env, current, resolvedHub, options);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
export function sessionBriefChoices(brief: SessionBrief): string[] {
|
|
523
|
+
return [SESSION_BRIEF_SKIP_LABEL, ...brief.tasks.map((item) => item.label), ...brief.plans.map((item) => item.label)];
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
export function itemFromChoice(brief: SessionBrief, choice: string | undefined): SessionWorkItem | undefined {
|
|
527
|
+
if (!choice || choice === SESSION_BRIEF_SKIP_LABEL) return undefined;
|
|
528
|
+
return [...brief.tasks, ...brief.plans].find((item) => item.label === choice);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
export function formatSessionBriefText(brief: SessionBrief): string {
|
|
532
|
+
const lines = [brief.statusLine, ""];
|
|
533
|
+
if (brief.tasks.length === 0 && brief.plans.length === 0) {
|
|
534
|
+
lines.push("No recent tasks or plans.");
|
|
535
|
+
return lines.join("\n");
|
|
536
|
+
}
|
|
537
|
+
if (brief.tasks.length > 0) {
|
|
538
|
+
lines.push("Tasks");
|
|
539
|
+
for (const task of brief.tasks) lines.push(` ${task.label.replace(/^Task\s+/, "")}`);
|
|
540
|
+
}
|
|
541
|
+
if (brief.plans.length > 0) {
|
|
542
|
+
lines.push("Plans");
|
|
543
|
+
for (const plan of brief.plans) lines.push(` ${plan.label.replace(/^Plan\s+/, "")}`);
|
|
544
|
+
}
|
|
545
|
+
lines.push("", "Reply with an item to continue, or start a fresh prompt. Pi TUI: /kxm");
|
|
546
|
+
return lines.join("\n");
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
export function sessionBriefPickerEnabled(input: { env?: NodeJS.ProcessEnv; mode?: string; reason?: string }): boolean {
|
|
550
|
+
if (input.env?.KXM_SESSION_BRIEF?.trim() === "off") return false;
|
|
551
|
+
if (input.mode !== "tui") return false;
|
|
552
|
+
const reason = input.reason ?? "startup";
|
|
553
|
+
return reason === "startup" || reason === "new" || reason === "fork";
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
export const KXM_SLASH_SUBCOMMANDS = ["status", "hub", "memory", "help"] as const;
|
|
557
|
+
export type KxmSlashCommand = "brief" | typeof KXM_SLASH_SUBCOMMANDS[number];
|
|
558
|
+
|
|
559
|
+
export function parseKxmSlashArgs(args: string | undefined): KxmSlashCommand {
|
|
560
|
+
const raw = String(args ?? "").trim().toLowerCase();
|
|
561
|
+
if (raw === "" || raw === "brief") return "brief";
|
|
562
|
+
if (raw === "status" || raw === "hub" || raw === "memory" || raw === "help") return raw;
|
|
563
|
+
return "help";
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
export function kxmSlashCompletions(prefix: string): Array<{ value: string; label: string }> {
|
|
567
|
+
const p = prefix.trim().toLowerCase();
|
|
568
|
+
return KXM_SLASH_SUBCOMMANDS
|
|
569
|
+
.filter((name) => name.startsWith(p))
|
|
570
|
+
.map((name) => ({ value: name, label: name }));
|
|
571
|
+
}
|