Add a thin orchestration layer that drives the existing Orb runtime to progress issue-scoped work. OrbProjectManager creates/resumes Orb runs, assembles context packs, projects OrbEvents into durable project events, detects needs-input and work-complete conditions, forwards follow-up messages, and runs the Git/Gitea lifecycle on completion. Port interfaces (OrbAdapter, OrbRunPort, GitLifecyclePort) make the orchestrator testable with fakes. The real adapters wrap OrbRuntime and runPostRunGiteaLifecycle without reimplementing Docker, AgentOS, or OpenCode. Idempotency: duplicate starts reuse active runs, duplicate completion returns cached results, cancel is idempotent. Never auto-merges. 88 tests pass (19 orchestration + 12 event-mapping + orb suite), 2 live tests skipped; lint and type checks clean.
110 lines
3.4 KiB
TypeScript
110 lines
3.4 KiB
TypeScript
import { Schema } from "effect";
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Context pack — assembled from ProjectIssue, evidence, project docs, artifacts
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export const ContextFile = Schema.Struct({
|
|
content: Schema.String,
|
|
label: Schema.String,
|
|
});
|
|
export type ContextFile = typeof ContextFile.Type;
|
|
|
|
export const ContextPackInput = Schema.Struct({
|
|
artifacts: Schema.Array(
|
|
Schema.Struct({
|
|
content: Schema.String,
|
|
path: Schema.String,
|
|
})
|
|
),
|
|
contextFiles: Schema.Array(ContextFile),
|
|
evidence: Schema.Array(
|
|
Schema.Struct({
|
|
content: Schema.String,
|
|
source: Schema.String,
|
|
})
|
|
),
|
|
issueBody: Schema.String,
|
|
issueNumber: Schema.Int,
|
|
issueTitle: Schema.String,
|
|
repositoryMetadata: Schema.Struct({
|
|
baseBranch: Schema.String,
|
|
repositoryName: Schema.String,
|
|
repositoryUrl: Schema.String,
|
|
}),
|
|
});
|
|
export type ContextPackInput = typeof ContextPackInput.Type;
|
|
|
|
const section = (heading: string, lines: readonly string[]): string => {
|
|
if (lines.length === 0) {
|
|
return "";
|
|
}
|
|
return `## ${heading}\n\n${lines.join("\n")}`;
|
|
};
|
|
|
|
const OPERATIONAL_INSTRUCTIONS = `## Operational Instructions
|
|
|
|
You are working inside an isolated sandbox on a dedicated work branch. The repository checkout is your working directory.
|
|
|
|
1. Inspect existing source before changing anything.
|
|
2. Implement the complete issue scope. Run the project's test or verification command.
|
|
3. If you need human input to proceed safely, emit exactly one line starting with \`NEEDS_INPUT:\` followed by your question, then stop.
|
|
4. When implementation and verification are complete, emit exactly one line starting with \`WORK_COMPLETE:\` followed by a one-sentence summary, then stop.
|
|
5. Do not merge, deploy, or push. The orchestrator handles the Git lifecycle after you signal completion.
|
|
6. Never claim a change you did not observe. Preserve command evidence.`;
|
|
|
|
/**
|
|
* Build a concise context pack prompt for the OpenCode session. The pack is
|
|
* sent as the first task and includes: issue details, project docs, evidence,
|
|
* prior artifacts, repository metadata, and operational instructions with the
|
|
* needs-input / work-complete marker protocol.
|
|
*/
|
|
export const buildContextPack = (input: ContextPackInput): string => {
|
|
const parts: string[] = [
|
|
`# Issue #${input.issueNumber}: ${input.issueTitle}\n\n${input.issueBody}`,
|
|
];
|
|
|
|
if (input.contextFiles.length > 0) {
|
|
parts.push(
|
|
section(
|
|
"Project Context",
|
|
input.contextFiles.map(
|
|
(file) => `### ${file.label}\n\n\`\`\`\n${file.content}\n\`\`\``
|
|
)
|
|
)
|
|
);
|
|
}
|
|
|
|
if (input.evidence.length > 0) {
|
|
parts.push(
|
|
section(
|
|
"Supporting Evidence",
|
|
input.evidence.map((item) => `- **${item.source}**: ${item.content}`)
|
|
)
|
|
);
|
|
}
|
|
|
|
if (input.artifacts.length > 0) {
|
|
parts.push(
|
|
section(
|
|
"Previous Work Artifacts",
|
|
input.artifacts.map(
|
|
(artifact) =>
|
|
`### ${artifact.path}\n\n\`\`\`\n${artifact.content}\n\`\`\``
|
|
)
|
|
)
|
|
);
|
|
}
|
|
|
|
parts.push(
|
|
section("Repository", [
|
|
`- Name: ${input.repositoryMetadata.repositoryName}`,
|
|
`- URL: ${input.repositoryMetadata.repositoryUrl}`,
|
|
`- Base branch: ${input.repositoryMetadata.baseBranch}`,
|
|
]),
|
|
OPERATIONAL_INSTRUCTIONS
|
|
);
|
|
|
|
return parts.filter((part) => part.length > 0).join("\n\n---\n\n");
|
|
};
|