@engineeros/connector 0.4.4 → 0.4.7

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.
@@ -147,7 +147,9 @@ async function connect() {
147
147
  if (message.type === "workspace.assessment") {
148
148
  if (
149
149
  active?.runId !== message.assessment_id &&
150
- !assessments.some((candidate) => candidate.assessment_id === message.assessment_id)
150
+ !assessments.some(
151
+ (candidate) => candidate.assessment_id === message.assessment_id,
152
+ )
151
153
  ) {
152
154
  assessments.push(message);
153
155
  }
@@ -327,7 +329,9 @@ function pump() {
327
329
 
328
330
  async function executePrompt(assignment) {
329
331
  const promptId = assignment.prompt_id;
330
- console.log(`Answering ${assignment.purpose || "project"} prompt with Codex CLI.`);
332
+ console.log(
333
+ `Answering ${assignment.purpose || "project"} prompt with Codex CLI.`,
334
+ );
331
335
  try {
332
336
  const result = await executeConnectedPrompt(assignment, config, {
333
337
  onProcess: (child) => {
@@ -366,8 +370,12 @@ async function executeAssessment(assignment) {
366
370
  const assessmentId = assignment.assessment_id;
367
371
  console.log(`Assessing workspace with Codex CLI (${assessmentId}).`);
368
372
  let progress = 10;
369
- const reportProgress = (message) => {
370
- if (socket.readyState !== WebSocket.OPEN || active?.runId !== assessmentId) return;
373
+ const reportedMilestones = new Set();
374
+ const reportProgress = (message, { milestone = true } = {}) => {
375
+ if (socket.readyState !== WebSocket.OPEN || active?.runId !== assessmentId)
376
+ return;
377
+ if (milestone && reportedMilestones.has(message)) return;
378
+ if (milestone) reportedMilestones.add(message);
371
379
  socket.send(
372
380
  JSON.stringify({
373
381
  type: "workspace.assessment.progress",
@@ -377,12 +385,16 @@ async function executeAssessment(assignment) {
377
385
  }),
378
386
  );
379
387
  };
380
- reportProgress("Codex is inspecting the workspace");
388
+ reportProgress("Starting read-only repository assessment");
381
389
  const heartbeat = setInterval(() => {
382
390
  progress = Math.min(90, progress + 5);
383
- reportProgress("Codex is building the evidence-backed assessment");
391
+ reportProgress("Assessment in progress", { milestone: false });
384
392
  }, 15_000);
385
393
  try {
394
+ if (assignment.assessment_mode === "incremental") {
395
+ progress = 15;
396
+ reportProgress("Calculating changed files and affected evidence");
397
+ }
386
398
  const result = await executeWorkspaceAssessment(assignment, config, {
387
399
  onProcess: (child) => {
388
400
  if (active?.runId === assessmentId) active.child = child;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@engineeros/connector",
3
- "version": "0.4.4",
3
+ "version": "0.4.7",
4
4
  "description": "Connect a local Codex CLI workspace to EngineerOS over an outbound WebSocket.",
5
5
  "private": false,
6
6
  "type": "module",
package/src/runner.mjs CHANGED
@@ -145,9 +145,23 @@ export async function executeWorkspaceAssessment(
145
145
  "This workspace is no longer at the commit inventoried by EngineerOS. Refresh the workspace inventory before assessing it.",
146
146
  );
147
147
  }
148
+ const changeImpact =
149
+ assignment.assessment_mode === "incremental"
150
+ ? await workspaceChangeImpact(
151
+ config.workspace,
152
+ assignment.base_head_revision,
153
+ assignment.target_head_revision,
154
+ )
155
+ : { changedFiles: [], markdown: null };
156
+ const prompt = changeImpact.markdown
157
+ ? execution.prompt.replace(
158
+ "<!-- ENGINEEROS_CHANGE_IMPACT -->",
159
+ changeImpact.markdown,
160
+ )
161
+ : execution.prompt;
148
162
  const controller = launchCodexProcess(
149
163
  config.workspace,
150
- execution.prompt,
164
+ prompt,
151
165
  execution.sandboxMode,
152
166
  callbacks,
153
167
  );
@@ -173,9 +187,97 @@ export async function executeWorkspaceAssessment(
173
187
  return {
174
188
  report_markdown: report,
175
189
  observed_head_revision: endingHead,
190
+ changed_files: changeImpact.changedFiles,
191
+ change_impact_markdown: changeImpact.markdown,
176
192
  };
177
193
  }
178
194
 
195
+ export async function workspaceChangeImpact(workspace, baseRevision, targetRevision) {
196
+ if (!baseRevision || !targetRevision) {
197
+ throw new Error(
198
+ "Incremental assessment requires both the previously assessed and current Git commits. Run a full assessment instead.",
199
+ );
200
+ }
201
+ const range = `${baseRevision}..${targetRevision}`;
202
+ const names = await run(
203
+ "git",
204
+ ["diff", "--name-status", "--find-renames", range],
205
+ workspace,
206
+ { allowFailure: true },
207
+ );
208
+ if (names.code !== 0) {
209
+ throw new Error(
210
+ "Codex could not compare the assessed and current commits. Fetch the missing Git history or run a full assessment.",
211
+ );
212
+ }
213
+ const committedFiles = await run(
214
+ "git",
215
+ ["-c", "core.quotepath=false", "diff", "--name-only", range],
216
+ workspace,
217
+ { allowFailure: true },
218
+ );
219
+ const workingFiles = await run(
220
+ "git",
221
+ ["-c", "core.quotepath=false", "ls-files", "--others", "--modified", "--deleted", "--exclude-standard"],
222
+ workspace,
223
+ { allowFailure: true },
224
+ );
225
+ const status = await run("git", ["status", "--short"], workspace, {
226
+ allowFailure: true,
227
+ });
228
+ const stat = await run(
229
+ "git",
230
+ ["diff", "--stat", "--compact-summary", range],
231
+ workspace,
232
+ { allowFailure: true },
233
+ );
234
+ const allChangedFiles = [...new Set([
235
+ ...pathLines(committedFiles.stdout),
236
+ ...pathLines(workingFiles.stdout),
237
+ ])];
238
+ if (allChangedFiles.length > 500) {
239
+ throw new Error(
240
+ `This change affects ${allChangedFiles.length} paths, above the 500-path incremental limit. Run a full reassessment instead.`,
241
+ );
242
+ }
243
+ const changedFiles = allChangedFiles;
244
+ if (!changedFiles.length) {
245
+ throw new Error(
246
+ "The inventoried repository changed but Git reports no assessable paths. Refresh inventory or run a full assessment.",
247
+ );
248
+ }
249
+ const lines = [
250
+ `Comparison: \`${range}\``,
251
+ `Changed paths: ${changedFiles.length}`,
252
+ "",
253
+ "### Name status",
254
+ "",
255
+ "```text",
256
+ boundedText(names.stdout, 12_000),
257
+ "```",
258
+ ];
259
+ if (status.stdout.trim()) {
260
+ lines.push("", "### Working tree", "", "```text", boundedText(status.stdout, 6_000), "```");
261
+ }
262
+ if (stat.stdout.trim()) {
263
+ lines.push("", "### Diff summary", "", "```text", boundedText(stat.stdout, 6_000), "```");
264
+ }
265
+ return { changedFiles, markdown: lines.join("\n") };
266
+ }
267
+
268
+ function pathLines(output) {
269
+ return String(output || "")
270
+ .split(/\r?\n/)
271
+ .map((line) => line.trim())
272
+ .filter(Boolean)
273
+ .map((line) => line.replace(/^"|"$/g, ""));
274
+ }
275
+
276
+ function boundedText(value, maximum) {
277
+ const text = String(value || "").trim();
278
+ return text.length <= maximum ? text : `${text.slice(0, maximum)}\n... truncated by EngineerOS`;
279
+ }
280
+
179
281
  export async function executeConnectedPrompt(assignment, config, callbacks) {
180
282
  const execution = connectorExecution(assignment);
181
283
  const controller = launchCodexProcess(
@@ -229,32 +331,45 @@ export function codexFailureMessage(output, code) {
229
331
  export function assessmentProgressMessage(event) {
230
332
  if (!event || typeof event !== "object") return null;
231
333
  if (event.type === "turn.started")
232
- return "Codex started analyzing repository evidence";
334
+ return "Reviewing repository structure and current Git state";
233
335
  if (event.type === "item.started") {
234
336
  if (event.item?.type === "command_execution") {
235
- const command = compactCommand(event.item.command);
236
- return command
237
- ? `Inspecting with ${command}`
238
- : "Inspecting repository files";
337
+ return assessmentCommandMilestone(event.item.command);
239
338
  }
240
339
  if (event.item?.type === "mcp_tool_call")
241
- return "Consulting a connected analysis tool";
340
+ return "Tracing architecture and code relationships";
242
341
  if (event.item?.type === "web_search")
243
342
  return "Checking an external technical reference";
244
343
  return null;
245
344
  }
246
345
  if (event.type === "item.completed" && event.item?.type === "agent_message") {
247
- return "Assessment report prepared";
346
+ return "Synthesizing findings and highest-return actions";
248
347
  }
249
348
  return null;
250
349
  }
251
350
 
252
- function compactCommand(command) {
351
+ function assessmentCommandMilestone(command) {
253
352
  const value = Array.isArray(command)
254
353
  ? command.join(" ")
255
354
  : String(command || "");
256
- const compact = value.replace(/\s+/g, " ").trim();
257
- return compact.length > 96 ? `${compact.slice(0, 93)}...` : compact;
355
+ const normalized = value.replace(/\s+/g, " ").trim().toLowerCase();
356
+ if (!normalized) return "Inspecting repository evidence";
357
+ if (/\bgit\s+(status|log|diff|show|rev-parse)\b/.test(normalized)) {
358
+ return "Comparing Git history and workspace changes";
359
+ }
360
+ if (
361
+ /\b(test|pytest|vitest|jest|ruff|eslint|tsc|build|lint)\b/.test(normalized)
362
+ ) {
363
+ return "Checking verification and delivery signals";
364
+ }
365
+ if (
366
+ /\b(audit|dependency|dependencies|lockfile|package-lock|pnpm-lock|requirements)\b/.test(
367
+ normalized,
368
+ )
369
+ ) {
370
+ return "Reviewing dependencies and security signals";
371
+ }
372
+ return "Tracing architecture and code relationships";
258
373
  }
259
374
 
260
375
  export function connectorExecution(assignment) {
@@ -470,20 +585,17 @@ function runCodexCommand(command, args, cwd) {
470
585
  async function changedFilePaths(workspace) {
471
586
  const tracked = await run(
472
587
  "git",
473
- ["diff", "--name-only", "HEAD", "--"],
588
+ ["diff", "--name-only", "-z", "HEAD", "--"],
474
589
  workspace,
475
590
  );
476
591
  const untracked = await run(
477
592
  "git",
478
- ["ls-files", "--others", "--exclude-standard"],
593
+ ["ls-files", "-z", "--others", "--exclude-standard"],
479
594
  workspace,
480
595
  );
481
596
  return [
482
597
  ...new Set(
483
- `${tracked.stdout}\n${untracked.stdout}`
484
- .split(/\r?\n/)
485
- .map(normalizePath)
486
- .filter(Boolean),
598
+ [...gitPathList(tracked.stdout), ...gitPathList(untracked.stdout)],
487
599
  ),
488
600
  ].sort();
489
601
  }
@@ -495,16 +607,15 @@ async function boundedDiff(workspace, changedFiles) {
495
607
  workspace,
496
608
  );
497
609
  const untracked = new Set(
498
- (
499
- await run(
500
- "git",
501
- ["ls-files", "--others", "--exclude-standard"],
502
- workspace,
503
- )
504
- ).stdout
505
- .split(/\r?\n/)
506
- .map(normalizePath)
507
- .filter(Boolean),
610
+ gitPathList(
611
+ (
612
+ await run(
613
+ "git",
614
+ ["ls-files", "-z", "--others", "--exclude-standard"],
615
+ workspace,
616
+ )
617
+ ).stdout,
618
+ ),
508
619
  );
509
620
  const parts = [tracked.stdout];
510
621
  for (const relative of changedFiles.filter((item) => untracked.has(item))) {
@@ -523,15 +634,13 @@ async function boundedDiff(workspace, changedFiles) {
523
634
  return diff;
524
635
  }
525
636
 
526
- async function repositoryArchive(workspace) {
637
+ export async function repositoryArchive(workspace) {
527
638
  const listed = await run(
528
639
  "git",
529
- ["ls-files", "-co", "--exclude-standard"],
640
+ ["ls-files", "-z", "-co", "--exclude-standard"],
530
641
  workspace,
531
642
  );
532
- const files = [
533
- ...new Set(listed.stdout.split(/\r?\n/).map(normalizePath).filter(Boolean)),
534
- ].sort();
643
+ const files = [...new Set(gitPathList(listed.stdout))].sort();
535
644
  return createZip(
536
645
  await Promise.all(
537
646
  files.map(async (relative) => ({
@@ -686,6 +795,13 @@ function normalizePath(value) {
686
795
  .replace(/^\.\//, "");
687
796
  }
688
797
 
798
+ function gitPathList(value) {
799
+ return String(value ?? "")
800
+ .split("\0")
801
+ .map(normalizePath)
802
+ .filter(Boolean);
803
+ }
804
+
689
805
  function run(command, args, cwd, options = {}) {
690
806
  return new Promise((resolve, reject) => {
691
807
  const child = spawn(command, args, {