@rallycry/conveyor-agent 10.9.0 → 10.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -16,6 +16,7 @@ import {
16
16
  buildSessionPreviewPorts,
17
17
  cleanTerminalOutput,
18
18
  createServiceLogger,
19
+ defineTool,
19
20
  fetchBootstrap,
20
21
  findOnPath,
21
22
  inheritedEnv,
@@ -27,8 +28,9 @@ import {
27
28
  runSetupCommand,
28
29
  runStartCommand,
29
30
  sampleKeyUsage,
30
- terminateProcessGroup
31
- } from "./chunk-H3OGNJS4.js";
31
+ terminateProcessGroup,
32
+ textResult
33
+ } from "./chunk-KEKGEDN2.js";
32
34
  import "./chunk-7TQO4ZF4.js";
33
35
 
34
36
  // src/cli.ts
@@ -43,7 +45,7 @@ import { dirname } from "path";
43
45
  var POSTGRES_TIMEOUT_MS = 12e4;
44
46
  var FIREBASE_TIMEOUT_MS = 6e4;
45
47
  var FALLBACK_TIMEOUT_MS = 3e4;
46
- var DEFAULT_POLL_INTERVAL_MS = 1e3;
48
+ var DEFAULT_SIDECAR_POLL_INTERVAL_MS = 1e3;
47
49
  var DEFAULT_PROBE_TIMEOUT_MS = 2e3;
48
50
  var POSTGRES_DEFAULT_PORT = 5432;
49
51
  var FIREBASE_DEFAULT_PORT = 9099;
@@ -187,7 +189,7 @@ async function waitForSidecars(opts = {}) {
187
189
  onLog = () => {
188
190
  },
189
191
  timeoutMs,
190
- pollIntervalMs = DEFAULT_POLL_INTERVAL_MS,
192
+ pollIntervalMs = DEFAULT_SIDECAR_POLL_INTERVAL_MS,
191
193
  probe = defaultProbe,
192
194
  startLazy = true,
193
195
  signal
@@ -824,8 +826,277 @@ function resolveTuiAdapter(kind = "claude-code") {
824
826
  }
825
827
  }
826
828
 
