@engineeros/connector 0.8.9 → 0.9.1

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/README.md CHANGED
@@ -26,10 +26,16 @@ npx --yes @engineeros/connector@latest pair PAIRING-CODE --url http://localhost:
26
26
 
27
27
  Connect a local Codex CLI workspace to EngineerOS through an outbound WebSocket.
28
28
 
29
- This package is only a connector. EngineerOS owns every prompt, Goal instruction, and
30
- execution boundary. The connector validates the assignment envelope, passes the supplied
31
- Markdown to Codex CLI unchanged, streams lifecycle events, and returns bounded results.
32
- It contains no product, assessment, planning, architecture, or delivery prompt templates.
29
+ ## Agent roles and skills
30
+
31
+ The selected ACP or Codex agent is the execution engine. EngineerOS chooses a provider-neutral role for each activity and the connector injects only that role's bundled skill:
32
+
33
+ - `research` uses `codebase-research` with read-only access for questions and workspace assessment.
34
+ - `planning` uses `change-planning` with read-only access for shaping, specifications, architecture, and design work.
35
+ - `implementation` uses `goal-execution` with workspace-write access only for a registered Goal.
36
+ - `verification` uses `change-verification` with read-only access for artifact review and independent Goal proof.
37
+
38
+ The connector rejects a role whose access does not match the assignment before starting the agent. Role-specific sessions preserve useful context without mixing planning, research, implementation, or verification responsibilities. A shared interaction contract makes the Agent infer the current project situation, lead with the useful outcome, and suggest one concrete next activity only when it genuinely helps. User-visible responses refer neutrally to the Agent; provider and harness details remain operational metadata.
33
39
 
34
40
  ## Onboard a workspace
35
41
 
@@ -45,7 +51,7 @@ From **Project steering -> Workspace**, run the workspace assessment to use the
45
51
 
46
52
  After onboarding, every project prompt is routed to this connection. Copilot, shaping, planning, architecture, and experience generation use the connected agent subscription and workspace context. Interactive prompts run independently from assessments and Goal scheduling. Prompt runs are read-only; only an explicitly registered Goal Run receives workspace-write access. If the connector is offline, EngineerOS asks the user to reconnect instead of silently switching models.
47
53
 
48
- Project prompts use resumable, purpose-specific coding-agent sessions. The connector keeps the external session identifiers in its local configuration, so Copilot and artifact conversations survive connector restarts. Changing the purpose, model, or reasoning effort starts a separate session. Goal implementation and verification remain isolated runs.
54
+ Project prompts use resumable, role- and purpose-specific agent sessions. The connector keeps the external session identifiers in its local configuration, so Copilot and artifact conversations survive connector restarts. Changing the role, purpose, model, or reasoning effort starts a separate session. Goal implementation and verification remain isolated runs.
49
55
 
50
56
  An empty or document-only folder establishes a greenfield baseline. A code-bearing folder is assessed as brownfield. Use **Rescan** in Steering after the local workspace changes.
51
57
 
@@ -86,3 +92,13 @@ npm install -g @openai/codex@latest
86
92
  codex --version
87
93
  npx @engineeros/connector start --workspace .
