@ctrl-spc/cli 1.2.3 → 1.3.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/commands/run.js +7 -3
- package/dist/dispatch.js +104 -0
- package/dist/mcp.js +373 -32
- package/dist/protocol.js +38 -17
- package/dist/skills.js +10 -8
- package/package.json +1 -1
package/dist/commands/run.js
CHANGED
|
@@ -12,6 +12,7 @@ import { MCP_PORT } from '../env.js';
|
|
|
12
12
|
import { startMcpServer } from '../mcp.js';
|
|
13
13
|
import { getMachineIdentity } from '../config.js';
|
|
14
14
|
import { discoverRepositoryTopology } from '../topology.js';
|
|
15
|
+
import { createAgentDispatchController } from '../dispatch.js';
|
|
15
16
|
function mcpUrl() {
|
|
16
17
|
return `http://localhost:${MCP_PORT}/mcp`;
|
|
17
18
|
}
|
|
@@ -611,12 +612,13 @@ export function createProjectPresenceReconciler(client, userId, profileName, mac
|
|
|
611
612
|
* tool call can flip presence back to "working" mid-shutdown) and only then
|
|
612
613
|
* untracks presence, before exiting.
|
|
613
614
|
*/
|
|
614
|
-
function installShutdownHandler(presence, mcpServer) {
|
|
615
|
+
function installShutdownHandler(presence, mcpServer, dispatch) {
|
|
615
616
|
let shuttingDown = false;
|
|
616
617
|
const shutdown = () => {
|
|
617
618
|
if (shuttingDown)
|
|
618
619
|
return;
|
|
619
620
|
shuttingDown = true;
|
|
621
|
+
dispatch?.stop();
|
|
620
622
|
console.log('\nShutting down — closing MCP server, untracking presence...');
|
|
621
623
|
mcpServer
|
|
622
624
|
.close()
|
|
@@ -712,8 +714,10 @@ export async function run() {
|
|
|
712
714
|
}
|
|
713
715
|
console.log(`MCP: ${url} (tools: list_tasks, get_task, begin_work, heartbeat_work, reserve_work_paths, ` +
|
|
714
716
|
'release_work_paths, transfer_coordination, acknowledge_unavailable_checkout, record_context_exploration, ' +
|
|
715
|
-
'ask_question,
|
|
717
|
+
'ask_question, record_user_input, submit_workflow_stage, decide_workflow_review, ' +
|
|
718
|
+
'end_work, create_task, update_task, create_artifact, add_comment)');
|
|
716
719
|
console.log(`Projects: ${projects.length > 0 ? projects.map((project) => project.name).join(', ') : 'none initialized on this machine'}`);
|
|
717
720
|
console.log('Presence: available');
|
|
718
|
-
|
|
721
|
+
const dispatch = createAgentDispatchController(client, machine.id, registeredAgents);
|
|
722
|
+
installShutdownHandler(presence, mcpServer, dispatch);
|
|
719
723
|
}
|
package/dist/dispatch.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { resolveAgentCommand } from './agents.js';
|
|
3
|
+
function shellQuote(value) {
|
|
4
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
5
|
+
}
|
|
6
|
+
function appleScriptString(value) {
|
|
7
|
+
return `"${value.replaceAll('\\', '\\\\').replaceAll('"', '\\"')}"`;
|
|
8
|
+
}
|
|
9
|
+
export function agentTerminalCommand(request, executable) {
|
|
10
|
+
const prompt = shellQuote(request.prompt);
|
|
11
|
+
const binary = shellQuote(executable);
|
|
12
|
+
const fresh = `${binary} ${prompt}`;
|
|
13
|
+
const resume = request.agent_kind === 'claude'
|
|
14
|
+
? `${binary} --continue ${prompt}`
|
|
15
|
+
: `${binary} resume --last ${prompt}`;
|
|
16
|
+
const agentCommand = request.mode === 'resume' ? `( ${resume} || ${fresh} )` : fresh;
|
|
17
|
+
return `cd ${shellQuote(request.workspace_root)} && ${agentCommand}`;
|
|
18
|
+
}
|
|
19
|
+
/** Opens a visible agent conversation. Resume is best-effort and falls back
|
|
20
|
+
* to a fresh task conversation in the same project checkout. */
|
|
21
|
+
export async function launchAgentDispatch(request) {
|
|
22
|
+
if (process.platform !== 'darwin') {
|
|
23
|
+
throw new Error('Automatic agent launch currently requires macOS Terminal.');
|
|
24
|
+
}
|
|
25
|
+
const executable = resolveAgentCommand(request.agent_kind);
|
|
26
|
+
if (!executable)
|
|
27
|
+
throw new Error(`${request.agent_kind} is not installed on this machine.`);
|
|
28
|
+
const command = agentTerminalCommand(request, executable);
|
|
29
|
+
const script = `tell application "Terminal" to activate\ntell application "Terminal" to do script ${appleScriptString(command)}`;
|
|
30
|
+
await new Promise((resolve, reject) => {
|
|
31
|
+
const child = spawn('/usr/bin/osascript', ['-e', script], {
|
|
32
|
+
stdio: ['ignore', 'ignore', 'pipe'],
|
|
33
|
+
});
|
|
34
|
+
let stderr = '';
|
|
35
|
+
child.stderr.on('data', (chunk) => { stderr += String(chunk); });
|
|
36
|
+
child.once('error', reject);
|
|
37
|
+
child.once('exit', (code) => {
|
|
38
|
+
if (code === 0)
|
|
39
|
+
resolve();
|
|
40
|
+
else
|
|
41
|
+
reject(new Error(stderr.trim() || `Terminal launch exited with code ${code ?? 'unknown'}.`));
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
export function createAgentDispatchController(client, machineId, agents, deps = {}) {
|
|
46
|
+
const claim = deps.claim ?? (async () => {
|
|
47
|
+
const { data, error } = await client.rpc('claim_agent_dispatch_request', {
|
|
48
|
+
p_machine: machineId,
|
|
49
|
+
p_agents: agents,
|
|
50
|
+
});
|
|
51
|
+
if (error)
|
|
52
|
+
throw error;
|
|
53
|
+
return data;
|
|
54
|
+
});
|
|
55
|
+
const complete = deps.complete ?? (async (id, state, errorMessage) => {
|
|
56
|
+
const { error } = await client.rpc('complete_agent_dispatch_request', {
|
|
57
|
+
p_request: id,
|
|
58
|
+
p_state: state,
|
|
59
|
+
p_error: errorMessage ?? null,
|
|
60
|
+
});
|
|
61
|
+
if (error)
|
|
62
|
+
throw error;
|
|
63
|
+
});
|
|
64
|
+
const launch = deps.launch ?? launchAgentDispatch;
|
|
65
|
+
let stopped = false;
|
|
66
|
+
let polling = false;
|
|
67
|
+
let timer;
|
|
68
|
+
const poll = async () => {
|
|
69
|
+
if (stopped || polling)
|
|
70
|
+
return;
|
|
71
|
+
polling = true;
|
|
72
|
+
try {
|
|
73
|
+
const request = await claim();
|
|
74
|
+
if (!request)
|
|
75
|
+
return;
|
|
76
|
+
try {
|
|
77
|
+
await launch(request);
|
|
78
|
+
await complete(request.id, 'launched');
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
await complete(request.id, 'failed', err instanceof Error ? err.message : String(err));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
polling = false;
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
timer = setInterval(() => {
|
|
89
|
+
void poll().catch((err) => console.warn(`Warning: agent dispatch poll failed: ${err.message}`));
|
|
90
|
+
}, deps.intervalMs ?? 2500);
|
|
91
|
+
timer.unref();
|
|
92
|
+
if (deps.startImmediately !== false) {
|
|
93
|
+
void poll().catch((err) => console.warn(`Warning: agent dispatch poll failed: ${err.message}`));
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
poll,
|
|
97
|
+
stop() {
|
|
98
|
+
stopped = true;
|
|
99
|
+
if (timer)
|
|
100
|
+
clearInterval(timer);
|
|
101
|
+
timer = undefined;
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
package/dist/mcp.js
CHANGED
|
@@ -82,6 +82,25 @@ function requiredProjectId(deps, action) {
|
|
|
82
82
|
throw new Error(`${action} requires a project-bound agent run`);
|
|
83
83
|
return deps.projectId;
|
|
84
84
|
}
|
|
85
|
+
function normalizeWorkflowContext(value) {
|
|
86
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
87
|
+
return { enabled: false };
|
|
88
|
+
const context = value;
|
|
89
|
+
return context.enabled === true ? context : { enabled: false };
|
|
90
|
+
}
|
|
91
|
+
async function loadWorkflowContext(client, taskId) {
|
|
92
|
+
const value = await must(client.rpc('get_task_workflow_context', { p_task: taskId }));
|
|
93
|
+
return normalizeWorkflowContext(value);
|
|
94
|
+
}
|
|
95
|
+
function workflowCapabilityError(run, capability, action) {
|
|
96
|
+
const workflow = run.workflow;
|
|
97
|
+
if (!workflow?.enabled)
|
|
98
|
+
return null;
|
|
99
|
+
if (workflow.current_stage?.capabilities?.includes(capability))
|
|
100
|
+
return null;
|
|
101
|
+
return errorResult(`${action} is not allowed during workflow stage "${workflow.current_stage?.name ?? 'unknown'}". ` +
|
|
102
|
+
'Follow the stored stage instructions returned by get_task.');
|
|
103
|
+
}
|
|
85
104
|
/**
|
|
86
105
|
* One future-proof task read model. `*` keeps every direct task column without
|
|
87
106
|
* requiring a CLI release when an additive migration lands. Every relationship
|
|
@@ -363,8 +382,8 @@ export async function beginAgentRunHandler(deps, session, args) {
|
|
|
363
382
|
'Call end_work before beginning a different task in the same agent session.');
|
|
364
383
|
}
|
|
365
384
|
const task = deps.projectId
|
|
366
|
-
? await must(deps.client.from('tasks').select('id,project_id').eq('id', args.task_id).eq('project_id', deps.projectId).maybeSingle())
|
|
367
|
-
: await must(deps.client.from('tasks').select('id,project_id').eq('id', args.task_id).maybeSingle());
|
|
385
|
+
? await must(deps.client.from('tasks').select('id,project_id,description').eq('id', args.task_id).eq('project_id', deps.projectId).maybeSingle())
|
|
386
|
+
: await must(deps.client.from('tasks').select('id,project_id,description').eq('id', args.task_id).maybeSingle());
|
|
368
387
|
if (!task)
|
|
369
388
|
return errorResult(`No accessible task found for id "${args.task_id}".`);
|
|
370
389
|
const taskProjectId = task.project_id ?? deps.projectId;
|
|
@@ -434,7 +453,29 @@ export async function beginAgentRunHandler(deps, session, args) {
|
|
|
434
453
|
previousExploration.topologyRevision === Number(result.topology_revision)
|
|
435
454
|
? previousExploration
|
|
436
455
|
: null;
|
|
456
|
+
try {
|
|
457
|
+
session.run.workflow = normalizeWorkflowContext(await must(deps.client.rpc('begin_workflow_stage', { p_run: result.run_id })));
|
|
458
|
+
if (!session.run.workflow.enabled) {
|
|
459
|
+
// A real task row always has a string description. Treat an omitted
|
|
460
|
+
// field as already provided only for backwards-compatible embedded
|
|
461
|
+
// clients/test doubles that predate this contract.
|
|
462
|
+
const persistedInstruction = typeof task.description === 'string' ? task.description.trim() : null;
|
|
463
|
+
session.run.noWorkflowIntent = persistedInstruction === '' && !args.prompt_instruction?.trim()
|
|
464
|
+
? 'missing'
|
|
465
|
+
: 'provided';
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
catch (workflowError) {
|
|
469
|
+
await deps.client.rpc('end_agent_run', {
|
|
470
|
+
p_run: result.run_id,
|
|
471
|
+
p_reason: 'workflow stage unavailable',
|
|
472
|
+
});
|
|
473
|
+
session.run = null;
|
|
474
|
+
session.exploration = null;
|
|
475
|
+
throw workflowError;
|
|
476
|
+
}
|
|
437
477
|
const collaborator = result.role === 'collaborator';
|
|
478
|
+
const workflow = session.run.workflow;
|
|
438
479
|
return textResult({
|
|
439
480
|
coordination: {
|
|
440
481
|
status: collaborator ? 'joined_as_collaborator' : 'coordinating',
|
|
@@ -446,6 +487,16 @@ export async function beginAgentRunHandler(deps, session, args) {
|
|
|
446
487
|
: 'You are coordinating this task. Inspect every repository, then call reserve_work_paths before editing. Collaborators may join with disjoint reservations.',
|
|
447
488
|
},
|
|
448
489
|
agent_run: session.run,
|
|
490
|
+
workflow,
|
|
491
|
+
...(workflow?.enabled ? {
|
|
492
|
+
workflow_instruction: workflow.current_stage?.state === 'awaiting_review'
|
|
493
|
+
? `Current stage: ${workflow.current_stage?.name ?? 'unknown'} is awaiting review. ` +
|
|
494
|
+
'Only record the user\'s explicit approval or change request with decide_workflow_review; do not redo or advance the stage first.'
|
|
495
|
+
: `Current stage: ${workflow.current_stage?.name ?? 'unknown'}. ` +
|
|
496
|
+
`${workflow.current_stage?.instructions ?? 'Follow the stored workflow instructions.'}`,
|
|
497
|
+
} : session.run.noWorkflowIntent === 'missing' ? {
|
|
498
|
+
no_workflow_instruction: 'This No workflow item has no instructions. After context exploration, ask the user what should be done with ask_question(category: intent). Do not plan, create another artifact, reserve paths, or edit first.',
|
|
499
|
+
} : {}),
|
|
449
500
|
});
|
|
450
501
|
}
|
|
451
502
|
catch (err) {
|
|
@@ -485,6 +536,11 @@ export async function reserveWorkPathsHandler(deps, session, args) {
|
|
|
485
536
|
if (!Array.isArray(args.reservations) || args.reservations.length === 0) {
|
|
486
537
|
return errorResult('reserve_work_paths requires at least one repository/path reservation.');
|
|
487
538
|
}
|
|
539
|
+
if (args.reservations.some((reservation) => (reservation.mode ?? 'write') === 'write')) {
|
|
540
|
+
const workflowError = workflowCapabilityError(active, 'reserve_write', 'reserve_work_paths');
|
|
541
|
+
if (workflowError)
|
|
542
|
+
return workflowError;
|
|
543
|
+
}
|
|
488
544
|
const missingCheckout = args.reservations.find((reservation) => (reservation.mode ?? 'write') === 'write' && !reservation.repository_checkout_id);
|
|
489
545
|
if (missingCheckout) {
|
|
490
546
|
return errorResult('Every write reservation requires repository_checkout_id from get_task topology.checkouts. ' +
|
|
@@ -777,11 +833,180 @@ export async function askQuestionHandler(deps, attribution, args, agentRun) {
|
|
|
777
833
|
if (!first || first.type !== 'text')
|
|
778
834
|
return errorResult('ask_question returned no persisted comment.');
|
|
779
835
|
const payload = JSON.parse(first.text);
|
|
836
|
+
const commentId = typeof payload.comment?.id === 'string' ? payload.comment.id : null;
|
|
837
|
+
let workflowQuestionId = null;
|
|
838
|
+
if (agentRun.workflow?.enabled) {
|
|
839
|
+
if (!commentId)
|
|
840
|
+
return errorResult('ask_question returned no persisted comment id for the workflow question.');
|
|
841
|
+
workflowQuestionId = await must(deps.client.rpc('open_workflow_question', {
|
|
842
|
+
p_run: agentRun.runId,
|
|
843
|
+
p_comment: commentId,
|
|
844
|
+
p_category: args.category,
|
|
845
|
+
p_question: question,
|
|
846
|
+
}));
|
|
847
|
+
agentRun.workflow = await loadWorkflowContext(deps.client, agentRun.taskId);
|
|
848
|
+
}
|
|
780
849
|
return textResult({
|
|
781
|
-
question: {
|
|
850
|
+
question: {
|
|
851
|
+
category: args.category,
|
|
852
|
+
text: question,
|
|
853
|
+
comment_id: commentId,
|
|
854
|
+
workflow_question_id: workflowQuestionId,
|
|
855
|
+
},
|
|
782
856
|
comment: payload.comment ?? null,
|
|
857
|
+
...(agentRun.workflow?.enabled ? { workflow: agentRun.workflow } : {}),
|
|
783
858
|
});
|
|
784
859
|
}
|
|
860
|
+
export async function recordUserInputHandler(deps, attribution, session, args) {
|
|
861
|
+
const active = joinedRunOrError(session, 'record_user_input');
|
|
862
|
+
if ('content' in active)
|
|
863
|
+
return active;
|
|
864
|
+
if (!active.workflow?.enabled)
|
|
865
|
+
return errorResult('record_user_input requires a workflow task.');
|
|
866
|
+
const capabilityError = workflowCapabilityError(active, 'record_user_input', 'record_user_input');
|
|
867
|
+
if (capabilityError)
|
|
868
|
+
return capabilityError;
|
|
869
|
+
if (!Array.isArray(args.question_ids) || args.question_ids.length === 0) {
|
|
870
|
+
return errorResult('record_user_input requires at least one workflow question id.');
|
|
871
|
+
}
|
|
872
|
+
if (!args.answer?.trim())
|
|
873
|
+
return errorResult('record_user_input requires the user\'s exact answer.');
|
|
874
|
+
try {
|
|
875
|
+
const openQuestions = active.workflow.open_questions ?? [];
|
|
876
|
+
const questionText = args.question_ids.map((id) => {
|
|
877
|
+
const question = openQuestions.find((candidate) => candidate.id === id);
|
|
878
|
+
return `- ${typeof question?.question === 'string' ? question.question : id}`;
|
|
879
|
+
}).join('\n');
|
|
880
|
+
const artifactResult = await createArtifactHandler(bindProject(deps, active.projectId), attribution, {
|
|
881
|
+
task_id: active.taskId,
|
|
882
|
+
type: 'analysis',
|
|
883
|
+
format: 'md',
|
|
884
|
+
content: `# User input\n\n## Questions\n\n${questionText}\n\n## User answer\n\n${args.answer.trim()}`,
|
|
885
|
+
coverage: args.coverage,
|
|
886
|
+
}, active);
|
|
887
|
+
if (artifactResult.isError)
|
|
888
|
+
return artifactResult;
|
|
889
|
+
const first = artifactResult.content[0];
|
|
890
|
+
if (!first || first.type !== 'text')
|
|
891
|
+
throw new Error('answer artifact returned no text result');
|
|
892
|
+
const payload = JSON.parse(first.text);
|
|
893
|
+
const artifactId = typeof payload.artifact?.id === 'string' ? payload.artifact.id : null;
|
|
894
|
+
if (!artifactId)
|
|
895
|
+
throw new Error('answer artifact returned no id');
|
|
896
|
+
active.workflow = normalizeWorkflowContext(await must(deps.client.rpc('record_workflow_answer', {
|
|
897
|
+
p_run: active.runId,
|
|
898
|
+
p_question_ids: args.question_ids,
|
|
899
|
+
p_artifact: artifactId,
|
|
900
|
+
})));
|
|
901
|
+
return textResult({
|
|
902
|
+
recorded: true,
|
|
903
|
+
artifact_id: artifactId,
|
|
904
|
+
question_ids: args.question_ids,
|
|
905
|
+
workflow: active.workflow,
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
catch (err) {
|
|
909
|
+
return coordinationError('record_user_input', err);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
export async function submitWorkflowStageHandler(deps, session, args) {
|
|
913
|
+
const active = joinedRunOrError(session, 'submit_workflow_stage');
|
|
914
|
+
if ('content' in active)
|
|
915
|
+
return active;
|
|
916
|
+
if (!active.workflow?.enabled)
|
|
917
|
+
return errorResult('submit_workflow_stage requires a workflow task.');
|
|
918
|
+
const capabilityError = workflowCapabilityError(active, 'submit_stage', 'submit_workflow_stage');
|
|
919
|
+
if (capabilityError)
|
|
920
|
+
return capabilityError;
|
|
921
|
+
if (!Number.isInteger(args.expected_workflow_revision) || args.expected_workflow_revision < 1) {
|
|
922
|
+
return errorResult('submit_workflow_stage requires the current positive workflow revision from get_task.');
|
|
923
|
+
}
|
|
924
|
+
if (!Array.isArray(args.artifacts))
|
|
925
|
+
return errorResult('submit_workflow_stage requires an artifacts array.');
|
|
926
|
+
try {
|
|
927
|
+
active.workflow = normalizeWorkflowContext(await must(deps.client.rpc('submit_workflow_stage', {
|
|
928
|
+
p_run: active.runId,
|
|
929
|
+
p_expected_workflow_revision: args.expected_workflow_revision,
|
|
930
|
+
p_artifacts: args.artifacts,
|
|
931
|
+
})));
|
|
932
|
+
const waiting = active.workflow.state === 'awaiting_user';
|
|
933
|
+
return textResult({
|
|
934
|
+
submitted: true,
|
|
935
|
+
workflow: active.workflow,
|
|
936
|
+
instruction: waiting
|
|
937
|
+
? 'This stage is awaiting explicit user review. Do not begin the next stage. End this agent run after persisting its receipt.'
|
|
938
|
+
: `Continue with workflow stage "${active.workflow.current_stage?.name ?? 'next'}" and follow its stored instructions.`,
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
catch (err) {
|
|
942
|
+
return coordinationError('submit_workflow_stage', err);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
export async function decideWorkflowReviewHandler(deps, session, args) {
|
|
946
|
+
const active = joinedRunOrError(session, 'decide_workflow_review');
|
|
947
|
+
if ('content' in active)
|
|
948
|
+
return active;
|
|
949
|
+
if (active.taskId !== args.task_id) {
|
|
950
|
+
return errorResult(`decide_workflow_review must stay on this agent run's task (${active.taskId}).`);
|
|
951
|
+
}
|
|
952
|
+
if (!active.workflow?.enabled || active.workflow.current_stage?.stage_run_id !== args.stage_run_id ||
|
|
953
|
+
active.workflow.current_stage?.state !== 'awaiting_review') {
|
|
954
|
+
return errorResult('decide_workflow_review requires this live agent run to be bound to the current review stage.');
|
|
955
|
+
}
|
|
956
|
+
if (!args.user_message?.trim()) {
|
|
957
|
+
return errorResult('decide_workflow_review requires the user\'s exact approval or change-request message as evidence.');
|
|
958
|
+
}
|
|
959
|
+
try {
|
|
960
|
+
const scopes = await activeRepositoryScopes(deps.client, active.projectId);
|
|
961
|
+
const evidenceResult = await createArtifactHandler(bindProject(deps, active.projectId), attributionFromClientName(session.clientName), {
|
|
962
|
+
task_id: active.taskId,
|
|
963
|
+
type: 'analysis',
|
|
964
|
+
format: 'md',
|
|
965
|
+
content: `# Workflow review decision\n\n${args.user_message.trim()}`,
|
|
966
|
+
coverage: scopes.map(scope => ({
|
|
967
|
+
repository_scope_id: scope.repository_scope_id,
|
|
968
|
+
disposition: 'context_only',
|
|
969
|
+
reason: 'Conversation review evidence applies to this workflow task.',
|
|
970
|
+
})),
|
|
971
|
+
}, active);
|
|
972
|
+
if (evidenceResult.isError)
|
|
973
|
+
return evidenceResult;
|
|
974
|
+
const evidenceBlock = evidenceResult.content[0];
|
|
975
|
+
if (!evidenceBlock || evidenceBlock.type !== 'text')
|
|
976
|
+
throw new Error('review evidence artifact returned no text result');
|
|
977
|
+
const evidencePayload = JSON.parse(evidenceBlock.text);
|
|
978
|
+
const evidenceArtifactId = typeof evidencePayload.artifact?.id === 'string' ? evidencePayload.artifact.id : null;
|
|
979
|
+
if (!evidenceArtifactId)
|
|
980
|
+
throw new Error('review evidence artifact returned no id');
|
|
981
|
+
const context = normalizeWorkflowContext(await must(deps.client.rpc('decide_workflow_review', {
|
|
982
|
+
p_task: args.task_id,
|
|
983
|
+
p_stage_run: args.stage_run_id,
|
|
984
|
+
p_expected_workflow_revision: args.expected_workflow_revision,
|
|
985
|
+
p_decision: args.decision,
|
|
986
|
+
p_source: 'agent_conversation',
|
|
987
|
+
p_evidence: args.user_message.trim(),
|
|
988
|
+
p_agent_run: active.runId,
|
|
989
|
+
p_evidence_artifact: evidenceArtifactId,
|
|
990
|
+
p_dispatch_agent: false,
|
|
991
|
+
})));
|
|
992
|
+
if (session.run?.state === 'joined' && session.run.taskId === args.task_id) {
|
|
993
|
+
session.run.workflow = context;
|
|
994
|
+
}
|
|
995
|
+
session.reviewEvidenceArtifactId = evidenceArtifactId;
|
|
996
|
+
return textResult({
|
|
997
|
+
recorded: true,
|
|
998
|
+
decision: args.decision,
|
|
999
|
+
evidence_artifact_id: evidenceArtifactId,
|
|
1000
|
+
workflow: context,
|
|
1001
|
+
instruction: context.state === 'completed'
|
|
1002
|
+
? 'The user approved completion. The workflow and work item are Done.'
|
|
1003
|
+
: `The workflow remains on stage "${context.current_stage?.name ?? 'unknown'}". Call begin_work before continuing agent work.`,
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
catch (err) {
|
|
1007
|
+
return coordinationError('decide_workflow_review', err);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
785
1010
|
export async function endAgentRunHandler(deps, session, args = {}, options = {}) {
|
|
786
1011
|
const active = runOrError(session, 'end_work');
|
|
787
1012
|
if ('content' in active)
|
|
@@ -789,18 +1014,19 @@ export async function endAgentRunHandler(deps, session, args = {}, options = {})
|
|
|
789
1014
|
try {
|
|
790
1015
|
if (options.requirePersistenceReceipt) {
|
|
791
1016
|
const exploration = session.exploration;
|
|
1017
|
+
const reviewEvidenceArtifactId = session.reviewEvidenceArtifactId ?? null;
|
|
792
1018
|
const receipt = args.persistence_receipt;
|
|
793
|
-
if (!exploration || !receipt) {
|
|
794
|
-
return errorResult('end_work requires a persistence_receipt and this session\'s persisted context
|
|
1019
|
+
if ((!exploration && !reviewEvidenceArtifactId) || !receipt) {
|
|
1020
|
+
return errorResult('end_work requires a persistence_receipt and this session\'s persisted context or review evidence artifact.');
|
|
795
1021
|
}
|
|
796
1022
|
const outcome = receipt.outcome ?? 'completed';
|
|
797
|
-
if (exploration
|
|
1023
|
+
if (exploration?.status === 'blocked' && !['blocked', 'aborted'].includes(outcome)) {
|
|
798
1024
|
return errorResult('A subagents_unavailable session may end only with outcome blocked or aborted.');
|
|
799
1025
|
}
|
|
800
|
-
if (exploration
|
|
1026
|
+
if (exploration?.status === 'explored' && !['completed', 'blocked'].includes(outcome)) {
|
|
801
1027
|
return errorResult('An explored session may end as completed, or blocked after persisting a question for the user.');
|
|
802
1028
|
}
|
|
803
|
-
if (exploration
|
|
1029
|
+
if (exploration?.status === 'explored' && outcome === 'blocked') {
|
|
804
1030
|
const questionRows = (await must(deps.client
|
|
805
1031
|
.from('comments')
|
|
806
1032
|
.select('id,task_id,agent_run_id,body,created_at')
|
|
@@ -822,8 +1048,9 @@ export async function endAgentRunHandler(deps, session, args = {}, options = {})
|
|
|
822
1048
|
if (artifactIds.length !== receipt.artifact_ids.length) {
|
|
823
1049
|
return errorResult('end_work persistence_receipt artifact_ids must be unique.');
|
|
824
1050
|
}
|
|
825
|
-
|
|
826
|
-
|
|
1051
|
+
const requiredEvidenceArtifactId = exploration?.artifactId ?? reviewEvidenceArtifactId;
|
|
1052
|
+
if (!artifactIds.includes(requiredEvidenceArtifactId)) {
|
|
1053
|
+
return errorResult('end_work persistence_receipt must include this session\'s required evidence artifact id.');
|
|
827
1054
|
}
|
|
828
1055
|
const task = await must(deps.client
|
|
829
1056
|
.from('tasks')
|
|
@@ -851,10 +1078,10 @@ export async function endAgentRunHandler(deps, session, args = {}, options = {})
|
|
|
851
1078
|
`this task run. Missing: ${missingArtifactIds.join(', ') || 'none'}. ` +
|
|
852
1079
|
`Foreign or deleted: ${foreignArtifactIds.join(', ') || 'none'}.`);
|
|
853
1080
|
}
|
|
854
|
-
const
|
|
855
|
-
if (!
|
|
856
|
-
|
|
857
|
-
return errorResult('end_work could not verify this session\'s
|
|
1081
|
+
const evidenceArtifact = artifactsById.get(requiredEvidenceArtifactId);
|
|
1082
|
+
if (!evidenceArtifact || evidenceArtifact.task_id !== active.taskId ||
|
|
1083
|
+
evidenceArtifact.agent_run_id !== active.runId) {
|
|
1084
|
+
return errorResult('end_work could not verify this session\'s required evidence artifact.');
|
|
858
1085
|
}
|
|
859
1086
|
}
|
|
860
1087
|
const ended = await must(deps.client.rpc('end_agent_run', {
|
|
@@ -863,6 +1090,7 @@ export async function endAgentRunHandler(deps, session, args = {}, options = {})
|
|
|
863
1090
|
}));
|
|
864
1091
|
session.run = null;
|
|
865
1092
|
session.exploration = null;
|
|
1093
|
+
session.reviewEvidenceArtifactId = null;
|
|
866
1094
|
return textResult({
|
|
867
1095
|
ended: Boolean(ended),
|
|
868
1096
|
agent_run_id: active.runId,
|
|
@@ -940,7 +1168,7 @@ export async function getTaskHandler(deps, _attribution, args, machineId) {
|
|
|
940
1168
|
}
|
|
941
1169
|
const task = parseTaskRow(row);
|
|
942
1170
|
const topologyMachineId = machineId ?? deps.machineId;
|
|
943
|
-
const [rawComments, rawArtifacts, rawVersions, rawAgentRuns, rawTaskClaims, collaborationDocument, topology] = await Promise.all([
|
|
1171
|
+
const [rawComments, rawArtifacts, rawVersions, rawAgentRuns, rawTaskClaims, collaborationDocument, topology, workflow] = await Promise.all([
|
|
944
1172
|
must(client
|
|
945
1173
|
.from('comments')
|
|
946
1174
|
.select(COMMENT_SELECT)
|
|
@@ -986,6 +1214,7 @@ export async function getTaskHandler(deps, _attribution, args, machineId) {
|
|
|
986
1214
|
topologyMachineId
|
|
987
1215
|
? loadAgentProjectTopology(client, row.project_id, deps.userId, topologyMachineId)
|
|
988
1216
|
: Promise.resolve(undefined),
|
|
1217
|
+
loadWorkflowContext(client, row.id),
|
|
989
1218
|
]);
|
|
990
1219
|
const comments = sortRecords((rawComments ?? []).map(normalizeComment), 'created_at', 'id');
|
|
991
1220
|
const normalizedArtifacts = sortRecords((rawArtifacts ?? []).map(normalizeArtifact), 'created_at', 'id');
|
|
@@ -1043,6 +1272,7 @@ export async function getTaskHandler(deps, _attribution, args, machineId) {
|
|
|
1043
1272
|
active_work_reservations: activeWorkReservations,
|
|
1044
1273
|
collaborative_description: collaborativeDescription,
|
|
1045
1274
|
...(topology ? { topology } : {}),
|
|
1275
|
+
workflow,
|
|
1046
1276
|
highlighted_artifact_id: highlightArtifactId,
|
|
1047
1277
|
protocol: PROTOCOL_SHORT,
|
|
1048
1278
|
});
|
|
@@ -1058,20 +1288,16 @@ export async function createTaskHandler(deps, _attribution, args) {
|
|
|
1058
1288
|
const { client, userId } = deps;
|
|
1059
1289
|
const projectId = requiredProjectId(deps, 'create_task');
|
|
1060
1290
|
const tags = await resolveOrCreateTags(client, projectId, args.tags ?? []);
|
|
1061
|
-
const
|
|
1062
|
-
|
|
1063
|
-
.
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
.single());
|
|
1072
|
-
if (!inserted)
|
|
1073
|
-
throw new Error('Task insert returned no row.');
|
|
1074
|
-
const taskId = inserted.id;
|
|
1291
|
+
const taskId = await must(client.rpc('create_task_with_workflow', {
|
|
1292
|
+
p_project: projectId,
|
|
1293
|
+
p_name: args.name,
|
|
1294
|
+
p_description: args.description ?? '',
|
|
1295
|
+
p_owner: userId,
|
|
1296
|
+
p_use_workflow: true,
|
|
1297
|
+
p_workflow_version: null,
|
|
1298
|
+
}));
|
|
1299
|
+
if (!taskId)
|
|
1300
|
+
throw new Error('Task insert returned no id.');
|
|
1075
1301
|
if (tags.length > 0) {
|
|
1076
1302
|
await must(client.from('task_tags').insert(tags.map((t) => ({ task_id: taskId, tag_id: t.id }))));
|
|
1077
1303
|
}
|
|
@@ -1098,6 +1324,14 @@ export async function updateTaskHandler(deps, _attribution, args, agentRun) {
|
|
|
1098
1324
|
if (agentRun && (id !== agentRun.taskId || projectId !== agentRun.projectId)) {
|
|
1099
1325
|
return errorResult(`update_task must stay on this agent run's task ${agentRun.taskId} in project ${agentRun.projectId}.`);
|
|
1100
1326
|
}
|
|
1327
|
+
if (agentRun?.workflow?.enabled) {
|
|
1328
|
+
const capabilityError = workflowCapabilityError(agentRun, 'update_task', 'update_task');
|
|
1329
|
+
if (capabilityError)
|
|
1330
|
+
return capabilityError;
|
|
1331
|
+
if (fields.status !== undefined) {
|
|
1332
|
+
return errorResult('Workflow task status is controlled by workflow transitions and final user approval.');
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1101
1335
|
if (!Number.isInteger(expected_revision) || expected_revision < 1) {
|
|
1102
1336
|
return errorResult('update_task requires expected_revision from the most recent get_task response.');
|
|
1103
1337
|
}
|
|
@@ -1399,6 +1633,15 @@ export async function startMcpServer(deps) {
|
|
|
1399
1633
|
}
|
|
1400
1634
|
return null;
|
|
1401
1635
|
}
|
|
1636
|
+
function requireNoWorkflowIntent(session, tool) {
|
|
1637
|
+
const run = session.context.run;
|
|
1638
|
+
if (!run || run.state !== 'joined' || run.workflow?.enabled || run.noWorkflowIntent === 'provided')
|
|
1639
|
+
return null;
|
|
1640
|
+
return errorResult(`${tool} is blocked because this No workflow item has no instructions. ` +
|
|
1641
|
+
(run.noWorkflowIntent === 'missing'
|
|
1642
|
+
? 'Call ask_question with category intent to ask the user what should be done.'
|
|
1643
|
+
: 'Persist the user\'s answer in the task description with update_task before continuing.'));
|
|
1644
|
+
}
|
|
1402
1645
|
function buildServer(session) {
|
|
1403
1646
|
const server = new McpServer({ name: 'ctrl-spc', version: BROKER_VERSION });
|
|
1404
1647
|
function attribution() {
|
|
@@ -1426,7 +1669,10 @@ export async function startMcpServer(deps) {
|
|
|
1426
1669
|
});
|
|
1427
1670
|
server.registerTool('begin_work', {
|
|
1428
1671
|
description: 'Explicitly begin work on a task. The first live agent coordinates; later agents join as collaborators. No agent may edit repository paths until reserve_work_paths succeeds.',
|
|
1429
|
-
inputSchema: {
|
|
1672
|
+
inputSchema: {
|
|
1673
|
+
task_id: z.string(),
|
|
1674
|
+
prompt_instruction: z.string().optional().describe('Exact work instruction supplied alongside the copied command. Omit when the paste contains only the command.'),
|
|
1675
|
+
},
|
|
1430
1676
|
}, async (args) => {
|
|
1431
1677
|
syncClientIdentity();
|
|
1432
1678
|
const result = await beginAgentRunHandler(handlerDeps, session.context, args);
|
|
@@ -1454,6 +1700,9 @@ export async function startMcpServer(deps) {
|
|
|
1454
1700
|
const missing = requireExploredSessionRun(session, 'reserve_work_paths');
|
|
1455
1701
|
if (missing)
|
|
1456
1702
|
return missing;
|
|
1703
|
+
const intent = requireNoWorkflowIntent(session, 'reserve_work_paths');
|
|
1704
|
+
if (intent)
|
|
1705
|
+
return intent;
|
|
1457
1706
|
return reserveWorkPathsHandler(handlerDeps, session.context, args);
|
|
1458
1707
|
});
|
|
1459
1708
|
server.registerTool('record_context_exploration', {
|
|
@@ -1483,6 +1732,10 @@ export async function startMcpServer(deps) {
|
|
|
1483
1732
|
const missing = requireActiveSessionRun(session, 'record_context_exploration');
|
|
1484
1733
|
if (missing)
|
|
1485
1734
|
return missing;
|
|
1735
|
+
const run = session.context.run;
|
|
1736
|
+
const workflowError = workflowCapabilityError(run, 'record_context', 'record_context_exploration');
|
|
1737
|
+
if (workflowError)
|
|
1738
|
+
return workflowError;
|
|
1486
1739
|
return recordContextExplorationHandler(handlerDeps, attribution(), session.context, args);
|
|
1487
1740
|
});
|
|
1488
1741
|
server.registerTool('release_work_paths', {
|
|
@@ -1535,7 +1788,13 @@ export async function startMcpServer(deps) {
|
|
|
1535
1788
|
const missing = requireExploredSessionRun(session, 'create_task');
|
|
1536
1789
|
if (missing)
|
|
1537
1790
|
return missing;
|
|
1791
|
+
const intent = requireNoWorkflowIntent(session, 'create_task');
|
|
1792
|
+
if (intent)
|
|
1793
|
+
return intent;
|
|
1538
1794
|
const run = session.context.run;
|
|
1795
|
+
const workflowError = workflowCapabilityError(run, 'create_task', 'create_task');
|
|
1796
|
+
if (workflowError)
|
|
1797
|
+
return workflowError;
|
|
1539
1798
|
return createTaskHandler(bindProject(handlerDeps, run.projectId), attribution(), args);
|
|
1540
1799
|
});
|
|
1541
1800
|
server.registerTool('update_task', {
|
|
@@ -1559,7 +1818,22 @@ export async function startMcpServer(deps) {
|
|
|
1559
1818
|
if (missing)
|
|
1560
1819
|
return missing;
|
|
1561
1820
|
const run = session.context.run;
|
|
1562
|
-
|
|
1821
|
+
if (!run.workflow?.enabled && run.noWorkflowIntent !== 'provided') {
|
|
1822
|
+
if (run.noWorkflowIntent !== 'intent_question_asked') {
|
|
1823
|
+
return requireNoWorkflowIntent(session, 'update_task');
|
|
1824
|
+
}
|
|
1825
|
+
if (!args.fields.description?.trim() || Object.keys(args.fields).some(field => field !== 'description')) {
|
|
1826
|
+
return errorResult('After the No workflow intent question, update_task must first persist only the user\'s answer in the task description.');
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
const workflowError = workflowCapabilityError(run, 'update_task', 'update_task');
|
|
1830
|
+
if (workflowError)
|
|
1831
|
+
return workflowError;
|
|
1832
|
+
const result = await updateTaskHandler(bindProject(handlerDeps, run.projectId), attribution(), args, run);
|
|
1833
|
+
if (!result.isError && !run.workflow?.enabled && run.noWorkflowIntent === 'intent_question_asked') {
|
|
1834
|
+
run.noWorkflowIntent = 'provided';
|
|
1835
|
+
}
|
|
1836
|
+
return result;
|
|
1563
1837
|
});
|
|
1564
1838
|
server.registerTool('create_artifact', {
|
|
1565
1839
|
description: 'Persist an analysis/plan/spec/diagram/mock/wireframe on a task after context exploration. Never write planning documents into a repository.',
|
|
@@ -1579,7 +1853,13 @@ export async function startMcpServer(deps) {
|
|
|
1579
1853
|
const missing = requireExploredSessionRun(session, 'create_artifact');
|
|
1580
1854
|
if (missing)
|
|
1581
1855
|
return missing;
|
|
1856
|
+
const intent = requireNoWorkflowIntent(session, 'create_artifact');
|
|
1857
|
+
if (intent)
|
|
1858
|
+
return intent;
|
|
1582
1859
|
const run = session.context.run;
|
|
1860
|
+
const workflowError = workflowCapabilityError(run, 'create_artifact', 'create_artifact');
|
|
1861
|
+
if (workflowError)
|
|
1862
|
+
return workflowError;
|
|
1583
1863
|
return createArtifactHandler(bindProject(handlerDeps, run.projectId), attribution(), args, run);
|
|
1584
1864
|
});
|
|
1585
1865
|
server.registerTool('ask_question', {
|
|
@@ -1594,7 +1874,65 @@ export async function startMcpServer(deps) {
|
|
|
1594
1874
|
if (missing)
|
|
1595
1875
|
return missing;
|
|
1596
1876
|
const run = session.context.run;
|
|
1597
|
-
|
|
1877
|
+
if (!run.workflow?.enabled && run.noWorkflowIntent === 'missing' && args.category !== 'intent') {
|
|
1878
|
+
return errorResult('This No workflow item has no instructions. The first question must use category intent and ask what should be done.');
|
|
1879
|
+
}
|
|
1880
|
+
if (!run.workflow?.enabled && run.noWorkflowIntent === 'intent_question_asked') {
|
|
1881
|
+
return requireNoWorkflowIntent(session, 'ask_question');
|
|
1882
|
+
}
|
|
1883
|
+
const workflowError = workflowCapabilityError(run, 'ask_question', 'ask_question');
|
|
1884
|
+
if (workflowError)
|
|
1885
|
+
return workflowError;
|
|
1886
|
+
const result = await askQuestionHandler(handlerDeps, attribution(), args, run);
|
|
1887
|
+
if (!result.isError && !run.workflow?.enabled && run.noWorkflowIntent === 'missing') {
|
|
1888
|
+
run.noWorkflowIntent = 'intent_question_asked';
|
|
1889
|
+
}
|
|
1890
|
+
return result;
|
|
1891
|
+
});
|
|
1892
|
+
server.registerTool('record_user_input', {
|
|
1893
|
+
description: 'Persist the user\'s exact answer as an artifact and resolve the current workflow questions before continuing.',
|
|
1894
|
+
inputSchema: {
|
|
1895
|
+
question_ids: z.array(z.string()).min(1),
|
|
1896
|
+
answer: z.string(),
|
|
1897
|
+
coverage: z.array(z.object({
|
|
1898
|
+
repository_scope_id: z.string(),
|
|
1899
|
+
disposition: z.enum(['affected', 'reviewed_no_change', 'context_only', 'unavailable_acknowledged']),
|
|
1900
|
+
reason: z.string(),
|
|
1901
|
+
acknowledgement_id: z.string().nullable().optional(),
|
|
1902
|
+
})).min(1),
|
|
1903
|
+
},
|
|
1904
|
+
}, async (args) => {
|
|
1905
|
+
const missing = requireExploredSessionRun(session, 'record_user_input');
|
|
1906
|
+
if (missing)
|
|
1907
|
+
return missing;
|
|
1908
|
+
return recordUserInputHandler(handlerDeps, attribution(), session.context, args);
|
|
1909
|
+
});
|
|
1910
|
+
server.registerTool('submit_workflow_stage', {
|
|
1911
|
+
description: 'Submit the current workflow stage. The database validates its stored requirements and either advances or waits for user review.',
|
|
1912
|
+
inputSchema: {
|
|
1913
|
+
expected_workflow_revision: z.number().int().positive(),
|
|
1914
|
+
artifacts: z.array(z.object({ artifact_id: z.string(), role: z.string() })),
|
|
1915
|
+
},
|
|
1916
|
+
}, async (args) => {
|
|
1917
|
+
const missing = requireExploredSessionRun(session, 'submit_workflow_stage');
|
|
1918
|
+
if (missing)
|
|
1919
|
+
return missing;
|
|
1920
|
+
return submitWorkflowStageHandler(handlerDeps, session.context, args);
|
|
1921
|
+
});
|
|
1922
|
+
server.registerTool('decide_workflow_review', {
|
|
1923
|
+
description: 'Record the user\'s explicit approval or request for changes from this conversation. Uses the same workflow gate as the web app.',
|
|
1924
|
+
inputSchema: {
|
|
1925
|
+
task_id: z.string(),
|
|
1926
|
+
stage_run_id: z.string(),
|
|
1927
|
+
expected_workflow_revision: z.number().int().positive(),
|
|
1928
|
+
decision: z.enum(['approved', 'changes_requested']),
|
|
1929
|
+
user_message: z.string(),
|
|
1930
|
+
},
|
|
1931
|
+
}, async (args) => {
|
|
1932
|
+
const missing = requireActiveSessionRun(session, 'decide_workflow_review');
|
|
1933
|
+
if (missing)
|
|
1934
|
+
return missing;
|
|
1935
|
+
return decideWorkflowReviewHandler(handlerDeps, session.context, args);
|
|
1598
1936
|
});
|
|
1599
1937
|
server.registerTool('add_comment', {
|
|
1600
1938
|
description: 'Legacy question-only alias. Agent progress narration is not persisted; use ask_question for categorized questions.',
|
|
@@ -1604,6 +1942,9 @@ export async function startMcpServer(deps) {
|
|
|
1604
1942
|
if (missing)
|
|
1605
1943
|
return missing;
|
|
1606
1944
|
const run = session.context.run;
|
|
1945
|
+
const intent = requireNoWorkflowIntent(session, 'add_comment');
|
|
1946
|
+
if (intent)
|
|
1947
|
+
return intent;
|
|
1607
1948
|
if (args.task_id !== run.taskId) {
|
|
1608
1949
|
return errorResult(`add_comment must stay on this agent run's task (${run.taskId}).`);
|
|
1609
1950
|
}
|
package/dist/protocol.js
CHANGED
|
@@ -13,7 +13,16 @@ export const PROTOCOL_FULL = `## 5a. Agent working protocol
|
|
|
13
13
|
|
|
14
14
|
Delivered redundantly (installed skill + MCP), phrased imperatively for the agent:
|
|
15
15
|
|
|
16
|
-
**
|
|
16
|
+
**Workflow authority.** The \`workflow\` object returned by \`get_task\` is the durable
|
|
17
|
+
source of truth. When \`workflow.enabled\` is true, follow only its current stage,
|
|
18
|
+
stored instructions, capabilities, requirements, and gate. Never infer or skip a
|
|
19
|
+
stage. Call \`submit_workflow_stage\` with the stage's persisted artifacts when its
|
|
20
|
+
requirements are satisfied. A stage awaiting review never advances until the user
|
|
21
|
+
approves through the web app or explicitly in conversation. Requested changes stay
|
|
22
|
+
in the same stage. Only final user approval moves the item to Done.
|
|
23
|
+
|
|
24
|
+
**HARD STOP — no-workflow unclear novel feature.** This rule applies only when
|
|
25
|
+
\`workflow.enabled\` is false. After \`record_context_exploration\` succeeds,
|
|
17
26
|
if the task does not explicitly request a build or name a deliverable, the immediate
|
|
18
27
|
next tool call is \`ask_question\` with category \`intent\` and the exact question:
|
|
19
28
|
“What should I produce for this feature? Select one or more: build, plan, spec,
|
|
@@ -36,8 +45,11 @@ is saved to the work item. I am applying the task's explicit intent gate now.”
|
|
|
36
45
|
collaboration, claims, or reservations. The ID is the only context in the paste;
|
|
37
46
|
everything current lives in ctrl-spc.
|
|
38
47
|
2. **Begin explicitly.** Read every active repository scope, checkout, topology revision,
|
|
39
|
-
unmet dependency, and collaborator, then call \`begin_work\`.
|
|
40
|
-
|
|
48
|
+
unmet dependency, and collaborator, then call \`begin_work\`. If the user supplied an
|
|
49
|
+
exact instruction alongside the copied command, pass it as \`prompt_instruction\`; never
|
|
50
|
+
invent one. The first live run
|
|
51
|
+
becomes coordinator and \`begin_work\` moves a Backlog item to In Progress. Workflow
|
|
52
|
+
tasks remain In Progress through every active stage and review gate. Later runs
|
|
41
53
|
join as collaborators without repeating that transition. Only the current coordinator
|
|
42
54
|
may make later task-status changes, and no agent marks an item Done autonomously.
|
|
43
55
|
3. **Repair missing context.** If \`begin_work\` reports unavailable checkouts, warn the
|
|
@@ -76,13 +88,15 @@ is saved to the work item. I am applying the task's explicit intent gate now.”
|
|
|
76
88
|
never disclose findings, conclusions, likely impact, or decisions. After persistence,
|
|
77
89
|
terminal summaries may reference the persisted artifact/question but must add no
|
|
78
90
|
unpersisted facts.
|
|
79
|
-
6. **
|
|
80
|
-
|
|
91
|
+
6. **Follow the workflow; otherwise ask exactly what is needed.** For an assigned workflow,
|
|
92
|
+
the stored current-stage instructions and capabilities decide the work; never skip ahead
|
|
93
|
+
based on task wording. Without a workflow, after exploration is recorded, classify intent
|
|
94
|
+
from the work item's explicit wording and accepted answers,
|
|
81
95
|
never from the exploration findings. Findings do not turn an unclear request into an
|
|
82
96
|
authorized deliverable. Perform the action only when the item explicitly names it; do
|
|
83
97
|
not ask for confirmation. Otherwise use \`ask_question\`. For a bug, ask for reproduction steps,
|
|
84
98
|
expected behavior, or actual behavior only when each is missing and not discoverable.
|
|
85
|
-
For a novel feature with no explicit build action or deliverable, apply the HARD STOP
|
|
99
|
+
For a no-workflow novel feature with no explicit build action or deliverable, apply the HARD STOP
|
|
86
100
|
above; the immediate next call is the exact build/plan/spec/diagram/mock/wireframe
|
|
87
101
|
\`ask_question\`.
|
|
88
102
|
Generic goals such as “improve,” “coordinate,” “prepare,” or “support” do not authorize
|
|
@@ -94,11 +108,15 @@ is saved to the work item. I am applying the task's explicit intent gate now.”
|
|
|
94
108
|
collision handoff. After \`ask_question\` succeeds, do no more work in that run: call
|
|
95
109
|
\`end_work\` with reason \`pending_user_answer\`, a receipt outcome of \`blocked\`, the
|
|
96
110
|
final task revision, and every live artifact ID; continue only in a later pasted run.
|
|
97
|
-
7. **Persist every accepted answer and durable finding.**
|
|
98
|
-
|
|
99
|
-
|
|
111
|
+
7. **Persist every accepted answer and durable finding.** For workflow questions, call
|
|
112
|
+
\`record_user_input\` with the open question IDs and the user's exact answer; it creates
|
|
113
|
+
the required answer artifact before the stage continues. For no-workflow tasks, use
|
|
114
|
+
revision-safe \`update_task\` for accepted decisions and constraints. Immediately call \`update_task\`
|
|
115
|
+
after accepting a no-workflow answer so a later agent can recover it.
|
|
100
116
|
Never leave an accepted answer or finding only in terminal, chat, or a subagent report.
|
|
101
|
-
8. **Reserve the exact write surface.**
|
|
117
|
+
8. **Reserve the exact write surface only when allowed.** Read-only workflow stages never
|
|
118
|
+
reserve write paths or edit files. When the current stage allows repository writes, call
|
|
119
|
+
\`reserve_work_paths\` with
|
|
102
120
|
the topology revision and, for every intended edit, the exact
|
|
103
121
|
\`repository_scope_id\`, selected \`repository_checkout_id\`, repo-relative path, and
|
|
104
122
|
write mode. An empty path reserves the entire scope. Do not edit until the full atomic
|
|
@@ -106,12 +124,14 @@ is saved to the work item. I am applying the task's explicit intent gate now.”
|
|
|
106
124
|
9. **Never edit through a collision.** A collision reserves nothing. Narrow to disjoint
|
|
107
125
|
paths, wait, ask for an explicit handoff, or use an isolated Git worktree. Never edit a
|
|
108
126
|
conflicting path, overwrite unexpected local changes, or assume another agent stopped.
|
|
109
|
-
10. **Persist every substantive non-question output.** Use \`create_artifact\` for every
|
|
127
|
+
10. **Persist and submit every substantive non-question output.** Use \`create_artifact\` for every
|
|
110
128
|
plan, specification, research/finding report, schema, diagram, mock, and wireframe before
|
|
111
129
|
presenting it as complete. Use type \`analysis\` for research/findings, \`plan\` for plans,
|
|
112
130
|
\`spec\` for specifications or proposed schemas, and the matching visual type. Never
|
|
113
131
|
write planning documents into a repository. Every artifact has exactly one coverage disposition and non-empty
|
|
114
|
-
reason for every active repository scope at the current topology revision.
|
|
132
|
+
reason for every active repository scope at the current topology revision. For workflow
|
|
133
|
+
tasks, pass those artifact IDs and requirement roles to \`submit_workflow_stage\`; the
|
|
134
|
+
database alone decides whether the next stage starts or a review gate opens.
|
|
115
135
|
11. **Keep the work item self-sufficient.** Questions are durable records created through
|
|
116
136
|
\`ask_question\`; accepted answers and durable facts live in the description; substantive
|
|
117
137
|
outputs live in artifacts. No progress comments. Never leave useful
|
|
@@ -127,10 +147,11 @@ is saved to the work item. I am applying the task's explicit intent gate now.”
|
|
|
127
147
|
abandoning the run. Missing, stale, foreign, duplicate, or incomplete receipts are
|
|
128
148
|
rejected. Ending releases only this session's work.`;
|
|
129
149
|
export const PROTOCOL_SHORT = 'ctrl-spc protocol (compact — full text: MCP prompt `ctrl-spc-protocol`): ' +
|
|
130
|
-
'
|
|
131
|
-
'
|
|
150
|
+
'Workflow is authoritative: follow only get_task.workflow.current_stage instructions, capabilities, requirements, and gate. submit_workflow_stage cannot skip stages; requested changes stay in-stage; only final user approval sets Done. ' +
|
|
151
|
+
'For no-workflow unclear features only, MUST use ask_question and end blocked before doing unrequested work; never infer from findings. Generic goals improve, coordinate, prepare, and support are not deliverables. ' +
|
|
152
|
+
'1) get_task is read-only; begin_work (run coordinator) with exact prompt_instruction when supplied; resolve missing checkouts via fetch/link or acknowledge_unavailable_checkout. ' +
|
|
132
153
|
'2) Every item needs read-only subagents covering scope IDs once. Codex spawn_agent MUST use fork_turns:none; its self-contained prompt has exact IDs/paths/focus, no copied command. Child calls no ctrl-spc tools and writes nothing. If unavailable, record blocked; stop. ' +
|
|
133
154
|
'3) Parent must record_context_exploration before deciding, asking, planning, reserving, or editing. Before success narrate process only—no findings/conclusions/impact/decisions. Later summaries add no unpersisted facts. ' +
|
|
134
|
-
'4)
|
|
135
|
-
'5)
|
|
136
|
-
'6) reserve_work_paths exact checkout
|
|
155
|
+
'4) Workflow stage data decides intent. Without a workflow, intent comes only from task, prompt, and accepted answers—never from findings. ' +
|
|
156
|
+
'5) Workflow answers use record_user_input artifacts; no-workflow answers use revision-safe update_task. Persist every substantive output with create_artifact and every active scope covered. No progress comments or terminal-only findings. ' +
|
|
157
|
+
'6) Call reserve_work_paths only when the stage allows writes and target the exact checkout; never edit collisions. release_work_paths, then end_work with persistence_receipt={final_task_revision,artifact_ids,outcome}.';
|
package/dist/skills.js
CHANGED
|
@@ -38,21 +38,23 @@ export function codexLegacySkillPath() {
|
|
|
38
38
|
function protocolBody(mcpUrl) {
|
|
39
39
|
return `Resolve a CTRL+SPC work item or artifact pasted as \`/ctrl-spc work <id>\` or \`/ctrl-spc artifact <id>\`, then follow the ctrl-spc working protocol:
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
WORKFLOW AUTHORITY — when \`get_task.workflow.enabled\` is true, follow only its current stage, stored instructions, capabilities, requirements, and gate. Never infer or skip a stage. Persist each required artifact, then call \`submit_workflow_stage\`. Review gates advance only after explicit user approval in the web app or conversation; requested changes stay in the same stage. Only final approval sets Done.
|
|
42
|
+
|
|
43
|
+
HARD STOP — unclear novel feature (No workflow only): when \`workflow.enabled\` is false, after \`record_context_exploration\` succeeds, if the task does not explicitly request a build or name a deliverable, the next tool call MUST be \`ask_question\` with category \`intent\` and this exact question: “What should I produce for this feature? Select one or more: build, plan, spec, diagram, mock, wireframe.” Then call \`end_work\` with reason \`pending_user_answer\` and outcome \`blocked\`. In that run, never call \`get_task\` again, \`update_task\`, \`create_artifact\`, \`reserve_work_paths\`, or any other tool between the context artifact and \`ask_question\`. The context artifact is the only allowed artifact. Do not copy findings into the task description before the user answers. A read-only execution sandbox is not a missing checkout and must not change this intent question. The user may select one or more: build, plan, spec, diagram, mock, wireframe.
|
|
42
44
|
|
|
43
45
|
Before the context artifact exists, never state or imply a requested deliverable and never mention repository counts, contents, tests, findings, or likely impact. If progress commentary is required, use only: “The required CTRL+SPC context review is in progress; no decision or repository change has been made.” After the context artifact and before the intent question, either say nothing or use only: “The required context review is saved to the work item. I am applying the task's explicit intent gate now.”
|
|
44
46
|
|
|
45
47
|
1. Resolve without side effects — call \`get_task\` before anything else. It is read-only: it does not change status, presence, claims, or reservations. The ID is the only context in the paste; everything current lives in ctrl-spc.
|
|
46
|
-
2. Begin explicitly — call \`begin_work\` after reading the topology. The first live run is coordinator and \`begin_work\` moves Backlog to In Progress; later runs join as collaborators. Only the coordinator changes status. Never mark an item \`done\` autonomously.
|
|
48
|
+
2. Begin explicitly — call \`begin_work\` after reading the topology. If the user supplied an exact instruction alongside the copied command, pass it as \`prompt_instruction\`; never invent one. The first live run is coordinator and \`begin_work\` moves Backlog to In Progress; later runs join as collaborators. Only the coordinator changes status. Never mark an item \`done\` autonomously.
|
|
47
49
|
3. Repair missing context — if \`begin_work\` reports an unavailable checkout, warn the user and offer to fetch or link it. Never fetch without approval. If the user explicitly continues without it, call \`acknowledge_unavailable_checkout\`, then call \`begin_work\` again after every missing scope is fetched or acknowledged.
|
|
48
50
|
4. Delegate exploration — every pasted work item MUST use at least one read-only subagent. After \`begin_work\` joins, partition every available repository scope exactly once across bounded subagent invocations. Every child prompt must explicitly list its assigned \`repository_scope_ids\`; across the batch each available ID appears exactly once. When using Codex \`spawn_agent\`, MUST pass \`fork_turns: "none"\`; never pass \`all\` or a recent-turn count. Give each child a self-contained assignment containing only its exact scope IDs, checkout paths, inspection focus, read-only restrictions, and required report shape. Never include or inherit the parent \`/ctrl-spc work\` command. The child must not call any ctrl-spc tool—including \`get_task\` or \`begin_work\`—or pick up the work item; it only inspects assigned paths and returns its report. If zero scopes are available after explicit acknowledgements, one child inspects the task/artifact context and reports those unavailable scopes without pretending they were reviewed. Children inspect only: no file mutation, path reservation, ctrl-spc call, \`tee\`, shell redirection, temp/capture file, install, formatter, cache-generating command, or any command that can write. Inspection-first is sufficient; do not decide intent before reports are persisted. Each report uses \`{ agent, focus, repository_scope_ids, inspected_paths, findings, likely_impact, unresolved_questions }\`. If no subagent is available, do not explore directly: call \`record_context_exploration\` with \`blocked_reason = subagents_unavailable\`, tell the user, and stop.
|
|
49
|
-
5. Persist context before deciding — the parent consolidates every child report and MUST call \`record_context_exploration\` to create the work item's analysis artifact before deciding what to do, asking a task question, planning, reserving, or editing. Pass the structured reports plus a separate coverage array; include every active scope exactly once, and give unavailable scopes their acknowledgement. Before the tool succeeds, commentary may state process only: never disclose findings, conclusions, likely impact, or decisions. After persistence, terminal summaries may reference the persisted artifact/question but must add no unpersisted facts.
|
|
50
|
-
6. Decide or ask deterministically —
|
|
51
|
-
7. Persist answers and findings — after every accepted answer, call \`update_task\` to add the decision, constraint, acceptance criterion, and durable findings to the description.
|
|
52
|
-
8. Reserve before writing —
|
|
53
|
-
9. Persist every output — every substantive non-question output must
|
|
51
|
+
5. Persist context before deciding — the parent consolidates every child report and MUST call \`record_context_exploration\` to create the work item's analysis artifact before deciding what to do, asking a task question, planning, reserving, or editing. Pass the structured reports plus a separate coverage array; include every active repository scope exactly once, and give unavailable scopes their acknowledgement. Before the tool succeeds, commentary may state process only: never disclose findings, conclusions, likely impact, or decisions. After persistence, terminal summaries may reference the persisted artifact/question but must add no unpersisted facts.
|
|
52
|
+
6. Decide or ask deterministically — assigned workflow stage data decides the work. Without a workflow, classify intent only from the work item's explicit wording and accepted answers, never from findings. Findings do not authorize a deliverable. Infer and perform the action only when the item explicitly names it. Otherwise persist only the necessary question with \`ask_question\`. For a bug, ask for reproduction steps, expected behavior, or actual behavior only when each is missing and not discoverable. For a novel feature with no explicit build action or deliverable, apply the HARD STOP above: the mandatory next mutating call after \`record_context_exploration\` is \`ask_question\`; it must say “select one or more: build, plan, spec, diagram, mock, wireframe.” Generic goals such as “improve,” “coordinate,” “prepare,” or “support” are not deliverables. In this state do not call \`update_task\`, \`create_artifact\`, \`reserve_work_paths\`, or \`create_task\`; do not decide, edit, or complete. The context analysis is the only allowed artifact before the answer. Ask before overriding an unmet dependency, splitting work, fetching, or accepting a collision handoff. After \`ask_question\` succeeds, do no more work in that run: call \`end_work\` with reason \`pending_user_answer\` and outcome \`blocked\`, then wait for a later pasted run.
|
|
53
|
+
7. Persist answers and findings — workflow answers MUST use \`record_user_input\` with open question IDs and the exact user answer so an artifact is saved. For No workflow, after every accepted answer, call revision-safe \`update_task\` to add the decision, constraint, acceptance criterion, and durable findings to the description. Do not use progress comments; do not leave findings only in the terminal.
|
|
54
|
+
8. Reserve before writing — read-only stages never reserve writes or edit files. When the current stage allows writes, call \`reserve_work_paths\` for the exact checkout and paths; never edit through a collision. Never edit a conflicting path; narrow the reservation or use an isolated Git worktree.
|
|
55
|
+
9. Persist every output — call \`create_artifact\` for every substantive non-question output. Every artifact must have exactly one coverage disposition for every active repository scope. For workflow tasks, submit required artifact IDs and roles through \`submit_workflow_stage\`; the database decides whether to advance or wait for review. Never write planning documents into a repository.
|
|
54
56
|
10. Split only with approval — when work is too large, use \`ask_question\`; on approval create dependency-linked items with \`create_task\`, a shared feature tag, and matching board order.
|
|
55
|
-
11. Keep the item self-sufficient — questions are persisted through \`ask_question\`; accepted
|
|
57
|
+
11. Keep the item self-sufficient — questions are persisted through \`ask_question\`; every accepted answer and durable fact lives in the description; substantive outputs live in artifacts. Do not use progress comments; \`add_comment\` is not for routine progress. A fresh agent must need no terminal, chat, or subagent history.
|
|
56
58
|
12. End with proof — release finished reservations with \`release_work_paths\`, then call \`end_work\` with \`persistence_receipt = { final_task_revision, artifact_ids, outcome }\`. \`artifact_ids\` must exactly match every live artifact produced by this run and include the context-exploration artifact. Use outcome \`completed\` only for finished work, \`blocked\` for a persisted question or unavailable subagents, and \`aborted\` only when intentionally abandoning the run. A missing, stale, foreign, duplicate, or incomplete receipt is rejected; ending releases only this session's work.
|
|
57
59
|
|
|
58
60
|
Full protocol: MCP prompt \`ctrl-spc-protocol\`. Tools live at ${mcpUrl}.
|