@kontextmind/kxm 0.6.0 → 0.7.10
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 +1 -1
- package/.kxm/agents/coordinator.yaml +9 -0
- package/.kxm/agents/critic-arch.yaml +13 -0
- package/.kxm/agents/critic-cli.yaml +13 -0
- package/.kxm/agents/implementer.yaml +13 -0
- package/.kxm/gates.yaml +8 -0
- package/.kxm/producers.yaml +22 -0
- package/.kxm/project.yaml +15 -0
- package/.kxm/roles/writer.yaml +7 -0
- package/.kxm/workflows/default.yaml +47 -0
- package/CHANGELOG.md +39 -7
- package/README.md +1 -0
- package/docs/README.md +5 -0
- package/docs/adr/ADR-0002-browser-automation-steel-doks.md +103 -0
- package/docs/agent-skills.md +135 -0
- package/docs/architecture.md +1 -1
- package/docs/assignment-runner.md +21 -8
- package/docs/browser-automation.md +116 -0
- package/docs/configuration.md +11 -2
- package/docs/getting-started.md +21 -0
- package/docs/kb/how-credentials-retrieved-safely.md +31 -0
- package/docs/kb/how-to-capture-and-annotate-section.md +60 -0
- package/docs/kb/how-to-connect-playwright-to-steel.md +54 -0
- package/docs/kb/how-to-recover-expired-session-or-orphan.md +54 -0
- package/docs/kb/how-to-resume-after-mfa.md +28 -0
- package/docs/kb/how-to-take-over-session.md +32 -0
- package/docs/kb/why-authentication-disappeared.md +32 -0
- package/docs/kb/why-automation-opened-different-browser.md +32 -0
- package/docs/kb/why-session-viewer-cannot-control.md +31 -0
- package/docs/kxm-handbook.md +3 -3
- package/docs/operations.md +24 -0
- package/docs/operator-pi-packages.md +63 -0
- package/docs/prompts/browser-annotate-feedback.md +41 -0
- package/docs/prompts/browser-diagnose-recover.md +38 -0
- package/docs/prompts/browser-explore.md +42 -0
- package/docs/prompts/browser-repro-fix.md +48 -0
- package/docs/prompts/browser-start.md +41 -0
- package/docs/prompts/browser-takeover.md +50 -0
- package/docs/skills/repo-work-delivery.md +107 -0
- package/docs/skills.md +2 -0
- package/docs/test-matrix.md +4 -3
- package/docs/troubleshooting.md +41 -1
- package/docs/vnext/validation.md +9 -0
- package/docs/webhook-workflows.md +2 -2
- package/examples/README.md +1 -1
- package/package.json +16 -17
- package/plugins/kxm/.claude-plugin/plugin.json +1 -1
- package/plugins/kxm/README.md +1 -1
- package/plugins/kxm/dist/cli.js +41620 -35578
- package/plugins/kxm/dist/core.js +271 -34
- package/plugins/kxm/dist/extension.js +7759 -86
- package/plugins/kxm/dist/mcp-server.js +75 -21
- package/plugins/kxm/dist/runtime.js +8218 -2328
- package/plugins/kxm/dist/server.js +3125 -2260
- package/plugins/kxm/dist/vnext-runtime-supervisor.js +5961 -661
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/skills/SUITE.md +5 -0
- package/plugins/kxm/skills/hints.json +103 -0
- package/plugins/kxm/skills/kxm/SKILL.md +30 -83
- package/plugins/kxm/skills/kxm-browser-annotate/SKILL.md +90 -0
- package/plugins/kxm/skills/kxm-browser-auth/SKILL.md +47 -0
- package/plugins/kxm/skills/kxm-browser-diagnostics/SKILL.md +48 -0
- package/plugins/kxm/skills/kxm-browser-explore/SKILL.md +48 -0
- package/plugins/kxm/skills/kxm-browser-session/SKILL.md +94 -0
- package/plugins/kxm/skills/kxm-browser-takeover/SKILL.md +87 -0
- package/plugins/kxm/skills/kxm-browser-verify/SKILL.md +71 -0
- package/plugins/kxm/skills/kxm-context-memory/SKILL.md +69 -0
- package/plugins/kxm/skills/kxm-definitions/SKILL.md +65 -0
- package/plugins/kxm/skills/kxm-harness-auth/SKILL.md +34 -0
- package/plugins/kxm/skills/kxm-harvest/SKILL.md +48 -0
- package/plugins/kxm/skills/kxm-hub-ops/SKILL.md +43 -0
- package/plugins/kxm/skills/kxm-insights/SKILL.md +48 -0
- package/plugins/kxm/skills/kxm-mind/SKILL.md +59 -0
- package/plugins/kxm/skills/kxm-peer/SKILL.md +110 -0
- package/plugins/kxm/skills/kxm-project-setup/SKILL.md +42 -0
- package/plugins/kxm/skills/kxm-projects/SKILL.md +43 -0
- package/plugins/kxm/skills/kxm-protocol/SKILL.md +66 -0
- package/plugins/kxm/skills/kxm-query/SKILL.md +45 -0
- package/plugins/kxm/skills/kxm-routing-improve/SKILL.md +30 -0
- package/plugins/kxm/skills/kxm-runs/SKILL.md +29 -0
- package/plugins/kxm/skills/kxm-setup/SKILL.md +55 -0
- package/plugins/kxm/skills/kxm-skill-lifecycle/SKILL.md +31 -0
- package/plugins/kxm/skills/kxm-tasks/SKILL.md +33 -0
- package/plugins/kxm/skills/kxm-triage/SKILL.md +47 -0
- package/plugins/kxm/skills/kxm-work/SKILL.md +44 -0
- package/plugins/kxm/skills/kxm-workflow/SKILL.md +45 -0
- package/plugins/kxm/src/autocomplete.ts +9 -3
- package/plugins/kxm/src/browser.ts +603 -0
- package/plugins/kxm/src/cli/context-skills.ts +373 -0
- package/plugins/kxm/src/cli/hub.ts +614 -0
- package/plugins/kxm/src/cli/roles.ts +615 -0
- package/plugins/kxm/src/cli/system.ts +906 -0
- package/plugins/kxm/src/cli/tasks.ts +364 -0
- package/plugins/kxm/src/cli/types.ts +270 -0
- package/plugins/kxm/src/cli/vnext.ts +698 -0
- package/plugins/kxm/src/cli/workflows.ts +699 -0
- package/plugins/kxm/src/cli.ts +362 -2849
- package/plugins/kxm/src/commands.ts +150 -8
- package/plugins/kxm/src/completion-install.ts +223 -0
- package/plugins/kxm/src/config.ts +7 -4
- package/plugins/kxm/src/context-packet.ts +172 -0
- package/plugins/kxm/src/database.ts +1 -1
- package/plugins/kxm/src/extension.ts +36 -1
- package/plugins/kxm/src/external-effects.ts +357 -8
- package/plugins/kxm/src/hub-env.ts +193 -0
- package/plugins/kxm/src/hub.ts +2 -4
- package/plugins/kxm/src/improve.ts +72 -0
- package/plugins/kxm/src/init-guide-setup.ts +547 -0
- package/plugins/kxm/src/local-snapshot.ts +1 -1
- package/plugins/kxm/src/mcp-server.ts +1 -1
- package/plugins/kxm/src/model-inventory.ts +127 -0
- package/plugins/kxm/src/modes.ts +348 -0
- package/plugins/kxm/src/policy-draft.d.mts +55 -0
- package/plugins/kxm/src/policy-draft.mjs +565 -0
- package/plugins/kxm/src/price-calc.ts +17 -18
- package/plugins/kxm/src/prices.ts +32 -16
- package/plugins/kxm/src/producers.ts +71 -0
- package/plugins/kxm/src/protocol.ts +111 -0
- package/plugins/kxm/src/restricted-yaml.d.mts +31 -0
- package/plugins/kxm/src/restricted-yaml.mjs +145 -0
- package/plugins/kxm/src/role.ts +710 -0
- package/plugins/kxm/src/routing.ts +99 -1
- package/plugins/kxm/src/runtime.ts +4 -0
- package/plugins/kxm/src/safety-integrity.ts +76 -0
- package/plugins/kxm/src/session-work.ts +9 -2
- package/plugins/kxm/src/sqlite.ts +76 -0
- package/plugins/kxm/src/ssh-remote.ts +560 -0
- package/plugins/kxm/src/store.ts +1 -1
- package/plugins/kxm/src/studio-layout.ts +660 -17
- package/plugins/kxm/src/subagent-control.ts +312 -0
- package/plugins/kxm/src/suggest.ts +7 -13
- package/plugins/kxm/src/telemetry.ts +82 -0
- package/plugins/kxm/src/tui.ts +140 -0
- package/plugins/kxm/src/vnext-bindings.ts +1 -1
- package/plugins/kxm/src/vnext-config.ts +53 -111
- package/plugins/kxm/src/vnext-engine-command.ts +2 -0
- package/plugins/kxm/src/vnext-engine.ts +214 -62
- package/plugins/kxm/src/vnext-harness.ts +336 -84
- package/plugins/kxm/src/vnext-oneshot-evidence.ts +117 -0
- package/plugins/kxm/src/vnext-oneshot-process.ts +187 -0
- package/plugins/kxm/src/vnext-oneshot-producer.ts +182 -224
- package/plugins/kxm/src/vnext-pi-producer.ts +11 -7
- package/plugins/kxm/src/vnext-runtime-store.ts +36 -2
- package/plugins/kxm/src/vnext-runtime-supervisor.ts +122 -5
- package/plugins/kxm/src/vnext-runtime.ts +14 -0
- package/plugins/kxm/src/workflow-manager.ts +392 -0
- package/plugins/kxm/src/workflow-tui.ts +255 -0
- package/plugins/kxm/src/workflow.ts +144 -0
- package/schemas/policy-draft/README.md +17 -0
- package/schemas/policy-draft/model.v2.schema.json +140 -0
- package/schemas/policy-draft/role.v2.schema.json +91 -0
- package/schemas/vnext/modes.schema.json +56 -0
- package/schemas/vnext/role.schema.json +76 -0
- package/schemas/vnext/run-event.schema.json +1 -0
- package/scripts/assignment-run.d.mts +1 -1
- package/scripts/assignment-run.mjs +44 -35
- package/scripts/check-generated.mjs +33 -9
- package/scripts/emit-codex-artifacts.mjs +255 -11
- package/scripts/harness-run.d.mts +12 -4
- package/scripts/harness-run.mjs +65 -17
- package/scripts/kxm-bump-version.mjs +146 -0
- package/scripts/kxm-hub.mjs +150 -2
- package/scripts/kxm-publish-npm.mjs +3 -1
- package/scripts/kxm-release-github.mjs +3 -1
- package/scripts/kxm.mjs +0 -0
- package/scripts/native-critic.d.mts +5 -0
- package/scripts/native-critic.mjs +60 -0
- package/.kxm/config/README.md +0 -5
- package/.kxm/config/agents.json +0 -43
- package/.kxm/config/env.example +0 -56
- package/.kxm/config/update.example.yaml +0 -9
- package/.kxm/config/workflows/fix.json +0 -160
- package/.kxm/config/workflows/jira-development.json +0 -116
- package/.kxm/config/workflows/provenance-quorum.json +0 -150
- package/.kxm/config/workflows/v04-dogfood.json +0 -72
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KXM Workflow TUI module for Pi Extension.
|
|
3
|
+
* Renders active workflow run, stage progress stepper, assigned roles, and model/spend metrics.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { DatabaseSync } from "./sqlite.ts";
|
|
9
|
+
import { repoConfigDirectory } from "./config.ts";
|
|
10
|
+
import { readRoutingRecords, telemetryPath } from "./telemetry.ts";
|
|
11
|
+
|
|
12
|
+
export interface WorkflowProgressMetrics {
|
|
13
|
+
totalSpendUsd?: number | undefined;
|
|
14
|
+
inputTokens?: number | undefined;
|
|
15
|
+
outputTokens?: number | undefined;
|
|
16
|
+
cacheReadTokens?: number | undefined;
|
|
17
|
+
latencyMs?: number | undefined;
|
|
18
|
+
harness?: string | undefined;
|
|
19
|
+
model?: string | undefined;
|
|
20
|
+
effort?: string | undefined;
|
|
21
|
+
memoryRevision?: string | undefined;
|
|
22
|
+
transitions?: number | undefined;
|
|
23
|
+
maxTransitions?: number | undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface WorkflowProgressState {
|
|
27
|
+
runId?: string | undefined;
|
|
28
|
+
workflowId: string;
|
|
29
|
+
status: "idle" | "running" | "waiting" | "passed" | "failed" | "completed" | "cancelled";
|
|
30
|
+
currentStage?: string | undefined;
|
|
31
|
+
currentRole?: string | undefined;
|
|
32
|
+
attempt?: number | undefined;
|
|
33
|
+
maxAttempts?: number | undefined;
|
|
34
|
+
stages: Array<{
|
|
35
|
+
id: string;
|
|
36
|
+
label?: string | undefined;
|
|
37
|
+
kind?: "agent" | "gate" | "wait" | "approval" | undefined;
|
|
38
|
+
status: "pending" | "running" | "passed" | "failed" | "waiting" | "cancelled";
|
|
39
|
+
role?: string | undefined;
|
|
40
|
+
attempts?: number | undefined;
|
|
41
|
+
}>;
|
|
42
|
+
metrics?: WorkflowProgressMetrics | undefined;
|
|
43
|
+
updatedAt?: string | undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Load current active or latest workflow progress from SQLite state and telemetry.
|
|
48
|
+
*/
|
|
49
|
+
export function loadActiveWorkflowProgress(
|
|
50
|
+
repoRoot = process.cwd(),
|
|
51
|
+
targetRunId?: string,
|
|
52
|
+
): WorkflowProgressState | undefined {
|
|
53
|
+
const dbPath = join(repoConfigDirectory(repoRoot), "state", "kxm.db");
|
|
54
|
+
if (!existsSync(dbPath)) return undefined;
|
|
55
|
+
|
|
56
|
+
let db: DatabaseSync | undefined;
|
|
57
|
+
try {
|
|
58
|
+
db = new DatabaseSync(dbPath, { readOnly: true });
|
|
59
|
+
let row: { record: string } | undefined;
|
|
60
|
+
if (targetRunId) {
|
|
61
|
+
row = db.prepare("SELECT record FROM workflow_runs WHERE id = ?").get(targetRunId) as { record: string } | undefined;
|
|
62
|
+
} else {
|
|
63
|
+
row = db.prepare(
|
|
64
|
+
"SELECT record FROM workflow_runs ORDER BY CASE json_extract(record, '$.status') WHEN 'running' THEN 1 WHEN 'waiting' THEN 2 ELSE 3 END, json_extract(record, '$.updatedAt') DESC LIMIT 1",
|
|
65
|
+
).get() as { record: string } | undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!row) return undefined;
|
|
69
|
+
|
|
70
|
+
const raw = JSON.parse(row.record) as Record<string, unknown>;
|
|
71
|
+
const runId = (raw.id as string) || targetRunId;
|
|
72
|
+
const workflowId = (raw.definitionId as string) || (raw.workflowId as string) || "workflow";
|
|
73
|
+
const status = (raw.status as WorkflowProgressState["status"]) || "running";
|
|
74
|
+
const currentStage = (raw.currentStage as string) || undefined;
|
|
75
|
+
const updatedAt = (raw.updatedAt as string) || undefined;
|
|
76
|
+
|
|
77
|
+
const stages: WorkflowProgressState["stages"] = [];
|
|
78
|
+
const rawStages = Array.isArray(raw.stages) ? raw.stages : [];
|
|
79
|
+
for (const s of rawStages) {
|
|
80
|
+
if (s && typeof s === "object") {
|
|
81
|
+
stages.push({
|
|
82
|
+
id: String((s as Record<string, unknown>).id || ""),
|
|
83
|
+
label: (s as Record<string, unknown>).label as string | undefined,
|
|
84
|
+
kind: (s as Record<string, unknown>).kind as any,
|
|
85
|
+
status: ((s as Record<string, unknown>).status as any) || "pending",
|
|
86
|
+
role: (s as Record<string, unknown>).role as string | undefined,
|
|
87
|
+
attempts: typeof (s as Record<string, unknown>).attempts === "number" ? ((s as Record<string, unknown>).attempts as number) : undefined,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let metrics: WorkflowProgressMetrics | undefined;
|
|
93
|
+
try {
|
|
94
|
+
const logsDir = join(repoConfigDirectory(repoRoot), "logs");
|
|
95
|
+
const telemFile = telemetryPath(logsDir);
|
|
96
|
+
if (existsSync(telemFile)) {
|
|
97
|
+
const records = readRoutingRecords(telemFile);
|
|
98
|
+
const routingRunId = (routing: (typeof records)[number]["routing"]): string | undefined =>
|
|
99
|
+
"runId" in routing ? routing.runId : routing.workflowRunId;
|
|
100
|
+
let matching = records.filter((r) => routingRunId(r.routing) === runId);
|
|
101
|
+
if (matching.length === 0) {
|
|
102
|
+
const rawLines = readFileSync(telemFile, "utf8").split(/\r?\n/);
|
|
103
|
+
for (const line of rawLines) {
|
|
104
|
+
if (!line.trim()) continue;
|
|
105
|
+
try {
|
|
106
|
+
const p = JSON.parse(line) as Record<string, unknown>;
|
|
107
|
+
const rt = (p.routing || p) as any;
|
|
108
|
+
if (rt && (routingRunId(rt) === runId || p.runId === runId)) {
|
|
109
|
+
matching.push({ recordedAt: (p.timestamp as string) || new Date().toISOString(), routing: rt });
|
|
110
|
+
}
|
|
111
|
+
} catch {
|
|
112
|
+
// ignore
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (matching.length > 0) {
|
|
117
|
+
const latest = matching[matching.length - 1]!.routing;
|
|
118
|
+
let totalSpend = 0;
|
|
119
|
+
let totalIn = 0;
|
|
120
|
+
let totalOut = 0;
|
|
121
|
+
let totalCache = 0;
|
|
122
|
+
let totalLatency = 0;
|
|
123
|
+
for (const m of matching) {
|
|
124
|
+
const rt = m.routing;
|
|
125
|
+
if (typeof rt.costUsd === "number") totalSpend += rt.costUsd;
|
|
126
|
+
if ("tokensIn" in rt && typeof rt.tokensIn === "number") totalIn += rt.tokensIn;
|
|
127
|
+
else if ("tokens" in rt && rt.tokens && typeof (rt.tokens as { input?: unknown }).input === "number") totalIn += (rt.tokens as { input: number }).input;
|
|
128
|
+
if ("tokensOut" in rt && typeof rt.tokensOut === "number") totalOut += rt.tokensOut;
|
|
129
|
+
else if ("tokens" in rt && rt.tokens && typeof (rt.tokens as { output?: unknown }).output === "number") totalOut += (rt.tokens as { output: number }).output;
|
|
130
|
+
if ("cacheReadTokens" in rt && typeof rt.cacheReadTokens === "number") totalCache += rt.cacheReadTokens;
|
|
131
|
+
else if ("tokens" in rt && rt.tokens && typeof (rt.tokens as { cacheRead?: unknown }).cacheRead === "number") totalCache += (rt.tokens as { cacheRead: number }).cacheRead;
|
|
132
|
+
if ("latencyMs" in rt && typeof rt.latencyMs === "number") totalLatency += rt.latencyMs;
|
|
133
|
+
}
|
|
134
|
+
const latestRecord = latest as unknown as Record<string, unknown>;
|
|
135
|
+
const latestMetadata = latestRecord.providerMetadata && typeof latestRecord.providerMetadata === "object"
|
|
136
|
+
? latestRecord.providerMetadata as Record<string, unknown>
|
|
137
|
+
: undefined;
|
|
138
|
+
metrics = {
|
|
139
|
+
totalSpendUsd: totalSpend,
|
|
140
|
+
inputTokens: totalIn,
|
|
141
|
+
outputTokens: totalOut,
|
|
142
|
+
cacheReadTokens: totalCache,
|
|
143
|
+
latencyMs: totalLatency,
|
|
144
|
+
harness: typeof latestRecord.harness === "string" ? latestRecord.harness : (typeof latestMetadata?.harness === "string" ? latestMetadata.harness : undefined),
|
|
145
|
+
model: typeof latestRecord.effectiveModel === "string" ? latestRecord.effectiveModel : (typeof latestRecord.model === "string" ? latestRecord.model : (typeof latestRecord.requestedModel === "string" ? latestRecord.requestedModel : undefined)),
|
|
146
|
+
effort: typeof latestRecord.thinking === "string" ? latestRecord.thinking : (typeof latestRecord.reasoningEffort === "string" ? latestRecord.reasoningEffort : undefined),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
} catch {
|
|
151
|
+
// Telemetry optional
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const activeStageObj = stages.find((s) => s.id === currentStage);
|
|
155
|
+
const currentRole = activeStageObj?.role || (raw.targetAgentName as string) || undefined;
|
|
156
|
+
const attempt = activeStageObj?.attempts || 1;
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
runId,
|
|
160
|
+
workflowId,
|
|
161
|
+
status,
|
|
162
|
+
currentStage,
|
|
163
|
+
currentRole,
|
|
164
|
+
attempt,
|
|
165
|
+
stages,
|
|
166
|
+
metrics,
|
|
167
|
+
updatedAt,
|
|
168
|
+
};
|
|
169
|
+
} catch {
|
|
170
|
+
return undefined;
|
|
171
|
+
} finally {
|
|
172
|
+
try {
|
|
173
|
+
db?.close();
|
|
174
|
+
} catch {
|
|
175
|
+
// ignore
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Format stage progress indicator into a compact horizontal visual stepper.
|
|
182
|
+
* e.g.: [✔ implement] ──> [▶ review-arch] ──> [⧗ review-cli] ──> [○ verify]
|
|
183
|
+
*/
|
|
184
|
+
export function formatStageStepper(stages: WorkflowProgressState["stages"], currentStage?: string): string {
|
|
185
|
+
if (stages.length === 0) {
|
|
186
|
+
return currentStage ? `[▶ ${currentStage}]` : "[idle]";
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const parts = stages.map((s) => {
|
|
190
|
+
let icon = "○"; // pending
|
|
191
|
+
if (s.status === "passed") icon = "✔";
|
|
192
|
+
else if (s.id === currentStage || s.status === "running") icon = "▶";
|
|
193
|
+
else if (s.status === "waiting") icon = "⧗";
|
|
194
|
+
else if (s.status === "failed") icon = "✖";
|
|
195
|
+
else if (s.status === "cancelled") icon = "⊘";
|
|
196
|
+
return `[${icon} ${s.id}]`;
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
return parts.join(" ──> ");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function formatTokenK(tokens?: number): string {
|
|
203
|
+
if (tokens === undefined || tokens === null) return "0";
|
|
204
|
+
if (tokens >= 1000) {
|
|
205
|
+
return `${(tokens / 1000).toFixed(1)}k`;
|
|
206
|
+
}
|
|
207
|
+
return String(tokens);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Render Pi Extension live widget lines (for `ctx.ui.setWidget("kxm-workflow", lines)`).
|
|
212
|
+
*/
|
|
213
|
+
export function renderWorkflowWidgetLines(state?: WorkflowProgressState): string[] {
|
|
214
|
+
if (!state || !state.runId) {
|
|
215
|
+
return [
|
|
216
|
+
"┌─ KXM Workflow ──────────────────────────────────────────────────────────────┐",
|
|
217
|
+
"│ No active workflow running. Use 'kxm workflow start <id>' │",
|
|
218
|
+
"└─────────────────────────────────────────────────────────────────────────────┘",
|
|
219
|
+
];
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const shortRun = state.runId ? state.runId.slice(0, 16) : "-";
|
|
223
|
+
const header = `┌─ KXM Workflow: ${state.workflowId} (${shortRun}) ─`;
|
|
224
|
+
const topBorder = header + "─".repeat(Math.max(2, 79 - header.length)) + "┐";
|
|
225
|
+
|
|
226
|
+
const stepper = formatStageStepper(state.stages, state.currentStage);
|
|
227
|
+
const statusStr = state.status.toUpperCase();
|
|
228
|
+
const attemptStr = state.attempt ? ` (Attempt ${state.attempt})` : "";
|
|
229
|
+
const roleStr = state.currentRole ? ` · Role: ${state.currentRole}` : "";
|
|
230
|
+
|
|
231
|
+
const lines: string[] = [topBorder];
|
|
232
|
+
lines.push(`│ Stage: ${stepper}`);
|
|
233
|
+
lines.push(`│ Status: ${statusStr}${attemptStr}${roleStr}`);
|
|
234
|
+
|
|
235
|
+
if (state.metrics) {
|
|
236
|
+
const costStr = state.metrics.totalSpendUsd !== undefined ? `$${state.metrics.totalSpendUsd.toFixed(2)}` : "unmetered";
|
|
237
|
+
const routeStr = state.metrics.model ? `${state.metrics.harness ?? "pi"}:${state.metrics.model}` : "harness";
|
|
238
|
+
const effortStr = state.metrics.effort ? ` (${state.metrics.effort})` : "";
|
|
239
|
+
const inTokens = formatTokenK(state.metrics.inputTokens);
|
|
240
|
+
const outTokens = formatTokenK(state.metrics.outputTokens);
|
|
241
|
+
const cacheTokens = formatTokenK(state.metrics.cacheReadTokens);
|
|
242
|
+
lines.push(`│ Model: ${routeStr}${effortStr} · Spend: ${costStr}`);
|
|
243
|
+
lines.push(`│ Tokens: In: ${inTokens} | Out: ${outTokens} | Cache: ${cacheTokens}`);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
lines.push("└" + "─".repeat(78) + "┘");
|
|
247
|
+
return lines;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Render detailed full-width TUI banner / notification string.
|
|
252
|
+
*/
|
|
253
|
+
export function renderWorkflowTuiText(state?: WorkflowProgressState): string {
|
|
254
|
+
return renderWorkflowWidgetLines(state).join("\n");
|
|
255
|
+
}
|
|
@@ -3,11 +3,17 @@ import {
|
|
|
3
3
|
MAX_MESSAGE_TTL_MS,
|
|
4
4
|
MIN_MESSAGE_TTL_MS,
|
|
5
5
|
ProtocolError,
|
|
6
|
+
TERMINAL_RECEIPT_SCHEMA,
|
|
6
7
|
newId,
|
|
7
8
|
requireString,
|
|
9
|
+
validateTerminalReceipt,
|
|
8
10
|
type ImprovementArea,
|
|
9
11
|
type JournalCategory,
|
|
10
12
|
type MessageRecord,
|
|
13
|
+
type TerminalReceipt,
|
|
14
|
+
type TerminalReceiptEvidence,
|
|
15
|
+
type TerminalReceiptMetrics,
|
|
16
|
+
type TerminalReceiptStatus,
|
|
11
17
|
type WorkflowCheckpointStatus,
|
|
12
18
|
type WorkflowEvidenceInput,
|
|
13
19
|
type WorkflowEvidenceReference,
|
|
@@ -18,11 +24,16 @@ import {
|
|
|
18
24
|
export type {
|
|
19
25
|
ImprovementArea,
|
|
20
26
|
JournalCategory,
|
|
27
|
+
TerminalReceipt,
|
|
28
|
+
TerminalReceiptEvidence,
|
|
29
|
+
TerminalReceiptMetrics,
|
|
30
|
+
TerminalReceiptStatus,
|
|
21
31
|
WorkflowCheckpointStatus,
|
|
22
32
|
WorkflowEvidenceInput,
|
|
23
33
|
WorkflowEvidenceReference,
|
|
24
34
|
WorkflowEvidenceReferenceInput,
|
|
25
35
|
} from "./protocol.ts";
|
|
36
|
+
export { TERMINAL_RECEIPT_SCHEMA, validateTerminalReceipt } from "./protocol.ts";
|
|
26
37
|
|
|
27
38
|
export type WorkflowRunStatus = "running" | "waiting" | "completed" | "failed";
|
|
28
39
|
export type WorkflowStageStatus = "pending" | "in_progress" | "waiting" | WorkflowCheckpointStatus;
|
|
@@ -153,6 +164,8 @@ export interface WorkflowStageDefinition {
|
|
|
153
164
|
instructions: string;
|
|
154
165
|
requiredEvidence: string[];
|
|
155
166
|
maxAttempts: number;
|
|
167
|
+
/** Bounded in-place retry limit before audit escalation (default: 2). */
|
|
168
|
+
autoResumeLimit?: number;
|
|
156
169
|
area?: ImprovementArea;
|
|
157
170
|
evidencePolicies?: WorkflowEvidencePolicies;
|
|
158
171
|
/** Typed outcome map (v0.5). Keys are outcome identities ("passed",
|
|
@@ -250,6 +263,13 @@ export interface WorkflowStageState extends WorkflowStageDefinition {
|
|
|
250
263
|
startedAt?: string;
|
|
251
264
|
completedAt?: string;
|
|
252
265
|
updatedAt?: string;
|
|
266
|
+
receipt?: TerminalReceipt;
|
|
267
|
+
auditEscalation?: {
|
|
268
|
+
reason: string;
|
|
269
|
+
timestamp: string;
|
|
270
|
+
ruling?: string;
|
|
271
|
+
receipt?: TerminalReceipt;
|
|
272
|
+
};
|
|
253
273
|
}
|
|
254
274
|
|
|
255
275
|
export interface CapturedOracle {
|
|
@@ -1003,12 +1023,18 @@ export function parseWorkflowDefinitions(
|
|
|
1003
1023
|
&& (!Number.isInteger(stageMaxTransitions) || (stageMaxTransitions as number) < 1 || (stageMaxTransitions as number) > 100)) {
|
|
1004
1024
|
throw new Error(`stage ${stageId} maxTransitions must be an integer between 1 and 100`);
|
|
1005
1025
|
}
|
|
1026
|
+
const autoResumeLimit = stage.autoResumeLimit;
|
|
1027
|
+
if (autoResumeLimit !== undefined
|
|
1028
|
+
&& (!Number.isInteger(autoResumeLimit) || (autoResumeLimit as number) < 1 || (autoResumeLimit as number) > 20)) {
|
|
1029
|
+
throw new Error(`stage ${stageId} autoResumeLimit must be an integer between 1 and 20`);
|
|
1030
|
+
}
|
|
1006
1031
|
return {
|
|
1007
1032
|
id: stageId,
|
|
1008
1033
|
label: requireString(stage.label ?? stageId, "stage.label", { max: 128 }),
|
|
1009
1034
|
instructions: requireString(stage.instructions, "stage.instructions", { max: 4_000 }),
|
|
1010
1035
|
requiredEvidence,
|
|
1011
1036
|
maxAttempts: maxAttempts as number,
|
|
1037
|
+
...(autoResumeLimit !== undefined ? { autoResumeLimit: autoResumeLimit as number } : {}),
|
|
1012
1038
|
...(area ? { area } : {}),
|
|
1013
1039
|
...(evidencePolicies ? { evidencePolicies } : {}),
|
|
1014
1040
|
...(on ? { on } : {}),
|
|
@@ -1377,6 +1403,31 @@ export function checkpointRun(
|
|
|
1377
1403
|
delete run.currentStage;
|
|
1378
1404
|
return { retry: false, completed: false, run };
|
|
1379
1405
|
}
|
|
1406
|
+
if (stage.autoResumeLimit !== undefined && stage.attempts >= stage.autoResumeLimit) {
|
|
1407
|
+
stage.status = "in_progress";
|
|
1408
|
+
const reason = summary || `autoResumeLimit of ${stage.autoResumeLimit} reached on stage ${stage.id}`;
|
|
1409
|
+
const receipt = validateTerminalReceipt({
|
|
1410
|
+
schema: TERMINAL_RECEIPT_SCHEMA,
|
|
1411
|
+
status: "audit_escalation",
|
|
1412
|
+
seat: stage.id,
|
|
1413
|
+
runId: run.id,
|
|
1414
|
+
stageId: stage.id,
|
|
1415
|
+
timestamp,
|
|
1416
|
+
host: "pi",
|
|
1417
|
+
model: "default",
|
|
1418
|
+
escalationReason: reason,
|
|
1419
|
+
});
|
|
1420
|
+
const expiresAt = new Date(Date.parse(timestamp) + 24 * 60 * 60 * 1000).toISOString();
|
|
1421
|
+
waitForWorkflowSignal(run, stage.id, "audit_escalation", reason, timestamp, expiresAt);
|
|
1422
|
+
stage.receipt = receipt;
|
|
1423
|
+
stage.auditEscalation = {
|
|
1424
|
+
reason,
|
|
1425
|
+
timestamp,
|
|
1426
|
+
receipt,
|
|
1427
|
+
};
|
|
1428
|
+
return { retry: false, completed: false, run };
|
|
1429
|
+
}
|
|
1430
|
+
stage.status = status;
|
|
1380
1431
|
// Declared failure outcomes create typed transitions (bounded); the
|
|
1381
1432
|
// outcome identity defaults to the checkpoint status.
|
|
1382
1433
|
const outcomeKey = outcome ?? status;
|
|
@@ -1546,3 +1597,96 @@ export function approveWorkflowDegradation(
|
|
|
1546
1597
|
run.updatedAt = timestamp;
|
|
1547
1598
|
return { run, approval, created: true };
|
|
1548
1599
|
}
|
|
1600
|
+
|
|
1601
|
+
export function escalateWorkflowStage(
|
|
1602
|
+
run: WorkflowRun,
|
|
1603
|
+
stageId: string,
|
|
1604
|
+
reason: string,
|
|
1605
|
+
timestamp: string,
|
|
1606
|
+
options: {
|
|
1607
|
+
seat?: string;
|
|
1608
|
+
host?: string;
|
|
1609
|
+
model?: string;
|
|
1610
|
+
evidence?: TerminalReceiptEvidence;
|
|
1611
|
+
metrics?: TerminalReceiptMetrics;
|
|
1612
|
+
} = {},
|
|
1613
|
+
): { run: WorkflowRun; receipt: TerminalReceipt } {
|
|
1614
|
+
if (run.status !== "running") throw new ProtocolError(409, `workflow is ${run.status}`, "workflow_terminal");
|
|
1615
|
+
const stage = run.stages.find((s) => s.id === stageId);
|
|
1616
|
+
if (!stage) throw new ProtocolError(404, `workflow stage not found: ${stageId}`, "workflow_stage_not_found");
|
|
1617
|
+
if (stage.id !== run.currentStage || stage.status !== "in_progress") {
|
|
1618
|
+
throw new ProtocolError(409, `stage ${stageId} is not currently active`, "workflow_stage_out_of_order");
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
const receipt: TerminalReceipt = validateTerminalReceipt({
|
|
1622
|
+
schema: TERMINAL_RECEIPT_SCHEMA,
|
|
1623
|
+
status: "audit_escalation",
|
|
1624
|
+
seat: options.seat ?? stage.id,
|
|
1625
|
+
runId: run.id,
|
|
1626
|
+
stageId,
|
|
1627
|
+
timestamp,
|
|
1628
|
+
host: options.host ?? "pi",
|
|
1629
|
+
model: options.model ?? "default",
|
|
1630
|
+
escalationReason: reason,
|
|
1631
|
+
...(options.evidence ? { evidence: options.evidence } : {}),
|
|
1632
|
+
...(options.metrics ? { metrics: options.metrics } : {}),
|
|
1633
|
+
});
|
|
1634
|
+
|
|
1635
|
+
const expiresAt = new Date(Date.parse(timestamp) + 24 * 60 * 60 * 1000).toISOString();
|
|
1636
|
+
waitForWorkflowSignal(run, stageId, "audit_escalation", reason, timestamp, expiresAt);
|
|
1637
|
+
|
|
1638
|
+
stage.receipt = receipt;
|
|
1639
|
+
stage.auditEscalation = {
|
|
1640
|
+
reason,
|
|
1641
|
+
timestamp,
|
|
1642
|
+
receipt,
|
|
1643
|
+
};
|
|
1644
|
+
|
|
1645
|
+
return { run, receipt };
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
export function resumeWorkflowFromRuling(
|
|
1649
|
+
run: WorkflowRun,
|
|
1650
|
+
ruling: string,
|
|
1651
|
+
timestamp: string,
|
|
1652
|
+
options: {
|
|
1653
|
+
status?: WorkflowCheckpointStatus;
|
|
1654
|
+
evidence?: WorkflowEvidenceInput;
|
|
1655
|
+
} = {},
|
|
1656
|
+
): { retry: boolean; completed: boolean; run: WorkflowRun; stageId: string } {
|
|
1657
|
+
if (run.status !== "waiting" || !run.waiting) {
|
|
1658
|
+
throw new ProtocolError(409, `workflow is ${run.status}`, "workflow_not_waiting");
|
|
1659
|
+
}
|
|
1660
|
+
if (run.waiting.signalKey !== "audit_escalation") {
|
|
1661
|
+
throw new ProtocolError(
|
|
1662
|
+
409,
|
|
1663
|
+
`workflow is waiting for signal '${run.waiting.signalKey}', not 'audit_escalation'`,
|
|
1664
|
+
"workflow_signal_mismatch",
|
|
1665
|
+
);
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
const stageId = run.waiting.stageId;
|
|
1669
|
+
const stage = run.stages.find((s) => s.id === stageId);
|
|
1670
|
+
if (!stage) throw new ProtocolError(404, `workflow stage not found: ${stageId}`, "workflow_stage_not_found");
|
|
1671
|
+
|
|
1672
|
+
const status = options.status ?? "passed";
|
|
1673
|
+
const evidence = options.evidence ?? {};
|
|
1674
|
+
|
|
1675
|
+
if (stage.auditEscalation) {
|
|
1676
|
+
stage.auditEscalation.ruling = ruling;
|
|
1677
|
+
if (stage.auditEscalation.receipt) {
|
|
1678
|
+
stage.auditEscalation.receipt.ruling = ruling;
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
stage.summary = `Resumed by operator ruling: ${ruling}`;
|
|
1682
|
+
|
|
1683
|
+
return resumeWorkflowFromSignal(
|
|
1684
|
+
run,
|
|
1685
|
+
"audit_escalation",
|
|
1686
|
+
status,
|
|
1687
|
+
`Resumed: ${ruling}`,
|
|
1688
|
+
evidence,
|
|
1689
|
+
timestamp,
|
|
1690
|
+
);
|
|
1691
|
+
}
|
|
1692
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Policy-draft schemas (non-authoritative)
|
|
2
|
+
|
|
3
|
+
`kxm.model.v2` and `kxm.role.v2` here are **passive scaffolding**. They are not
|
|
4
|
+
operator settings, live registry identities, or admission.
|
|
5
|
+
|
|
6
|
+
- Live project models remain `kxm.model.v1` under `schemas/vnext/`.
|
|
7
|
+
- Live CLI roles remain `kxm.role.v1`.
|
|
8
|
+
- Runner admission remains `.kxm/roster.json` via the trusted control Git loader.
|
|
9
|
+
- This directory is not discovered as a vNext project resource.
|
|
10
|
+
|
|
11
|
+
Pure validation lives in `plugins/kxm/src/policy-draft.mjs`
|
|
12
|
+
(`validatePolicyDraft`). Callers must supply draft documents, evidence bytes,
|
|
13
|
+
and code-owned ceilings explicitly. The function does not read files, Git,
|
|
14
|
+
credentials, or cwd, and it does not dispatch or admit a route.
|
|
15
|
+
|
|
16
|
+
Roster `route` lists are configured preference order, not a measured ranking.
|
|
17
|
+
`status: candidate` is stored as candidate data and is never treated as admitted.
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://schemas.kxm.dev/policy-draft/model.v2.schema.json",
|
|
4
|
+
"title": "KXM policy-draft model route",
|
|
5
|
+
"description": "Passive kxm.model.v2 scaffolding. Not live registry selection, admission, or operator settings.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["schema", "harness", "model", "vendor", "status", "permissions"],
|
|
8
|
+
"additionalProperties": false,
|
|
9
|
+
"properties": {
|
|
10
|
+
"schema": { "const": "kxm.model.v2" },
|
|
11
|
+
"id": {
|
|
12
|
+
"type": "string",
|
|
13
|
+
"pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$",
|
|
14
|
+
"minLength": 1,
|
|
15
|
+
"maxLength": 64
|
|
16
|
+
},
|
|
17
|
+
"harness": {
|
|
18
|
+
"type": "string",
|
|
19
|
+
"pattern": "^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$",
|
|
20
|
+
"minLength": 1,
|
|
21
|
+
"maxLength": 64
|
|
22
|
+
},
|
|
23
|
+
"model": { "type": "string", "minLength": 1, "maxLength": 200 },
|
|
24
|
+
"vendor": { "type": "string", "minLength": 1, "maxLength": 64 },
|
|
25
|
+
"status": { "enum": ["admitted", "candidate", "retired"] },
|
|
26
|
+
"permissions": {
|
|
27
|
+
"type": "array",
|
|
28
|
+
"items": { "enum": ["edit", "read-only"] },
|
|
29
|
+
"minItems": 1,
|
|
30
|
+
"maxItems": 2,
|
|
31
|
+
"uniqueItems": true
|
|
32
|
+
},
|
|
33
|
+
"origin": {
|
|
34
|
+
"type": "object",
|
|
35
|
+
"required": ["source", "sha256"],
|
|
36
|
+
"additionalProperties": false,
|
|
37
|
+
"properties": {
|
|
38
|
+
"source": { "type": "string", "minLength": 1, "maxLength": 512 },
|
|
39
|
+
"sha256": { "type": "string", "pattern": "^[a-f0-9]{64}$" }
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"thinking": { "type": "string", "minLength": 1, "maxLength": 64 },
|
|
43
|
+
"tags": {
|
|
44
|
+
"type": "array",
|
|
45
|
+
"items": {
|
|
46
|
+
"type": "string",
|
|
47
|
+
"pattern": "^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$",
|
|
48
|
+
"minLength": 1,
|
|
49
|
+
"maxLength": 64
|
|
50
|
+
},
|
|
51
|
+
"uniqueItems": true,
|
|
52
|
+
"maxItems": 32
|
|
53
|
+
},
|
|
54
|
+
"capabilities": {
|
|
55
|
+
"type": "array",
|
|
56
|
+
"items": {
|
|
57
|
+
"type": "string",
|
|
58
|
+
"pattern": "^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$",
|
|
59
|
+
"minLength": 1,
|
|
60
|
+
"maxLength": 64
|
|
61
|
+
},
|
|
62
|
+
"uniqueItems": true,
|
|
63
|
+
"maxItems": 32
|
|
64
|
+
},
|
|
65
|
+
"priority": { "type": "integer", "minimum": -10000, "maximum": 10000 },
|
|
66
|
+
"fallbacks": {
|
|
67
|
+
"type": "array",
|
|
68
|
+
"maxItems": 8,
|
|
69
|
+
"items": {
|
|
70
|
+
"oneOf": [
|
|
71
|
+
{
|
|
72
|
+
"type": "object",
|
|
73
|
+
"required": ["profile"],
|
|
74
|
+
"additionalProperties": false,
|
|
75
|
+
"properties": {
|
|
76
|
+
"profile": {
|
|
77
|
+
"type": "string",
|
|
78
|
+
"pattern": "^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$",
|
|
79
|
+
"minLength": 1,
|
|
80
|
+
"maxLength": 64
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"type": "object",
|
|
86
|
+
"required": ["tag"],
|
|
87
|
+
"additionalProperties": false,
|
|
88
|
+
"properties": {
|
|
89
|
+
"tag": {
|
|
90
|
+
"type": "string",
|
|
91
|
+
"pattern": "^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$",
|
|
92
|
+
"minLength": 1,
|
|
93
|
+
"maxLength": 64
|
|
94
|
+
},
|
|
95
|
+
"capabilities": {
|
|
96
|
+
"type": "array",
|
|
97
|
+
"items": {
|
|
98
|
+
"type": "string",
|
|
99
|
+
"pattern": "^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$",
|
|
100
|
+
"minLength": 1,
|
|
101
|
+
"maxLength": 64
|
|
102
|
+
},
|
|
103
|
+
"uniqueItems": true,
|
|
104
|
+
"maxItems": 32
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"type": "object",
|
|
110
|
+
"required": ["provider", "model"],
|
|
111
|
+
"additionalProperties": false,
|
|
112
|
+
"properties": {
|
|
113
|
+
"provider": {
|
|
114
|
+
"type": "string",
|
|
115
|
+
"pattern": "^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$",
|
|
116
|
+
"minLength": 1,
|
|
117
|
+
"maxLength": 64
|
|
118
|
+
},
|
|
119
|
+
"model": { "type": "string", "minLength": 1, "maxLength": 200 }
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
]
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
"limits": {
|
|
126
|
+
"type": "object",
|
|
127
|
+
"additionalProperties": false,
|
|
128
|
+
"properties": {
|
|
129
|
+
"contextTokens": { "type": "integer", "minimum": 1 },
|
|
130
|
+
"outputTokens": { "type": "integer", "minimum": 1 },
|
|
131
|
+
"timeoutMs": { "type": "integer", "minimum": 0, "maximum": 31536000000 }
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
"if": {
|
|
136
|
+
"properties": { "harness": { "const": "pi" } },
|
|
137
|
+
"required": ["harness"]
|
|
138
|
+
},
|
|
139
|
+
"then": { "required": ["origin"] }
|
|
140
|
+
}
|