88
94
  ```
95
+
96
+ ## Publish the connector
97
+
98
+ Run the release workflow from this package instead of calling `npm publish` directly:
99
+
100
+ ```sh
101
+ npm run release:patch
102
+ ```
103
+
104
+ The workflow checks the versions already present on npm, keeps the current version when it is unpublished, or advances to the next unused patch version when necessary. It then runs the connector tests, syntax checks, and package dry run before publishing. A direct `npm publish` now stops early with the corrective command when its version already exists.
@@ -396,7 +396,7 @@ async function submitWorkspaceSnapshot() {
396
396
  `Sent ${snapshot.file_count} safe file(s); ${snapshot.excluded_file_count} sensitive or generated path(s) excluded.`,
397
397
  );
398
398
  console.log(
399
- `Workspace assessed as ${result.workspace_kind}. EngineerOS System State is current.`,
399
+ `Workspace inventory registered as ${result.workspace_kind}. Assessment queued.`,
400
400
  );
401
401
  snapshotInFlight = false;
402
402
  } catch (error) {
@@ -553,9 +553,7 @@ async function cancelPrompt(promptState) {
553
553
 
554
554
  async function executeAssessment(assignment) {
555
555
  const assessmentId = assignment.assessment_id;
556
- console.log(
557
- `Assessing workspace with ${codingAgent.name} (${assessmentId}).`,
558
- );
556
+ console.log(`Agent is assessing the workspace (${assessmentId}).`);
559
557
  let progress = 10;
560
558
  const reportedMilestones = new Set();
561
559
  const reportProgress = (message, { milestone = true } = {}) => {
@@ -607,9 +605,7 @@ async function executeAssessment(assignment) {
607
605
  `EngineerOS rejected the assessment (${response.status}): ${await response.text()}`,
608
606
  );
609
607
  }
610
- console.log(
611
- `${codingAgent.name} workspace assessment is current in EngineerOS.`,
612
- );
608
+ console.log("Workspace assessment is current in EngineerOS.");
613
609
  } catch (error) {
614
610
  if (socket.readyState === WebSocket.OPEN) {
615
611
  socket.send(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@engineeros/connector",
3
- "version": "0.8.9",
3
+ "version": "0.9.1",
4
4
  "description": "Connect a local coding agent to EngineerOS, using ACP when supported.",
5
5
  "private": false,
6
6
  "type": "module",
@@ -15,8 +15,10 @@
15
15
  },
16
16
  "scripts": {
17
17
  "start": "node ./bin/engineeros-connector.mjs",
18
+ "prepublishOnly": "node ./scripts/release.mjs check",
19
+ "release:patch": "node ./scripts/release.mjs patch",
18
20
  "test": "node --test --test-concurrency=1",
19
- "type-check": "node --check ./bin/engineeros-connector.mjs && node --check ./src/acp-client.mjs && node --check ./src/agent-registry.mjs && node --check ./src/capabilities.mjs && node --check ./src/cli-args.mjs && node --check ./src/config.mjs && node --check ./src/connection.mjs && node --check ./src/mcp-server.mjs && node --check ./src/runner.mjs"
21
+ "type-check": "node --check ./bin/engineeros-connector.mjs && node --check ./scripts/release.mjs && node --check ./src/acp-client.mjs && node --check ./src/agent-harness.mjs && node --check ./src/agent-registry.mjs && node --check ./src/capabilities.mjs && node --check ./src/cli-args.mjs && node --check ./src/config.mjs && node --check ./src/connection.mjs && node --check ./src/mcp-server.mjs && node --check ./src/runner.mjs"
20
22
  },
21
23
  "engines": {
22
24
  "node": ">=22"
@@ -0,0 +1,183 @@
1
+ import { readFileSync } from "node:fs";
2
+
3
+ const ROLE_DEFINITIONS = Object.freeze({
4
+ research: Object.freeze({
5
+ title: "Repository Research",
6
+ sandboxMode: "read-only",
7
+ skill: "codebase-research",
8
+ instruction:
9
+ "Investigate the connected workspace deeply enough to answer from observed evidence. Trace relevant relationships, distinguish facts from inference, and do not change the workspace.",
10
+ }),
11
+ planning: Object.freeze({
12
+ title: "Change Planning",
13
+ sandboxMode: "read-only",
14
+ skill: "change-planning",
15
+ instruction:
16
+ "Turn the request and current workspace evidence into a decision-complete change plan. Resolve discoverable questions through inspection, state consequential assumptions, and do not implement the plan.",
17
+ }),
18
+ implementation: Object.freeze({
19
+ title: "Goal Implementation",
20
+ sandboxMode: "workspace-write",
21
+ skill: "goal-execution",
22
+ instruction:
23
+ "Execute the bounded Goal autonomously, keep changes inside its boundary, and verify the result. EngineerOS owns the final commit and acceptance workflow.",
24
+ }),
25
+ verification: Object.freeze({
26
+ title: "Independent Verification",
27
+ sandboxMode: "read-only",
28
+ skill: "change-verification",
29
+ instruction:
30
+ "Independently inspect and test the supplied revision. Report only directly observed evidence and do not repair or otherwise modify the workspace.",
31
+ }),
32
+ });
33
+
34
+ const skillCache = new Map();
35
+
36
+ export const AGENT_ROLE_IDS = Object.freeze(Object.keys(ROLE_DEFINITIONS));
37
+ export const AGENT_SKILL_IDS = Object.freeze(
38
+ AGENT_ROLE_IDS.map((roleId) => ROLE_DEFINITIONS[roleId].skill),
39
+ );
40
+
41
+ export function agentHarnessCapabilities() {
42
+ return {
43
+ agent_roles: [...AGENT_ROLE_IDS],
44
+ agent_skills: [...AGENT_SKILL_IDS],
45
+ };
46
+ }
47
+
48
+ export function buildAgentHarnessPrompt({
49
+ agentRole,
50
+ prompt,
51
+ sandboxMode,
52
+ requiredOutputHeading,
53
+ }) {
54
+ const role = ROLE_DEFINITIONS[agentRole];
55
+ if (!role) {
56
+ throw new Error(
57
+ `EngineerOS assignment has an unsupported agent_role. Expected one of: ${AGENT_ROLE_IDS.join(", ")}.`,
58
+ );
59
+ }
60
+ if (role.sandboxMode !== sandboxMode) {
61
+ throw new Error(
62
+ `EngineerOS ${agentRole} role requires ${role.sandboxMode} access, but the assignment requested ${sandboxMode}.`,
63
+ );
64
+ }
65
+ const assignment = String(prompt || "").trim();
66
+ if (!assignment) {
67
+ throw new Error("EngineerOS assignment is missing prompt_markdown.");
68
+ }
69
+ const outputHeading = normalizedOutputHeading(requiredOutputHeading);
70
+
71
+ const sections = [
72
+ "# EngineerOS Agent Assignment",
73
+ "",
74
+ "## Active Role",
75
+ "",
76
+ `- Role: ${role.title}`,
77
+ `- Access: ${role.sandboxMode}`,
78
+ `- Responsibility: ${role.instruction}`,
79
+ "- User-facing language: refer to yourself neutrally as the Agent. Do not expose internal role, harness, provider, or coding-agent terminology unless the user asks.",
80
+ ];
81
+ if (!outputHeading) {
82
+ sections.push(
83
+ "",
84
+ "## Interaction Contract",
85
+ "",
86
+ "- Infer the current project situation from the assignment and workspace evidence before responding.",
87
+ "- Lead with the useful answer or outcome. Do not narrate routine searches, tool calls, or internal work.",
88
+ "- When one next activity clearly follows and would help, offer exactly one short, concrete suggestion. Do not force a next step when none is useful.",
89
+ "- Ask a question only when a consequential choice cannot be resolved from available evidence.",
90
+ "- If the Assignment defines a response format, follow it exactly instead of adding conversational guidance.",
91
+ );
92
+ }
93
+ sections.push(
94
+ "",
95
+ "## Active Skill",
96
+ "",
97
+ bundledSkill(role.skill),
98
+ "",
99
+ "## Assignment",
100
+ "",
101
+ assignment,
102
+ );
103
+ if (outputHeading) {
104
+ sections.push(
105
+ "",
106
+ "## Final Response Contract",
107
+ "",
108
+ "- Return only the structured Markdown required by the Assignment.",
109
+ `- The first non-whitespace line must be exactly: ${outputHeading}`,
110
+ "- Do not add a preamble, status message, commentary, or code fence.",
111
+ "- Do not append a conversational summary or next activity.",
112
+ );
113
+ }
114
+ return sections.join("\n");
115
+ }
116
+
117
+ export function normalizeAgentStructuredOutput(
118
+ response,
119
+ requiredOutputHeading,
120
+ ) {
121
+ const outputHeading = normalizedOutputHeading(requiredOutputHeading);
122
+ if (!outputHeading) {
123
+ throw new Error(
124
+ "EngineerOS structured output requires a heading contract.",
125
+ );
126
+ }
127
+ const content = String(response || "").trim();
128
+ if (!content) {
129
+ throw new Error("Agent completed without returning a response.");
130
+ }
131
+ const lines = content.split(/\r?\n/);
132
+ const headingIndex = lines.findIndex(
133
+ (line, index) => index < 20 && line.trim() === outputHeading,
134
+ );
135
+ const preamble =
136
+ headingIndex >= 0 ? lines.slice(0, headingIndex).join("\n") : content;
137
+ if (headingIndex < 0) {
138
+ throw new Error(
139
+ `Agent response is missing the required heading '${outputHeading}'.`,
140
+ );
141
+ }
142
+ if (preamble.length > 2_000) {
143
+ throw new Error(
144
+ `Agent response contains too much text before the required heading '${outputHeading}'.`,
145
+ );
146
+ }
147
+ const normalized = lines.slice(headingIndex).join("\n").trim();
148
+ if (/```/.test(preamble) || /(?:^|\n)```\s*$/.test(normalized)) {
149
+ throw new Error(
150
+ `Agent response must return '${outputHeading}' as plain Markdown, without a code fence.`,
151
+ );
152
+ }
153
+ return normalized;
154
+ }
155
+
156
+ function bundledSkill(skillName) {
157
+ const cached = skillCache.get(skillName);
158
+ if (cached) return cached;
159
+ try {
160
+ const content = readFileSync(
161
+ new URL(`./skills/${skillName}/SKILL.md`, import.meta.url),
162
+ "utf8",
163
+ ).trim();
164
+ skillCache.set(skillName, content);
165
+ return content;
166
+ } catch (error) {
167
+ throw new Error(
168
+ `EngineerOS bundled skill '${skillName}' is unavailable. Reinstall @engineeros/connector.`,
169
+ { cause: error },
170
+ );
171
+ }
172
+ }
173
+
174
+ function normalizedOutputHeading(value) {
175
+ if (value === undefined || value === null) return null;
176
+ const heading = String(value).trim();
177
+ if (!/^# [^\r\n]+$/.test(heading)) {
178
+ throw new Error(
179
+ "EngineerOS requiredOutputHeading must be one level-one Markdown heading.",
180
+ );
181
+ }
182
+ return heading;
183
+ }
@@ -1,4 +1,5 @@
1
1
  import path from "node:path";
2
+ import { agentHarnessCapabilities } from "./agent-harness.mjs";
2
3
 
3
4
  export function advertisedCapabilities(config, codingAgent) {
4
5
  const configuredModels = String(
@@ -15,6 +16,7 @@ export function advertisedCapabilities(config, codingAgent) {
15
16
  workspace_name: path.basename(config.workspace),
16
17
  agent_name: codingAgent.name,
17
18
  agent_version: codingAgent.version,
19
+ ...agentHarnessCapabilities(),
18
20
  execution_profiles: {
19
21
  model_selection: codingAgent.protocol === "codex",
20
22
  models: codingAgent.protocol === "codex" ? configuredModels : [],
package/src/runner.mjs CHANGED
@@ -12,6 +12,10 @@ import path from "node:path";
12
12
  import { promisify } from "node:util";
13
13
  import { deflateRaw } from "node:zlib";
14
14
  import { launchAcpAgent } from "./acp-client.mjs";
15
+ import {
16
+ buildAgentHarnessPrompt,
17
+ normalizeAgentStructuredOutput,
18
+ } from "./agent-harness.mjs";
15
19
 
16
20
  const deflate = promisify(deflateRaw);
17
21
  const MAX_ARCHIVE_BYTES = 25 * 1024 * 1024;
@@ -109,15 +113,25 @@ export async function executeAssignment(assignment, config, callbacks) {
109
113
  await controller.completed;
110
114
  const changedFiles = await changedFilePaths(runWorkspace);
111
115
  if (!changedFiles.length)
112
- throw new Error("Codex completed without changing any files.");
116
+ throw new Error("Agent completed without changing any files.");
113
117
  const committed = await commitRunChange(
114
118
  runWorkspace,
115
119
  assignment.base_revision,
116
120
  assignment.run_id,
117
121
  );
122
+ const verificationHeading = "# Verification Report";
118
123
  const verifier = launchAgentProcess(
119
124
  runWorkspace,
120
- verificationPrompt(assignment, committed.revision, committed.changedFiles),
125
+ buildAgentHarnessPrompt({
126
+ agentRole: "verification",
127
+ prompt: verificationPrompt(
128
+ assignment,
129
+ committed.revision,
130
+ committed.changedFiles,
131
+ ),
132
+ sandboxMode: "read-only",
133
+ requiredOutputHeading: verificationHeading,
134
+ }),
121
135
  "read-only",
122
136
  config,
123
137
  callbacks,
@@ -125,8 +139,12 @@ export async function executeAssignment(assignment, config, callbacks) {
125
139
  );
126
140
  callbacks.onProcess?.(verifier.child);
127
141
  const verification = await verifier.completed;
128
- const proofEvidence = parseVerificationReport(
142
+ const verificationReport = normalizeAgentStructuredOutput(
129
143
  verification.finalMessage,
144
+ verificationHeading,
145
+ );
146
+ const proofEvidence = parseVerificationReport(
147
+ verificationReport,
130
148
  assignment.proof_checks,
131
149
  config.connector_id,
132
150
  assignment.run_id,
@@ -138,7 +156,7 @@ export async function executeAssignment(assignment, config, callbacks) {
138
156
  diff_patch: committed.diffPatch,
139
157
  changed_files: committed.changedFiles,
140
158
  proof_evidence: proofEvidence,
141
- verification_report: verification.finalMessage,
159
+ verification_report: verificationReport,
142
160
  };
143
161
  }
144
162
 
@@ -290,7 +308,7 @@ export async function executeWorkspaceAssessment(
290
308
  config,
291
309
  callbacks,
292
310
  ) {
293
- const execution = connectorExecution(assignment);
311
+ const execution = workspaceAssessmentExecution(assignment);
294
312
  const startingRevision = await run(
295
313
  "git",
296
314
  ["rev-parse", "HEAD"],
@@ -333,8 +351,10 @@ export async function executeWorkspaceAssessment(
333
351
  );
334
352
  callbacks.onProcess?.(controller.child);
335
353
  const completed = await controller.completed;
336
- const report = completed.finalMessage.trim();
337
- if (!report) throw new Error("Codex completed without returning a response.");
354
+ const report = normalizeAgentStructuredOutput(
355
+ completed.finalMessage,
356
+ execution.requiredOutputHeading,
357
+ );
338
358
  const endingRevision = await run(
339
359
  "git",
340
360
  ["rev-parse", "HEAD"],
@@ -495,7 +515,7 @@ export async function executeConnectedPrompt(assignment, config, callbacks) {
495
515
  const completed = await controller.completed;
496
516
  const content = completed.finalMessage.trim();
497
517
  if (!content) {
498
- throw new Error("Codex completed without returning a response.");
518
+ throw new Error("Agent completed without returning a response.");
499
519
  }
500
520
  return {
501
521
  content,
@@ -655,9 +675,20 @@ function assessmentCommandMilestone(command) {
655
675
  return "Tracing architecture and code relationships";
656
676
  }
657
677
 
658
- export function connectorExecution(assignment) {
659
- const prompt = assignment?.prompt_markdown;
660
- if (typeof prompt !== "string" || !prompt.trim()) {
678
+ export function workspaceAssessmentExecution(assignment) {
679
+ const requiredOutputHeading =
680
+ assignment?.assessment_mode === "incremental"
681
+ ? "# Workspace Assessment Delta"
682
+ : "# Workspace Assessment";
683
+ return {
684
+ ...connectorExecution(assignment, { requiredOutputHeading }),
685
+ requiredOutputHeading,
686
+ };
687
+ }
688
+
689
+ export function connectorExecution(assignment, options = {}) {
690
+ const rawPrompt = assignment?.prompt_markdown;
691
+ if (typeof rawPrompt !== "string" || !rawPrompt.trim()) {
661
692
  throw new Error("EngineerOS assignment is missing prompt_markdown.");
662
693
  }
663
694
  const sandboxMode = assignment?.sandbox_mode;
@@ -676,14 +707,24 @@ export function connectorExecution(assignment) {
676
707
  "EngineerOS assignment has an unsupported reasoning effort.",
677
708
  );
678
709
  }
710
+ const agentRole = assignment?.agent_role;
711
+ const prompt = buildAgentHarnessPrompt({
712
+ agentRole,
713
+ prompt: rawPrompt,
714
+ sandboxMode,
715
+ requiredOutputHeading: options.requiredOutputHeading,
716
+ });
679
717
  return {
680
718
  prompt,
719
+ agentRole,
681
720
  sandboxMode,
682
721
  sessionKey:
683
722
  typeof assignment.session_key === "string" &&
684
723
  assignment.session_key.trim()
685
724
  ? assignment.session_key.trim()
686
- : `project-${String(assignment.purpose || "general")
725
+ : `project-${String(agentRole)}-${String(
726
+ assignment.purpose || "general",
727
+ )
687
728
  .toLowerCase()
688
729
  .replace(/[^a-z0-9]+/g, "-")
689
730
  .slice(0, 80)}`,
@@ -895,7 +936,7 @@ function launchCodexProcess(
895
936
  else if (code === 0)
896
937
  reject(
897
938
  new Error(
898
- "Codex completed without announcing a resumable session id.",
939
+ "Agent completed without announcing a resumable session id.",
899
940
  ),
900
941
  );
901
942
  else reject(new Error(codexFailureMessage(output, code)));
@@ -0,0 +1,12 @@
1
+ ---
2
+ name: change-planning
3
+ description: Produce a decision-complete implementation plan grounded in the connected repository.
4
+ ---
5
+
6
+ # Change Planning
7
+
8
+ 1. Inspect current behavior, ownership boundaries, call paths, tests, and project instructions before proposing changes.
9
+ 2. Define the intended outcome, affected components, non-goals, risks, and proof of completion.
10
+ 3. Resolve questions answerable from the repository. Label only genuinely unavailable facts as assumptions.
11
+ 4. Sequence bounded implementation steps with the files or symbols each step affects and the verification it requires.
12
+ 5. Stay read-only and hand off a plan that an implementation Agent can execute without rediscovering the problem.
@@ -0,0 +1,12 @@
1
+ ---
2
+ name: change-verification
3
+ description: Independently verify a completed change against its explicit proof contract.
4
+ ---
5
+
6
+ # Change Verification
7
+
8
+ 1. Inspect the exact supplied revision, changed paths, Goal boundaries, constraints, and every Proof item.
9
+ 2. Run or inspect each check independently; do not rely on the implementation Agent's claims.
10
+ 3. Stay read-only. Do not repair failures, edit files, install dependencies, commit, or push.
11
+ 4. Mark a check passed only when directly observed evidence matches its expected result.
12
+ 5. Return the requested structured verification report with concise commands, exit codes, and evidence.
@@ -0,0 +1,12 @@
1
+ ---
2
+ name: codebase-research
3
+ description: Investigate a connected repository and answer from directly observed evidence.
4
+ ---
5
+
6
+ # Codebase Research
7
+
8
+ 1. Define the exact question and inspect the smallest relevant surface first.
9
+ 2. Trace callers, dependencies, data flow, configuration, and tests when they materially affect the answer.
10
+ 3. Separate observed facts from inferences and cite concrete repository paths for important claims.
11
+ 4. Stay read-only. Do not install, generate, edit, delete, commit, or start long-running services.
12
+ 5. Return the answer first, followed by supporting evidence and unresolved gaps only when they matter.
@@ -0,0 +1,12 @@
1
+ ---
2
+ name: goal-execution
3
+ description: Implement one bounded EngineerOS Goal and prove the resulting behavior.
4
+ ---
5
+
6
+ # Goal Execution
7
+
8
+ 1. Read the complete Goal packet, repository instructions, current Git state, and relevant implementation paths.
9
+ 2. Implement the smallest coherent change that satisfies the included outcome and constraints.
10
+ 3. Preserve unrelated user changes and stay inside the stated boundary; stop only for a genuine missing authority or prerequisite.
11
+ 4. Run focused checks while working, then the proportionate final tests, lint, type checks, or build required by the Goal.
12
+ 5. Do not commit or push. EngineerOS creates the isolated run commit after the implementation completes.