How Spequi attaches an agent and runs skills
This document describes the current implementation: how the app hands work to an existing coding agent, how that agent follows another skill, how questions wait for an answer in the UI, and how results return to the app.
The app does not call a model or start a coding agent. You invoke the spequi skill in an existing agent session. That agent runs local CLI commands, reads workflow instructions, performs the work through its tools, and reports completion.
There is no await invokeSkill() implementation. A skill is an instruction document that the current agent reads and follows. Completion is an explicit job update, not a return value from another skill process.
1. The components
| Component |
Responsibility |
Implementation |
| Browser UI |
Submit actions, display progress, collect answers |
public/workflow.js, src/studio-view.mjs |
| Local server |
Protect browser mutations, route actions and replies, notify the UI |
src/server.mjs |
| Lifecycle layer |
Check readiness and enqueue the requested action |
src/lifecycle.mjs |
| Job queue |
Persist requests, claim ownership, choose workflows, store results |
src/jobs.mjs |
| Agent CLI |
Report a session, poll, ask, wait, log, finish or fail |
bin/spequi.mjs |
| Session evidence |
Store credential-free agent/MCP reports and evaluate freshness |
src/agent-session.mjs |
| Question storage |
Retain questions and answers on the same owned job |
src/job-dialogue.mjs |
| Agent skill |
Instruct the current agent to coordinate the loop |
spequi SKILL.md |
The browser talks to the server over local HTTP and receives WebSocket notifications. The agent’s CLI commands use the project server’s authenticated loopback protocol when it is running. .spequi/server.json binds that connection to the project root; an unavailable saved connection is an explicit restart error. Headless commands without a server retain the file protocol. They do not send prompts over the browser's WebSocket connection.
Typical retained state looks like:
.spequi/
cli.mjs Project launcher for the initialized package
server.mjs Project server launcher
server.json Private root-bound agent protocol connection
drafts/ Autosaved ideas and unfinished planning references
agent.json Latest agent heartbeat
agent-session.json Session ID and reported MCP metadata
jobs/
<job-id>.json Action, ownership, status, questions, logs and result
previews/
<job-id>/
baseline/ Files captured before implementation
work/ Isolated project copy assigned to the agent
proposed/ Frozen changed files after completion
manifest.json Baseline fingerprints
result.json Diff metadata and application state
2. Attachment starts in the coding agent
The user invokes the installed skill using the host's convention, such as $spequi in Codex, /skills spequi in Sber/GitVerse GigaCode CLI, or /spequi in a CLI with that wrapper. The host makes the skill instructions available to the existing agent session.
The skill tells the agent to reuse the project's initialized launcher and existing server. This avoids requiring a global installation or starting a second agent.
GigaCode registration
The installer registers GigaCode alongside the existing agents. Project initialization and skill install create:
.gigacode/
skills/spequi/SKILL.md
commands/spequi.md
skill install --global creates the same paths under the user's home directory. The command wrappers reference the skill in the matching scope and use GigaCode's {{args}} parameter syntax. After discovery, invoke /skills spequi or /spequi. These paths and formats follow GigaCode's skills and commands documentation.
The installer registers the viewer skill; it does not install other OpenSpec workflows. Those must be discoverable separately. Registration and refresh behavior are tested locally; indefinite polling in a real GigaCode session still requires an integration test.
Register a session
The agent creates a small report under .spequi. This is an illustrative report, not a real attachment identifier:
{
"sessionId": "example-attachment",
"mcps": {
"supported": false,
"connections": []
}
}
Then it runs this command from the project root:
node .spequi/cli.mjs agent report \
--file .spequi/session-report.json \
--name Codex
reportSession() validates the report and stores the attachment evidence. The essential writes are:
const report = {
agent,
sessionId: id,
reportedAt: Date.now(),
mcps: { supported: mcps.supported, connections },
};
await writeJSON(runtime(root, "agent-session.json"), report);
await writeJSON(runtime(root, "agent.json"), {
agent,
sessionId: id,
at: Date.now(),
});
Source: reportSession().
This session ID belongs to the viewer attachment. It is not a provider API credential or an integration with the coding tool's internal conversation ID. The agent retains it and uses it on subsequent polls.
Wait, then claim
node .spequi/cli.mjs agent wait --wait 300 \
--name Codex --session-id example-attachment
In GigaCode, launch this bounded wait and the retained server using the host Shell tool’s is_background: true argument. Keep the task ID and monitor the completion result. The wait refreshes consumer liveness and returns available: true when work is available, without claiming it. Once this session receives the output, claim in the foreground:
node .spequi/cli.mjs agent next --wait 0 \
--name Codex --session-id example-attachment
A first-attachment agent wait --wait 1 checks host completion delivery. If GigaCode does not resume the session automatically, use its task monitor or retained foreground polling; a background process alone cannot wake the model. Attachment lasts while the coding session remains open, including idle periods supported by that notification mechanism.
Waits are bounded at five minutes and check for work every 250 ms. An empty result includes queue state. Contention and blocked locks are errors, never an empty queue. An owned active request blocks additional claims. Finish or answer it before claiming another; review partial planning work before any retry.
Source: agent CLI, root-bound client, and agent protocol routes.
3. A UI action becomes a durable job
For example, a user requests an implementation preview. The browser submits the selected change to the lifecycle route:
// Simplified browser request. The selected change must already be ready.
await fetch("/api/lifecycle/actions", {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
"X-CSRF-Token": document.querySelector("meta[name=csrf-token]").content,
},
body: JSON.stringify({
action: "preview",
change: "example-change",
}),
});
The server checks the local request boundary. The lifecycle layer checks whether the selected action is permitted, then creates a queued job. A simplified shape is:
{
"id": "example-job-id",
"action": "preview",
"change": "example-change",
"status": "queued",
"message": ""
}
The job is written under .spequi/jobs/. It remains available if the browser reloads or no agent is currently polling.
Source: requestLifecycle() and createJob() / enqueue().
4. Claiming establishes ownership and selects the skill
claimNext() updates the heartbeat, acquires the queue lock and selects the oldest queued request. It changes the job to running, assigns a random claim token and records the agent attachment identity. Only one workflow runs per project. The short claim releases the queue lock before snapshot copying or plan capture; preparation failures are retained as failed requests. Locks record their process owner and recover abandoned dead-owner or aged legacy empty files. Live lock contention remains explicit.
The claim token is required for the agent's subsequent log, question, input and completion commands. A second agent cannot complete the same job just by knowing its ID.
The action-to-skill mapping currently includes:
const skillRoutes = {
explore: { skill: "openspec-explore", required: false },
propose: { skill: "openspec-propose", required: true },
preview: { skill: "openspec-apply-change", required: true },
verify: { skill: "openspec-verify-change", required: false },
archive: { skill: "openspec-archive-change", required: true },
impact: { skill: null, required: false },
"update-specs": { skill: "openspec-update-change", required: true },
"create-change": { skill: null, required: false },
"start-plan": { skill: null, required: false },
"plan-next": { skill: null, required: false },
"revise-plan": { skill: "openspec-update-change", required: true },
"sync-specs": { skill: "openspec-sync-specs", required: true },
};
Full-plan start-plan requests use the proposal route. Next-artifact requests use the bounded CLI workflow directly; they do not require another skill.
After selecting the route, claimNext() constructs the workflow:
job.workingDirectory =
job.action === "preview" ? await createPreview(root, job.id) : root;
job.workflow = {
mode: "same-session",
...(job.action === "start-plan" && job.planMode === "full"
? skillRoutes.propose
: skillRoutes[job.action]),
directory: job.workingDirectory,
protocolDirectory: root,
change: job.change,
};
job.instructions = instructions(job);
Source: skillRoutes, instructions() and claimNext().
5. What “invoke another skill” actually means
The CLI returns a job containing a workflow such as this abbreviated example:
{
"action": "preview",
"workflow": {
"mode": "same-session",
"skill": "openspec-apply-change",
"required": true,
"directory": "/project/.spequi/previews/example-job-id/work",
"protocolDirectory": "/project",
"change": "example-change"
}
}
The returned instructions explicitly tell the agent to load and follow that installed skill in the current session. They also prohibit starting another coding-agent process, subagent or model endpoint for the handoff.
The agent locates the skill through its host's skill discovery. In this repository, reading the apply skill can look like:
cat .agents/skills/openspec-apply-change/SKILL.md
Reading that file does not execute a skill. It gives the agent instructions. The agent then carries out those instructions through its normal tools: resolving OpenSpec status, reading artifacts, editing the assigned files and running checks.
For example, the apply workflow includes commands such as:
# Run from the job's workflow.directory, which is the isolated copy for previews.
openspec context --json
openspec status --change example-change --json
openspec instructions apply --change example-change --json
The same agent continues the task after reading the skill. There is no separate skill process to await, no injected slash command, and no new model session whose response must be collected.
If a required skill is missing, the agent must report failure instead of claiming the workflow ran. Optional read-only workflows have their documented direct-execution fallback.
The behavior contract lives in the same-session routing section of the skill. It is an agent instruction contract, not a JavaScript skill execution engine.
6. Preview work uses two different directories
For an implementation preview:
| Directory |
What happens there |
workflow.directory |
Read the copied planning context, implement, install needed dependencies and run checks inside the isolated copy |
workflow.protocolDirectory |
Run agent log, ask, input, finish and fail against the original project's queue |
All implementation edits, including task checkboxes, stay inside the preview copy. The agent must re-resolve OpenSpec roots there and refuse paths that escape it. The snapshot is not an OS sandbox; the agent must honor that boundary.
When the agent finishes, the application freezes the resulting changed files. It does not copy them into the original project automatically. The user reviews the diff and explicitly uses Apply reviewed preview, which checks for baseline conflicts.
Source: createPreview(), freezePreview() and applyPreview().
Spec links in implementation previews
Before finishing, the agent writes .spequi/spec-links.json inside workingDirectory. The job instructions and installed spequi skill include this format:
{
"version": 1,
"links": [
{
"file": "src/preferences.js",
"side": "proposed",
"start": 12,
"end": 18,
"spec": "openspec/changes/email-preferences/specs/email-preferences/spec.md",
"specStart": 3,
"specEnd": 9
}
]
}
Code and spec ranges are one-based and inclusive. Use final proposed line numbers for additions or modifications, or "side": "current" and baseline line numbers for deletions. Cite the original copied plan, with a range that explains the changed code. Supported sources are Markdown artifacts in the selected change and main specs under openspec/specs/**/spec.md. Do not invent links for unexplained edits.
On completion, the viewer validates that code ranges overlap actual changes, retains at most eight links per changed file, and verifies up to 80 spec lines per quote against the baseline manifest. It stores locations and fingerprints in result.json; quoted text is always read from the retained baseline. Invalid links are omitted with a notice. The report is optional, limited to 64 KiB and 100 links, and excluded from proposed source and application.
Both Unified and Split diffs show Agent linked quotes beside the corresponding change blocks. Repeated references link to the first quote. Old previews and imported patches without recorded links can receive Suggested · file-level quotes based on explicit file paths, unique filenames, or backticked changed identifiers in the saved change artifacts. These are candidate context, not evidence of why a specific block changed. No match or unavailable snapshot remains explicit; the viewer does not substitute the current working plan.
Source: spec-context.mjs, spec-context-view.mjs.
7. Questions wait for an answer through the same job
If the selected workflow needs clarification or confirmation, the agent runs the following from workflow.protocolDirectory. JOB_ID and CLAIM_TOKEN stand for values returned by the claim; they are not literal identifiers to reuse.
node .spequi/cli.mjs agent ask JOB_ID \
--token CLAIM_TOKEN \
--message "Which authentication method should this change support?" \
--options '["OAuth","Environment variable"]'
askJob() verifies ownership, saves the question and transitions the job:
const question = {
id: randomUUID(),
message: message.trim(),
options,
askedAt: new Date().toISOString(),
};
(job.questions ||= []).push(question);
job.status = "awaiting-input";
The agent then waits with:
node .spequi/cli.mjs agent input JOB_ID \
--token CLAIM_TOKEN --wait 300
The CLI checks the owned job every 250 ms until the status changes or the bounded wait expires. If it is still awaiting-input, the agent repeats the bounded wait. It does not release the job or make a second claim.
The user answers through this protected browser route:
POST /api/lifecycle/jobs/<job-id>/reply
answerJob() checks that the submitted question is still current and unanswered, then stores the answer:
question.answer = answer.trim();
question.answeredAt = new Date().toISOString();
job.status = "running";
The next agent input result contains that retained answer. The current agent reads it and resumes the same workflow. If the request is cancelled instead, the agent stops that work rather than continuing or waiting indefinitely. Browser reloads do not discard the question or answer because both are stored on the job.
Source: askJob(), answerJob() and jobInput(), plus the reply route in src/server.mjs.
8. Progress and completion are explicit
While working, the agent reports meaningful progress:
node .spequi/cli.mjs agent log JOB_ID \
--token CLAIM_TOKEN \
--message "Implemented the form; checking validation and keyboard behavior."
On success:
node .spequi/cli.mjs agent finish JOB_ID \
--token CLAIM_TOKEN \
--message "Implemented the requested scope; checks passed."
On failure:
node .spequi/cli.mjs agent fail JOB_ID \
--token CLAIM_TOKEN \
--message "The required workflow is unavailable; implementation did not run."
updateJob() checks the token and current state. It performs action-specific completion checks, such as requiring actual new planning artifacts, checking full-plan readiness, validating an impact report or freezing an implementation preview. It then persists the result:
job.status = "completed";
job.result = message || "Completed";
The app waits for this explicit completion. It does not infer completion because the agent read a skill, produced a chat response, checked some task boxes, or stopped polling. The checks also do not prove every instruction was followed or every behavior is correct; verification and code review still matter.
After finishing or failing, the agent returns to agent wait, then claims in the foreground while attached.
Source: updateJob().
9. Liveness, MCP evidence and recovery
The current heartbeat check is:
const connected = Boolean(state && Date.now() - state.at < 45000);
A recent heartbeat means the attachment has recently communicated. It does not prove a particular model request is progressing. Likewise, a missing heartbeat does not prove the coding process crashed: a long tool call or an agent that stopped running the loop can also make the attachment appear unresponsive.
Polling and owned log/input/question calls refresh the heartbeat. A session report alone means connected, not ready. Ready additionally requires a current consumer wait lease; finishing or disconnecting that wait clears readiness. Working and awaiting-input states follow the owned request. The header exposes queued counts, last heartbeat and blocked/stalled ownership separately. The agent must keep communicating during work; the implementation does not start an independent background heartbeat daemon.
MCP status is a separate report. The host must supply authoritative connection evidence; a configured server or a tool catalog alone is insufficient. Reports contain display names and allowed connection states, not credentials or transport configuration. MCP evidence also expires after five minutes and must match the current attachment.
The header displays MCPs when current connection evidence is unavailable. 0 MCPs requires a fresh supported report confirming zero connected servers.
If the agent becomes unresponsive:
- Resume the
spequi skill in the existing coding session.
- Check the agent indicator's recovery details and last heartbeat.
- If an old running request is stuck, stop its agent in the coding tool first. The app permits ending that request after its owner's heartbeat expires, then a new request can be submitted.
Ending a request changes queue state; it does not terminate an agent process or undo edits. Reattaching does not automatically reclaim or restart an old running job. The app cannot wake a stopped agent session by itself.
Source: heartbeat() and agentStatus(), session freshness, and the header/recovery UI.
10. The complete sequence
sequenceDiagram
actor User
participant UI as Browser UI
participant App as Local server and job files
participant Agent as Existing coding-agent session
participant Skill as Installed SKILL.md
User->>Agent: Invoke spequi
Agent->>App: agent report and monitored agent wait
User->>UI: Request a workflow
UI->>App: POST lifecycle action
App->>App: Persist queued job
Agent->>App: agent next
App-->>Agent: Owned job, skill name, directories, instructions
Agent->>Skill: Read the selected skill
Skill-->>Agent: Workflow instructions
Note over Agent: The same agent executes the workflow through its tools
opt Clarification needed
Agent->>App: agent ask
App-->>UI: Question available
par Agent waits
loop While awaiting-input
Agent->>App: agent input --wait 300
App-->>Agent: Current question and status
end
and User answers
User->>UI: Submit answer
UI->>App: POST job reply
end
Note over Agent,App: The input result contains the retained answer and running status
end
Agent->>App: agent log
Agent->>App: agent finish or agent fail
App->>App: Validate completion and persist result
App-->>UI: Updated status and result
Agent->>App: Return to monitored agent wait
The queue, ownership, persistence, questions and status checks are application code. Carrying out a selected skill is the cooperating agent's work in its existing session. Keeping those responsibilities explicit explains both why the flow works across skill-capable coding tools and why it pauses when the agent stops serving the queue.
Agent liveness and MCP reports
Merge this section into your existing docs/agent-workflow.md (it already covers queue, skill routing and spec links).
The header's agent indicator reflects a recent agent heartbeat, independent of the browser's connection to the local server.
It distinguishes: live, working, awaiting an answer, not responding, and unattached.
Connection details show the last heartbeat and MCP metadata reported by the current session.
Reconnect agent opens instructions to resume the spequi skill in the same coding session, with a Check connection action.
Requests and answers stay saved. Spequi never restarts an agent automatically.
Reporting MCP status
The agent can write a credential-free report under .spequi, then run:
node .spequi/cli.mjs agent report --file .spequi/session-report.json --name "Spequi Agent"
{
"sessionId": "a-stable-random-id-for-this-attachment",
"mcps": {
"supported": true,
"connections": [{ "name": "Filesystem", "status": "connected" }]
}
}
- Allowed statuses:
connected, disconnected, unavailable, unknown.
- Only display names and statuses are accepted. Omit credentials, server URLs, environment variables and transport configuration.
- If the host cannot enumerate connection status, report
{"mcps":{"supported":false,"connections":[]}}.
- A tool catalog or a configured server alone is not evidence of a healthy connection.
- Keep the returned session ID for refreshes, pass it as
agent next --session-id <id>, and use the same agent name when polling.
- Refresh at least every five minutes or whenever connections change.
- Missing, expired or disconnected-session reports show as unknown, never as a fabricated count.
- The header shows MCPs when the session has not supplied evidence, and 0 MCPs only when a fresh supported report confirms zero.
- Reporting does not replace serving the agent queue.