@mrciphersmith/keryx 0.2.153 → 0.2.154

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.
Files changed (3) hide show
  1. package/README.md +29 -0
  2. package/dist/cli.js +2471 -103
  3. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -51578,7 +51578,7 @@ import path47 from "path";
51578
51578
  // package.json
51579
51579
  var package_default = {
51580
51580
  name: "@mrciphersmith/keryx",
51581
- version: "0.2.153",
51581
+ version: "0.2.154",
51582
51582
  description: "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
51583
51583
  private: false,
51584
51584
  publishConfig: {
@@ -51623,7 +51623,7 @@ var package_default = {
51623
51623
  typecheck: "tsc --noEmit",
51624
51624
  "typecheck:scripts": "tsc --project tsconfig.scripts.json --noEmit",
51625
51625
  test: "bun test",
51626
- "test:core": "bun test src/cli src/core src/shell-source-audits.test.ts src/assets/ src/capability/ src/commands/ src/contracts/ src/ctx/ src/eval/ src/flow/ src/forgetting/ src/gdgraph/ src/gdskills/ src/health/ src/job/ src/lib/ src/mcp/ src/memory/ src/metrics/ src/retention/ src/review/ src/sac/ src/security/ src/standard/ src/sync/ src/testing/ src/wiki/",
51626
+ "test:core": "bun test src/cli src/core src/shell-source-audits.test.ts src/acp/ src/assets/ src/capability/ src/commands/ src/contracts/ src/ctx/ src/eval/ src/flow/ src/forgetting/ src/gdgraph/ src/gdskills/ src/health/ src/job/ src/lib/ src/mcp/ src/memory/ src/metrics/ src/retention/ src/review/ src/sac/ src/security/ src/standard/ src/sync/ src/testing/ src/trigger/ src/wiki/",
51627
51627
  "test:client:terminal": "bun test src/tui/ src/commands/shell",
51628
51628
  "test:client:streaming": "bun test src/harness/provider/",
51629
51629
  "test:client:cancel-resume": "bun test src/harness/run/ src/harness/resume/ src/harness/session/ src/session/ src/bus/ src/commands/sessions",
@@ -73546,6 +73546,20 @@ var COMMAND_DESCRIPTORS = [
73546
73546
  "writes the memory provenance record"
73547
73547
  ]
73548
73548
  },
73549
+ {
73550
+ module: "trigger",
73551
+ command: "trigger run",
73552
+ summary: `Perform exactly one pass of a declared trigger's action (.metaproject/triggers.json). Only "reconcile" ` + '(-> sync --apply) and "rebuild" (-> gdgraph build) run today; "open-flow"/"flow-next" refuse cleanly.',
73553
+ intent: ["\u0437\u0430\u043F\u0443\u0441\u0442\u0438 \u0442\u0440\u0438\u0433\u0433\u0435\u0440", "run trigger", "fire trigger", "trigger run"],
73554
+ args: [{ name: "<name>", type: "string", required: true, desc: "the trigger's name, as declared in .metaproject/triggers.json" }],
73555
+ json: false,
73556
+ read: false,
73557
+ sideEffects: [
73558
+ 'reconcile: writes graph/wiki/memory via "keryx sync --apply"',
73559
+ 'rebuild: writes the code graph via "keryx gdgraph build"',
73560
+ "briefly writes .metaproject/data/trigger/.run.lock while the action runs"
73561
+ ]
73562
+ },
73549
73563
  {
73550
73564
  module: "testing",
73551
73565
  command: "test analyze",
@@ -111598,12 +111612,1364 @@ Shell:
111598
111612
  `);
111599
111613
  }
111600
111614
 
111601
- // src/commands/bus.ts
111615
+ // src/commands/acp.ts
111616
+ import { randomUUID as randomUUID37 } from "crypto";
111617
+
111618
+ // src/acp/server.ts
111619
+ init_agent();
111602
111620
  import { randomUUID as randomUUID36 } from "crypto";
111603
111621
 
111622
+ // src/acp/agent-io.ts
111623
+ import { randomUUID as randomUUID35 } from "crypto";
111624
+
111625
+ // src/acp/permission.ts
111626
+ var ACP_PERMISSION_OPTION_IDS = {
111627
+ allowOnce: "allow_once",
111628
+ allowAlways: "allow_always",
111629
+ rejectOnce: "reject_once",
111630
+ rejectAlways: "reject_always"
111631
+ };
111632
+ var OPTION_ALLOW_ONCE = Object.freeze({
111633
+ optionId: ACP_PERMISSION_OPTION_IDS.allowOnce,
111634
+ name: "Allow once",
111635
+ kind: "allow_once"
111636
+ });
111637
+ var OPTION_ALLOW_ALWAYS = Object.freeze({
111638
+ optionId: ACP_PERMISSION_OPTION_IDS.allowAlways,
111639
+ name: "Always allow",
111640
+ kind: "allow_always"
111641
+ });
111642
+ var OPTION_REJECT_ONCE = Object.freeze({
111643
+ optionId: ACP_PERMISSION_OPTION_IDS.rejectOnce,
111644
+ name: "Reject",
111645
+ kind: "reject_once"
111646
+ });
111647
+ var OPTION_REJECT_ALWAYS = Object.freeze({
111648
+ optionId: ACP_PERMISSION_OPTION_IDS.rejectAlways,
111649
+ name: "Always reject",
111650
+ kind: "reject_always"
111651
+ });
111652
+ function permissionIsEscalated(meta) {
111653
+ return meta?.destructive === true || meta?.credentials === true || meta?.publishLease === true || meta?.untrustedOrigin === true;
111654
+ }
111655
+ function permissionOptionsFor(meta) {
111656
+ return permissionIsEscalated(meta) ? [OPTION_ALLOW_ONCE, OPTION_REJECT_ONCE, OPTION_REJECT_ALWAYS] : [OPTION_ALLOW_ONCE, OPTION_ALLOW_ALWAYS, OPTION_REJECT_ONCE, OPTION_REJECT_ALWAYS];
111657
+ }
111658
+ function kindOf(optionId, options) {
111659
+ return options.find((option) => option.optionId === optionId)?.kind;
111660
+ }
111661
+ function approvalFromPermissionResponse(response, options, fingerprint) {
111662
+ const raw = response.outcome;
111663
+ if (typeof raw !== "object" || raw === null) {
111664
+ return false;
111665
+ }
111666
+ const outcome = raw;
111667
+ if (outcome.outcome !== "selected") {
111668
+ return false;
111669
+ }
111670
+ switch (kindOf(outcome.optionId, options)) {
111671
+ case "allow_once":
111672
+ return true;
111673
+ case "allow_always":
111674
+ return fingerprint === undefined ? true : { approved: true, fingerprint };
111675
+ default:
111676
+ return false;
111677
+ }
111678
+ }
111679
+
111680
+ // src/acp/jsonrpc.ts
111681
+ var JSONRPC_VERSION = "2.0";
111682
+ var JSON_RPC_ERROR_CODES = {
111683
+ parseError: -32700,
111684
+ invalidRequest: -32600,
111685
+ methodNotFound: -32601,
111686
+ invalidParams: -32602,
111687
+ internalError: -32603,
111688
+ requestCancelled: -32800,
111689
+ authRequired: -32000,
111690
+ resourceNotFound: -32002
111691
+ };
111692
+
111693
+ class AcpError extends Error {
111694
+ code;
111695
+ data;
111696
+ constructor(code, message, data) {
111697
+ super(message);
111698
+ this.name = "AcpError";
111699
+ this.code = code;
111700
+ this.data = data;
111701
+ }
111702
+ toErrorObject() {
111703
+ return jsonRpcError(this.code, this.message, this.data);
111704
+ }
111705
+ static from(code, message, data) {
111706
+ return new AcpError(code, message, data);
111707
+ }
111708
+ }
111709
+ function jsonRpcError(code, message, data) {
111710
+ return data === undefined ? { code, message } : { code, message, data };
111711
+ }
111712
+ function parseError(data) {
111713
+ return jsonRpcError(JSON_RPC_ERROR_CODES.parseError, "Parse error", data);
111714
+ }
111715
+ function invalidRequest(data) {
111716
+ return jsonRpcError(JSON_RPC_ERROR_CODES.invalidRequest, "Invalid request", data);
111717
+ }
111718
+ function methodNotFound(method, data) {
111719
+ return jsonRpcError(JSON_RPC_ERROR_CODES.methodNotFound, `Method not found: ${method}`, data);
111720
+ }
111721
+ function invalidParams(message, data) {
111722
+ return jsonRpcError(JSON_RPC_ERROR_CODES.invalidParams, message, data);
111723
+ }
111724
+ function internalError(message, data) {
111725
+ return jsonRpcError(JSON_RPC_ERROR_CODES.internalError, message, data);
111726
+ }
111727
+ function successResponse(id, result) {
111728
+ return { jsonrpc: JSONRPC_VERSION, id, result };
111729
+ }
111730
+ function errorResponse(id, error) {
111731
+ return { jsonrpc: JSONRPC_VERSION, id, error };
111732
+ }
111733
+ function requestMessage(id, method, params) {
111734
+ return params === undefined ? { jsonrpc: JSONRPC_VERSION, id, method } : { jsonrpc: JSONRPC_VERSION, id, method, params };
111735
+ }
111736
+ function notificationMessage(method, params) {
111737
+ return params === undefined ? { jsonrpc: JSONRPC_VERSION, method } : { jsonrpc: JSONRPC_VERSION, method, params };
111738
+ }
111739
+ function isPlainObject10(value) {
111740
+ return typeof value === "object" && value !== null && !Array.isArray(value);
111741
+ }
111742
+ function isJsonRpcId(value) {
111743
+ return value === null || typeof value === "string" || typeof value === "number";
111744
+ }
111745
+ function recoverId(value) {
111746
+ if (!isPlainObject10(value)) {
111747
+ return null;
111748
+ }
111749
+ const id = value["id"];
111750
+ return isJsonRpcId(id) ? id : null;
111751
+ }
111752
+ function classifyJsonRpcMessage(value) {
111753
+ if (!isPlainObject10(value)) {
111754
+ return {
111755
+ kind: "invalid",
111756
+ id: null,
111757
+ error: invalidRequest({ reason: "message is not a JSON object" })
111758
+ };
111759
+ }
111760
+ if (value["jsonrpc"] !== JSONRPC_VERSION) {
111761
+ return {
111762
+ kind: "invalid",
111763
+ id: recoverId(value),
111764
+ error: invalidRequest({
111765
+ reason: `jsonrpc must be "${JSONRPC_VERSION}"`,
111766
+ received: value["jsonrpc"]
111767
+ })
111768
+ };
111769
+ }
111770
+ const hasMethod = "method" in value;
111771
+ const hasResult = "result" in value;
111772
+ const hasError = "error" in value;
111773
+ if (hasMethod) {
111774
+ const method = value["method"];
111775
+ if (typeof method !== "string") {
111776
+ return {
111777
+ kind: "invalid",
111778
+ id: recoverId(value),
111779
+ error: invalidRequest({ reason: "method must be a string", received: method })
111780
+ };
111781
+ }
111782
+ if (!("id" in value)) {
111783
+ const notification = "params" in value ? { jsonrpc: JSONRPC_VERSION, method, params: value["params"] } : { jsonrpc: JSONRPC_VERSION, method };
111784
+ return { kind: "notification", message: notification };
111785
+ }
111786
+ const id = value["id"];
111787
+ if (!isJsonRpcId(id)) {
111788
+ return {
111789
+ kind: "invalid",
111790
+ id: null,
111791
+ error: invalidRequest({ reason: "id must be a string, number or null", received: id })
111792
+ };
111793
+ }
111794
+ const request = "params" in value ? { jsonrpc: JSONRPC_VERSION, id, method, params: value["params"] } : { jsonrpc: JSONRPC_VERSION, id, method };
111795
+ return { kind: "request", message: request };
111796
+ }
111797
+ if (hasResult || hasError) {
111798
+ const id = value["id"];
111799
+ if (!isJsonRpcId(id)) {
111800
+ return {
111801
+ kind: "invalid",
111802
+ id: null,
111803
+ error: invalidRequest({ reason: "response id must be a string, number or null" })
111804
+ };
111805
+ }
111806
+ if (hasError) {
111807
+ const raw = value["error"];
111808
+ const code = isPlainObject10(raw) ? raw["code"] : undefined;
111809
+ const message = isPlainObject10(raw) ? raw["message"] : undefined;
111810
+ if (typeof code !== "number" || typeof message !== "string") {
111811
+ return {
111812
+ kind: "invalid",
111813
+ id,
111814
+ error: invalidRequest({ reason: "error must carry a numeric code and a string message" })
111815
+ };
111816
+ }
111817
+ const data = isPlainObject10(raw) ? raw["data"] : undefined;
111818
+ return { kind: "response", message: errorResponse(id, jsonRpcError(code, message, data)) };
111819
+ }
111820
+ return { kind: "response", message: successResponse(id, value["result"]) };
111821
+ }
111822
+ return {
111823
+ kind: "invalid",
111824
+ id: recoverId(value),
111825
+ error: invalidRequest({ reason: "message carries neither method, result nor error" })
111826
+ };
111827
+ }
111828
+
111829
+ // src/acp/protocol.ts
111830
+ var ACP_PROTOCOL_VERSION = 1;
111831
+ var ACP_SUPPORTED_PROTOCOL_VERSIONS = [ACP_PROTOCOL_VERSION];
111832
+ var ACP_AGENT_METHODS = {
111833
+ initialize: "initialize",
111834
+ authenticate: "authenticate",
111835
+ logout: "logout",
111836
+ sessionNew: "session/new",
111837
+ sessionLoad: "session/load",
111838
+ sessionResume: "session/resume",
111839
+ sessionPrompt: "session/prompt",
111840
+ sessionCancel: "session/cancel",
111841
+ sessionClose: "session/close",
111842
+ sessionDelete: "session/delete",
111843
+ sessionList: "session/list",
111844
+ sessionSetMode: "session/set_mode",
111845
+ sessionSetConfigOption: "session/set_config_option"
111846
+ };
111847
+ var ACP_CLIENT_METHODS = {
111848
+ fsReadTextFile: "fs/read_text_file",
111849
+ fsWriteTextFile: "fs/write_text_file",
111850
+ sessionRequestPermission: "session/request_permission",
111851
+ sessionUpdate: "session/update",
111852
+ elicitationCreate: "elicitation/create",
111853
+ elicitationComplete: "elicitation/complete",
111854
+ terminalCreate: "terminal/create",
111855
+ terminalOutput: "terminal/output",
111856
+ terminalKill: "terminal/kill",
111857
+ terminalRelease: "terminal/release",
111858
+ terminalWaitForExit: "terminal/wait_for_exit"
111859
+ };
111860
+ var ACP_IMPLEMENTED_AGENT_METHODS = [
111861
+ ACP_AGENT_METHODS.initialize,
111862
+ ACP_AGENT_METHODS.sessionNew,
111863
+ ACP_AGENT_METHODS.sessionLoad,
111864
+ ACP_AGENT_METHODS.sessionList,
111865
+ ACP_AGENT_METHODS.sessionPrompt,
111866
+ ACP_AGENT_METHODS.sessionCancel
111867
+ ];
111868
+ var ACP_REFUSED_AGENT_METHODS = new Map([
111869
+ [
111870
+ ACP_AGENT_METHODS.authenticate,
111871
+ {
111872
+ code: JSON_RPC_ERROR_CODES.methodNotFound,
111873
+ reason: "keryx advertises no authMethods; there is nothing to authenticate against"
111874
+ }
111875
+ ],
111876
+ [
111877
+ ACP_AGENT_METHODS.logout,
111878
+ {
111879
+ code: JSON_RPC_ERROR_CODES.methodNotFound,
111880
+ reason: "keryx does not advertise agentCapabilities.auth.logout"
111881
+ }
111882
+ ],
111883
+ [
111884
+ ACP_AGENT_METHODS.sessionResume,
111885
+ {
111886
+ code: JSON_RPC_ERROR_CODES.methodNotFound,
111887
+ reason: "keryx does not advertise sessionCapabilities.resume; use session/load, which replays history"
111888
+ }
111889
+ ],
111890
+ [
111891
+ ACP_AGENT_METHODS.sessionClose,
111892
+ {
111893
+ code: JSON_RPC_ERROR_CODES.methodNotFound,
111894
+ reason: "keryx does not advertise sessionCapabilities.close; sessions are durable on disk"
111895
+ }
111896
+ ],
111897
+ [
111898
+ ACP_AGENT_METHODS.sessionDelete,
111899
+ {
111900
+ code: JSON_RPC_ERROR_CODES.methodNotFound,
111901
+ reason: "keryx does not advertise sessionCapabilities.delete; session retention is an operator decision, not a client one"
111902
+ }
111903
+ ],
111904
+ [
111905
+ ACP_AGENT_METHODS.sessionSetMode,
111906
+ {
111907
+ code: JSON_RPC_ERROR_CODES.methodNotFound,
111908
+ reason: "keryx returns no modes from session/new, so there is no mode to set"
111909
+ }
111910
+ ],
111911
+ [
111912
+ ACP_AGENT_METHODS.sessionSetConfigOption,
111913
+ {
111914
+ code: JSON_RPC_ERROR_CODES.methodNotFound,
111915
+ reason: "keryx returns no configOptions from session/new, so there is no option to set"
111916
+ }
111917
+ ]
111918
+ ]);
111919
+ function refusalError(method) {
111920
+ const refusal = ACP_REFUSED_AGENT_METHODS.get(method);
111921
+ if (refusal === undefined) {
111922
+ return;
111923
+ }
111924
+ return methodNotFound(method, {
111925
+ reason: refusal.reason,
111926
+ protocolVersion: ACP_PROTOCOL_VERSION,
111927
+ implemented: ACP_IMPLEMENTED_AGENT_METHODS
111928
+ });
111929
+ }
111930
+ function unsupportedVersionError(requested, reason) {
111931
+ return invalidParams(`Unsupported ACP protocol version: ${reason}`, {
111932
+ requested,
111933
+ supported: ACP_SUPPORTED_PROTOCOL_VERSIONS,
111934
+ latest: ACP_PROTOCOL_VERSION
111935
+ });
111936
+ }
111937
+ function negotiateProtocolVersion(requested) {
111938
+ if (typeof requested !== "number" || !Number.isInteger(requested)) {
111939
+ return {
111940
+ kind: "refuse",
111941
+ error: unsupportedVersionError(requested, "protocolVersion must be an integer")
111942
+ };
111943
+ }
111944
+ if (requested < 0 || requested > 65535) {
111945
+ return {
111946
+ kind: "refuse",
111947
+ error: unsupportedVersionError(requested, "protocolVersion is outside the uint16 range")
111948
+ };
111949
+ }
111950
+ if (ACP_SUPPORTED_PROTOCOL_VERSIONS.includes(requested)) {
111951
+ return { kind: "exact", version: requested };
111952
+ }
111953
+ if (requested > ACP_PROTOCOL_VERSION) {
111954
+ return { kind: "offer", version: ACP_PROTOCOL_VERSION, requested };
111955
+ }
111956
+ return {
111957
+ kind: "refuse",
111958
+ error: unsupportedVersionError(requested, `keryx serves ${ACP_SUPPORTED_PROTOCOL_VERSIONS.join(", ")} and cannot speak anything older`)
111959
+ };
111960
+ }
111961
+ function textBlock(text) {
111962
+ return { type: "text", text };
111963
+ }
111964
+ var KERYX_AGENT_CAPABILITIES = Object.freeze({
111965
+ loadSession: true,
111966
+ promptCapabilities: Object.freeze({ image: false, audio: false, embeddedContext: true }),
111967
+ mcpCapabilities: Object.freeze({ http: false, sse: false }),
111968
+ sessionCapabilities: Object.freeze({ list: Object.freeze({}) }),
111969
+ auth: Object.freeze({})
111970
+ });
111971
+ var KERYX_AUTH_METHODS = Object.freeze([]);
111972
+ function requireObjectParams(params, method) {
111973
+ if (typeof params !== "object" || params === null || Array.isArray(params)) {
111974
+ const error = invalidParams(`${method}: params must be an object`, { received: params });
111975
+ throw new AcpError(error.code, error.message, error.data);
111976
+ }
111977
+ return params;
111978
+ }
111979
+ function requireStringField(params, field, method) {
111980
+ const value = params[field];
111981
+ if (typeof value !== "string" || value === "") {
111982
+ const error = invalidParams(`${method}: ${field} must be a non-empty string`, {
111983
+ field,
111984
+ received: value
111985
+ });
111986
+ throw new AcpError(error.code, error.message, error.data);
111987
+ }
111988
+ return value;
111989
+ }
111990
+
111991
+ // src/acp/agent-io.ts
111992
+ var TOOL_KIND_BY_NAME = {
111993
+ get_cwd: "read",
111994
+ list_dir: "read",
111995
+ read_file: "read",
111996
+ search_code: "search",
111997
+ apply_patch: "edit",
111998
+ shell_exec: "execute",
111999
+ web_fetch: "fetch",
112000
+ web_search: "search"
112001
+ };
112002
+ function toolKindFor(name) {
112003
+ return TOOL_KIND_BY_NAME[name] ?? "other";
112004
+ }
112005
+ function tryParseJson(text) {
112006
+ try {
112007
+ return JSON.parse(text);
112008
+ } catch {
112009
+ return text;
112010
+ }
112011
+ }
112012
+ function createAcpAgentIo(sessionId, send, askPermission) {
112013
+ const pendingByName = new Map;
112014
+ const emit = (update) => send(sessionId, update);
112015
+ const openToolCall = (name, input, claimed) => {
112016
+ const toolCallId = randomUUID35();
112017
+ const queue = pendingByName.get(name) ?? [];
112018
+ queue.push({ toolCallId, input, claimed });
112019
+ pendingByName.set(name, queue);
112020
+ emit({
112021
+ sessionUpdate: "tool_call",
112022
+ toolCallId,
112023
+ title: name,
112024
+ name,
112025
+ kind: toolKindFor(name),
112026
+ status: "in_progress",
112027
+ rawInput: tryParseJson(input)
112028
+ });
112029
+ return toolCallId;
112030
+ };
112031
+ const requestApproval = async (tool, input, meta) => {
112032
+ if (askPermission === undefined) {
112033
+ return false;
112034
+ }
112035
+ const pending = pendingByName.get(tool)?.find((entry) => entry.input === input);
112036
+ const toolCallId = pending?.toolCallId ?? openToolCall(tool, input, false);
112037
+ const options = permissionOptionsFor(meta);
112038
+ const response = await askPermission({
112039
+ toolCall: {
112040
+ toolCallId,
112041
+ title: tool,
112042
+ name: tool,
112043
+ kind: toolKindFor(tool),
112044
+ status: "pending",
112045
+ rawInput: tryParseJson(input)
112046
+ },
112047
+ options
112048
+ });
112049
+ if (response === undefined) {
112050
+ return false;
112051
+ }
112052
+ return approvalFromPermissionResponse(response, options, meta?.fingerprint);
112053
+ };
112054
+ return {
112055
+ ...askPermission !== undefined ? { requestApproval } : {},
112056
+ write: (text) => {
112057
+ if (text.length === 0) {
112058
+ return;
112059
+ }
112060
+ emit({ sessionUpdate: "agent_message_chunk", content: textBlock(text) });
112061
+ },
112062
+ onReasoningDelta: (delta) => {
112063
+ if (delta.redacted === true || delta.text === undefined || delta.text.length === 0) {
112064
+ return;
112065
+ }
112066
+ emit({ sessionUpdate: "agent_thought_chunk", content: textBlock(delta.text) });
112067
+ },
112068
+ onToolCall: (name, input) => {
112069
+ const announcedAhead = pendingByName.get(name)?.find((entry) => !entry.claimed && entry.input === input);
112070
+ if (announcedAhead !== undefined) {
112071
+ announcedAhead.claimed = true;
112072
+ return;
112073
+ }
112074
+ openToolCall(name, input, true);
112075
+ },
112076
+ onToolResult: (name, result) => {
112077
+ const queue = pendingByName.get(name);
112078
+ const index = queue?.findIndex((entry) => entry.claimed) ?? -1;
112079
+ const pending = queue?.splice(index >= 0 ? index : 0, 1)[0];
112080
+ const toolCallId = pending?.toolCallId;
112081
+ emit({
112082
+ sessionUpdate: "tool_call_update",
112083
+ toolCallId: toolCallId ?? randomUUID35(),
112084
+ status: result.isError ? "failed" : "completed",
112085
+ content: [{ type: "content", content: textBlock(result.output) }],
112086
+ rawOutput: result.output
112087
+ });
112088
+ },
112089
+ onSystem: (text) => {
112090
+ if (text.length === 0) {
112091
+ return;
112092
+ }
112093
+ emit({ sessionUpdate: "agent_message_chunk", content: textBlock(text) });
112094
+ }
112095
+ };
112096
+ }
112097
+
112098
+ // src/acp/capability-tools.ts
112099
+ function acpAwareReadFileTool(base, root, clientCapabilities, readViaClient) {
112100
+ if (clientCapabilities?.fs?.readTextFile !== true) {
112101
+ return base;
112102
+ }
112103
+ return {
112104
+ definition: base.definition,
112105
+ invoke: async (input, ctx) => {
112106
+ const requested = typeof input.path === "string" ? input.path : "";
112107
+ if (requested.length === 0) {
112108
+ return base.invoke(input, ctx);
112109
+ }
112110
+ const target = confineToRoot(root, requested);
112111
+ if (target === null) {
112112
+ return base.invoke(input, ctx);
112113
+ }
112114
+ const startLine = typeof input.start_line === "number" ? input.start_line : undefined;
112115
+ const viaClient = await readViaClient(target, startLine);
112116
+ if (viaClient !== undefined) {
112117
+ return { output: viaClient, isError: false };
112118
+ }
112119
+ return base.invoke(input, ctx);
112120
+ }
112121
+ };
112122
+ }
112123
+
112124
+ // src/acp/client-requests.ts
112125
+ class AcpClientRequests {
112126
+ options;
112127
+ pending = new Map;
112128
+ closedReason;
112129
+ constructor(options) {
112130
+ this.options = options;
112131
+ }
112132
+ get pendingCount() {
112133
+ return this.pending.size;
112134
+ }
112135
+ request(method, params, onId) {
112136
+ if (this.closedReason !== undefined) {
112137
+ return Promise.resolve({ kind: "closed", reason: this.closedReason });
112138
+ }
112139
+ const id = `acp-agent-${this.options.idSeq()}`;
112140
+ onId?.(id);
112141
+ return new Promise((resolve) => {
112142
+ this.pending.set(id, resolve);
112143
+ try {
112144
+ this.options.send(requestMessage(id, method, params));
112145
+ } catch (error) {
112146
+ this.pending.delete(id);
112147
+ resolve({
112148
+ kind: "closed",
112149
+ reason: `the request could not be written: ${error instanceof Error ? error.message : String(error)}`
112150
+ });
112151
+ }
112152
+ });
112153
+ }
112154
+ cancel(id, reason) {
112155
+ const settle = this.pending.get(id);
112156
+ if (settle === undefined) {
112157
+ return false;
112158
+ }
112159
+ this.pending.delete(id);
112160
+ settle({ kind: "closed", reason });
112161
+ return true;
112162
+ }
112163
+ resolve(message) {
112164
+ const key = idKey(message.id);
112165
+ if (key === undefined) {
112166
+ return false;
112167
+ }
112168
+ const settle = this.pending.get(key);
112169
+ if (settle === undefined) {
112170
+ return false;
112171
+ }
112172
+ this.pending.delete(key);
112173
+ if ("error" in message) {
112174
+ settle({ kind: "error", error: message.error });
112175
+ return true;
112176
+ }
112177
+ settle({ kind: "result", result: message.result });
112178
+ return true;
112179
+ }
112180
+ close(reason) {
112181
+ this.closedReason ??= reason;
112182
+ const outstanding = [...this.pending.values()];
112183
+ this.pending.clear();
112184
+ for (const settle of outstanding) {
112185
+ settle({ kind: "closed", reason });
112186
+ }
112187
+ }
112188
+ }
112189
+ function idKey(id) {
112190
+ if (typeof id === "string") {
112191
+ return id;
112192
+ }
112193
+ if (typeof id === "number") {
112194
+ return String(id);
112195
+ }
112196
+ return;
112197
+ }
112198
+
112199
+ // src/acp/framing.ts
112200
+ var ACP_MESSAGE_DELIMITER = `
112201
+ `;
112202
+ var ACP_DEFAULT_MAX_LINE_BYTES = 64 * 1024 * 1024;
112203
+
112204
+ class AcpFramingError extends Error {
112205
+ bufferedBytes;
112206
+ constructor(message, bufferedBytes) {
112207
+ super(message);
112208
+ this.name = "AcpFramingError";
112209
+ this.bufferedBytes = bufferedBytes;
112210
+ }
112211
+ }
112212
+ function encodeAcpMessage(message) {
112213
+ const json = JSON.stringify(message);
112214
+ if (json === undefined) {
112215
+ throw new AcpFramingError("message is not JSON-serialisable", 0);
112216
+ }
112217
+ if (json.includes(`
112218
+ `) || json.includes("\r")) {
112219
+ throw new AcpFramingError("encoded message contains an embedded newline", json.length);
112220
+ }
112221
+ return `${json}${ACP_MESSAGE_DELIMITER}`;
112222
+ }
112223
+
112224
+ class AcpLineFramer {
112225
+ buffer = "";
112226
+ decoder = new TextDecoder("utf-8");
112227
+ maxLineBytes;
112228
+ constructor(options = {}) {
112229
+ this.maxLineBytes = options.maxLineBytes ?? ACP_DEFAULT_MAX_LINE_BYTES;
112230
+ }
112231
+ get buffered() {
112232
+ return this.buffer.length;
112233
+ }
112234
+ push(chunk) {
112235
+ const text = typeof chunk === "string" ? chunk : this.decoder.decode(chunk, { stream: true });
112236
+ if (text === "") {
112237
+ this.assertWithinLimit();
112238
+ return [];
112239
+ }
112240
+ this.buffer += text;
112241
+ const parts = this.buffer.split(ACP_MESSAGE_DELIMITER);
112242
+ this.buffer = parts.pop() ?? "";
112243
+ this.assertWithinLimit();
112244
+ const lines = [];
112245
+ for (const part of parts) {
112246
+ const line = normaliseLine(part);
112247
+ if (line !== "") {
112248
+ lines.push(line);
112249
+ }
112250
+ }
112251
+ return lines;
112252
+ }
112253
+ flush() {
112254
+ this.buffer += this.decoder.decode();
112255
+ const line = normaliseLine(this.buffer);
112256
+ this.buffer = "";
112257
+ return line === "" ? [] : [line];
112258
+ }
112259
+ assertWithinLimit() {
112260
+ if (this.buffer.length > this.maxLineBytes) {
112261
+ const buffered = this.buffer.length;
112262
+ this.buffer = "";
112263
+ throw new AcpFramingError(`no message delimiter within ${this.maxLineBytes} bytes; dropped the buffer`, buffered);
112264
+ }
112265
+ }
112266
+ }
112267
+ function normaliseLine(part) {
112268
+ return part.replace(/\r$/, "").trim();
112269
+ }
112270
+ function decodeAcpLine(line) {
112271
+ try {
112272
+ return { ok: true, value: JSON.parse(line) };
112273
+ } catch (error) {
112274
+ return {
112275
+ ok: false,
112276
+ error: parseError({
112277
+ reason: error instanceof Error ? error.message : String(error)
112278
+ })
112279
+ };
112280
+ }
112281
+ }
112282
+
112283
+ // src/acp/dispatch.ts
112284
+ class AcpDispatcher {
112285
+ requests = new Map;
112286
+ notifications = new Map;
112287
+ options;
112288
+ constructor(handlers = {}, options = {}) {
112289
+ for (const [method, handler] of Object.entries(handlers.requests ?? {})) {
112290
+ this.requests.set(method, handler);
112291
+ }
112292
+ for (const [method, handler] of Object.entries(handlers.notifications ?? {})) {
112293
+ this.notifications.set(method, handler);
112294
+ }
112295
+ this.options = options;
112296
+ }
112297
+ onRequest(method, handler) {
112298
+ this.requests.set(method, handler);
112299
+ return this;
112300
+ }
112301
+ onNotification(method, handler) {
112302
+ this.notifications.set(method, handler);
112303
+ return this;
112304
+ }
112305
+ registeredRequestMethods() {
112306
+ return [...this.requests.keys()].sort();
112307
+ }
112308
+ registeredNotificationMethods() {
112309
+ return [...this.notifications.keys()].sort();
112310
+ }
112311
+ async handleLine(line) {
112312
+ const decoded = decodeAcpLine(line);
112313
+ if (!decoded.ok) {
112314
+ return { kind: "reply", message: errorResponse(null, decoded.error) };
112315
+ }
112316
+ return this.handleMessage(decoded.value);
112317
+ }
112318
+ async handleMessage(value) {
112319
+ const classified = classifyJsonRpcMessage(value);
112320
+ switch (classified.kind) {
112321
+ case "invalid":
112322
+ return { kind: "reply", message: errorResponse(classified.id, classified.error) };
112323
+ case "response":
112324
+ return { kind: "incoming-response", message: classified.message };
112325
+ case "notification":
112326
+ await this.handleNotification(classified.message);
112327
+ return { kind: "none" };
112328
+ case "request":
112329
+ return { kind: "reply", message: await this.handleRequest(classified.message) };
112330
+ }
112331
+ }
112332
+ async handleRequest(message) {
112333
+ const handler = this.requests.get(message.method);
112334
+ if (handler === undefined) {
112335
+ return errorResponse(message.id, this.unknownMethodError(message.method));
112336
+ }
112337
+ try {
112338
+ const result = await handler(message.params, { method: message.method, id: message.id });
112339
+ return successResponse(message.id, result === undefined ? {} : result);
112340
+ } catch (error) {
112341
+ this.options.onHandlerError?.(message.method, error);
112342
+ if (error instanceof AcpError) {
112343
+ return errorResponse(message.id, error.toErrorObject());
112344
+ }
112345
+ return errorResponse(message.id, internalError(error instanceof Error ? error.message : String(error), {
112346
+ method: message.method
112347
+ }));
112348
+ }
112349
+ }
112350
+ async handleNotification(message) {
112351
+ const handler = this.notifications.get(message.method);
112352
+ if (handler === undefined) {
112353
+ this.options.onUnhandledNotification?.(message.method, message.params);
112354
+ return;
112355
+ }
112356
+ try {
112357
+ await handler(message.params, { method: message.method });
112358
+ } catch (error) {
112359
+ this.options.onHandlerError?.(message.method, error);
112360
+ }
112361
+ }
112362
+ unknownMethodError(method) {
112363
+ return refusalError(method) ?? methodNotFound(method, {
112364
+ protocolVersion: ACP_PROTOCOL_VERSION,
112365
+ implemented: ACP_IMPLEMENTED_AGENT_METHODS
112366
+ });
112367
+ }
112368
+ }
112369
+
112370
+ // src/acp/prompt-content.ts
112371
+ function isContentBlock(value) {
112372
+ return typeof value === "object" && value !== null && typeof value.type === "string";
112373
+ }
112374
+ function renderAcpPromptContent(blocks) {
112375
+ const parts = [];
112376
+ for (const block of blocks) {
112377
+ if (!isContentBlock(block)) {
112378
+ continue;
112379
+ }
112380
+ switch (block.type) {
112381
+ case "text":
112382
+ parts.push(block.text);
112383
+ break;
112384
+ case "resource": {
112385
+ const resource = block.resource;
112386
+ if ("text" in resource) {
112387
+ parts.push(resource.text);
112388
+ } else {
112389
+ parts.push(`[embedded resource omitted (binary, ${resource.mimeType ?? "unknown type"}): ${resource.uri}]`);
112390
+ }
112391
+ break;
112392
+ }
112393
+ case "resource_link":
112394
+ parts.push(`[resource link: ${block.uri}]`);
112395
+ break;
112396
+ case "image":
112397
+ parts.push("[image content omitted \u2014 keryx does not accept image prompt input (promptCapabilities.image: false)]");
112398
+ break;
112399
+ case "audio":
112400
+ parts.push("[audio content omitted \u2014 keryx does not accept audio prompt input (promptCapabilities.audio: false)]");
112401
+ break;
112402
+ default:
112403
+ break;
112404
+ }
112405
+ }
112406
+ return parts.join(`
112407
+
112408
+ `);
112409
+ }
112410
+
112411
+ // src/acp/session.ts
112412
+ class AcpSessionTranscriptUnreadableError extends Error {
112413
+ sessionId;
112414
+ cause;
112415
+ constructor(sessionId, cause) {
112416
+ super(`session ${sessionId}: ${cause.message}`);
112417
+ this.sessionId = sessionId;
112418
+ this.cause = cause;
112419
+ this.name = "AcpSessionTranscriptUnreadableError";
112420
+ }
112421
+ }
112422
+
112423
+ class AcpSessionRegistry {
112424
+ options;
112425
+ sessions = new Map;
112426
+ constructor(options) {
112427
+ this.options = options;
112428
+ }
112429
+ create(cwd, clientCapabilities) {
112430
+ const resolvedRoot = resolveProjectRoot2(cwd);
112431
+ const handle = createSession({
112432
+ cwd: resolvedRoot,
112433
+ provider: this.options.providerId,
112434
+ model: this.options.modelId,
112435
+ ...this.options.dataDir !== undefined ? { dataDir: this.options.dataDir } : {}
112436
+ });
112437
+ const state = {
112438
+ sessionId: handle.summary.id,
112439
+ requestedCwd: cwd,
112440
+ resolvedRoot,
112441
+ handle,
112442
+ history: [],
112443
+ clientCapabilities
112444
+ };
112445
+ this.sessions.set(state.sessionId, state);
112446
+ return state;
112447
+ }
112448
+ get(sessionId) {
112449
+ return this.sessions.get(sessionId);
112450
+ }
112451
+ load(sessionId, cwd, clientCapabilities) {
112452
+ const resolvedRoot = resolveProjectRoot2(cwd);
112453
+ const known = listSessions(cwd, this.options.dataDir).some((summary) => summary.id === sessionId);
112454
+ if (!known) {
112455
+ return;
112456
+ }
112457
+ let opened;
112458
+ try {
112459
+ opened = openSession({
112460
+ cwd,
112461
+ resumeId: sessionId,
112462
+ ...this.options.dataDir !== undefined ? { dataDir: this.options.dataDir } : {},
112463
+ provider: this.options.providerId,
112464
+ model: this.options.modelId
112465
+ });
112466
+ } catch (cause) {
112467
+ if (cause instanceof TranscriptUnreadableError) {
112468
+ throw new AcpSessionTranscriptUnreadableError(sessionId, cause);
112469
+ }
112470
+ throw cause;
112471
+ }
112472
+ const state = {
112473
+ sessionId: opened.handle.summary.id,
112474
+ requestedCwd: cwd,
112475
+ resolvedRoot,
112476
+ handle: opened.handle,
112477
+ history: opened.history,
112478
+ clientCapabilities
112479
+ };
112480
+ this.sessions.set(state.sessionId, state);
112481
+ return state;
112482
+ }
112483
+ updateHandle(sessionId, handle) {
112484
+ const state = this.sessions.get(sessionId);
112485
+ if (state === undefined) {
112486
+ return;
112487
+ }
112488
+ this.sessions.set(sessionId, { ...state, handle });
112489
+ }
112490
+ }
112491
+
112492
+ // src/acp/server.ts
112493
+ var DEFAULT_AGENT_INFO = { name: "keryx", version: "0" };
112494
+ function finishReasonToStopReason(finishReason) {
112495
+ switch (finishReason) {
112496
+ case "budget":
112497
+ return "max_tokens";
112498
+ case "tool-call-budget":
112499
+ return "max_turn_requests";
112500
+ case "no-progress":
112501
+ case undefined:
112502
+ return "end_turn";
112503
+ }
112504
+ }
112505
+ function refuse3(error) {
112506
+ throw new AcpError(error.code, error.message, error.data);
112507
+ }
112508
+ async function runAcpServer(options) {
112509
+ let initialized = false;
112510
+ let clientCapabilities;
112511
+ const idSeq = options.idSeq ?? (() => randomUUID36());
112512
+ const registry = new AcpSessionRegistry({
112513
+ providerId: options.providerId,
112514
+ modelId: options.modelId,
112515
+ ...options.dataDir !== undefined ? { dataDir: options.dataDir } : {}
112516
+ });
112517
+ const writeMessage = (message) => {
112518
+ options.write(encodeAcpMessage(message));
112519
+ };
112520
+ const sendUpdate = (sessionId, update) => {
112521
+ const notification = { sessionId, update };
112522
+ writeMessage(notificationMessage(ACP_CLIENT_METHODS.sessionUpdate, notification));
112523
+ };
112524
+ const clientRequests = new AcpClientRequests({
112525
+ send: (message) => {
112526
+ options.write(encodeAcpMessage(message));
112527
+ },
112528
+ idSeq
112529
+ });
112530
+ let clientAnswersPermissions = true;
112531
+ const activeTurns = new Map;
112532
+ function refuseIfBusy(method, sessionId) {
112533
+ if (!activeTurns.has(sessionId)) {
112534
+ return;
112535
+ }
112536
+ refuse3(invalidRequest({
112537
+ reason: `${method}: a turn is already running for session ${sessionId}; ` + "wait for its session/prompt response \u2014 send session/cancel first if you do not want to wait for it to finish, " + "then prompt again once the cancelled turn has replied",
112538
+ method,
112539
+ sessionId,
112540
+ condition: "session-busy"
112541
+ }));
112542
+ }
112543
+ const askPermissionFor = (sessionId, turn) => {
112544
+ return async (ask) => {
112545
+ if (!clientAnswersPermissions) {
112546
+ options.logError(`acp: ${ACP_CLIENT_METHODS.sessionRequestPermission} not asked for ${ask.toolCall.name ?? "a tool call"}: ` + "this client already refused the method; the call is denied");
112547
+ return;
112548
+ }
112549
+ const params = {
112550
+ sessionId,
112551
+ toolCall: ask.toolCall,
112552
+ options: [...ask.options]
112553
+ };
112554
+ let requestId;
112555
+ const outcome = await clientRequests.request(ACP_CLIENT_METHODS.sessionRequestPermission, params, (id) => {
112556
+ requestId = id;
112557
+ turn.permissionRequestIds.add(id);
112558
+ });
112559
+ if (requestId !== undefined) {
112560
+ turn.permissionRequestIds.delete(requestId);
112561
+ }
112562
+ if (outcome.kind === "error") {
112563
+ clientAnswersPermissions = false;
112564
+ options.logError(`acp: ${ACP_CLIENT_METHODS.sessionRequestPermission} refused by the client ` + `(${outcome.error.code}: ${outcome.error.message}); the call is denied`);
112565
+ return;
112566
+ }
112567
+ if (outcome.kind === "closed") {
112568
+ options.logError(`acp: ${ACP_CLIENT_METHODS.sessionRequestPermission} went unanswered (${outcome.reason}); the call is denied`);
112569
+ return;
112570
+ }
112571
+ const result = outcome.result;
112572
+ if (typeof result !== "object" || result === null || Array.isArray(result)) {
112573
+ options.logError(`acp: ${ACP_CLIENT_METHODS.sessionRequestPermission} answered with a non-object result; the call is denied`);
112574
+ return;
112575
+ }
112576
+ return result;
112577
+ };
112578
+ };
112579
+ function requireInitialized(method) {
112580
+ if (!initialized) {
112581
+ refuse3(invalidRequest({ reason: `${method}: call initialize before any other request`, method }));
112582
+ }
112583
+ }
112584
+ function handleInitialize(params) {
112585
+ const obj = requireObjectParams(params, ACP_AGENT_METHODS.initialize);
112586
+ const negotiation = negotiateProtocolVersion(obj["protocolVersion"]);
112587
+ if (negotiation.kind === "refuse") {
112588
+ refuse3(negotiation.error);
112589
+ }
112590
+ initialized = true;
112591
+ const rawCaps = obj["clientCapabilities"];
112592
+ clientCapabilities = typeof rawCaps === "object" && rawCaps !== null && !Array.isArray(rawCaps) ? rawCaps : undefined;
112593
+ return {
112594
+ protocolVersion: negotiation.version,
112595
+ agentCapabilities: KERYX_AGENT_CAPABILITIES,
112596
+ authMethods: KERYX_AUTH_METHODS,
112597
+ agentInfo: options.agentInfo ?? DEFAULT_AGENT_INFO
112598
+ };
112599
+ }
112600
+ function handleSessionNew(params) {
112601
+ requireInitialized(ACP_AGENT_METHODS.sessionNew);
112602
+ const obj = requireObjectParams(params, ACP_AGENT_METHODS.sessionNew);
112603
+ const cwd = requireStringField(obj, "cwd", ACP_AGENT_METHODS.sessionNew);
112604
+ const mcpServers = obj["mcpServers"];
112605
+ if (mcpServers !== undefined && !Array.isArray(mcpServers)) {
112606
+ refuse3(invalidParams(`${ACP_AGENT_METHODS.sessionNew}: mcpServers must be an array`, { received: mcpServers }));
112607
+ }
112608
+ if (Array.isArray(mcpServers) && mcpServers.length > 0) {
112609
+ refuse3(invalidParams(`${ACP_AGENT_METHODS.sessionNew}: keryx has no in-memory MCP server registration seam for a single ` + "session; configure servers with `keryx mcp`/`keryx integrate` (project-wide) and omit mcpServers, " + "or send an empty list", { requestedServers: mcpServers.length }));
112610
+ }
112611
+ const state = registry.create(cwd, clientCapabilities);
112612
+ return { sessionId: state.sessionId };
112613
+ }
112614
+ async function handleSessionPrompt(params) {
112615
+ requireInitialized(ACP_AGENT_METHODS.sessionPrompt);
112616
+ const obj = requireObjectParams(params, ACP_AGENT_METHODS.sessionPrompt);
112617
+ const sessionId = requireStringField(obj, "sessionId", ACP_AGENT_METHODS.sessionPrompt);
112618
+ const promptField = obj["prompt"];
112619
+ if (!Array.isArray(promptField)) {
112620
+ refuse3(invalidParams(`${ACP_AGENT_METHODS.sessionPrompt}: prompt must be an array of content blocks`, {
112621
+ received: promptField
112622
+ }));
112623
+ }
112624
+ const state = registry.get(sessionId);
112625
+ if (state === undefined) {
112626
+ refuse3({
112627
+ code: JSON_RPC_ERROR_CODES.resourceNotFound,
112628
+ message: `Unknown ACP session: ${sessionId}`,
112629
+ data: { sessionId }
112630
+ });
112631
+ }
112632
+ refuseIfBusy(ACP_AGENT_METHODS.sessionPrompt, sessionId);
112633
+ const userLine = renderAcpPromptContent(promptField);
112634
+ const readViaClient = async (path, line) => {
112635
+ const params = {
112636
+ sessionId,
112637
+ path,
112638
+ ...line !== undefined ? { line } : {}
112639
+ };
112640
+ const outcome = await clientRequests.request(ACP_CLIENT_METHODS.fsReadTextFile, params);
112641
+ if (outcome.kind !== "result") {
112642
+ return;
112643
+ }
112644
+ const result = outcome.result;
112645
+ if (typeof result !== "object" || result === null) {
112646
+ return;
112647
+ }
112648
+ const content = result.content;
112649
+ return typeof content === "string" ? content : undefined;
112650
+ };
112651
+ const tools = [
112652
+ ...builtinReadOnlyTools(state.resolvedRoot).map((tool) => tool.definition.name === "read_file" ? acpAwareReadFileTool(tool, state.resolvedRoot, state.clientCapabilities, readViaClient) : tool),
112653
+ shellExecTool(state.resolvedRoot),
112654
+ applyPatchTool(state.resolvedRoot)
112655
+ ];
112656
+ const toolNames = tools.map((tool) => tool.definition.name);
112657
+ const deps = {
112658
+ provider: options.provider,
112659
+ providerId: options.providerId,
112660
+ modelId: options.modelId,
112661
+ tools,
112662
+ systemInstruction: buildAgentSystemInstruction(undefined, {
112663
+ providerId: options.providerId,
112664
+ modelId: options.modelId,
112665
+ toolNames
112666
+ }),
112667
+ idSeq
112668
+ };
112669
+ const turn = {
112670
+ controller: new AbortController,
112671
+ cancelled: false,
112672
+ permissionRequestIds: new Set
112673
+ };
112674
+ activeTurns.set(sessionId, turn);
112675
+ try {
112676
+ const io = createAcpAgentIo(sessionId, sendUpdate, askPermissionFor(sessionId, turn));
112677
+ const result = await runAgentTurn(io, deps, state.history, userLine, { signal: turn.controller.signal });
112678
+ const updatedHandle = persistHistory(state.handle, state.history, {
112679
+ provider: options.providerId,
112680
+ model: options.modelId
112681
+ });
112682
+ registry.updateHandle(sessionId, updatedHandle);
112683
+ return { stopReason: turn.cancelled ? "cancelled" : finishReasonToStopReason(result.finishReason) };
112684
+ } finally {
112685
+ if (activeTurns.get(sessionId) === turn) {
112686
+ activeTurns.delete(sessionId);
112687
+ }
112688
+ }
112689
+ }
112690
+ function handleSessionCancel(params) {
112691
+ const obj = requireObjectParams(params, ACP_AGENT_METHODS.sessionCancel);
112692
+ const sessionId = requireStringField(obj, "sessionId", ACP_AGENT_METHODS.sessionCancel);
112693
+ const turn = activeTurns.get(sessionId);
112694
+ if (turn === undefined) {
112695
+ options.logError(`acp: ${ACP_AGENT_METHODS.sessionCancel} for ${sessionId}: no turn is currently running for this session`);
112696
+ return;
112697
+ }
112698
+ turn.cancelled = true;
112699
+ turn.controller.abort();
112700
+ for (const requestId of turn.permissionRequestIds) {
112701
+ clientRequests.cancel(requestId, "the turn was cancelled by session/cancel");
112702
+ }
112703
+ turn.permissionRequestIds.clear();
112704
+ }
112705
+ function handleSessionList(params) {
112706
+ requireInitialized(ACP_AGENT_METHODS.sessionList);
112707
+ const obj = requireObjectParams(params, ACP_AGENT_METHODS.sessionList);
112708
+ const rawCwd = obj["cwd"];
112709
+ if (rawCwd !== undefined && rawCwd !== null && typeof rawCwd !== "string") {
112710
+ refuse3(invalidParams(`${ACP_AGENT_METHODS.sessionList}: cwd must be a string, null or omitted`, { received: rawCwd }));
112711
+ }
112712
+ const cwd = typeof rawCwd === "string" && rawCwd.length > 0 ? rawCwd : process.cwd();
112713
+ const summaries = listSessions(cwd, options.dataDir);
112714
+ const sessions = summaries.map((summary) => ({
112715
+ sessionId: summary.id,
112716
+ cwd: summary.projectPath,
112717
+ title: summary.title,
112718
+ updatedAt: summary.updatedAt
112719
+ }));
112720
+ return { sessions };
112721
+ }
112722
+ function replayHistory(sessionId, history) {
112723
+ for (const message of history) {
112724
+ if (message.role === "system") {
112725
+ continue;
112726
+ }
112727
+ if (message.role === "user") {
112728
+ if (message.content.length > 0) {
112729
+ sendUpdate(sessionId, { sessionUpdate: "user_message_chunk", content: textBlock(message.content) });
112730
+ }
112731
+ continue;
112732
+ }
112733
+ if (message.role === "assistant") {
112734
+ if (message.content.length > 0) {
112735
+ sendUpdate(sessionId, { sessionUpdate: "agent_message_chunk", content: textBlock(message.content) });
112736
+ }
112737
+ for (const call of message.toolCalls ?? []) {
112738
+ sendUpdate(sessionId, {
112739
+ sessionUpdate: "tool_call",
112740
+ toolCallId: call.id,
112741
+ title: call.name,
112742
+ name: call.name,
112743
+ kind: toolKindFor(call.name),
112744
+ status: "pending",
112745
+ rawInput: tryParseJson(call.arguments)
112746
+ });
112747
+ }
112748
+ continue;
112749
+ }
112750
+ const toolCallId = message.toolCallId ?? randomUUID36();
112751
+ sendUpdate(sessionId, {
112752
+ sessionUpdate: "tool_call_update",
112753
+ toolCallId,
112754
+ status: "completed",
112755
+ content: [{ type: "content", content: textBlock(message.content) }],
112756
+ rawOutput: message.content
112757
+ });
112758
+ }
112759
+ }
112760
+ function handleSessionLoad(params) {
112761
+ requireInitialized(ACP_AGENT_METHODS.sessionLoad);
112762
+ const obj = requireObjectParams(params, ACP_AGENT_METHODS.sessionLoad);
112763
+ const sessionId = requireStringField(obj, "sessionId", ACP_AGENT_METHODS.sessionLoad);
112764
+ const cwd = requireStringField(obj, "cwd", ACP_AGENT_METHODS.sessionLoad);
112765
+ const mcpServers = obj["mcpServers"];
112766
+ if (mcpServers !== undefined && !Array.isArray(mcpServers)) {
112767
+ refuse3(invalidParams(`${ACP_AGENT_METHODS.sessionLoad}: mcpServers must be an array`, { received: mcpServers }));
112768
+ }
112769
+ if (Array.isArray(mcpServers) && mcpServers.length > 0) {
112770
+ refuse3(invalidParams(`${ACP_AGENT_METHODS.sessionLoad}: keryx has no in-memory MCP server registration seam for a single ` + "session; configure servers with `keryx mcp`/`keryx integrate` (project-wide) and omit mcpServers, " + "or send an empty list", { requestedServers: mcpServers.length }));
112771
+ }
112772
+ refuseIfBusy(ACP_AGENT_METHODS.sessionLoad, sessionId);
112773
+ let state;
112774
+ try {
112775
+ state = registry.load(sessionId, cwd, clientCapabilities);
112776
+ } catch (cause) {
112777
+ if (!(cause instanceof AcpSessionTranscriptUnreadableError)) {
112778
+ throw cause;
112779
+ }
112780
+ refuse3({
112781
+ code: JSON_RPC_ERROR_CODES.internalError,
112782
+ message: `Cannot load session ${sessionId}: ${cause.cause.message}`,
112783
+ data: { sessionId, cwd, file: cause.cause.file, reason: cause.cause.reason }
112784
+ });
112785
+ }
112786
+ if (state === undefined) {
112787
+ refuse3({
112788
+ code: JSON_RPC_ERROR_CODES.resourceNotFound,
112789
+ message: `Unknown ACP session: ${sessionId}`,
112790
+ data: { sessionId, cwd }
112791
+ });
112792
+ }
112793
+ replayHistory(sessionId, state.history);
112794
+ return {};
112795
+ }
112796
+ const dispatcher = new AcpDispatcher({
112797
+ requests: {
112798
+ [ACP_AGENT_METHODS.initialize]: (params) => handleInitialize(params),
112799
+ [ACP_AGENT_METHODS.sessionNew]: (params) => handleSessionNew(params),
112800
+ [ACP_AGENT_METHODS.sessionPrompt]: (params) => handleSessionPrompt(params),
112801
+ [ACP_AGENT_METHODS.sessionList]: (params) => handleSessionList(params),
112802
+ [ACP_AGENT_METHODS.sessionLoad]: (params) => handleSessionLoad(params)
112803
+ },
112804
+ notifications: {
112805
+ [ACP_AGENT_METHODS.sessionCancel]: (params) => handleSessionCancel(params)
112806
+ }
112807
+ }, {
112808
+ onUnhandledNotification: (method) => options.logError(`acp: unhandled notification ${method}`),
112809
+ onHandlerError: (method, error) => options.logError(`acp: ${method} failed: ${error instanceof Error ? error.message : String(error)}`)
112810
+ });
112811
+ const handleOutcome = (outcome) => {
112812
+ if (outcome.kind === "reply") {
112813
+ writeMessage(outcome.message);
112814
+ return;
112815
+ }
112816
+ if (outcome.kind === "incoming-response") {
112817
+ if (!clientRequests.resolve(outcome.message)) {
112818
+ options.logError(`acp: unexpected response with no pending request: ${JSON.stringify(outcome.message)}`);
112819
+ }
112820
+ }
112821
+ };
112822
+ const inflight = new Set;
112823
+ const dispatch = (line) => {
112824
+ const task = dispatcher.handleLine(line).then(handleOutcome, (error) => {
112825
+ options.logError(`acp: dispatch failed: ${error instanceof Error ? error.message : String(error)}`);
112826
+ });
112827
+ inflight.add(task);
112828
+ task.finally(() => {
112829
+ inflight.delete(task);
112830
+ });
112831
+ };
112832
+ const framer = new AcpLineFramer;
112833
+ for await (const chunk of options.input) {
112834
+ let lines;
112835
+ try {
112836
+ lines = framer.push(chunk);
112837
+ } catch (error) {
112838
+ options.logError(`acp: framing error: ${error instanceof Error ? error.message : String(error)}`);
112839
+ continue;
112840
+ }
112841
+ for (const line of lines) {
112842
+ dispatch(line);
112843
+ }
112844
+ }
112845
+ for (const line of framer.flush()) {
112846
+ dispatch(line);
112847
+ }
112848
+ clientRequests.close("the client closed the connection before answering");
112849
+ while (inflight.size > 0) {
112850
+ await Promise.allSettled([...inflight]);
112851
+ }
112852
+ }
112853
+
112854
+ // src/acp/fixture-provider.ts
112855
+ import { readFileSync as readFileSync14 } from "fs";
112856
+ function loadAcpFixtureProvider(path) {
112857
+ const raw = readFileSync14(path, "utf-8");
112858
+ const parsed = JSON.parse(raw);
112859
+ const turns = parsed.turns;
112860
+ let call = 0;
112861
+ const description = {
112862
+ capabilities: {
112863
+ streaming: true,
112864
+ toolCalls: true,
112865
+ parallelToolCalls: false,
112866
+ structuredOutput: false,
112867
+ reasoningMetadata: true,
112868
+ promptCaching: false,
112869
+ vision: false,
112870
+ tokenCounting: false,
112871
+ modelListing: false
112872
+ },
112873
+ descriptor: { providerId: "acp-fixture" }
112874
+ };
112875
+ return {
112876
+ describe: () => description,
112877
+ stream: (_request, opts) => {
112878
+ const events = turns[call] ?? [];
112879
+ call += 1;
112880
+ return async function* () {
112881
+ let sequence = 0;
112882
+ for (const partial of events) {
112883
+ yield { sequence: sequence++, attemptId: opts.attemptId, ...partial };
112884
+ }
112885
+ }();
112886
+ }
112887
+ };
112888
+ }
112889
+
112890
+ // src/commands/acp.ts
112891
+ init_make_provider();
112892
+ function parseArgs3(args) {
112893
+ let provider = "fake";
112894
+ let model = "fake-model";
112895
+ let baseUrl;
112896
+ let fixture;
112897
+ let dataDir;
112898
+ for (let i = 0;i < args.length; i += 1) {
112899
+ const arg = args[i];
112900
+ if (arg === "--provider") {
112901
+ provider = args[i += 1] ?? provider;
112902
+ } else if (arg === "--model") {
112903
+ model = args[i += 1] ?? model;
112904
+ } else if (arg === "--base-url") {
112905
+ baseUrl = args[i += 1];
112906
+ } else if (arg === "--fixture") {
112907
+ fixture = args[i += 1];
112908
+ } else if (arg === "--data-dir") {
112909
+ dataDir = args[i += 1];
112910
+ }
112911
+ }
112912
+ return {
112913
+ provider,
112914
+ model,
112915
+ ...baseUrl !== undefined ? { baseUrl } : {},
112916
+ ...fixture !== undefined ? { fixture } : {},
112917
+ ...dataDir !== undefined ? { dataDir } : {}
112918
+ };
112919
+ }
112920
+ async function acpCommand(args) {
112921
+ if (args[0] === "help" || args.includes("--help") || args.includes("-h")) {
112922
+ printHelp18();
112923
+ return;
112924
+ }
112925
+ const parsed = parseArgs3(args);
112926
+ const provider = parsed.fixture !== undefined ? loadAcpFixtureProvider(parsed.fixture) : makeProvider(parsed.provider, parsed.model, {
112927
+ fetch,
112928
+ env: process.env,
112929
+ ...parsed.baseUrl !== undefined ? { baseUrl: parsed.baseUrl } : {}
112930
+ });
112931
+ await runAcpServer({
112932
+ input: process.stdin,
112933
+ write: (chunk) => {
112934
+ process.stdout.write(chunk);
112935
+ },
112936
+ logError: (line) => {
112937
+ process.stderr.write(`${line}
112938
+ `);
112939
+ },
112940
+ provider,
112941
+ providerId: parsed.provider,
112942
+ modelId: parsed.model,
112943
+ agentInfo: { name: "keryx", version: package_default.version },
112944
+ ...parsed.dataDir !== undefined ? { dataDir: parsed.dataDir } : {},
112945
+ idSeq: () => randomUUID37()
112946
+ });
112947
+ }
112948
+ function printHelp18() {
112949
+ console.log(`keryx acp
112950
+
112951
+ Speak the Agent Client Protocol (ACP) v1 over stdio: newline-delimited
112952
+ JSON-RPC 2.0 on stdin, the same framing on stdout. Launch this as a
112953
+ subprocess from an ACP client (an editor, e.g.) \u2014 it answers initialize,
112954
+ session/new, session/prompt (streaming session/update notifications as a
112955
+ turn runs rather than only at the end), session/cancel, session/list and
112956
+ session/load; every other agent method ACP defines is refused with -32601.
112957
+ A gated tool call is asked through session/request_permission \u2014 only an
112958
+ explicit allow runs it. Writes and shell execution always stay local; there
112959
+ is no fs/write_text_file or terminal/* call in this release. Nothing but
112960
+ protocol frames is ever written to stdout; every diagnostic goes to stderr.
112961
+
112962
+ Usage:
112963
+ keryx acp [--provider <p>] [--model <m>] [--base-url <url>] [--data-dir <dir>]
112964
+ `);
112965
+ }
112966
+
112967
+ // src/commands/bus.ts
112968
+ import { randomUUID as randomUUID39 } from "crypto";
112969
+
111604
112970
  // src/bus/prune.ts
111605
112971
  init_fs();
111606
- import { randomUUID as randomUUID35 } from "crypto";
112972
+ import { randomUUID as randomUUID38 } from "crypto";
111607
112973
  var GONE_PRESENCE_RETENTION_MS = 24 * 60 * 60 * 1000;
111608
112974
 
111609
112975
  class LeaseStillActive extends Error {
@@ -111644,7 +113010,7 @@ async function pruneBus(root, options = {}) {
111644
113010
  const file = leasePath(root, lease.leaseId);
111645
113011
  try {
111646
113012
  await appendEvent(root, {
111647
- from: { instanceId: randomUUID35(), name: "system", origin: "system" },
113013
+ from: { instanceId: randomUUID38(), name: "system", origin: "system" },
111648
113014
  to: ["*"],
111649
113015
  toLabel: "@all",
111650
113016
  kind: "lease-expired",
@@ -111838,7 +113204,7 @@ ${PAUSE_USAGE3}`);
111838
113204
  if (toLabel === undefined || reason === undefined || reason.length === 0) {
111839
113205
  throw new BusRefusal("invalid-event", PAUSE_USAGE3);
111840
113206
  }
111841
- const holder = { instanceId: randomUUID36(), name: "cli", origin: "cli" };
113207
+ const holder = { instanceId: randomUUID39(), name: "cli", origin: "cli" };
111842
113208
  const lease = await createPauseLease(busRoot, {
111843
113209
  holder,
111844
113210
  toLabel,
@@ -111860,7 +113226,7 @@ async function resume(args, busRoot, ctx) {
111860
113226
  if (leaseId === undefined) {
111861
113227
  throw new BusRefusal("invalid-event", "usage: keryx bus resume <leaseId> [--json]");
111862
113228
  }
111863
- await resumePauseLease(busRoot, { leaseId, by: { instanceId: randomUUID36(), name: "cli", origin: "cli" }, now: ctx.now });
113229
+ await resumePauseLease(busRoot, { leaseId, by: { instanceId: randomUUID39(), name: "cli", origin: "cli" }, now: ctx.now });
111864
113230
  if (args.includes("--json")) {
111865
113231
  ctx.out(JSON.stringify({ schemaVersion: 1, leaseId }, null, 2));
111866
113232
  } else {
@@ -112026,7 +113392,7 @@ function buildInitFlags(next, profile) {
112026
113392
  async function modulesCommand(args = []) {
112027
113393
  const sub = args[0];
112028
113394
  if (sub === "--help" || sub === "-h" || sub === "help") {
112029
- printHelp18();
113395
+ printHelp19();
112030
113396
  return;
112031
113397
  }
112032
113398
  const wantsJson = args.includes("--json") && (sub === undefined || sub === "status" || sub === "list" || sub === "--json");
@@ -112083,7 +113449,7 @@ async function modulesCommand(args = []) {
112083
113449
  }
112084
113450
  }
112085
113451
  } else {
112086
- printHelp18();
113452
+ printHelp19();
112087
113453
  process.exitCode = 1;
112088
113454
  return;
112089
113455
  }
@@ -112126,7 +113492,7 @@ function setsEqual(a, b) {
112126
113492
  }
112127
113493
  return true;
112128
113494
  }
112129
- function printHelp18() {
113495
+ function printHelp19() {
112130
113496
  helpTitle("keryx modules", "view and toggle Metaproject modules");
112131
113497
  helpUsage([
112132
113498
  "keryx modules",
@@ -112145,12 +113511,12 @@ function printHelp18() {
112145
113511
  }
112146
113512
 
112147
113513
  // src/commands/serve.ts
112148
- import { randomUUID as randomUUID39 } from "crypto";
113514
+ import { randomUUID as randomUUID42 } from "crypto";
112149
113515
  init_serve_config();
112150
113516
 
112151
113517
  // src/lib/serve-credential.ts
112152
113518
  init_config_dir();
112153
- import { createHash as createHash44, randomBytes as randomBytes5, randomUUID as randomUUID37 } from "crypto";
113519
+ import { createHash as createHash44, randomBytes as randomBytes5, randomUUID as randomUUID40 } from "crypto";
112154
113520
  import {
112155
113521
  chmodSync as chmodSync5,
112156
113522
  closeSync as closeSync4,
@@ -112234,7 +113600,7 @@ function readServeCredential(dir) {
112234
113600
  }
112235
113601
  function writeStore(store, dir) {
112236
113602
  const file = serveCredentialPath(dir);
112237
- const temp = `${file}.${randomUUID37()}.tmp`;
113603
+ const temp = `${file}.${randomUUID40()}.tmp`;
112238
113604
  try {
112239
113605
  ensureKeryxConfigDir(dir);
112240
113606
  const handle = openSync5(temp, "wx", 384);
@@ -112274,7 +113640,7 @@ function mintRecord(now) {
112274
113640
  const salt = randomBytes5(32).toString("hex");
112275
113641
  return {
112276
113642
  token,
112277
- record: { id: randomUUID37(), algorithm: "sha256", salt, hash: hashToken(salt, token), createdAt: now }
113643
+ record: { id: randomUUID40(), algorithm: "sha256", salt, hash: hashToken(salt, token), createdAt: now }
112278
113644
  };
112279
113645
  }
112280
113646
  function issueServeToken(dir, now = () => new Date().toISOString(), onWaiting) {
@@ -112591,7 +113957,7 @@ function releaseIdempotencyKey(project, idempotencyKey, turnId, dir) {
112591
113957
  }
112592
113958
 
112593
113959
  // src/lib/serve-turn.ts
112594
- import { randomUUID as randomUUID38 } from "crypto";
113960
+ import { randomUUID as randomUUID41 } from "crypto";
112595
113961
  import path203 from "path";
112596
113962
  init_service();
112597
113963
  var REMOTE_ORIGIN = "remote:http";
@@ -112710,7 +114076,7 @@ async function redactOut(security, text) {
112710
114076
  async function runRemoteTurn(input) {
112711
114077
  const scanRoot = input.scanRoot;
112712
114078
  const security = createSecurityService(scanRoot);
112713
- const newId = input.newId ?? (() => randomUUID38());
114079
+ const newId = input.newId ?? (() => randomUUID41());
112714
114080
  const clock = input.clock ?? (() => new Date().toISOString());
112715
114081
  const turnId = input.turnId ?? newId();
112716
114082
  const sessionId = input.request.sessionId ?? newId();
@@ -112853,7 +114219,7 @@ function outcomeOf(status, gate, unresolvedBlockerIds) {
112853
114219
  }
112854
114220
  function createSubmitTurn(deps) {
112855
114221
  return async (request, project) => {
112856
- const turnId = (deps.newId ?? (() => randomUUID38()))();
114222
+ const turnId = (deps.newId ?? (() => randomUUID41()))();
112857
114223
  const scanned = await scanPrompt(deps.dir, request.prompt);
112858
114224
  if (scanned.rejected) {
112859
114225
  return { kind: "rejected" };
@@ -112908,40 +114274,40 @@ function claimTurnKey(request, turnId, project, dir) {
112908
114274
  // src/lib/serve-server.ts
112909
114275
  init_serve_config();
112910
114276
  var MAX_TURN_BODY_BYTES = 128 * 1024;
112911
- function refuse3(reason, message) {
114277
+ function refuse4(reason, message) {
112912
114278
  return { ok: false, state: "refused", reason, message };
112913
114279
  }
112914
114280
  function resolveServeStartup(input) {
112915
114281
  const { config, credential } = input;
112916
114282
  if (config === null) {
112917
- return refuse3("no-configuration", serveConfigAdvice(input.configState ?? "absent"));
114283
+ return refuse4("no-configuration", serveConfigAdvice(input.configState ?? "absent"));
112918
114284
  }
112919
114285
  if (!config.enabled) {
112920
- return refuse3("disabled", serveConfigAdvice("valid"));
114286
+ return refuse4("disabled", serveConfigAdvice("valid"));
112921
114287
  }
112922
114288
  if (config.credentialRef.store !== "auth-json") {
112923
- return refuse3("unsupported-credential-store", `credentialRef.store "${config.credentialRef.store}" is not implemented in this release; only "auth-json" is supported.`);
114289
+ return refuse4("unsupported-credential-store", `credentialRef.store "${config.credentialRef.store}" is not implemented in this release; only "auth-json" is supported.`);
112924
114290
  }
112925
114291
  if (credential.status === "unreadable") {
112926
- return refuse3("unreadable-credential", `${credential.message}. Inspect it, then run \`keryx serve token rotate\`.`);
114292
+ return refuse4("unreadable-credential", `${credential.message}. Inspect it, then run \`keryx serve token rotate\`.`);
112927
114293
  }
112928
114294
  if (credential.status === "absent") {
112929
- return refuse3("no-credential", "no serve credential exists. Run `keryx serve token issue` \u2014 the token is printed once and never again.");
114295
+ return refuse4("no-credential", "no serve credential exists. Run `keryx serve token issue` \u2014 the token is printed once and never again.");
112930
114296
  }
112931
114297
  if (credential.record.id !== config.credentialRef.id) {
112932
- return refuse3("no-credential", "the configured credential reference does not match the credential in the store. Run `keryx serve token rotate` to re-issue and re-point it.");
114298
+ return refuse4("no-credential", "the configured credential reference does not match the credential in the store. Run `keryx serve token rotate` to re-issue and re-point it.");
112933
114299
  }
112934
114300
  const nonLoopback = !isLoopbackAddress(config.bind.address);
112935
114301
  if (nonLoopback && config.bind.acknowledgeNonLoopback !== true) {
112936
- return refuse3("non-loopback-not-acknowledged", `the configured bind address is reachable beyond loopback. Run \`keryx serve config set --bind ${config.bind.address} --acknowledge-non-loopback\` to acknowledge it explicitly; there is no TLS in this release.`);
114302
+ return refuse4("non-loopback-not-acknowledged", `the configured bind address is reachable beyond loopback. Run \`keryx serve config set --bind ${config.bind.address} --acknowledge-non-loopback\` to acknowledge it explicitly; there is no TLS in this release.`);
112937
114303
  }
112938
114304
  const remoteProfile = resolveRemoteProfile(config.profile);
112939
114305
  if (remoteProfile === null) {
112940
- return refuse3("unknown-profile", `profile "${config.profile}" is not implemented in this release; valid profiles are ${REMOTE_PROFILE_NAMES.join(", ")}. Run \`keryx serve config set --profile remote-restricted\``);
114306
+ return refuse4("unknown-profile", `profile "${config.profile}" is not implemented in this release; valid profiles are ${REMOTE_PROFILE_NAMES.join(", ")}. Run \`keryx serve config set --profile remote-restricted\``);
112941
114307
  }
112942
114308
  const comparison = compareProfiles((input.localBaseline ?? localBaselineProfile)(), remoteProfile);
112943
114309
  if (!comparison.ok) {
112944
- return refuse3("widening-profile", `profile "${config.profile}" would grant more than the local profile allows (${comparison.widened.join(", ")}). Run \`keryx serve config set --profile remote-restricted\``);
114310
+ return refuse4("widening-profile", `profile "${config.profile}" would grant more than the local profile allows (${comparison.widened.join(", ")}). Run \`keryx serve config set --profile remote-restricted\``);
112945
114311
  }
112946
114312
  return { ok: true, config, credential: credential.record, nonLoopback, profile: remoteProfile };
112947
114313
  }
@@ -112978,7 +114344,7 @@ function matchRoute(pathname) {
112978
114344
  return { route: "none" };
112979
114345
  }
112980
114346
  var JSON_HEADERS = { "content-type": "application/json; charset=utf-8" };
112981
- function errorResponse(status, code, message, headers = {}) {
114347
+ function errorResponse2(status, code, message, headers = {}) {
112982
114348
  return new Response(`${JSON.stringify({ error: { code, message } })}
112983
114349
  `, {
112984
114350
  status,
@@ -112986,7 +114352,7 @@ function errorResponse(status, code, message, headers = {}) {
112986
114352
  });
112987
114353
  }
112988
114354
  function unauthorized() {
112989
- return errorResponse(401, "unauthorized", "Unauthorized.", { "www-authenticate": "Bearer" });
114355
+ return errorResponse2(401, "unauthorized", "Unauthorized.", { "www-authenticate": "Bearer" });
112990
114356
  }
112991
114357
  function bearerToken(request) {
112992
114358
  const header = request.headers.get("authorization");
@@ -113013,33 +114379,33 @@ function summarizeRegistryWarnings(messages) {
113013
114379
  async function submitTurn(request, ctx) {
113014
114380
  const declared = Number(request.headers.get("content-length") ?? "0");
113015
114381
  if (Number.isFinite(declared) && declared > MAX_TURN_BODY_BYTES) {
113016
- return errorResponse(413, "too-large", "The request body exceeds the configured bound.");
114382
+ return errorResponse2(413, "too-large", "The request body exceeds the configured bound.");
113017
114383
  }
113018
114384
  const contentType = (request.headers.get("content-type") ?? "").split(";")[0]?.trim().toLowerCase();
113019
114385
  if (contentType !== "application/json") {
113020
- return errorResponse(400, "invalid-request", "Content-Type must be application/json.");
114386
+ return errorResponse2(400, "invalid-request", "Content-Type must be application/json.");
113021
114387
  }
113022
114388
  const raw = await request.text();
113023
114389
  if (Buffer.byteLength(raw, "utf8") > MAX_TURN_BODY_BYTES) {
113024
- return errorResponse(413, "too-large", "The request body exceeds the configured bound.");
114390
+ return errorResponse2(413, "too-large", "The request body exceeds the configured bound.");
113025
114391
  }
113026
114392
  let parsed;
113027
114393
  try {
113028
114394
  parsed = JSON.parse(raw);
113029
114395
  } catch {
113030
- return errorResponse(400, "invalid-request", "The request body is not valid JSON.");
114396
+ return errorResponse2(400, "invalid-request", "The request body is not valid JSON.");
113031
114397
  }
113032
114398
  const validated = validateTurnRequest(parsed);
113033
114399
  if (!validated.ok) {
113034
- return errorResponse(validated.problem.status, validated.problem.code, validated.problem.message);
114400
+ return errorResponse2(validated.problem.status, validated.problem.code, validated.problem.message);
113035
114401
  }
113036
114402
  const project = resolveProject(validated.request.project, ctx.dir);
113037
114403
  if (!project.ok) {
113038
- return errorResponse(404, project.code, project.message);
114404
+ return errorResponse2(404, project.code, project.message);
113039
114405
  }
113040
114406
  const submit = ctx.submitTurn;
113041
114407
  if (submit === undefined) {
113042
- return errorResponse(503, "unavailable", "Turn execution is not available on this listener.");
114408
+ return errorResponse2(503, "unavailable", "Turn execution is not available on this listener.");
113043
114409
  }
113044
114410
  const outcome = await submit(validated.request, project.project);
113045
114411
  if (outcome.kind === "duplicate") {
@@ -113047,10 +114413,10 @@ async function submitTurn(request, ctx) {
113047
114413
  `, { status: 200, headers: JSON_HEADERS });
113048
114414
  }
113049
114415
  if (outcome.kind === "rejected") {
113050
- return errorResponse(422, "prompt-rejected", "The prompt was rejected.");
114416
+ return errorResponse2(422, "prompt-rejected", "The prompt was rejected.");
113051
114417
  }
113052
114418
  if (outcome.kind === "unavailable") {
113053
- return errorResponse(500, "record-unreadable", "The durable record for this turn could not be read.");
114419
+ return errorResponse2(500, "record-unreadable", "The durable record for this turn could not be read.");
113054
114420
  }
113055
114421
  return new Response(`${JSON.stringify({ schemaVersion: "1.0.0", turnId: outcome.turnId, sessionId: outcome.sessionId }, null, 2)}
113056
114422
  `, { status: 202, headers: JSON_HEADERS });
@@ -113061,7 +114427,7 @@ function streamTurnEvents(request, turnId, ctx) {
113061
114427
  const after = Number.isInteger(parsed) && parsed >= 0 ? parsed : -1;
113062
114428
  const events = readTurnEvents(turnId, after, ctx.dir);
113063
114429
  if (!events.ok) {
113064
- return errorResponse(500, "record-unreadable", "The durable record for this turn could not be read.");
114430
+ return errorResponse2(500, "record-unreadable", "The durable record for this turn could not be read.");
113065
114431
  }
113066
114432
  const body = events.value.map((event) => `id: ${event.seq}
113067
114433
  data: ${JSON.stringify(event)}
@@ -113084,7 +114450,7 @@ async function handleServeRequest(request, ctx) {
113084
114450
  }
113085
114451
  function internalErrorResponse(cause) {
113086
114452
  console.error(`keryx serve: request failed: ${cause instanceof Error ? cause.message : String(cause)}`);
113087
- return errorResponse(500, "internal-error", "The request could not be completed.");
114453
+ return errorResponse2(500, "internal-error", "The request could not be completed.");
113088
114454
  }
113089
114455
  async function routeServeRequest(request, ctx) {
113090
114456
  const credential = ctx.resolveCredential();
@@ -113094,7 +114460,7 @@ async function routeServeRequest(request, ctx) {
113094
114460
  const standing = ctx.throttle.check(peer);
113095
114461
  const verdict = standing.throttled ? standing : ctx.throttle.recordFailure(peer);
113096
114462
  if (verdict.throttled) {
113097
- return errorResponse(429, "too-many-requests", "Too many requests.", {
114463
+ return errorResponse2(429, "too-many-requests", "Too many requests.", {
113098
114464
  "retry-after": String(verdict.retryAfterSeconds ?? 60)
113099
114465
  });
113100
114466
  }
@@ -113102,35 +114468,35 @@ async function routeServeRequest(request, ctx) {
113102
114468
  return unauthorized();
113103
114469
  }
113104
114470
  if (ctx.state() === "draining") {
113105
- return errorResponse(503, "draining", "The server is draining.");
114471
+ return errorResponse2(503, "draining", "The server is draining.");
113106
114472
  }
113107
114473
  const pathname = new URL(request.url).pathname;
113108
114474
  const matched = matchRoute(pathname);
113109
114475
  if (matched.route === "none") {
113110
- return errorResponse(404, "not-found", "Not found.");
114476
+ return errorResponse2(404, "not-found", "Not found.");
113111
114477
  }
113112
114478
  if (matched.route === "fixed" && pathname === "/v1/turns") {
113113
114479
  if (request.method !== "POST") {
113114
- return errorResponse(405, "method-not-allowed", "Method not allowed.", { allow: "POST" });
114480
+ return errorResponse2(405, "method-not-allowed", "Method not allowed.", { allow: "POST" });
113115
114481
  }
113116
114482
  return submitTurn(request, ctx);
113117
114483
  }
113118
114484
  if (request.method !== "GET") {
113119
- return errorResponse(405, "method-not-allowed", "Method not allowed.", { allow: "GET" });
114485
+ return errorResponse2(405, "method-not-allowed", "Method not allowed.", { allow: "GET" });
113120
114486
  }
113121
114487
  if (matched.route === "turn" || matched.route === "turn-events") {
113122
114488
  const record = readTurnRecord(matched.turnId, ctx.dir);
113123
114489
  if (!record.ok) {
113124
114490
  if (isServerFault(record.reason)) {
113125
- return errorResponse(500, "record-unreadable", "The durable record for this turn could not be read.");
114491
+ return errorResponse2(500, "record-unreadable", "The durable record for this turn could not be read.");
113126
114492
  }
113127
- return errorResponse(404, "not-found", "Not found.");
114493
+ return errorResponse2(404, "not-found", "Not found.");
113128
114494
  }
113129
114495
  if (matched.route === "turn-events") {
113130
114496
  return streamTurnEvents(request, matched.turnId, ctx);
113131
114497
  }
113132
114498
  if (record.value.result === undefined) {
113133
- return errorResponse(409, "turn-in-progress", "The turn has not reached a terminal state.");
114499
+ return errorResponse2(409, "turn-in-progress", "The turn has not reached a terminal state.");
113134
114500
  }
113135
114501
  return new Response(`${JSON.stringify(record.value.result, null, 2)}
113136
114502
  `, { headers: JSON_HEADERS });
@@ -113182,11 +114548,11 @@ async function startServeListener(input) {
113182
114548
  error: internalErrorResponse
113183
114549
  });
113184
114550
  } catch (error) {
113185
- return refuse3("bind-failed", `could not bind ${hostname}:${startup.config.bind.port}: ${error instanceof Error ? error.message : String(error)}`);
114551
+ return refuse4("bind-failed", `could not bind ${hostname}:${startup.config.bind.port}: ${error instanceof Error ? error.message : String(error)}`);
113186
114552
  }
113187
114553
  if (typeof server.port !== "number" || server.port <= 0) {
113188
114554
  await server.stop(true);
113189
- return refuse3("bind-failed", "the listener reported no bound port");
114555
+ return refuse4("bind-failed", "the listener reported no bound port");
113190
114556
  }
113191
114557
  boundPort = server.port;
113192
114558
  state = "listening";
@@ -113231,7 +114597,7 @@ function assembleSubmitTurn(profile, dir, seams = {}) {
113231
114597
  }
113232
114598
 
113233
114599
  // src/commands/serve.ts
113234
- function parseArgs3(args, valueFlags, booleanFlags) {
114600
+ function parseArgs4(args, valueFlags, booleanFlags) {
113235
114601
  const values = new Map;
113236
114602
  const flags = new Set;
113237
114603
  for (let index = 0;index < args.length; index += 1) {
@@ -113277,7 +114643,7 @@ var ENABLE_FLAG = "--enable";
113277
114643
  var DISABLE_FLAG = "--disable";
113278
114644
  async function serveCommand(args = []) {
113279
114645
  if (args.includes("--help") || args.includes("-h") || args[0] === "help") {
113280
- printHelp19();
114646
+ printHelp20();
113281
114647
  return;
113282
114648
  }
113283
114649
  const sub = args[0];
@@ -113298,14 +114664,14 @@ async function serveCommand(args = []) {
113298
114664
  return;
113299
114665
  }
113300
114666
  console.error(`Unknown serve command: ${sanitizeForDisplay(sub)}`);
113301
- printHelp19();
114667
+ printHelp20();
113302
114668
  process.exitCode = 1;
113303
114669
  }
113304
114670
  async function runServe(args) {
113305
- const parsed = parseArgs3(args, BIND_FLAGS, [ACK_FLAG]);
114671
+ const parsed = parseArgs4(args, BIND_FLAGS, [ACK_FLAG]);
113306
114672
  if (!parsed.ok) {
113307
114673
  console.error(parsed.message);
113308
- printHelp19();
114674
+ printHelp20();
113309
114675
  process.exitCode = 1;
113310
114676
  return;
113311
114677
  }
@@ -113404,10 +114770,10 @@ function readPort(raw, options) {
113404
114770
  return port;
113405
114771
  }
113406
114772
  function runStatus7(args) {
113407
- const parsed = parseArgs3(args, [], ["--json"]);
114773
+ const parsed = parseArgs4(args, [], ["--json"]);
113408
114774
  if (!parsed.ok) {
113409
114775
  console.error(parsed.message);
113410
- printHelp19();
114776
+ printHelp20();
113411
114777
  process.exitCode = 1;
113412
114778
  return;
113413
114779
  }
@@ -113451,10 +114817,10 @@ function runStatus7(args) {
113451
114817
  }
113452
114818
  function runToken(args) {
113453
114819
  const sub = args[0];
113454
- const rest = parseArgs3(args.slice(1), [], []);
114820
+ const rest = parseArgs4(args.slice(1), [], []);
113455
114821
  if (!rest.ok) {
113456
114822
  console.error(rest.message);
113457
- printHelp19();
114823
+ printHelp20();
113458
114824
  process.exitCode = 1;
113459
114825
  return;
113460
114826
  }
@@ -113495,7 +114861,7 @@ function runToken(args) {
113495
114861
  return;
113496
114862
  }
113497
114863
  console.error(sub === undefined ? "Missing token subcommand" : `Unknown token command: ${sanitizeForDisplay(sub)}`);
113498
- printHelp19();
114864
+ printHelp20();
113499
114865
  process.exitCode = 1;
113500
114866
  }
113501
114867
  function printTokenOnce(token) {
@@ -113517,10 +114883,10 @@ function pointConfigurationAt(credentialId) {
113517
114883
  function runConfig(args) {
113518
114884
  const sub = args[0];
113519
114885
  if (sub === "init") {
113520
- const parsed = parseArgs3(args.slice(1), BIND_FLAGS, [ACK_FLAG, FORCE_FLAG]);
114886
+ const parsed = parseArgs4(args.slice(1), BIND_FLAGS, [ACK_FLAG, FORCE_FLAG]);
113521
114887
  if (!parsed.ok) {
113522
114888
  console.error(parsed.message);
113523
- printHelp19();
114889
+ printHelp20();
113524
114890
  process.exitCode = 1;
113525
114891
  return;
113526
114892
  }
@@ -113541,7 +114907,7 @@ function runConfig(args) {
113541
114907
  return;
113542
114908
  }
113543
114909
  const credential = readServeCredential();
113544
- const credentialId = credential.status === "ok" ? credential.record.id : randomUUID39();
114910
+ const credentialId = credential.status === "ok" ? credential.record.id : randomUUID42();
113545
114911
  const config = defaultServeConfig(credentialId, {
113546
114912
  address: parsed.parsed.values.get("--bind") ?? DEFAULT_SERVE_BIND_ADDRESS,
113547
114913
  port: port ?? DEFAULT_SERVE_PORT,
@@ -113567,10 +114933,10 @@ function runConfig(args) {
113567
114933
  return;
113568
114934
  }
113569
114935
  if (sub === "show") {
113570
- const parsed = parseArgs3(args.slice(1), [], ["--json"]);
114936
+ const parsed = parseArgs4(args.slice(1), [], ["--json"]);
113571
114937
  if (!parsed.ok) {
113572
114938
  console.error(parsed.message);
113573
- printHelp19();
114939
+ printHelp20();
113574
114940
  process.exitCode = 1;
113575
114941
  return;
113576
114942
  }
@@ -113587,14 +114953,14 @@ function runConfig(args) {
113587
114953
  return;
113588
114954
  }
113589
114955
  console.error(sub === undefined ? "Missing config subcommand" : `Unknown config command: ${sanitizeForDisplay(sub)}`);
113590
- printHelp19();
114956
+ printHelp20();
113591
114957
  process.exitCode = 1;
113592
114958
  }
113593
114959
  function runConfigSet(args) {
113594
- const parsed = parseArgs3(args, BIND_FLAGS, [ACK_FLAG, NO_ACK_FLAG, ENABLE_FLAG, DISABLE_FLAG]);
114960
+ const parsed = parseArgs4(args, BIND_FLAGS, [ACK_FLAG, NO_ACK_FLAG, ENABLE_FLAG, DISABLE_FLAG]);
113595
114961
  if (!parsed.ok) {
113596
114962
  console.error(parsed.message);
113597
- printHelp19();
114963
+ printHelp20();
113598
114964
  process.exitCode = 1;
113599
114965
  return;
113600
114966
  }
@@ -113665,7 +115031,7 @@ function printConfig(config) {
113665
115031
  console.log(` approvals: expire after ${config.approval.expirySeconds}s, max ${config.approval.maxPendingPerSession} pending per session`);
113666
115032
  console.log(` non-loopback acknowledged: ${config.bind.acknowledgeNonLoopback === true}`);
113667
115033
  }
113668
- function printHelp19() {
115034
+ function printHelp20() {
113669
115035
  helpTitle("keryx serve", "loopback-bound HTTP entry over this install (off by default)");
113670
115036
  helpUsage([
113671
115037
  `keryx serve [--bind <addr>] [--port <n>] [--profile <name>] [${ACK_FLAG}]`,
@@ -113712,7 +115078,7 @@ init_templates3();
113712
115078
  async function updateCommand(args = []) {
113713
115079
  const options = parseUpdateArgs(args);
113714
115080
  if (options.help) {
113715
- printHelp20();
115081
+ printHelp21();
113716
115082
  return;
113717
115083
  }
113718
115084
  const projectRoot = process.cwd();
@@ -114922,7 +116288,7 @@ function runtimeSourcePath2(relativePath) {
114922
116288
  function escapeRegExp7(value) {
114923
116289
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
114924
116290
  }
114925
- function printHelp20() {
116291
+ function printHelp21() {
114926
116292
  helpTitle("keryx update", "refresh .metaproject service files (data left untouched)");
114927
116293
  helpUsage(["keryx update [--skip-runtime] [--hooks] [--no-tasks]"]);
114928
116294
  heading("Default behavior");
@@ -114953,7 +116319,7 @@ async function dashboardCommand(args = []) {
114953
116319
  const options = parseOptions(args);
114954
116320
  const subcommand = options.positionals[0];
114955
116321
  if (!subcommand || options.help) {
114956
- printHelp21();
116322
+ printHelp22();
114957
116323
  return;
114958
116324
  }
114959
116325
  if (subcommand === "build") {
@@ -114971,7 +116337,7 @@ async function dashboardCommand(args = []) {
114971
116337
  return;
114972
116338
  }
114973
116339
  console.log(` ${style.red(symbols.cross)} Unknown dashboard command: ${subcommand}`);
114974
- printHelp21();
116340
+ printHelp22();
114975
116341
  process.exitCode = 1;
114976
116342
  }
114977
116343
  function parseOptions(args) {
@@ -114996,7 +116362,7 @@ async function openFile(filePath) {
114996
116362
  });
114997
116363
  });
114998
116364
  }
114999
- function printHelp21() {
116365
+ function printHelp22() {
115000
116366
  helpTitle("keryx dashboard", "build and open the human dashboard");
115001
116367
  helpUsage([
115002
116368
  "keryx dashboard build",
@@ -115010,7 +116376,7 @@ function printHelp21() {
115010
116376
  }
115011
116377
 
115012
116378
  // src/commands/agents.ts
115013
- import { readFileSync as readFileSync14 } from "fs";
116379
+ import { readFileSync as readFileSync15 } from "fs";
115014
116380
 
115015
116381
  // src/agents/bootstrap.ts
115016
116382
  import { mkdir as mkdir66, readFile as readFile102, writeFile as writeFile55 } from "fs/promises";
@@ -115511,7 +116877,7 @@ function monitorCommand(args) {
115511
116877
  }
115512
116878
  let events;
115513
116879
  try {
115514
- events = parseAgentEvents(readFileSync14(source, "utf8"));
116880
+ events = parseAgentEvents(readFileSync15(source, "utf8"));
115515
116881
  } catch (error) {
115516
116882
  console.error(`Failed to read/parse agent-event source "${source}": ${error.message}`);
115517
116883
  process.exitCode = 1;
@@ -117546,7 +118912,7 @@ async function versionCommand(args, deps = {}) {
117546
118912
  // src/commands/workspace.ts
117547
118913
  init_args();
117548
118914
  init_workspace_service();
117549
- import { randomUUID as randomUUID40 } from "crypto";
118915
+ import { randomUUID as randomUUID43 } from "crypto";
117550
118916
  import { writeFile as writeFile57 } from "fs/promises";
117551
118917
 
117552
118918
  // src/sac/fwk-explain.ts
@@ -117617,7 +118983,7 @@ function service6() {
117617
118983
  async function workspaceCommand(args) {
117618
118984
  const subcommand = args[0];
117619
118985
  if (!subcommand || subcommand === "help" || args.includes("--help") || args.includes("-h"))
117620
- return printHelp22();
118986
+ return printHelp23();
117621
118987
  try {
117622
118988
  if (subcommand === "create") {
117623
118989
  rejectUnknownOptions(args.slice(1), new Set(["--title", "--component"]));
@@ -117626,7 +118992,7 @@ async function workspaceCommand(args) {
117626
118992
  const component = rawComponent === undefined ? undefined : requireWorkspaceReference(rawComponent);
117627
118993
  if (!title)
117628
118994
  throw new Error("Usage: keryx workspace create --title <title> [--component <workspace-relative-ref>]");
117629
- const workspace = await service6().create({ request: undefined, requestCorrelationId: randomUUID40(), id: newWorkspaceId(), title, ...component ? { component: { kind: "component", uri: component } } : {} });
118995
+ const workspace = await service6().create({ request: undefined, requestCorrelationId: randomUUID43(), id: newWorkspaceId(), title, ...component ? { component: { kind: "component", uri: component } } : {} });
117630
118996
  console.log(JSON.stringify(workspace, null, 2));
117631
118997
  return;
117632
118998
  }
@@ -117656,7 +119022,7 @@ async function workspaceCommand(args) {
117656
119022
  const revision = optionValue(args, "--revision");
117657
119023
  if (!workspaceId || !kind || !rawUri)
117658
119024
  throw new Error("Usage: keryx workspace add-resource <workspace-id> --kind <kind> --uri <workspace-relative-ref> [--revision <revision>]");
117659
- console.log(JSON.stringify(await service6().addResource({ request: undefined, requestCorrelationId: randomUUID40(), workspaceId, resource: { kind, uri: requireWorkspaceReference(rawUri), ...revision ? { revision } : {} } }), null, 2));
119025
+ console.log(JSON.stringify(await service6().addResource({ request: undefined, requestCorrelationId: randomUUID43(), workspaceId, resource: { kind, uri: requireWorkspaceReference(rawUri), ...revision ? { revision } : {} } }), null, 2));
117660
119026
  return;
117661
119027
  }
117662
119028
  if (subcommand === "archive") {
@@ -117664,7 +119030,7 @@ async function workspaceCommand(args) {
117664
119030
  const workspaceId = args[1];
117665
119031
  if (!workspaceId)
117666
119032
  throw new Error("Usage: keryx workspace archive <workspace-id>");
117667
- console.log(JSON.stringify(await service6().archive({ request: undefined, requestCorrelationId: randomUUID40(), workspaceId }), null, 2));
119033
+ console.log(JSON.stringify(await service6().archive({ request: undefined, requestCorrelationId: randomUUID43(), workspaceId }), null, 2));
117668
119034
  return;
117669
119035
  }
117670
119036
  if (subcommand === "remove-resource") {
@@ -117673,7 +119039,7 @@ async function workspaceCommand(args) {
117673
119039
  const uri = optionValue(args, "--uri");
117674
119040
  if (!workspaceId || !uri)
117675
119041
  throw new Error("Usage: keryx workspace remove-resource <workspace-id> --uri <workspace-relative-ref>");
117676
- console.log(JSON.stringify(await service6().removeResource({ request: undefined, requestCorrelationId: randomUUID40(), workspaceId, uri }), null, 2));
119042
+ console.log(JSON.stringify(await service6().removeResource({ request: undefined, requestCorrelationId: randomUUID43(), workspaceId, uri }), null, 2));
117677
119043
  return;
117678
119044
  }
117679
119045
  if (subcommand === "rename") {
@@ -117682,7 +119048,7 @@ async function workspaceCommand(args) {
117682
119048
  const title = optionValue(args, "--title");
117683
119049
  if (!workspaceId || !title)
117684
119050
  throw new Error("Usage: keryx workspace rename <workspace-id> --title <title>");
117685
- console.log(JSON.stringify(await service6().rename({ request: undefined, requestCorrelationId: randomUUID40(), workspaceId, title }), null, 2));
119051
+ console.log(JSON.stringify(await service6().rename({ request: undefined, requestCorrelationId: randomUUID43(), workspaceId, title }), null, 2));
117686
119052
  return;
117687
119053
  }
117688
119054
  if (subcommand === "overview") {
@@ -117694,7 +119060,7 @@ async function workspaceCommand(args) {
117694
119060
  const maxTokens = Number(optionValue(args, "--max-tokens") ?? "4096");
117695
119061
  if (!Number.isInteger(maxItems) || !Number.isInteger(maxTokens) || maxItems < 0 || maxTokens < 0)
117696
119062
  throw new Error("--max-items and --max-tokens must be non-negative integers");
117697
- const result = await createLocalFwkReadService(process.cwd()).overview({ workspaceId, request: undefined, requestCorrelationId: randomUUID40(), budget: { maxItems, maxTokens } });
119063
+ const result = await createLocalFwkReadService(process.cwd()).overview({ workspaceId, request: undefined, requestCorrelationId: randomUUID43(), budget: { maxItems, maxTokens } });
117698
119064
  const normalized = normalizeFwkResult(result);
117699
119065
  console.log(JSON.stringify(normalized, null, 2));
117700
119066
  if (args.includes("--explain"))
@@ -117711,7 +119077,7 @@ async function workspaceCommand(args) {
117711
119077
  const maxTokens = Number(optionValue(args, "--max-tokens") ?? "4096");
117712
119078
  if (!Number.isInteger(maxItems) || !Number.isInteger(maxTokens) || maxItems < 0 || maxTokens < 0)
117713
119079
  throw new Error("--max-items and --max-tokens must be non-negative integers");
117714
- const result = await createLocalFwkReadService(process.cwd()).read({ workspaceId, itemId, request: undefined, requestCorrelationId: randomUUID40(), budget: { maxItems, maxTokens } });
119080
+ const result = await createLocalFwkReadService(process.cwd()).read({ workspaceId, itemId, request: undefined, requestCorrelationId: randomUUID43(), budget: { maxItems, maxTokens } });
117715
119081
  const normalized = normalizeFwkResult(result);
117716
119082
  console.log(JSON.stringify(normalized, null, 2));
117717
119083
  if (args.includes("--explain"))
@@ -117733,14 +119099,14 @@ async function workspaceCommand(args) {
117733
119099
  const session = findSession(cwd, sessionRef);
117734
119100
  if (!session)
117735
119101
  throw new Error(`no session matching "${sessionRef}" in this project \u2014 use \`keryx sessions list\``);
117736
- const proposalId = `proposal-${randomUUID40().replace(/-/g, "").slice(0, 16)}`;
119102
+ const proposalId = `proposal-${randomUUID43().replace(/-/g, "").slice(0, 16)}`;
117737
119103
  const notePath = proposalNotePath(cwd, workspaceId, proposalId);
117738
119104
  const materializedNote = note === undefined ? undefined : prepareOutputForPersistence(await guardOutput({ cwd, content: note, target: "task", source: "generated", path: notePath }), note);
117739
119105
  if (materializedNote !== undefined && !materializedNote.allowed)
117740
119106
  throw new Error(materializedNote.reason);
117741
119107
  const persistedNote = materializedNote?.allowed ? materializedNote.content : undefined;
117742
119108
  const { service, wrapUpAuthority, authorizationServer } = createHarnessProposalLifecycleService(cwd, { workspaceId, ...persistedNote ? { note: persistedNote } : {} });
117743
- const requestCorrelationId = randomUUID40();
119109
+ const requestCorrelationId = randomUUID43();
117744
119110
  const actor = await authorizationServer.actorContextFor(undefined, requestCorrelationId);
117745
119111
  if (!actor)
117746
119112
  throw new Error("trusted ActorContext is required");
@@ -117781,13 +119147,13 @@ async function workspaceCommand(args) {
117781
119147
  const proposalId = args[2];
117782
119148
  const decision = optionValue(args, "--decision");
117783
119149
  const reason = optionValue(args, "--reason");
117784
- const idempotencyKey = optionValue(args, "--idempotency-key") ?? randomUUID40();
119150
+ const idempotencyKey = optionValue(args, "--idempotency-key") ?? randomUUID43();
117785
119151
  const confirmToken = optionValue(args, "--confirm-token");
117786
119152
  if (!workspaceId || !proposalId || !decision)
117787
119153
  throw new Error("Usage: keryx workspace review <workspace-id> <proposal-id> --decision <accepted|rejected|dismissed> [--reason <reason>] [--idempotency-key <key>] [--confirm-token <token>]");
117788
119154
  if (decision === "accepted" && !confirmToken)
117789
119155
  throw new Error("--decision accepted requires --confirm-token \u2014 run `keryx workspace confirm-review " + workspaceId + " " + proposalId + "` first");
117790
- const result = await createHarnessProposalLifecycleService(process.cwd(), { workspaceId }).service.review({ request: undefined, requestCorrelationId: randomUUID40(), workspaceId, proposalId, decision, idempotencyKey, interactive: true, ...reason ? { reason } : {}, ...confirmToken ? { confirmToken } : {} });
119156
+ const result = await createHarnessProposalLifecycleService(process.cwd(), { workspaceId }).service.review({ request: undefined, requestCorrelationId: randomUUID43(), workspaceId, proposalId, decision, idempotencyKey, interactive: true, ...reason ? { reason } : {}, ...confirmToken ? { confirmToken } : {} });
117791
119157
  console.log(JSON.stringify(normalizeProposalLifecycleResult(result), null, 2));
117792
119158
  return;
117793
119159
  }
@@ -117812,7 +119178,7 @@ async function workspaceCommand(args) {
117812
119178
  const artifactRef = optionValue(args, "--artifact");
117813
119179
  if (!workspaceId || !to || !artifactRef)
117814
119180
  throw new Error("Usage: keryx workspace handoff <workspace-id> --to <subject> --artifact <ref>");
117815
- const activity = await createLocalCollaborationService(process.cwd()).record({ request: undefined, requestCorrelationId: randomUUID40(), workspaceId, activity: { kind: "handoff-recorded", handoff: { to, artifactRef } } });
119181
+ const activity = await createLocalCollaborationService(process.cwd()).record({ request: undefined, requestCorrelationId: randomUUID43(), workspaceId, activity: { kind: "handoff-recorded", handoff: { to, artifactRef } } });
117816
119182
  console.log(JSON.stringify(activity, null, 2));
117817
119183
  return;
117818
119184
  }
@@ -117821,7 +119187,7 @@ async function workspaceCommand(args) {
117821
119187
  const workspaceId = args[1];
117822
119188
  if (!workspaceId)
117823
119189
  throw new Error("Usage: keryx workspace collaboration <workspace-id>");
117824
- console.log(JSON.stringify(await createLocalCollaborationService(process.cwd()).overview({ workspaceId, request: undefined, requestCorrelationId: randomUUID40() }), null, 2));
119190
+ console.log(JSON.stringify(await createLocalCollaborationService(process.cwd()).overview({ workspaceId, request: undefined, requestCorrelationId: randomUUID43() }), null, 2));
117825
119191
  return;
117826
119192
  }
117827
119193
  if (subcommand === "policy-readiness") {
@@ -117848,7 +119214,7 @@ async function workspaceCommand(args) {
117848
119214
  const workspaceId = args[1];
117849
119215
  const cwd = process.cwd();
117850
119216
  const authorizationServer = localWorkspaceAuthorizationServer();
117851
- const actor = await authorizationServer.actorContextFor(undefined, randomUUID40());
119217
+ const actor = await authorizationServer.actorContextFor(undefined, randomUUID43());
117852
119218
  if (!actor)
117853
119219
  throw new Error("trusted ActorContext is required");
117854
119220
  if (workspaceId) {
@@ -117906,7 +119272,7 @@ function booleanFlagDefaultTrue(args, name) {
117906
119272
  return false;
117907
119273
  throw new Error(`Unknown value for ${name}: "${value}" \u2014 expected true or false`);
117908
119274
  }
117909
- function printHelp22() {
119275
+ function printHelp23() {
117910
119276
  console.log(`keryx workspace create --title <title> [--component <workspace-relative-ref>]
117911
119277
  keryx workspace list [--include-archived]
117912
119278
  keryx workspace show <workspace-id>
@@ -118090,7 +119456,7 @@ function runAuthStatus(args) {
118090
119456
 
118091
119457
  // src/commands/retention.ts
118092
119458
  init_args();
118093
- function printHelp23() {
119459
+ function printHelp24() {
118094
119460
  console.log(`keryx retention status|sweep \u2014 bound the size of stores that grow with every routed search or refused write
118095
119461
 
118096
119462
  Usage:
@@ -118143,7 +119509,7 @@ function numericOption(args, name) {
118143
119509
  async function retentionCommand(args = [], deps = {}) {
118144
119510
  const subcommand = args[0];
118145
119511
  if (subcommand === undefined || subcommand === "--help" || subcommand === "-h") {
118146
- printHelp23();
119512
+ printHelp24();
118147
119513
  return;
118148
119514
  }
118149
119515
  const cwd = deps.cwd ?? process.cwd();
@@ -118159,7 +119525,7 @@ async function retentionCommand(args = [], deps = {}) {
118159
119525
  return;
118160
119526
  default:
118161
119527
  console.error(`Unknown retention command: ${subcommand}`);
118162
- printHelp23();
119528
+ printHelp24();
118163
119529
  process.exitCode = 1;
118164
119530
  }
118165
119531
  }
@@ -118241,7 +119607,7 @@ init_args();
118241
119607
  async function forgettingCommand(args) {
118242
119608
  const command = args[0];
118243
119609
  if (!command || command === "--help" || command === "-h" || args.includes("--help")) {
118244
- printHelp24();
119610
+ printHelp25();
118245
119611
  return;
118246
119612
  }
118247
119613
  if (command === "trail") {
@@ -118253,7 +119619,7 @@ async function forgettingCommand(args) {
118253
119619
  return;
118254
119620
  }
118255
119621
  console.error(`Unknown forgetting command: ${command}`);
118256
- printHelp24();
119622
+ printHelp25();
118257
119623
  process.exitCode = 1;
118258
119624
  }
118259
119625
  async function runTrail(args) {
@@ -118343,7 +119709,7 @@ function positional4(args) {
118343
119709
  }
118344
119710
  return;
118345
119711
  }
118346
- function printHelp24() {
119712
+ function printHelp25() {
118347
119713
  console.log(`keryx forgetting
118348
119714
 
118349
119715
  Read the deletion trail (.metaproject/data/forgetting/journal.jsonl): what was
@@ -118365,6 +119731,1000 @@ ${TRAIL_SCOPE_CAVEAT}
118365
119731
  `);
118366
119732
  }
118367
119733
 
119734
+ // src/commands/trigger.ts
119735
+ init_gdgraph();
119736
+ init_service8();
119737
+
119738
+ // src/trigger/run.ts
119739
+ init_fs();
119740
+ import path211 from "path";
119741
+
119742
+ // src/trigger/config.ts
119743
+ init_config_dir();
119744
+ import { existsSync as existsSync37 } from "fs";
119745
+ import path209 from "path";
119746
+ var TRIGGER_EVENT_NAMES = ["post-merge", "post-commit", "post-checkout", "ci"];
119747
+ var TRIGGER_ACTION_KINDS = ["reconcile", "rebuild", "open-flow", "flow-next"];
119748
+ var TRIGGER_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
119749
+ var CRON_FIELD_PATTERN = /^[0-9*/,-]+$|^[A-Za-z]{3}(?:[,-][A-Za-z]{3})*$/;
119750
+ function looksLikeCron(value) {
119751
+ const fields = value.trim().split(/\s+/);
119752
+ if (fields.length < 5 || fields.length > 6)
119753
+ return false;
119754
+ return fields.every((field) => CRON_FIELD_PATTERN.test(field));
119755
+ }
119756
+ var TRIGGERS_SCHEMA_VERSION = 1;
119757
+ function fireProblems(value) {
119758
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
119759
+ return ["on: must be an object naming an event or a schedule"];
119760
+ }
119761
+ const raw = value;
119762
+ if (raw.kind === "event") {
119763
+ if (typeof raw.event !== "string" || raw.event.length === 0) {
119764
+ return ["on.event: required, non-empty string"];
119765
+ }
119766
+ if (!TRIGGER_EVENT_NAMES.includes(raw.event)) {
119767
+ return [`on.event: unknown event "${raw.event}" (expected one of ${TRIGGER_EVENT_NAMES.join(", ")})`];
119768
+ }
119769
+ return [];
119770
+ }
119771
+ if (raw.kind === "schedule") {
119772
+ if (typeof raw.cron !== "string" || raw.cron.trim().length === 0) {
119773
+ return ["on.cron: required, non-empty string"];
119774
+ }
119775
+ if (!looksLikeCron(raw.cron)) {
119776
+ return [`on.cron: "${raw.cron}" does not look like a 5- or 6-field cron expression`];
119777
+ }
119778
+ return [];
119779
+ }
119780
+ return ['on.kind: must be "event" or "schedule"'];
119781
+ }
119782
+ function actionProblems(value) {
119783
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
119784
+ return ["action: must be an object naming what the trigger does"];
119785
+ }
119786
+ const raw = value;
119787
+ if (typeof raw.kind !== "string" || !TRIGGER_ACTION_KINDS.includes(raw.kind)) {
119788
+ return [`action.kind: must be one of ${TRIGGER_ACTION_KINDS.join(", ")}`];
119789
+ }
119790
+ if (raw.kind === "open-flow") {
119791
+ const problems = [];
119792
+ if (typeof raw.template !== "string" || raw.template.length === 0) {
119793
+ problems.push("action.template: required, non-empty string");
119794
+ }
119795
+ if (raw.skipIfOpen !== undefined && typeof raw.skipIfOpen !== "boolean") {
119796
+ problems.push("action.skipIfOpen: must be a boolean when present");
119797
+ }
119798
+ return problems;
119799
+ }
119800
+ if (raw.kind === "flow-next") {
119801
+ if (typeof raw.flow !== "string" || raw.flow.length === 0) {
119802
+ return ["action.flow: required, non-empty string"];
119803
+ }
119804
+ return [];
119805
+ }
119806
+ return [];
119807
+ }
119808
+ function triggerEntryProblems(value) {
119809
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
119810
+ return ["entry: must be an object"];
119811
+ }
119812
+ const raw = value;
119813
+ const problems = [];
119814
+ if (typeof raw.name !== "string" || raw.name.length === 0) {
119815
+ problems.push("name: required, non-empty string");
119816
+ } else if (!TRIGGER_NAME_PATTERN.test(raw.name)) {
119817
+ problems.push(`name: "${raw.name}" must start with a letter or digit and contain only letters, digits, "-", "_" or "." (max 64 chars) \u2014 it is passed as the \`keryx trigger run <name>\` argument`);
119818
+ }
119819
+ if (raw.on === undefined) {
119820
+ problems.push("on: required");
119821
+ } else {
119822
+ problems.push(...fireProblems(raw.on));
119823
+ }
119824
+ if (raw.action === undefined) {
119825
+ problems.push("action: required");
119826
+ } else {
119827
+ problems.push(...actionProblems(raw.action));
119828
+ }
119829
+ if (raw.enabled !== undefined && typeof raw.enabled !== "boolean") {
119830
+ problems.push("enabled: must be a boolean when present");
119831
+ }
119832
+ return problems;
119833
+ }
119834
+ function parseTriggerEntry(value) {
119835
+ try {
119836
+ if (triggerEntryProblems(value).length > 0)
119837
+ return;
119838
+ const raw = value;
119839
+ const fire = raw.on.kind === "event" ? { kind: "event", event: raw.on.event } : { kind: "schedule", cron: raw.on.cron };
119840
+ return {
119841
+ name: raw.name,
119842
+ fire,
119843
+ action: raw.action,
119844
+ enabled: raw.enabled ?? true
119845
+ };
119846
+ } catch {
119847
+ return;
119848
+ }
119849
+ }
119850
+ function triggersConfigPath(projectRoot) {
119851
+ return path209.join(projectRoot, ".metaproject", "triggers.json");
119852
+ }
119853
+ function loadTriggersConfig(projectRoot) {
119854
+ const empty = (fileProblem) => ({
119855
+ triggers: [],
119856
+ rejected: [],
119857
+ fileProblem
119858
+ });
119859
+ const file = triggersConfigPath(projectRoot);
119860
+ if (!existsSync37(file)) {
119861
+ return empty("absent");
119862
+ }
119863
+ const read = readConfigFile(file);
119864
+ if (!read.ok) {
119865
+ return empty("unreadable");
119866
+ }
119867
+ let parsed;
119868
+ try {
119869
+ parsed = JSON.parse(read.text);
119870
+ } catch {
119871
+ return empty("not-json");
119872
+ }
119873
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
119874
+ return empty("not-an-object");
119875
+ }
119876
+ const raw = parsed;
119877
+ if (raw.schemaVersion !== TRIGGERS_SCHEMA_VERSION) {
119878
+ return empty("wrong-schema-version");
119879
+ }
119880
+ if (!Array.isArray(raw.triggers)) {
119881
+ return empty("triggers-not-an-array");
119882
+ }
119883
+ const triggers = [];
119884
+ const rejected = [];
119885
+ const seenNames = new Set;
119886
+ raw.triggers.forEach((candidate, index) => {
119887
+ let reasons;
119888
+ try {
119889
+ reasons = triggerEntryProblems(candidate);
119890
+ } catch (error) {
119891
+ reasons = [`entry: threw during validation (${error instanceof Error ? error.message : String(error)})`];
119892
+ }
119893
+ const candidateName = candidate !== null && typeof candidate === "object" && typeof candidate.name === "string" ? candidate.name : undefined;
119894
+ if (reasons.length === 0 && candidateName !== undefined && seenNames.has(candidateName)) {
119895
+ reasons = [`name: "${candidateName}" is already used by an earlier entry in this file`];
119896
+ }
119897
+ if (reasons.length > 0) {
119898
+ rejected.push({ index, name: candidateName, reasons });
119899
+ return;
119900
+ }
119901
+ const entry = parseTriggerEntry(candidate);
119902
+ if (entry === undefined) {
119903
+ rejected.push({ index, name: candidateName, reasons: ["entry: failed to parse after passing validation"] });
119904
+ return;
119905
+ }
119906
+ seenNames.add(entry.name);
119907
+ triggers.push(entry);
119908
+ });
119909
+ return { triggers, rejected, fileProblem: undefined };
119910
+ }
119911
+
119912
+ // src/trigger/record.ts
119913
+ init_fs();
119914
+ import { appendFile as appendFile10, mkdir as mkdir68, readFile as readFile104, stat as stat21 } from "fs/promises";
119915
+ import path210 from "path";
119916
+ var TRIGGER_RUN_RECORD_VERSION = 1;
119917
+ function triggerDataDir(projectRoot) {
119918
+ return path210.join(projectRoot, ".metaproject", "data", "trigger");
119919
+ }
119920
+ function triggerRunsPath(projectRoot) {
119921
+ return path210.join(triggerDataDir(projectRoot), "runs.jsonl");
119922
+ }
119923
+ var NO_MODEL_COST = {
119924
+ recorded: false,
119925
+ reason: "this action does not call a model \u2014 reconcile/rebuild are deterministic, no spend to record"
119926
+ };
119927
+ async function appendTriggerRunRecord(projectRoot, record) {
119928
+ const file = triggerRunsPath(projectRoot);
119929
+ const full = { v: TRIGGER_RUN_RECORD_VERSION, ...record };
119930
+ try {
119931
+ await mkdir68(path210.dirname(file), { recursive: true });
119932
+ await appendFile10(file, `${JSON.stringify(full)}
119933
+ `, "utf8");
119934
+ return { status: "appended", path: file, record: full };
119935
+ } catch (error) {
119936
+ const message = error instanceof Error ? error.message : String(error);
119937
+ return {
119938
+ status: "failed",
119939
+ path: file,
119940
+ reason: `the trigger run record could not be appended (${message}). The run's own outcome above is still what ` + "happened \u2014 this failure is only about the record of it, and it must not be reported as recorded."
119941
+ };
119942
+ }
119943
+ }
119944
+ async function readTriggerRuns(projectRoot) {
119945
+ const file = triggerRunsPath(projectRoot);
119946
+ try {
119947
+ await stat21(file);
119948
+ } catch (error) {
119949
+ if (isNotFound(error))
119950
+ return { state: "absent", path: file };
119951
+ const message = error instanceof Error ? error.message : String(error);
119952
+ return {
119953
+ state: "unreadable",
119954
+ path: file,
119955
+ reason: `the trigger run record could not be examined (${message}). This is not the same as "nothing has ever fired".`
119956
+ };
119957
+ }
119958
+ let content;
119959
+ try {
119960
+ content = await readFile104(file, "utf8");
119961
+ } catch (error) {
119962
+ const message = error instanceof Error ? error.message : String(error);
119963
+ return {
119964
+ state: "unreadable",
119965
+ path: file,
119966
+ reason: `the trigger run record exists but could not be read (${message}). Its records are on disk, not absent.`
119967
+ };
119968
+ }
119969
+ const records = [];
119970
+ let damaged = 0;
119971
+ for (const line of content.split(`
119972
+ `)) {
119973
+ const trimmed = line.trim();
119974
+ if (trimmed.length === 0)
119975
+ continue;
119976
+ const parsed = parseRecord2(trimmed);
119977
+ if (parsed)
119978
+ records.push(parsed);
119979
+ else
119980
+ damaged += 1;
119981
+ }
119982
+ if (damaged > 0) {
119983
+ return {
119984
+ state: "unreadable",
119985
+ path: file,
119986
+ reason: `the trigger run record holds ${damaged} line${damaged === 1 ? "" : "s"} that could not be parsed. ` + "The readable remainder is not reported as the whole history."
119987
+ };
119988
+ }
119989
+ return { state: "present", path: file, records };
119990
+ }
119991
+ function latestRunByTrigger(records) {
119992
+ const latest = new Map;
119993
+ for (const record of records)
119994
+ latest.set(record.trigger, record);
119995
+ return latest;
119996
+ }
119997
+ function parseRecord2(line) {
119998
+ let value;
119999
+ try {
120000
+ value = JSON.parse(line);
120001
+ } catch {
120002
+ return null;
120003
+ }
120004
+ if (typeof value !== "object" || value === null || Array.isArray(value))
120005
+ return null;
120006
+ const raw = value;
120007
+ if (typeof raw["at"] !== "string" || typeof raw["trigger"] !== "string" || typeof raw["outcome"] !== "string") {
120008
+ return null;
120009
+ }
120010
+ return raw;
120011
+ }
120012
+
120013
+ // src/trigger/run.ts
120014
+ function resolveTriggerForRun(projectRoot, name) {
120015
+ const loaded = loadTriggersConfig(projectRoot);
120016
+ if (loaded.fileProblem === "absent") {
120017
+ return { kind: "config-absent" };
120018
+ }
120019
+ if (loaded.fileProblem !== undefined) {
120020
+ return { kind: "config-broken", fileProblem: loaded.fileProblem };
120021
+ }
120022
+ const entry = loaded.triggers.find((candidate) => candidate.name === name);
120023
+ if (entry !== undefined) {
120024
+ return entry.enabled ? { kind: "ready", entry } : { kind: "disabled", entry };
120025
+ }
120026
+ const rejected = loaded.rejected.find((candidate) => candidate.name === name);
120027
+ if (rejected !== undefined) {
120028
+ return { kind: "rejected", reasons: rejected.reasons };
120029
+ }
120030
+ return { kind: "unknown-name", known: loaded.triggers.map((candidate) => candidate.name) };
120031
+ }
120032
+ function triggerRunLockPath(projectRoot) {
120033
+ return path211.join(projectRoot, ".metaproject", "data", "trigger", ".run.lock");
120034
+ }
120035
+ var LOCK_TIMEOUT_PREFIX = "Timed out waiting for lock:";
120036
+ async function withTriggerRunLock(projectRoot, fn) {
120037
+ try {
120038
+ const result = await withFileLock(triggerRunLockPath(projectRoot), async () => {
120039
+ const holdMs = Number(process.env["KERYX_TRIGGER_RUN_HOLD_MS"] ?? "");
120040
+ if (Number.isFinite(holdMs) && holdMs > 0) {
120041
+ await new Promise((resolve) => setTimeout(resolve, holdMs));
120042
+ }
120043
+ return fn();
120044
+ }, { timeoutMs: 0 });
120045
+ return { acquired: true, result };
120046
+ } catch (error) {
120047
+ if (error instanceof Error && error.message.startsWith(LOCK_TIMEOUT_PREFIX)) {
120048
+ return {
120049
+ acquired: false,
120050
+ reason: "another `keryx trigger run` holds this project's trigger lock \u2014 refusing rather than waiting, " + "so this run stays exactly one pass (AC2) instead of blocking on another run's schedule. Retry on the next fire."
120051
+ };
120052
+ }
120053
+ throw error;
120054
+ }
120055
+ }
120056
+ async function recordedTriggerSpend(projectRoot) {
120057
+ const read = await readTriggerRuns(projectRoot);
120058
+ if (read.state === "absent")
120059
+ return { known: true, usd: 0 };
120060
+ if (read.state === "unreadable")
120061
+ return { known: false, reason: read.reason };
120062
+ const usd = read.records.reduce((sum, record) => sum + (record.cost.recorded ? record.cost.usd : 0), 0);
120063
+ return { known: true, usd };
120064
+ }
120065
+ async function evaluateTriggerBudget(projectRoot, actionKind, options = {}) {
120066
+ const spend = await recordedTriggerSpend(projectRoot);
120067
+ if (!spend.known) {
120068
+ const evaluation = evaluateSpendCap(undefined, options);
120069
+ return {
120070
+ allowed: false,
120071
+ evaluation,
120072
+ reason: `this project's fired-trigger run record could not be read (${spend.reason}) \u2014 spend cannot be verified ` + `against the ${evaluation.currency} ${evaluation.ceiling} spend ceiling, so refusing to start "${actionKind}" ` + "rather than proceed on an unknown ledger. Fix or restore `runs.jsonl` (see `keryx trigger status`), then retry."
120073
+ };
120074
+ }
120075
+ const evaluation = evaluateSpendCap(spend.usd, options);
120076
+ if (!evaluation.stop) {
120077
+ return { allowed: true, evaluation };
120078
+ }
120079
+ return {
120080
+ allowed: false,
120081
+ evaluation,
120082
+ reason: `this project has $${evaluation.spent} ${evaluation.currency} recorded against a $${evaluation.ceiling} ` + `${evaluation.currency} spend ceiling (over by $${evaluation.overBy}) \u2014 refusing to start "${actionKind}" ` + "rather than risk spending further. Raise the ceiling, or review `keryx trigger status`'s recorded cost, before retrying."
120083
+ };
120084
+ }
120085
+
120086
+ // src/lib/managed-hook.ts
120087
+ init_fs();
120088
+ init_git_hooks();
120089
+ import { chmod as chmod8, mkdir as mkdir69, readFile as readFile105, writeFile as writeFile58 } from "fs/promises";
120090
+ import path212 from "path";
120091
+ function escapeRegExp8(value) {
120092
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
120093
+ }
120094
+ function blockPatternFor(blockId) {
120095
+ const start = `# keryx:${blockId}:begin`;
120096
+ const end = `# keryx:${blockId}:end`;
120097
+ return new RegExp(`${escapeRegExp8(start)}[\\s\\S]*?${escapeRegExp8(end)}`);
120098
+ }
120099
+ async function installManagedHook3(projectRoot, hookName, blockId, content) {
120100
+ const hooksRoot = await resolveGitHooksRoot(projectRoot);
120101
+ if (!hooksRoot)
120102
+ return false;
120103
+ await mkdir69(hooksRoot, { recursive: true });
120104
+ const hookPath = path212.join(hooksRoot, hookName);
120105
+ const managedBlock = `# keryx:${blockId}:begin
120106
+ ${content.trim()}
120107
+ # keryx:${blockId}:end`;
120108
+ const existing = await pathExists(hookPath) ? await readFile105(hookPath, "utf8") : `#!/usr/bin/env sh
120109
+ `;
120110
+ const pattern = blockPatternFor(blockId);
120111
+ const next = pattern.test(existing) ? existing.replace(pattern, () => managedBlock) : `${existing.trimEnd()}
120112
+
120113
+ ${managedBlock}
120114
+ `;
120115
+ await writeFile58(hookPath, next, "utf8");
120116
+ await chmod8(hookPath, 493);
120117
+ return true;
120118
+ }
120119
+ async function removeManagedHook3(projectRoot, hookName, blockId) {
120120
+ const hooksRoot = await resolveGitHooksRoot(projectRoot);
120121
+ if (!hooksRoot)
120122
+ return false;
120123
+ const hookPath = path212.join(hooksRoot, hookName);
120124
+ if (!await pathExists(hookPath))
120125
+ return false;
120126
+ const existing = await readFile105(hookPath, "utf8");
120127
+ const start = `# keryx:${blockId}:begin`;
120128
+ const end = `# keryx:${blockId}:end`;
120129
+ const pattern = new RegExp(`\\n*${escapeRegExp8(start)}[\\s\\S]*?${escapeRegExp8(end)}\\n*`);
120130
+ if (!pattern.test(existing))
120131
+ return false;
120132
+ await writeFile58(hookPath, `${existing.replace(pattern, `
120133
+ `).trimEnd()}
120134
+ `, "utf8");
120135
+ await chmod8(hookPath, 493);
120136
+ return true;
120137
+ }
120138
+ async function hasManagedHook(projectRoot, hookName, blockId) {
120139
+ const hooksRoot = await resolveGitHooksRoot(projectRoot);
120140
+ if (!hooksRoot)
120141
+ return false;
120142
+ const hookPath = path212.join(hooksRoot, hookName);
120143
+ if (!await pathExists(hookPath))
120144
+ return false;
120145
+ const existing = await readFile105(hookPath, "utf8");
120146
+ return blockPatternFor(blockId).test(existing);
120147
+ }
120148
+
120149
+ // src/trigger/hooks.ts
120150
+ init_git_hooks();
120151
+ var HOOKABLE_EVENTS = ["post-merge", "post-commit", "post-checkout"];
120152
+ function isHookable(entry) {
120153
+ return entry.fire.kind === "event" && HOOKABLE_EVENTS.includes(entry.fire.event);
120154
+ }
120155
+ function blockId(name) {
120156
+ return `trigger-${name}`;
120157
+ }
120158
+ function hookBody2(name, hookName) {
120159
+ const fn = `keryx_trigger_${name.replace(/[^a-zA-Z0-9_]/g, "_")}_${hookName.replace(/-/g, "_")}`;
120160
+ const guard = hookName === "post-checkout" ? ' [ "${3:-1}" = "1" ] || return 0\n' : "";
120161
+ return `${fn}() {
120162
+ git rev-parse --is-inside-work-tree >/dev/null 2>&1 || return 0
120163
+ ${guard} command -v keryx >/dev/null 2>&1 || return 0
120164
+ # Fired trigger "${name}" \u2014 see .metaproject/triggers.json. Never fails the
120165
+ # git operation: the run's own outcome (including a failure) is recorded by
120166
+ # \`keryx trigger run\` itself (AC4), not surfaced as a hook error here.
120167
+ keryx trigger run ${name} >/dev/null 2>&1 || true
120168
+ return 0
120169
+ }
120170
+ ${fn} "$@"`;
120171
+ }
120172
+ async function installTriggerHooks(projectRoot) {
120173
+ const { triggers } = loadTriggersConfig(projectRoot);
120174
+ const results = [];
120175
+ for (const entry of triggers) {
120176
+ if (!isHookable(entry))
120177
+ continue;
120178
+ const hookName = entry.fire.event;
120179
+ const wrote = await installManagedHook3(projectRoot, hookName, blockId(entry.name), hookBody2(entry.name, hookName));
120180
+ results.push({ name: entry.name, hook: hookName, wrote });
120181
+ }
120182
+ return results;
120183
+ }
120184
+ async function uninstallTriggerHooks(projectRoot) {
120185
+ const { triggers } = loadTriggersConfig(projectRoot);
120186
+ const results = [];
120187
+ for (const entry of triggers) {
120188
+ if (!isHookable(entry))
120189
+ continue;
120190
+ const hookName = entry.fire.event;
120191
+ const wrote = await removeManagedHook3(projectRoot, hookName, blockId(entry.name));
120192
+ results.push({ name: entry.name, hook: hookName, wrote });
120193
+ }
120194
+ return results;
120195
+ }
120196
+ async function isTriggerHookInstalled(projectRoot, entry) {
120197
+ if (!isHookable(entry))
120198
+ return false;
120199
+ return hasManagedHook(projectRoot, entry.fire.event, blockId(entry.name));
120200
+ }
120201
+ async function hasGitHooksRoot(projectRoot) {
120202
+ return await resolveGitHooksRoot(projectRoot) !== null;
120203
+ }
120204
+
120205
+ // src/trigger/schedule.ts
120206
+ import path213 from "path";
120207
+ function resolveKeryxInvocation() {
120208
+ const script = process.argv[1];
120209
+ return {
120210
+ execPath: process.execPath,
120211
+ scriptPath: script ? path213.resolve(script) : process.execPath
120212
+ };
120213
+ }
120214
+ function resolveScheduleEntry(projectRoot, name) {
120215
+ const { triggers, fileProblem } = loadTriggersConfig(projectRoot);
120216
+ if (fileProblem !== undefined)
120217
+ return { kind: "config-absent" };
120218
+ const entry = triggers.find((candidate) => candidate.name === name);
120219
+ if (entry === undefined)
120220
+ return { kind: "unknown-name", known: triggers.map((t) => t.name) };
120221
+ if (entry.fire.kind !== "schedule")
120222
+ return { kind: "not-a-schedule", entry };
120223
+ return { kind: "ready", entry };
120224
+ }
120225
+ function shQuote2(value) {
120226
+ return `'${value.replace(/'/g, `'\\''`)}'`;
120227
+ }
120228
+ function systemdQuote(value) {
120229
+ if (!/[\s"\\]/.test(value))
120230
+ return value;
120231
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
120232
+ }
120233
+ function systemdEscapePercent(value) {
120234
+ return value.replace(/%/g, "%%");
120235
+ }
120236
+ function systemdValue(value) {
120237
+ return systemdQuote(systemdEscapePercent(value));
120238
+ }
120239
+ function cronEscapePercent(value) {
120240
+ return value.replace(/%/g, "\\%");
120241
+ }
120242
+ function renderScheduleLines(params) {
120243
+ const { projectRoot, name, cron, invocation } = params;
120244
+ const interpreterDir = path213.dirname(invocation.execPath);
120245
+ const assumedPath = [interpreterDir, "/usr/local/bin", "/usr/bin", "/bin"].join(":");
120246
+ const logDir = path213.join(projectRoot, ".metaproject", "data", "trigger");
120247
+ const logPath = path213.join(logDir, `${name}.schedule.log`);
120248
+ const cronCommand = `cd ${shQuote2(projectRoot)} && mkdir -p ${shQuote2(logDir)} && PATH=${shQuote2(assumedPath)} ` + `${shQuote2(invocation.execPath)} ${shQuote2(invocation.scriptPath)} trigger run ${shQuote2(name)} ` + `>> ${shQuote2(logPath)} 2>&1`;
120249
+ const cronLine = `${cron} ${cronEscapePercent(cronCommand)}`;
120250
+ const serviceUnitName = `keryx-trigger-${name}.service`;
120251
+ const timerUnitName = `keryx-trigger-${name}.timer`;
120252
+ const descriptionName = systemdEscapePercent(name);
120253
+ const descriptionProjectRoot = systemdEscapePercent(projectRoot);
120254
+ const systemdService = `# ${serviceUnitName} \u2014 install under /etc/systemd/system/ (or ~/.config/systemd/user/ for --user)
120255
+ [Unit]
120256
+ Description=keryx trigger "${descriptionName}" (${descriptionProjectRoot})
120257
+
120258
+ [Service]
120259
+ Type=oneshot
120260
+ WorkingDirectory=${systemdEscapePercent(projectRoot)}
120261
+ Environment=${systemdQuote(systemdEscapePercent(`PATH=${assumedPath}`))}
120262
+ # Same reason as the cron line's own \`mkdir -p\`: StandardOutput=append: does
120263
+ # not create a missing PARENT directory, only a missing file. The leading "-"
120264
+ # means systemd ignores this step's own exit status.
120265
+ ExecStartPre=-/bin/mkdir -p ${systemdValue(logDir)}
120266
+ ExecStart=${systemdValue(invocation.execPath)} ${systemdValue(invocation.scriptPath)} trigger run ${systemdValue(name)}
120267
+ StandardOutput=append:${systemdEscapePercent(logPath)}
120268
+ StandardError=append:${systemdEscapePercent(logPath)}
120269
+ `;
120270
+ const systemdTimer = `# ${timerUnitName} \u2014 install alongside ${serviceUnitName}
120271
+ [Unit]
120272
+ Description=Schedule for keryx trigger "${descriptionName}"
120273
+
120274
+ [Timer]
120275
+ # Translate the cron expression "${cron}" into OnCalendar= syntax, e.g. with
120276
+ # \`systemd-analyze calendar '<expression>'\` to verify it, then uncomment:
120277
+ # OnCalendar=
120278
+ Persistent=true
120279
+
120280
+ [Install]
120281
+ WantedBy=timers.target
120282
+ `;
120283
+ return { assumedPath, cronCommand, cronLine, systemdService, systemdTimer, serviceUnitName, timerUnitName, logPath };
120284
+ }
120285
+
120286
+ // src/commands/trigger.ts
120287
+ var flowService2 = null;
120288
+ function getFlowService2() {
120289
+ flowService2 ??= createFlowService(flowServiceDeps());
120290
+ return flowService2;
120291
+ }
120292
+ async function triggerCommand(args) {
120293
+ const sub = args[0];
120294
+ if (!sub || sub === "--help" || sub === "-h") {
120295
+ printHelp26();
120296
+ return;
120297
+ }
120298
+ switch (sub) {
120299
+ case "run":
120300
+ return runSubcommand(args);
120301
+ case "install":
120302
+ return installSubcommand();
120303
+ case "uninstall":
120304
+ return uninstallSubcommand();
120305
+ case "list":
120306
+ return listSubcommand();
120307
+ case "status":
120308
+ return statusSubcommand(args);
120309
+ case "schedule":
120310
+ return scheduleSubcommand(args);
120311
+ default:
120312
+ console.error(`Unknown trigger command: ${sub}. See \`keryx trigger --help\`.`);
120313
+ printHelp26();
120314
+ process.exitCode = 1;
120315
+ return;
120316
+ }
120317
+ }
120318
+ async function runSubcommand(args) {
120319
+ const name = args[1];
120320
+ if (args[1] === "--help" || args[1] === "-h") {
120321
+ printHelp26();
120322
+ return;
120323
+ }
120324
+ if (!name) {
120325
+ console.error("Usage: keryx trigger run <name>");
120326
+ process.exitCode = 1;
120327
+ return;
120328
+ }
120329
+ await runTrigger(process.cwd(), name);
120330
+ }
120331
+ async function runTrigger(projectRoot, name) {
120332
+ const resolution = resolveTriggerForRun(projectRoot, name);
120333
+ switch (resolution.kind) {
120334
+ case "config-absent": {
120335
+ console.log(`keryx trigger run ${name}: no trigger config declared for this project ` + `(${triggersConfigPath(projectRoot)} not found) \u2014 nothing to do.`);
120336
+ return;
120337
+ }
120338
+ case "config-broken": {
120339
+ console.error(`keryx trigger run ${name}: ${triggersConfigPath(projectRoot)} could not be read as a trigger ` + `config (${resolution.fileProblem}) \u2014 fix the file and retry.`);
120340
+ process.exitCode = 1;
120341
+ return;
120342
+ }
120343
+ case "unknown-name": {
120344
+ console.error(`keryx trigger run: unknown trigger "${name}". ` + (resolution.known.length > 0 ? `Known: ${resolution.known.join(", ")}` : "This project declares no valid triggers."));
120345
+ process.exitCode = 1;
120346
+ return;
120347
+ }
120348
+ case "rejected": {
120349
+ console.error(`keryx trigger run: "${name}" is declared but malformed, so it cannot run:`);
120350
+ for (const reason of resolution.reasons)
120351
+ console.error(` - ${reason}`);
120352
+ process.exitCode = 1;
120353
+ return;
120354
+ }
120355
+ case "disabled": {
120356
+ console.log(`keryx trigger run ${name}: disabled \u2014 nothing to do.`);
120357
+ await recordRun(projectRoot, resolution.entry, {
120358
+ outcome: "no-op",
120359
+ detail: "disabled \u2014 nothing to do.",
120360
+ cost: { recorded: false, reason: "trigger is disabled \u2014 its action never ran" }
120361
+ });
120362
+ return;
120363
+ }
120364
+ case "ready": {
120365
+ await runReadyTrigger(projectRoot, name, resolution);
120366
+ return;
120367
+ }
120368
+ }
120369
+ }
120370
+ async function runReadyTrigger(projectRoot, name, resolution) {
120371
+ const { entry } = resolution;
120372
+ const { action } = entry;
120373
+ if (action.kind === "open-flow") {
120374
+ await runOpenFlow(projectRoot, name, entry, action);
120375
+ return;
120376
+ }
120377
+ if (action.kind === "flow-next") {
120378
+ await runFlowNext(projectRoot, name, entry, action);
120379
+ return;
120380
+ }
120381
+ const outcome = await withTriggerRunLock(projectRoot, async () => {
120382
+ const before = process.exitCode;
120383
+ process.exitCode = 0;
120384
+ if (action.kind === "reconcile") {
120385
+ await syncCommand(["--apply"]);
120386
+ } else {
120387
+ await gdgraphCommand(["build"]);
120388
+ }
120389
+ const failed = (process.exitCode ?? 0) !== 0;
120390
+ process.exitCode = before;
120391
+ return { failed };
120392
+ });
120393
+ if (!outcome.acquired) {
120394
+ console.log(`keryx trigger run ${name}: ${outcome.reason}`);
120395
+ await recordRun(projectRoot, entry, {
120396
+ outcome: "lock-refused",
120397
+ detail: outcome.reason,
120398
+ cost: { recorded: false, reason: "run was refused before the action could start" }
120399
+ });
120400
+ return;
120401
+ }
120402
+ if (outcome.result.failed) {
120403
+ console.error(`keryx trigger run ${name}: action "${action.kind}" failed (see output above).`);
120404
+ process.exitCode = 1;
120405
+ await recordRun(projectRoot, entry, {
120406
+ outcome: "failed",
120407
+ detail: `action "${action.kind}" failed (see command output above for detail).`,
120408
+ cost: NO_MODEL_COST
120409
+ });
120410
+ return;
120411
+ }
120412
+ console.log(`keryx trigger run ${name}: ok \u2014 "${action.kind}" completed.`);
120413
+ await recordRun(projectRoot, entry, {
120414
+ outcome: "ok",
120415
+ detail: `action "${action.kind}" completed.`,
120416
+ cost: NO_MODEL_COST
120417
+ });
120418
+ }
120419
+ function findEquivalentOpenFlow(flows, template) {
120420
+ return flows.find((flow) => flow.title === template && flow.status !== "done");
120421
+ }
120422
+ function noModelCost(reason) {
120423
+ return { recorded: false, reason };
120424
+ }
120425
+ var OPEN_FLOW_NO_MODEL_COST = noModelCost('action "open-flow" does not call a model \u2014 opening a flow is deterministic bookkeeping (a new flow.json plus scaffold ' + "files), no spend to record.");
120426
+ var FLOW_NEXT_NO_MODEL_COST = noModelCost('action "flow-next" does not call a model in this build \u2014 it only reports the next task into this record; it never ' + "dispatches an agent turn, so there is nothing to record a cost for.");
120427
+ async function runOpenFlow(projectRoot, name, entry, action) {
120428
+ const budget = await evaluateTriggerBudget(projectRoot, "open-flow");
120429
+ if (!budget.allowed) {
120430
+ await refuseOnBudget(projectRoot, name, entry, budget.reason);
120431
+ return;
120432
+ }
120433
+ const service = getFlowService2();
120434
+ let outcome;
120435
+ try {
120436
+ outcome = await withTriggerRunLock(projectRoot, async () => {
120437
+ if (action.skipIfOpen) {
120438
+ const flows = await service.list({ cwd: projectRoot });
120439
+ const equivalent = findEquivalentOpenFlow(flows, action.template);
120440
+ if (equivalent) {
120441
+ return { kind: "skipped", equivalent };
120442
+ }
120443
+ }
120444
+ const result = await service.init({ cwd: projectRoot, title: action.template });
120445
+ return { kind: "opened", flowId: result.flow.id, dir: result.dir };
120446
+ });
120447
+ } catch (error) {
120448
+ const message = error instanceof Error ? error.message : String(error);
120449
+ console.error(`keryx trigger run ${name}: action "open-flow" failed: ${message}`);
120450
+ process.exitCode = 1;
120451
+ await recordRun(projectRoot, entry, {
120452
+ outcome: "failed",
120453
+ detail: `action "open-flow" failed: ${message}`,
120454
+ cost: OPEN_FLOW_NO_MODEL_COST
120455
+ });
120456
+ return;
120457
+ }
120458
+ if (!outcome.acquired) {
120459
+ console.log(`keryx trigger run ${name}: ${outcome.reason}`);
120460
+ await recordRun(projectRoot, entry, {
120461
+ outcome: "lock-refused",
120462
+ detail: outcome.reason,
120463
+ cost: { recorded: false, reason: "run was refused before the action could start" }
120464
+ });
120465
+ return;
120466
+ }
120467
+ if (outcome.result.kind === "skipped") {
120468
+ const { equivalent } = outcome.result;
120469
+ const detail = `flow ${equivalent.id} ("${equivalent.title}", status: ${equivalent.status}) is already open for ` + `template "${action.template}" and \`skipIfOpen\` is set \u2014 not opening a second one.`;
120470
+ console.log(`keryx trigger run ${name}: ${detail}`);
120471
+ await recordRun(projectRoot, entry, { outcome: "no-op", detail, cost: OPEN_FLOW_NO_MODEL_COST });
120472
+ return;
120473
+ }
120474
+ const { flowId, dir } = outcome.result;
120475
+ const detail = `opened flow ${flowId} ("${action.template}") at ${dir}.`;
120476
+ console.log(`keryx trigger run ${name}: ok \u2014 ${detail}`);
120477
+ await recordRun(projectRoot, entry, { outcome: "ok", detail, cost: OPEN_FLOW_NO_MODEL_COST });
120478
+ }
120479
+ async function runFlowNext(projectRoot, name, entry, action) {
120480
+ const budget = await evaluateTriggerBudget(projectRoot, "flow-next");
120481
+ if (!budget.allowed) {
120482
+ await refuseOnBudget(projectRoot, name, entry, budget.reason);
120483
+ return;
120484
+ }
120485
+ const service = getFlowService2();
120486
+ try {
120487
+ const decision = await service.next({ cwd: projectRoot, id: action.flow });
120488
+ const detail = describeNextTaskDecision(action.flow, decision);
120489
+ console.log(`keryx trigger run ${name}: ok \u2014 ${detail}`);
120490
+ await recordRun(projectRoot, entry, { outcome: "ok", detail, cost: FLOW_NEXT_NO_MODEL_COST });
120491
+ } catch (error) {
120492
+ const message = error instanceof Error ? error.message : String(error);
120493
+ console.error(`keryx trigger run ${name}: action "flow-next" failed: ${message}`);
120494
+ process.exitCode = 1;
120495
+ await recordRun(projectRoot, entry, {
120496
+ outcome: "failed",
120497
+ detail: `action "flow-next" failed: ${message}`,
120498
+ cost: FLOW_NEXT_NO_MODEL_COST
120499
+ });
120500
+ }
120501
+ }
120502
+ function describeNextTaskDecision(flowId, decision) {
120503
+ const unresolvedNote = decision.unresolved.length > 0 ? ` (${decision.unresolved.length} other not-done task(s) carry an open, unresolved attempt.)` : "";
120504
+ if (decision.kind === "ready") {
120505
+ return `flow ${flowId}'s next task is ${decision.task.id} ("${decision.task.title}"), resume: ${decision.resume.kind}.` + unresolvedNote;
120506
+ }
120507
+ if (decision.kind === "blocked") {
120508
+ const waiting = decision.blocked.map((entry) => `${entry.task.id} (waiting on ${entry.waitingOn.join(", ")})`).join("; ");
120509
+ return `flow ${flowId} has work remaining but nothing startable \u2014 blocked: ${waiting}.${unresolvedNote}`;
120510
+ }
120511
+ return `flow ${flowId} has no undone task \u2014 nothing to report.${unresolvedNote}`;
120512
+ }
120513
+ async function refuseOnBudget(projectRoot, name, entry, reason) {
120514
+ console.log(`keryx trigger run ${name}: ${reason}`);
120515
+ await recordRun(projectRoot, entry, {
120516
+ outcome: "budget-refused",
120517
+ detail: reason,
120518
+ cost: { recorded: false, reason: "run was refused on the spend ceiling before the action could start" }
120519
+ });
120520
+ }
120521
+ async function recordRun(projectRoot, entry, outcome) {
120522
+ const append = await appendTriggerRunRecord(projectRoot, {
120523
+ at: new Date().toISOString(),
120524
+ trigger: entry.name,
120525
+ firedBy: entry.fire,
120526
+ action: entry.action,
120527
+ outcome: outcome.outcome,
120528
+ detail: outcome.detail,
120529
+ cost: outcome.cost
120530
+ });
120531
+ if (append.status === "failed") {
120532
+ console.error(`keryx trigger run ${entry.name}: ! ${append.reason}`);
120533
+ }
120534
+ }
120535
+ async function installSubcommand() {
120536
+ const cwd = process.cwd();
120537
+ if (!await hasGitHooksRoot(cwd)) {
120538
+ console.log("No .git directory \u2014 nothing installed.");
120539
+ return;
120540
+ }
120541
+ const results = await installTriggerHooks(cwd);
120542
+ if (results.length === 0) {
120543
+ console.log("keryx trigger install: no event-fired trigger declared " + `(${triggersConfigPath(cwd)}) \u2014 nothing to install. Schedule-fired and \`ci\`-fired entries install no hook; ` + "see `keryx trigger schedule <name>` for a schedule entry.");
120544
+ return;
120545
+ }
120546
+ console.log(`keryx trigger install: installed ${results.length} hook block(s), extending the existing hook file(s):`);
120547
+ for (const result of results) {
120548
+ console.log(` - ${result.name} -> ${result.hook} (${result.wrote ? "written" : "NOT written"})`);
120549
+ }
120550
+ console.log("\nOther managed blocks already in these hook files (e.g. `keryx sync install-hooks`'s `keryx-sync` block, " + "or a `keryx update`-installed `*-post-commit` block) are untouched and still run.");
120551
+ }
120552
+ async function uninstallSubcommand() {
120553
+ const cwd = process.cwd();
120554
+ const results = await uninstallTriggerHooks(cwd);
120555
+ const removed = results.filter((r) => r.wrote);
120556
+ console.log(removed.length > 0 ? `keryx trigger uninstall: removed ${removed.length} hook block(s): ${removed.map((r) => `${r.name} (${r.hook})`).join(", ")}` : "keryx trigger uninstall: no installed trigger hook block found to remove.");
120557
+ }
120558
+ async function listSubcommand() {
120559
+ const cwd = process.cwd();
120560
+ const { triggers, rejected, fileProblem } = loadTriggersConfig(cwd);
120561
+ if (fileProblem === "absent") {
120562
+ console.log(`keryx trigger list: no trigger config declared (${triggersConfigPath(cwd)} not found).`);
120563
+ return;
120564
+ }
120565
+ if (fileProblem !== undefined) {
120566
+ console.error(`keryx trigger list: ${triggersConfigPath(cwd)} could not be read as a trigger config (${fileProblem}).`);
120567
+ process.exitCode = 1;
120568
+ return;
120569
+ }
120570
+ if (triggers.length === 0 && rejected.length === 0) {
120571
+ console.log("keryx trigger list: the trigger config declares no entries.");
120572
+ return;
120573
+ }
120574
+ console.log(`keryx trigger list (${triggersConfigPath(cwd)}):`);
120575
+ for (const entry of triggers) {
120576
+ const hookNote = await describeHookInstalled(cwd, entry);
120577
+ console.log(` - ${entry.name} [${entry.enabled ? "enabled" : "disabled"}] ${describeFire(entry.fire)} -> ${describeAction(entry.action)} hook: ${hookNote}`);
120578
+ }
120579
+ for (const bad of rejected) {
120580
+ console.log(` - ${bad.name ?? `(entry #${bad.index})`} [REJECTED] ${bad.reasons.join("; ")}`);
120581
+ }
120582
+ }
120583
+ async function describeHookInstalled(cwd, entry) {
120584
+ if (entry.fire.kind === "schedule") {
120585
+ return `n/a (schedule \u2014 see \`keryx trigger schedule ${entry.name}\`)`;
120586
+ }
120587
+ if (entry.fire.event === "ci") {
120588
+ return "n/a (ci \u2014 fired by a CI job's own `keryx trigger run` call)";
120589
+ }
120590
+ return await isTriggerHookInstalled(cwd, entry) ? `installed (${entry.fire.event})` : `NOT installed (${entry.fire.event})`;
120591
+ }
120592
+ async function statusSubcommand(args) {
120593
+ const filterName = args[1];
120594
+ const cwd = process.cwd();
120595
+ const { triggers, fileProblem } = loadTriggersConfig(cwd);
120596
+ if (fileProblem === "absent") {
120597
+ console.log(`keryx trigger status: no trigger config declared (${triggersConfigPath(cwd)} not found).`);
120598
+ return;
120599
+ }
120600
+ if (fileProblem !== undefined) {
120601
+ console.error(`keryx trigger status: ${triggersConfigPath(cwd)} could not be read as a trigger config (${fileProblem}).`);
120602
+ process.exitCode = 1;
120603
+ return;
120604
+ }
120605
+ const runsRead = await readTriggerRuns(cwd);
120606
+ if (runsRead.state === "unreadable") {
120607
+ console.error(`keryx trigger status: ${runsRead.reason}`);
120608
+ process.exitCode = 1;
120609
+ return;
120610
+ }
120611
+ const latest = latestRunByTrigger(runsRead.state === "present" ? runsRead.records : []);
120612
+ const entries = filterName ? triggers.filter((entry) => entry.name === filterName) : triggers;
120613
+ if (filterName && entries.length === 0) {
120614
+ console.error(`keryx trigger status: unknown trigger "${filterName}". Known: ${triggers.map((t) => t.name).join(", ") || "(none)"}`);
120615
+ process.exitCode = 1;
120616
+ return;
120617
+ }
120618
+ if (entries.length === 0) {
120619
+ console.log("keryx trigger status: the trigger config declares no entries.");
120620
+ return;
120621
+ }
120622
+ console.log(`keryx trigger status (reading ${runsRead.state === "present" ? runsRead.path : "no run record yet"}):`);
120623
+ for (const entry of entries) {
120624
+ console.log(` - ${entry.name} [${entry.enabled ? "enabled" : "disabled"}] ${describeFire(entry.fire)} -> ${describeAction(entry.action)}`);
120625
+ const record = latest.get(entry.name);
120626
+ console.log(` ${record ? describeRecord(record) : "never fired \u2014 no record yet"}`);
120627
+ }
120628
+ }
120629
+ function describeRecord(record) {
120630
+ const cost = record.cost.recorded ? `cost: $${record.cost.usd.toFixed(4)}` : `cost: n/a (${record.cost.reason})`;
120631
+ return `last: ${record.at} \u2014 ${record.outcome} \u2014 ${record.detail} [${cost}]`;
120632
+ }
120633
+ function describeFire(fire) {
120634
+ return fire.kind === "event" ? `event:${fire.event}` : `schedule:"${fire.cron}"`;
120635
+ }
120636
+ function describeAction(action) {
120637
+ if (action.kind === "open-flow")
120638
+ return `open-flow(${action.template}${action.skipIfOpen ? ", skipIfOpen" : ""})`;
120639
+ if (action.kind === "flow-next")
120640
+ return `flow-next(${action.flow})`;
120641
+ return action.kind;
120642
+ }
120643
+ async function scheduleSubcommand(args) {
120644
+ const name = args[1];
120645
+ if (args[1] === "--help" || args[1] === "-h") {
120646
+ console.log("Usage: keryx trigger schedule <name>");
120647
+ return;
120648
+ }
120649
+ if (!name) {
120650
+ console.error("Usage: keryx trigger schedule <name>");
120651
+ process.exitCode = 1;
120652
+ return;
120653
+ }
120654
+ const cwd = process.cwd();
120655
+ const resolution = resolveScheduleEntry(cwd, name);
120656
+ switch (resolution.kind) {
120657
+ case "config-absent": {
120658
+ console.error(`keryx trigger schedule: no trigger config declared (${triggersConfigPath(cwd)} not found).`);
120659
+ process.exitCode = 1;
120660
+ return;
120661
+ }
120662
+ case "unknown-name": {
120663
+ console.error(`keryx trigger schedule: unknown trigger "${name}". ` + (resolution.known.length > 0 ? `Known: ${resolution.known.join(", ")}` : "This project declares no valid triggers."));
120664
+ process.exitCode = 1;
120665
+ return;
120666
+ }
120667
+ case "not-a-schedule": {
120668
+ console.error(`keryx trigger schedule: "${name}" is ${describeFire(resolution.entry.fire)}, not a schedule entry \u2014 ` + "there is nothing to print. Event-fired entries are installed as git hooks with `keryx trigger install`.");
120669
+ process.exitCode = 1;
120670
+ return;
120671
+ }
120672
+ case "ready": {
120673
+ const lines = renderScheduleLines({
120674
+ projectRoot: cwd,
120675
+ name,
120676
+ cron: resolution.entry.fire.cron,
120677
+ invocation: resolveKeryxInvocation()
120678
+ });
120679
+ console.log(`keryx trigger schedule ${name}: keryx runs no daemon for this \u2014 install ONE of the two below with your own scheduler.
120680
+ `);
120681
+ console.log(`# --- cron (crontab -e) --------------------------------------------------`);
120682
+ console.log(lines.cronLine);
120683
+ console.log(`# cron-command-only: ${lines.cronCommand}`);
120684
+ console.log(`
120685
+ # --- systemd (an alternative to cron) -------------------------------------`);
120686
+ console.log(lines.systemdService);
120687
+ console.log(lines.systemdTimer);
120688
+ console.log(`# Assumed PATH baked into both: ${lines.assumedPath}`);
120689
+ console.log(`# Output is appended to: ${lines.logPath}`);
120690
+ return;
120691
+ }
120692
+ }
120693
+ }
120694
+ function printHelp26() {
120695
+ console.log(`keryx trigger \u2014 fire, install and inspect this project's declared triggers
120696
+
120697
+ Usage:
120698
+ keryx trigger run <name> Perform exactly one pass of <name>'s action
120699
+ keryx trigger install Install a git hook block for every event-fired entry
120700
+ keryx trigger uninstall Remove those hook blocks (other managed blocks are untouched)
120701
+ keryx trigger list List declared entries: enabled state, fire, action, hook status
120702
+ keryx trigger status [<name>] Show the last recorded outcome for one or every entry (AC4)
120703
+ keryx trigger schedule <name> Print the cron line / systemd timer unit for a schedule entry
120704
+
120705
+ Triggers are declared by hand in .metaproject/triggers.json (keryx never
120706
+ writes it) and loaded by name. \`run\`:
120707
+ - exits 0 and says "nothing to do" when there is no trigger config, or the
120708
+ named trigger is disabled;
120709
+ - exits non-zero when the name is unknown, the matching entry is malformed,
120710
+ or the action itself fails;
120711
+ - refuses cleanly (exit 0) when another \`trigger run\` already holds this
120712
+ project's trigger lock ("reconcile"/"rebuild" only), or when the
120713
+ project's recorded trigger spend is at or over its spend ceiling
120714
+ ("open-flow"/"flow-next" only \u2014 see \`keryx trigger status\` for the
120715
+ recorded cost).
120716
+
120717
+ Four actions run today: "reconcile" (-> \`keryx sync --apply\`), "rebuild"
120718
+ (-> \`keryx gdgraph build\`), "open-flow" (opens a flow from
120719
+ \`action.template\`, skipping a second one while an equivalent flow is
120720
+ already open when \`action.skipIfOpen\` is set), and "flow-next" (reports
120721
+ \`action.flow\`'s next task into the record \u2014 it does not dispatch an agent
120722
+ to work it). \`install\` writes hooks for event-fired entries only; a
120723
+ schedule entry's line comes from \`trigger schedule\`, and keryx never runs a
120724
+ daemon of its own for it.
120725
+ `);
120726
+ }
120727
+
118368
120728
  // src/cli.ts
118369
120729
  var VERSION2 = package_default.version;
118370
120730
  var CLI_ROUTES = {
@@ -118405,18 +120765,20 @@ var CLI_ROUTES = {
118405
120765
  shell: shellCommand,
118406
120766
  sessions: sessionsCommand,
118407
120767
  session: sessionsCommand,
120768
+ acp: acpCommand,
118408
120769
  bus: busCommand,
118409
120770
  version: versionCommand,
118410
120771
  workspace: workspaceCommand,
118411
120772
  retention: retentionCommand,
118412
- forgetting: forgettingCommand
120773
+ forgetting: forgettingCommand,
120774
+ trigger: triggerCommand
118413
120775
  };
118414
120776
  async function main() {
118415
120777
  setModelTurnPort(async (request) => runModelTurn(request));
118416
120778
  const args = process.argv.slice(2);
118417
120779
  const command = args[0];
118418
120780
  if (command === "--help" || command === "-h" || command === "help" || !command) {
118419
- printHelp25();
120781
+ printHelp27();
118420
120782
  return;
118421
120783
  }
118422
120784
  if (command === "--version" || command === "-v") {
@@ -118439,7 +120801,7 @@ ${usage}
118439
120801
  return;
118440
120802
  }
118441
120803
  console.error(`Unknown command: ${command}`);
118442
- printHelp25();
120804
+ printHelp27();
118443
120805
  process.exitCode = 1;
118444
120806
  }
118445
120807
  var USAGE_BODY = `Usage:
@@ -118448,6 +120810,8 @@ var USAGE_BODY = `Usage:
118448
120810
  Start TUI agent shell (sessions are per-project)
118449
120811
  keryx sessions list|fork <id>|export <id>|path
118450
120812
  List / branch / export sessions for the current project
120813
+ keryx acp [--provider <p>] [--model <m>] [--base-url <url>] [--data-dir <dir>]
120814
+ Agent Client Protocol agent server over stdio
118451
120815
  keryx bus list [--json] | log [--since <seq>] [--limit N] [--json] | send <@name|@all> [--kind <k>] [--reply-to <id>] <text> | prune
118452
120816
  Agent bus: peers, events and messages shared across this clone's worktrees
118453
120817
  keryx version check [--json] Check npm latest (advisory; never installs)
@@ -118562,12 +120926,15 @@ var USAGE_BODY = `Usage:
118562
120926
  keryx forgetting lookup "<ref-or-path>" [--layer <layer>] [--search] [--json]
118563
120927
  Read the deletion trail: what was removed, when, at
118564
120928
  whose request, on what basis
120929
+ keryx trigger run <name> Perform exactly one pass of a declared trigger's
120930
+ action (.metaproject/triggers.json)
118565
120931
  keryx --version
118566
120932
 
118567
120933
  Commands:
118568
120934
  shell Start the interactive TUI agent harness. Use --no-tui or --chat to opt out.
118569
120935
  Sessions: -c continue last in this project, -r [id] resume (per-project).
118570
120936
  sessions List or export per-project shell sessions
120937
+ acp Speak ACP v1 (newline-delimited JSON-RPC) over stdio, for an ACP client (e.g. an editor)
118571
120938
  bus Agent bus: list peers and leases, read the log, send a message, prune
118572
120939
  version Check whether a newer npm release is available
118573
120940
  harness Run a single provider turn (harness run) and print structured events
@@ -118606,8 +120973,9 @@ Commands:
118606
120973
  workspace Shared Agent Context: workspaces, FWK reads, propose/review (module sac)
118607
120974
  retention Bound stores that grow without bound (gdctx raw/artifacts, owner write-conflict sidecars)
118608
120975
  forgetting Read the deletion trail \u2014 was this removed, or did it never exist?
120976
+ trigger Fire one declared project trigger (git hook, cron line, CI job) \u2014 one pass, one exit code
118609
120977
  `;
118610
- function printHelp25() {
120978
+ function printHelp27() {
118611
120979
  console.log(`keryx ${VERSION2}
118612
120980
 
118613
120981
  ${USAGE_BODY}`);