829
+ // src/tools/project-tools.ts
830
+ import { z } from "zod";
831
+ var CONTEXT_PATH_SHAPE = z.object({
832
+ type: z.enum(["rule", "doc", "file", "folder"]).describe("Link kind"),
833
+ path: z.string().min(1).max(500).describe("Repo-relative path"),
834
+ label: z.string().max(100).optional()
835
+ });
836
+ function errText(prefix, error) {
837
+ return textResult(`${prefix}: ${error instanceof Error ? error.message : "Unknown error"}`);
838
+ }
839
+ function buildListTagsTool(connection, projectId) {
840
+ return defineTool(
841
+ "list_tags",
842
+ "List this project's tags (id, name, color). Use the ids with update_tag.",
843
+ {},
844
+ async () => {
845
+ try {
846
+ const tags = await connection.call("listProjectTags", { projectId });
847
+ return textResult(JSON.stringify(tags, null, 2));
848
+ } catch (error) {
849
+ return errText("Failed to list tags", error);
850
+ }
851
+ },
852
+ { annotations: { readOnlyHint: true } }
853
+ );
854
+ }
855
+ function buildCreateTagTool(connection, projectId) {
856
+ return defineTool(
857
+ "create_tag",
858
+ "Create a project tag. Include a crisp description and contextPaths (rule/doc/file/folder links agents auto-load when working on matching tasks). Fails if the name already exists.",
859
+ {
860
+ name: z.string().min(1).max(50),
861
+ color: z.string().regex(/^#[0-9a-fA-F]{6}$/).optional().describe("#RRGGBB (default gray)"),
862
+ description: z.string().max(500).optional(),
863
+ contextPaths: z.array(CONTEXT_PATH_SHAPE).max(20).optional()
864
+ },
865
+ async ({ name, color, description, contextPaths }) => {
866
+ try {
867
+ const result = await connection.call("createProjectTag", {
868
+ projectId,
869
+ name,
870
+ color,
871
+ description,
872
+ contextPaths
873
+ });
874
+ return textResult(`Tag created: ${result.id}`);
875
+ } catch (error) {
876
+ return errText("Failed to create tag", error);
877
+ }
878
+ }
879
+ );
880
+ }
881
+ function buildUpdateTagTool(connection, projectId) {
882
+ return defineTool(
883
+ "update_tag",
884
+ "Update a tag's name, color, description, or contextPaths. contextPaths is a FULL replacement \u2014 include existing links you want to keep. Verify paths exist in the repo first.",
885
+ {
886
+ tagId: z.string().describe("Tag id from list_tags"),
887
+ name: z.string().min(1).max(50).optional(),
888
+ color: z.string().regex(/^#[0-9a-fA-F]{6}$/).optional(),
889
+ description: z.string().max(500).optional(),
890
+ contextPaths: z.array(CONTEXT_PATH_SHAPE).max(20).optional()
891
+ },
892
+ async ({ tagId, name, color, description, contextPaths }) => {
893
+ try {
894
+ await connection.call("updateProjectTag", {
895
+ projectId,
896
+ tagId,
897
+ name,
898
+ color,
899
+ description,
900
+ contextPaths
901
+ });
902
+ return textResult(`Tag updated: ${tagId}`);
903
+ } catch (error) {
904
+ return errText("Failed to update tag", error);
905
+ }
906
+ }
907
+ );
908
+ }
909
+ function buildCreateSuggestionTool(connection, projectId) {
910
+ return defineTool(
911
+ "create_suggestion",
912
+ "File a project suggestion (idea/improvement for maintainers to review). Duplicates are AI-deduped into an existing suggestion with an upvote. Returns the suggestion id.",
913
+ {
914
+ title: z.string().min(1).describe("Short title"),
915
+ description: z.string().optional().describe("1-3 sentences: what should change and why"),
916
+ tag_names: z.array(z.string()).optional().describe("Tag names to categorize")
917
+ },
918
+ async ({ title, description, tag_names }) => {
919
+ try {
920
+ const result = await connection.call("createProjectSuggestion", {
921
+ projectId,
922
+ title,
923
+ description,
924
+ tagNames: tag_names
925
+ });
926
+ return textResult(
927
+ result.merged ? `Merged into existing suggestion ${result.mergedIntoId ?? result.id} (id: ${result.id})` : `Suggestion created: ${result.id}`
928
+ );
929
+ } catch (error) {
930
+ return errText("Failed to create suggestion", error);
931
+ }
932
+ }
933
+ );
934
+ }
935
+ function buildPostToProjectChatTool(connection, projectId) {
936
+ return defineTool(
937
+ "post_to_project_chat",
938
+ "Post a markdown message to the PROJECT chat \u2014 use once at the end of an audit for the summary the team reads.",
939
+ {
940
+ message: z.string().min(1).max(2e4)
941
+ },
942
+ async ({ message }) => {
943
+ try {
944
+ await connection.call("postToProjectChat", { projectId, content: message });
945
+ return textResult("Posted to project chat");
946
+ } catch (error) {
947
+ return errText("Failed to post to project chat", error);
948
+ }
949
+ }
950
+ );
951
+ }
952
+ function buildGetProjectTaskTool(connection, projectId) {
953
+ return defineTool(
954
+ "get_project_task",
955
+ "Fetch any task in the project by id or slug: title, description, plan, status, and metadata. The audit evidence trail starts here.",
956
+ {
957
+ taskId: z.string().describe("Task id or slug")
958
+ },
959
+ async ({ taskId }) => {
960
+ try {
961
+ const task = await connection.call("getProjectTask", { projectId, taskId });
962
+ return textResult(JSON.stringify(task, null, 2));
963
+ } catch (error) {
964
+ return errText("Failed to get task", error);
965
+ }
966
+ },
967
+ { annotations: { readOnlyHint: true } }
968
+ );
969
+ }
970
+ function buildReadProjectTaskChatTool(connection, projectId) {
971
+ return defineTool(
972
+ "read_project_task_chat",
973
+ "Read any project task's chat messages (newest last). role 'user' rows are HUMAN turns; 'assistant'/'system' rows are agent posts and activity-log entries.",
974
+ {
975
+ taskId: z.string().describe("Task id or slug"),
976
+ limit: z.number().int().min(1).max(200).optional().describe("Messages to fetch (default 50)")
977
+ },
978
+ async ({ taskId, limit }) => {
979
+ try {
980
+ const chat = await connection.call("getProjectTaskChat", {
981
+ projectId,
982
+ taskId,
983
+ limit: limit ?? 50
984
+ });
985
+ return textResult(JSON.stringify(chat, null, 2));
986
+ } catch (error) {
987
+ return errText("Failed to read task chat", error);
988
+ }
989
+ },
990
+ { annotations: { readOnlyHint: true } }
991
+ );
992
+ }
993
+ function buildGetProjectTaskLogsTool(connection, projectId) {
994
+ return defineTool(
995
+ "get_project_task_logs",
996
+ "Read any project task's persisted agent event stream (message / tool_use / turn_end / error / completed). Turn boundaries are turn_end events. Entries are truncated to ~2KB each; max 500 per call.",
997
+ {
998
+ taskId: z.string().describe("Task id or slug"),
999
+ limit: z.number().int().min(1).max(500).optional().describe("Entries to fetch (default 50)"),
1000
+ source: z.enum(["agent", "application"]).optional().describe("Filter: 'agent' = model events (default useful for grading)")
1001
+ },
1002
+ async ({ taskId, limit, source }) => {
1003
+ try {
1004
+ const logs = await connection.call("getProjectTaskCli", {
1005
+ projectId,
1006
+ taskId,
1007
+ limit: limit ?? 50,
1008
+ source
1009
+ });
1010
+ return textResult(JSON.stringify(logs, null, 2));
1011
+ } catch (error) {
1012
+ return errText("Failed to get task logs", error);
1013
+ }
1014
+ },
1015
+ { annotations: { readOnlyHint: true } }
1016
+ );
1017
+ }
1018
+ var TURN_GRADE_SHAPE = z.object({
1019
+ turnIndex: z.number().int().min(0),
1020
+ phase: z.enum(["planning", "building", "human"]),
1021
+ grade: z.enum(["correct", "neutral", "blunder"]),
1022
+ reasoning: z.string(),
1023
+ eventType: z.string().describe('e.g. "message", "tool_use", "human_message"'),
1024
+ eventSummary: z.string().max(200).describe("\u2264120 chars of what happened this turn")
1025
+ });
1026
+ var HUMAN_EVAL_SHAPE = z.object({
1027
+ messageIndex: z.number().int().min(0).describe("Index into the task's human messages, oldest first"),
1028
+ rating: z.number().int().min(-1).max(1),
1029
+ reasoning: z.string()
1030
+ });
1031
+ function buildReportTaskAuditResultTool(connection, projectId) {
1032
+ return defineTool(
1033
+ "report_task_audit_result",
1034
+ "Persist one audited task's grades (call once per task after grading it). Pass error instead to mark the audit failed when the evidence is unusable.",
1035
+ {
1036
+ taskId: z.string().describe("The audited task's id (NOT slug)"),
1037
+ summary: z.string().describe("3-6 sentences: what went well, what was wasted"),
1038
+ turnGrades: z.array(TURN_GRADE_SHAPE),
1039
+ planningAccuracy: z.number().min(0).max(1).nullable(),
1040
+ buildingAccuracy: z.number().min(0).max(1).nullable(),
1041
+ humanAccuracy: z.number().min(0).max(1).nullable(),
1042
+ planningCorrect: z.number().int().min(0),
1043
+ planningNeutral: z.number().int().min(0),
1044
+ planningBlunder: z.number().int().min(0),
1045
+ buildingCorrect: z.number().int().min(0),
1046
+ buildingNeutral: z.number().int().min(0),
1047
+ buildingBlunder: z.number().int().min(0),
1048
+ humanCorrect: z.number().int().min(0),
1049
+ humanNeutral: z.number().int().min(0),
1050
+ humanBlunder: z.number().int().min(0),
1051
+ humanEvaluations: z.array(HUMAN_EVAL_SHAPE).optional(),
1052
+ suggestionIds: z.array(z.string()).describe("Suggestion ids filed for this task, or []"),
1053
+ auditCostUsd: z.number().nullable(),
1054
+ model: z.string().nullable().describe("The model you are running as"),
1055
+ error: z.string().optional().describe("Set ONLY to mark this task's audit failed")
1056
+ },
1057
+ async (input) => {
1058
+ try {
1059
+ await connection.call("reportTaskAuditResult", {
1060
+ projectId,
1061
+ ...input,
1062
+ humanEvaluations: input.humanEvaluations ?? []
1063
+ });
1064
+ return textResult(
1065
+ input.error ? `Audit for ${input.taskId} marked failed` : `Audit result saved for ${input.taskId}`
1066
+ );
1067
+ } catch (error) {
1068
+ return errText("Failed to report audit result", error);
1069
+ }
1070
+ }
1071
+ );
1072
+ }
1073
+ function buildProjectTools(connection, projectId) {
1074
+ return [
1075
+ buildListTagsTool(connection, projectId),
1076
+ buildCreateTagTool(connection, projectId),
1077
+ buildUpdateTagTool(connection, projectId),
1078
+ buildCreateSuggestionTool(connection, projectId),
1079
+ buildPostToProjectChatTool(connection, projectId),
1080
+ buildGetProjectTaskTool(connection, projectId),
1081
+ buildReadProjectTaskChatTool(connection, projectId),
1082
+ buildGetProjectTaskLogsTool(connection, projectId),
1083
+ buildReportTaskAuditResultTool(connection, projectId)
1084
+ ];
1085
+ }
1086
+
827
1087
  // src/runner/adhoc-session-runner.ts
828
1088
  var ADHOC_SYSTEM_NOTE = "You are running in an ad-hoc Conveyor scratch pod \u2014 an interactive terminal on the project's repository checked out at its default branch. There is no task or plan; help the human with whatever they ask directly.";
1089
+ var HEADLESS_SYSTEM_NOTE = "You are running a HEADLESS Conveyor session on the project's repository \u2014 your instructions were auto-submitted as the first message. Work them to completion autonomously and never wait for user input; a human may attach to this terminal to watch or interject, but none is required.";
1090
+ function resolveInitialPrompt(env = process.env) {
1091
+ const b64 = env.CONVEYOR_INITIAL_PROMPT_B64;
1092
+ if (!b64) return null;
1093
+ try {
1094
+ const decoded = Buffer.from(b64, "base64").toString("utf8").trim();
1095
+ return decoded.length > 0 ? decoded : null;
1096
+ } catch {
1097
+ return null;
1098
+ }
1099
+ }
829
1100
  function resolveAdhocTui(env) {
830
1101
  const raw = env.CONVEYOR_TUI ?? "claude-code";
831
1102
  if (TUI_KINDS.includes(raw)) return raw;
@@ -839,24 +1110,29 @@ function buildAdhocPtyBridge(connection) {
839
1110
  onResize: (handler) => connection.onPtyResize(handler)
840
1111
  };
841
1112
  }
842
- function buildAdhocQueryOptions(workspaceDir, model, abortController, session, env = process.env) {
1113
+ function buildAdhocQueryOptions(workspaceDir, model, abortController, session, env = process.env, headless) {
1114
+ const systemNote = headless ? HEADLESS_SYSTEM_NOTE : ADHOC_SYSTEM_NOTE;
843
1115
  return {
844
1116
  model,
845
- systemPrompt: { type: "preset", preset: "claude_code", append: ADHOC_SYSTEM_NOTE },
846
- appendSystemPrompt: ADHOC_SYSTEM_NOTE,
1117
+ systemPrompt: { type: "preset", preset: "claude_code", append: systemNote },
1118
+ appendSystemPrompt: systemNote,
847
1119
  cwd: workspaceDir,
848
- // Human-driven interactive shell — no plan-mode gating, no Conveyor tools.
1120
+ // Human-driven interactive shell — no plan-mode gating. (Headless audit
1121
+ // runs keep the same bypass: they must finish with no one at the terminal.)
849
1122
  permissionMode: "bypassPermissions",
850
1123
  allowDangerouslySkipPermissions: true,
851
1124
  tools: { type: "preset", preset: "claude_code" },
852
- // Browser automation only — the baked playwright-mcp when present.
853
1125
  mcpServers: (() => {
854
1126
  const playwright = resolvePlaywrightMcpServer(env);
855
- return playwright ? { playwright } : {};
1127
+ return {
1128
+ ...playwright ? { playwright } : {},
1129
+ ...headless?.conveyorMcpServer ? { conveyor: headless.conveyorMcpServer } : {}
1130
+ };
856
1131
  })(),
857
1132
  settingSources: ["user", "project"],
858
- // Empty box, unsubmitted: the human types the first prompt themselves.
859
- promptDelivery: "prefill",
1133
+ // Interactive: empty box, unsubmitted the human types the first prompt.
1134
+ // Headless: paste + submit the server-assembled instructions immediately.
1135
+ promptDelivery: headless ? "submit" : "prefill",
860
1136
  abortController,
861
1137
  ...session?.sessionId ? { sessionId: session.sessionId } : {},
862
1138
  ...session?.resume ? { resume: session.resume } : {}
@@ -958,15 +1234,34 @@ var AdhocSessionRunner = class {
958
1234
  async runInteractiveTui() {
959
1235
  const model = this.config.model ?? process.env.CONVEYOR_AGENT_MODEL ?? process.env.CONVEYOR_ADHOC_MODEL ?? DEFAULT_SONNET_MODEL;
960
1236
  const session = resolveSessionStart(this.config.workspaceId, this.config.workspaceDir);
1237
+ const initialPrompt = this.config.initialPrompt ?? null;
1238
+ const headless = initialPrompt ? {
1239
+ initialPrompt,
1240
+ conveyorMcpServer: this.harness.createMcpServer({
1241
+ name: "conveyor",
1242
+ tools: buildProjectTools(this.connection, this.config.projectId)
1243
+ })
1244
+ } : void 0;
961
1245
  const options = buildAdhocQueryOptions(
962
1246
  this.config.workspaceDir,
963
1247
  model,
964
1248
  this.abortController,
965
- session
1249
+ session,
1250
+ process.env,
1251
+ headless
966
1252
  );
1253
+ let promptMarked = false;
967
1254
  try {
968
- for await (const event of this.harness.executeQuery({ prompt: "", options })) {
1255
+ for await (const event of this.harness.executeQuery({
1256
+ prompt: initialPrompt ?? "",
1257
+ options
1258
+ })) {
969
1259
  if (this.stopped) break;
1260
+ if (initialPrompt && !promptMarked) {
1261
+ promptMarked = true;
1262
+ void this.connection.call("markInitialPromptSubmitted", { sessionId: this.config.connection.sessionId }).catch(() => {
1263
+ });
1264
+ }
970
1265
  if (event.type === "result" && event.subtype === "error") {
971
1266
  throw new Error(event.errors.join("\n"));
972
1267
  }
@@ -1173,7 +1468,6 @@ var ReviewChildSupervisor = class {
1173
1468
  // src/runner/session-child.ts
1174
1469
  import { spawn as nodeSpawn2 } from "child_process";
1175
1470
  var logger3 = createServiceLogger("SessionChild");
1176
- var KILL_WAIT_MS2 = 5e3;
1177
1471
  function buildSpawnedChildEnv(baseEnv, data) {
1178
1472
  const env = { ...baseEnv };
1179
1473
  env.CONVEYOR_TASK_TOKEN = data.sessionJwt;
@@ -1283,7 +1577,7 @@ var SessionChildSupervisor = class {
1283
1577
  } catch {
1284
1578
  }
1285
1579
  resolve();
1286
- }, KILL_WAIT_MS2);
1580
+ }, KILL_WAIT_MS);
1287
1581
  timer.unref();
1288
1582
  child.once("exit", () => {
1289
1583
  clearTimeout(timer);
@@ -1577,6 +1871,9 @@ if (!CONVEYOR_TASK_ID && projectIdentity && CONVEYOR_MODE === "adhoc") {
1577
1871
  projectId: projectIdentity.projectId,
1578
1872
  workspaceId: projectIdentity.workspaceId,
1579
1873
  workspaceDir: CONVEYOR_WORKSPACE,
1874
+ // Headless sessions: un-submitted audit instructions from the bundle.
1875
+ // Resolved ONLY here — spawned same-pod tabs must not inherit them.
1876
+ initialPrompt: resolveInitialPrompt(process.env),
1580
1877
  ...process.env.CLAUDESPACE_NAME ? { lifecycle: { idleTimeoutMs: 60 * 60 * 1e3 } } : {}
1581
1878
  },
1582
1879
  {