@ctrl-spc/cli 1.3.5 → 1.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/run.js +1 -1
- package/dist/mcp.js +297 -138
- package/dist/protocol.js +23 -13
- package/dist/skills.js +6 -6
- package/package.json +1 -1
package/dist/commands/run.js
CHANGED
|
@@ -881,7 +881,7 @@ export async function run() {
|
|
|
881
881
|
'release_work_paths, transfer_coordination, acknowledge_unavailable_checkout, record_context_exploration, ' +
|
|
882
882
|
'set_task_role_slugs, ' +
|
|
883
883
|
'ask_question, record_user_input, submit_workflow_stage, decide_workflow_review, ' +
|
|
884
|
-
'end_work, create_task, update_task, create_artifact, add_comment)');
|
|
884
|
+
'end_work, create_task, update_task, create_artifact, update_artifact, add_comment)');
|
|
885
885
|
console.log(`Projects: ${projects.length > 0 ? projects.map((project) => project.name).join(', ') : 'none initialized on this machine'}`);
|
|
886
886
|
console.log('Presence: available');
|
|
887
887
|
const dispatch = createAgentDispatchController(client, machine.id, registeredAgents);
|
package/dist/mcp.js
CHANGED
|
@@ -437,7 +437,15 @@ export async function beginAgentRunHandler(deps, session, args) {
|
|
|
437
437
|
// field as already provided only for backwards-compatible embedded
|
|
438
438
|
// clients/test doubles that predate this contract.
|
|
439
439
|
const persistedInstruction = typeof task.description === 'string' ? task.description.trim() : null;
|
|
440
|
-
|
|
440
|
+
const acceptedIntentDecisions = (await must(deps.client
|
|
441
|
+
.from('decisions')
|
|
442
|
+
.select('id')
|
|
443
|
+
.eq('task_id', args.task_id)
|
|
444
|
+
.eq('category', 'intent')
|
|
445
|
+
.eq('state', 'decided')
|
|
446
|
+
.order('decided_at', { ascending: false }))) ?? [];
|
|
447
|
+
session.run.noWorkflowIntent = persistedInstruction === '' &&
|
|
448
|
+
!args.prompt_instruction?.trim() && acceptedIntentDecisions.length === 0
|
|
441
449
|
? 'missing'
|
|
442
450
|
: 'provided';
|
|
443
451
|
}
|
|
@@ -683,7 +691,7 @@ function explorationArtifactContent(args, orderedCoverage) {
|
|
|
683
691
|
* mutation. The receipt is deliberately session-local while its evidence is a
|
|
684
692
|
* durable analysis artifact tied to the exact task, run, and topology revision.
|
|
685
693
|
*/
|
|
686
|
-
export async function recordContextExplorationHandler(deps,
|
|
694
|
+
export async function recordContextExplorationHandler(deps, _attribution, session, args) {
|
|
687
695
|
const active = joinedRunOrError(session, 'record_context_exploration');
|
|
688
696
|
if ('content' in active)
|
|
689
697
|
return active;
|
|
@@ -742,20 +750,23 @@ export async function recordContextExplorationHandler(deps, attribution, session
|
|
|
742
750
|
return errorResult('Exploration coverage acknowledgement_id is required only for unavailable_acknowledged repositories.');
|
|
743
751
|
}
|
|
744
752
|
}
|
|
745
|
-
const
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
753
|
+
const content = explorationArtifactContent(args, orderedCoverage);
|
|
754
|
+
const existingContext = await must(deps.client
|
|
755
|
+
.from('artifacts')
|
|
756
|
+
.select('revision')
|
|
757
|
+
.eq('task_id', active.taskId)
|
|
758
|
+
.eq('system_kind', 'context')
|
|
759
|
+
.is('deleted_at', null)
|
|
760
|
+
.maybeSingle());
|
|
761
|
+
const artifact = await must(deps.client.rpc('record_context_exploration_artifact', {
|
|
762
|
+
p_run: active.runId,
|
|
763
|
+
p_claim_fencing_token: active.fencingToken,
|
|
764
|
+
p_topology_revision: active.topologyRevision,
|
|
765
|
+
p_expected_revision: existingContext ? Number(existingContext.revision) : null,
|
|
766
|
+
p_content: content,
|
|
767
|
+
p_coverage: orderedCoverage,
|
|
768
|
+
}));
|
|
769
|
+
const artifactId = typeof artifact?.id === 'string' ? artifact.id : null;
|
|
759
770
|
if (!artifactId)
|
|
760
771
|
throw new Error('analysis artifact returned no id');
|
|
761
772
|
session.exploration = {
|
|
@@ -778,6 +789,10 @@ export async function recordContextExplorationHandler(deps, attribution, session
|
|
|
778
789
|
});
|
|
779
790
|
}
|
|
780
791
|
catch (err) {
|
|
792
|
+
if (err instanceof Error && err.message === 'context_conflict') {
|
|
793
|
+
return errorResult('record_context_exploration found newer work-item context. Reload the work item, ' +
|
|
794
|
+
'reconcile the latest context with these reports, and retry so no findings are lost.');
|
|
795
|
+
}
|
|
781
796
|
return coordinationError('record_context_exploration', err);
|
|
782
797
|
}
|
|
783
798
|
}
|
|
@@ -785,6 +800,25 @@ function isRealQuestion(text) {
|
|
|
785
800
|
const trimmed = text?.trim() ?? '';
|
|
786
801
|
return trimmed.length >= 4 && /[\p{L}\p{N}]/u.test(trimmed) && trimmed.includes('?');
|
|
787
802
|
}
|
|
803
|
+
function decisionQuestionConfiguration(answerMode, rawOptions) {
|
|
804
|
+
const mode = answerMode ?? 'free_text';
|
|
805
|
+
const options = rawOptions ?? [];
|
|
806
|
+
if (mode === 'free_text') {
|
|
807
|
+
return options.length === 0
|
|
808
|
+
? { answerMode: mode, options }
|
|
809
|
+
: 'Free-text questions cannot include choices.';
|
|
810
|
+
}
|
|
811
|
+
if (options.length < 2 || options.length > 12) {
|
|
812
|
+
return 'Single- and multi-select questions require between 2 and 12 choices.';
|
|
813
|
+
}
|
|
814
|
+
if (options.some(option => option.length === 0 || option !== option.trim())) {
|
|
815
|
+
return 'Decision choices must be non-empty and cannot start or end with spaces.';
|
|
816
|
+
}
|
|
817
|
+
if (new Set(options.map(option => option.toLocaleLowerCase())).size !== options.length) {
|
|
818
|
+
return 'Decision choices must be unique, including capitalization-only differences.';
|
|
819
|
+
}
|
|
820
|
+
return { answerMode: mode, options };
|
|
821
|
+
}
|
|
788
822
|
function isPersistedAgentQuestion(row, taskId, agentRunId) {
|
|
789
823
|
if (row.task_id !== taskId || row.agent_run_id !== agentRunId || typeof row.body !== 'string')
|
|
790
824
|
return false;
|
|
@@ -793,92 +827,122 @@ function isPersistedAgentQuestion(row, taskId, agentRunId) {
|
|
|
793
827
|
QUESTION_CATEGORIES.includes(match[1]) &&
|
|
794
828
|
isRealQuestion(match[2]));
|
|
795
829
|
}
|
|
796
|
-
export async function askQuestionHandler(deps,
|
|
830
|
+
export async function askQuestionHandler(deps, _attribution, args, agentRun) {
|
|
797
831
|
if (args.task_id !== agentRun.taskId) {
|
|
798
832
|
return errorResult(`ask_question must stay on this agent run's task (${agentRun.taskId}).`);
|
|
799
833
|
}
|
|
800
834
|
if (!QUESTION_CATEGORIES.includes(args.category))
|
|
801
835
|
return errorResult('ask_question requires a supported category.');
|
|
836
|
+
if (!args.context?.trim())
|
|
837
|
+
return errorResult('ask_question requires concise context for the decision.');
|
|
802
838
|
if (!isRealQuestion(args.question)) {
|
|
803
839
|
return errorResult('ask_question requires real question text containing a question mark.');
|
|
804
840
|
}
|
|
841
|
+
const configuration = decisionQuestionConfiguration(args.answer_mode, args.options);
|
|
842
|
+
if (typeof configuration === 'string')
|
|
843
|
+
return errorResult(configuration);
|
|
805
844
|
const question = args.question.trim();
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
const first = result.content[0];
|
|
810
|
-
if (!first || first.type !== 'text')
|
|
811
|
-
return errorResult('ask_question returned no persisted comment.');
|
|
812
|
-
const payload = JSON.parse(first.text);
|
|
813
|
-
const commentId = typeof payload.comment?.id === 'string' ? payload.comment.id : null;
|
|
814
|
-
let workflowQuestionId = null;
|
|
815
|
-
if (agentRun.workflow?.enabled) {
|
|
816
|
-
if (!commentId)
|
|
817
|
-
return errorResult('ask_question returned no persisted comment id for the workflow question.');
|
|
818
|
-
workflowQuestionId = await must(deps.client.rpc('open_workflow_question', {
|
|
845
|
+
let opened;
|
|
846
|
+
try {
|
|
847
|
+
opened = await must(deps.client.rpc('ask_agent_question_v2', {
|
|
819
848
|
p_run: agentRun.runId,
|
|
820
|
-
p_comment: commentId,
|
|
821
849
|
p_category: args.category,
|
|
850
|
+
p_context: args.context.trim(),
|
|
822
851
|
p_question: question,
|
|
852
|
+
p_answer_mode: configuration.answerMode,
|
|
853
|
+
p_options: configuration.options,
|
|
854
|
+
p_related_artifact: args.related_artifact_id ?? null,
|
|
823
855
|
}));
|
|
856
|
+
}
|
|
857
|
+
catch (err) {
|
|
858
|
+
return coordinationError('ask_question', err);
|
|
859
|
+
}
|
|
860
|
+
if (!opened)
|
|
861
|
+
return errorResult('ask_question returned no persisted question.');
|
|
862
|
+
const comment = opened?.comment && typeof opened.comment === 'object'
|
|
863
|
+
? opened.comment
|
|
864
|
+
: null;
|
|
865
|
+
const commentId = typeof comment?.id === 'string' ? comment.id : null;
|
|
866
|
+
if (!commentId)
|
|
867
|
+
return errorResult('ask_question returned no persisted comment id for the decision.');
|
|
868
|
+
const decisionId = typeof opened?.decision_id === 'string' ? opened.decision_id : null;
|
|
869
|
+
const workflowQuestionId = typeof opened?.workflow_question_id === 'string' ? opened.workflow_question_id : null;
|
|
870
|
+
if (!decisionId)
|
|
871
|
+
return errorResult('ask_question returned no persisted decision id.');
|
|
872
|
+
if (agentRun.workflow?.enabled) {
|
|
824
873
|
agentRun.workflow = await loadWorkflowContext(deps.client, agentRun.taskId);
|
|
825
874
|
}
|
|
826
875
|
return textResult({
|
|
827
876
|
question: {
|
|
828
877
|
category: args.category,
|
|
829
878
|
text: question,
|
|
879
|
+
context: args.context.trim(),
|
|
880
|
+
answer_mode: configuration.answerMode,
|
|
881
|
+
options: configuration.options,
|
|
830
882
|
comment_id: commentId,
|
|
883
|
+
decision_id: decisionId,
|
|
831
884
|
workflow_question_id: workflowQuestionId,
|
|
885
|
+
related_artifact_id: args.related_artifact_id ?? null,
|
|
832
886
|
},
|
|
833
|
-
comment
|
|
887
|
+
comment,
|
|
834
888
|
...(agentRun.workflow?.enabled ? { workflow: agentRun.workflow } : {}),
|
|
835
889
|
});
|
|
836
890
|
}
|
|
837
|
-
export async function recordUserInputHandler(deps,
|
|
891
|
+
export async function recordUserInputHandler(deps, _attribution, session, args) {
|
|
838
892
|
const active = joinedRunOrError(session, 'record_user_input');
|
|
839
893
|
if ('content' in active)
|
|
840
894
|
return active;
|
|
841
|
-
if (!active.workflow?.enabled)
|
|
842
|
-
return errorResult('record_user_input requires a workflow task.');
|
|
843
895
|
const capabilityError = workflowCapabilityError(active, 'record_user_input', 'record_user_input');
|
|
844
896
|
if (capabilityError)
|
|
845
897
|
return capabilityError;
|
|
846
|
-
|
|
847
|
-
|
|
898
|
+
const structuredResponses = args.responses;
|
|
899
|
+
const decisionIds = args.decision_ids ?? args.question_ids;
|
|
900
|
+
if (structuredResponses && (args.decision_ids || args.question_ids || args.answer !== undefined)) {
|
|
901
|
+
return errorResult('record_user_input accepts either structured responses or the legacy decision_ids plus answer shape, not both.');
|
|
902
|
+
}
|
|
903
|
+
if (structuredResponses) {
|
|
904
|
+
if (structuredResponses.length === 0) {
|
|
905
|
+
return errorResult('record_user_input requires at least one structured decision response.');
|
|
906
|
+
}
|
|
907
|
+
const ids = structuredResponses.map(response => response.decision_id);
|
|
908
|
+
if (ids.some(id => !id?.trim()) || new Set(ids).size !== ids.length) {
|
|
909
|
+
return errorResult('record_user_input requires one unique decision_id per structured response.');
|
|
910
|
+
}
|
|
911
|
+
if (structuredResponses.some(response => {
|
|
912
|
+
const selected = response.selected_options ?? [];
|
|
913
|
+
return new Set(selected).size !== selected.length;
|
|
914
|
+
})) {
|
|
915
|
+
return errorResult('record_user_input cannot submit the same selected option more than once.');
|
|
916
|
+
}
|
|
917
|
+
try {
|
|
918
|
+
active.workflow = normalizeWorkflowContext(await must(deps.client.rpc('record_task_decision_responses', {
|
|
919
|
+
p_run: active.runId,
|
|
920
|
+
p_responses: structuredResponses,
|
|
921
|
+
})));
|
|
922
|
+
return textResult({
|
|
923
|
+
recorded: true,
|
|
924
|
+
decision_ids: ids,
|
|
925
|
+
workflow: active.workflow,
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
catch (err) {
|
|
929
|
+
return coordinationError('record_user_input', err);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
if (!Array.isArray(decisionIds) || decisionIds.length === 0) {
|
|
933
|
+
return errorResult('record_user_input requires at least one decision id returned by ask_question.');
|
|
848
934
|
}
|
|
849
935
|
if (!args.answer?.trim())
|
|
850
936
|
return errorResult('record_user_input requires the user\'s exact answer.');
|
|
851
937
|
try {
|
|
852
|
-
|
|
853
|
-
const questionText = args.question_ids.map((id) => {
|
|
854
|
-
const question = openQuestions.find((candidate) => candidate.id === id);
|
|
855
|
-
return `- ${typeof question?.question === 'string' ? question.question : id}`;
|
|
856
|
-
}).join('\n');
|
|
857
|
-
const artifactResult = await createArtifactHandler(bindProject(deps, active.projectId), attribution, {
|
|
858
|
-
task_id: active.taskId,
|
|
859
|
-
type: 'analysis',
|
|
860
|
-
format: 'md',
|
|
861
|
-
content: `# User input\n\n## Questions\n\n${questionText}\n\n## User answer\n\n${args.answer.trim()}`,
|
|
862
|
-
coverage: args.coverage,
|
|
863
|
-
}, active);
|
|
864
|
-
if (artifactResult.isError)
|
|
865
|
-
return artifactResult;
|
|
866
|
-
const first = artifactResult.content[0];
|
|
867
|
-
if (!first || first.type !== 'text')
|
|
868
|
-
throw new Error('answer artifact returned no text result');
|
|
869
|
-
const payload = JSON.parse(first.text);
|
|
870
|
-
const artifactId = typeof payload.artifact?.id === 'string' ? payload.artifact.id : null;
|
|
871
|
-
if (!artifactId)
|
|
872
|
-
throw new Error('answer artifact returned no id');
|
|
873
|
-
active.workflow = normalizeWorkflowContext(await must(deps.client.rpc('record_workflow_answer', {
|
|
938
|
+
active.workflow = normalizeWorkflowContext(await must(deps.client.rpc('record_task_decisions', {
|
|
874
939
|
p_run: active.runId,
|
|
875
|
-
|
|
876
|
-
|
|
940
|
+
p_decision_ids: decisionIds,
|
|
941
|
+
p_answer: args.answer,
|
|
877
942
|
})));
|
|
878
943
|
return textResult({
|
|
879
944
|
recorded: true,
|
|
880
|
-
|
|
881
|
-
question_ids: args.question_ids,
|
|
945
|
+
decision_ids: decisionIds,
|
|
882
946
|
workflow: active.workflow,
|
|
883
947
|
});
|
|
884
948
|
}
|
|
@@ -934,27 +998,6 @@ export async function decideWorkflowReviewHandler(deps, session, args) {
|
|
|
934
998
|
return errorResult('decide_workflow_review requires the user\'s exact approval or change-request message as evidence.');
|
|
935
999
|
}
|
|
936
1000
|
try {
|
|
937
|
-
const scopes = await activeRepositoryScopes(deps.client, active.projectId);
|
|
938
|
-
const evidenceResult = await createArtifactHandler(bindProject(deps, active.projectId), attributionFromClientName(session.clientName), {
|
|
939
|
-
task_id: active.taskId,
|
|
940
|
-
type: 'analysis',
|
|
941
|
-
format: 'md',
|
|
942
|
-
content: `# Workflow review decision\n\n${args.user_message.trim()}`,
|
|
943
|
-
coverage: scopes.map(scope => ({
|
|
944
|
-
repository_scope_id: scope.repository_scope_id,
|
|
945
|
-
disposition: 'context_only',
|
|
946
|
-
reason: 'Conversation review evidence applies to this workflow task.',
|
|
947
|
-
})),
|
|
948
|
-
}, active);
|
|
949
|
-
if (evidenceResult.isError)
|
|
950
|
-
return evidenceResult;
|
|
951
|
-
const evidenceBlock = evidenceResult.content[0];
|
|
952
|
-
if (!evidenceBlock || evidenceBlock.type !== 'text')
|
|
953
|
-
throw new Error('review evidence artifact returned no text result');
|
|
954
|
-
const evidencePayload = JSON.parse(evidenceBlock.text);
|
|
955
|
-
const evidenceArtifactId = typeof evidencePayload.artifact?.id === 'string' ? evidencePayload.artifact.id : null;
|
|
956
|
-
if (!evidenceArtifactId)
|
|
957
|
-
throw new Error('review evidence artifact returned no id');
|
|
958
1001
|
const context = normalizeWorkflowContext(await must(deps.client.rpc('decide_workflow_review', {
|
|
959
1002
|
p_task: args.task_id,
|
|
960
1003
|
p_stage_run: args.stage_run_id,
|
|
@@ -963,17 +1006,15 @@ export async function decideWorkflowReviewHandler(deps, session, args) {
|
|
|
963
1006
|
p_source: 'agent_conversation',
|
|
964
1007
|
p_evidence: args.user_message.trim(),
|
|
965
1008
|
p_agent_run: active.runId,
|
|
966
|
-
p_evidence_artifact:
|
|
1009
|
+
p_evidence_artifact: null,
|
|
967
1010
|
p_dispatch_agent: false,
|
|
968
1011
|
})));
|
|
969
1012
|
if (session.run?.state === 'joined' && session.run.taskId === args.task_id) {
|
|
970
1013
|
session.run.workflow = context;
|
|
971
1014
|
}
|
|
972
|
-
session.reviewEvidenceArtifactId = evidenceArtifactId;
|
|
973
1015
|
return textResult({
|
|
974
1016
|
recorded: true,
|
|
975
1017
|
decision: args.decision,
|
|
976
|
-
evidence_artifact_id: evidenceArtifactId,
|
|
977
1018
|
workflow: context,
|
|
978
1019
|
instruction: context.state === 'completed'
|
|
979
1020
|
? 'The user approved completion. The workflow and work item are Done.'
|
|
@@ -991,11 +1032,9 @@ export async function endAgentRunHandler(deps, session, args = {}, options = {})
|
|
|
991
1032
|
try {
|
|
992
1033
|
if (options.requirePersistenceReceipt) {
|
|
993
1034
|
const exploration = session.exploration;
|
|
994
|
-
const reviewEvidenceArtifactId = session.reviewEvidenceArtifactId ?? null;
|
|
995
1035
|
const receipt = args.persistence_receipt;
|
|
996
|
-
if (
|
|
997
|
-
return errorResult('end_work requires a persistence_receipt
|
|
998
|
-
}
|
|
1036
|
+
if (!receipt)
|
|
1037
|
+
return errorResult('end_work requires a persistence_receipt for this session.');
|
|
999
1038
|
const outcome = receipt.outcome ?? 'completed';
|
|
1000
1039
|
if (exploration?.status === 'blocked' && !['blocked', 'aborted'].includes(outcome)) {
|
|
1001
1040
|
return errorResult('A subagents_unavailable session may end only with outcome blocked or aborted.');
|
|
@@ -1018,16 +1057,15 @@ export async function endAgentRunHandler(deps, session, args = {}, options = {})
|
|
|
1018
1057
|
if (!Number.isInteger(receipt.final_task_revision) || receipt.final_task_revision < 1) {
|
|
1019
1058
|
return errorResult('end_work persistence_receipt requires a positive final_task_revision.');
|
|
1020
1059
|
}
|
|
1021
|
-
if (!Array.isArray(receipt.artifact_ids)
|
|
1022
|
-
return errorResult('end_work persistence_receipt requires
|
|
1023
|
-
}
|
|
1060
|
+
if (!Array.isArray(receipt.artifact_ids))
|
|
1061
|
+
return errorResult('end_work persistence_receipt requires artifact_ids.');
|
|
1024
1062
|
const artifactIds = [...new Set(receipt.artifact_ids)];
|
|
1025
1063
|
if (artifactIds.length !== receipt.artifact_ids.length) {
|
|
1026
1064
|
return errorResult('end_work persistence_receipt artifact_ids must be unique.');
|
|
1027
1065
|
}
|
|
1028
|
-
const
|
|
1029
|
-
if (!artifactIds.includes(
|
|
1030
|
-
return errorResult('end_work persistence_receipt must include this session\'s
|
|
1066
|
+
const requiredContextArtifactId = exploration?.artifactId ?? null;
|
|
1067
|
+
if (requiredContextArtifactId && !artifactIds.includes(requiredContextArtifactId)) {
|
|
1068
|
+
return errorResult('end_work persistence_receipt must include this session\'s context artifact id.');
|
|
1031
1069
|
}
|
|
1032
1070
|
const task = await must(deps.client
|
|
1033
1071
|
.from('tasks')
|
|
@@ -1040,25 +1078,29 @@ export async function endAgentRunHandler(deps, session, args = {}, options = {})
|
|
|
1040
1078
|
}
|
|
1041
1079
|
const artifactRows = (await must(deps.client
|
|
1042
1080
|
.from('artifacts')
|
|
1043
|
-
.select('id,task_id,agent_run_id,deleted_at')
|
|
1081
|
+
.select('id,task_id,agent_run_id,updated_by_agent_run_id,deleted_at')
|
|
1044
1082
|
.eq('task_id', active.taskId)
|
|
1045
|
-
.eq('agent_run_id', active.runId)
|
|
1046
1083
|
.is('deleted_at', null)
|
|
1047
1084
|
.order('created_at', { ascending: true })
|
|
1048
1085
|
.order('id', { ascending: true }))) ?? [];
|
|
1049
1086
|
const artifactsById = new Map(artifactRows.map((artifact) => [artifact.id, artifact]));
|
|
1050
|
-
const persistedArtifactIds = artifactRows
|
|
1087
|
+
const persistedArtifactIds = artifactRows
|
|
1088
|
+
.filter((artifact) => (artifact.agent_run_id === active.runId || artifact.updated_by_agent_run_id === active.runId))
|
|
1089
|
+
.map((artifact) => artifact.id);
|
|
1090
|
+
if (requiredContextArtifactId && !persistedArtifactIds.includes(requiredContextArtifactId)) {
|
|
1091
|
+
persistedArtifactIds.push(requiredContextArtifactId);
|
|
1092
|
+
}
|
|
1051
1093
|
const missingArtifactIds = persistedArtifactIds.filter((id) => !artifactIds.includes(id));
|
|
1052
|
-
const
|
|
1094
|
+
const persistedArtifactIdSet = new Set(persistedArtifactIds);
|
|
1095
|
+
const foreignArtifactIds = artifactIds.filter((id) => !persistedArtifactIdSet.has(id));
|
|
1053
1096
|
if (missingArtifactIds.length > 0 || foreignArtifactIds.length > 0) {
|
|
1054
1097
|
return errorResult('end_work persistence_receipt artifact_ids must exactly match every non-deleted artifact persisted by ' +
|
|
1055
1098
|
`this task run. Missing: ${missingArtifactIds.join(', ') || 'none'}. ` +
|
|
1056
1099
|
`Foreign or deleted: ${foreignArtifactIds.join(', ') || 'none'}.`);
|
|
1057
1100
|
}
|
|
1058
|
-
const
|
|
1059
|
-
if (!
|
|
1060
|
-
|
|
1061
|
-
return errorResult('end_work could not verify this session\'s required evidence artifact.');
|
|
1101
|
+
const contextArtifact = requiredContextArtifactId ? artifactsById.get(requiredContextArtifactId) : null;
|
|
1102
|
+
if (requiredContextArtifactId && (!contextArtifact || contextArtifact.task_id !== active.taskId)) {
|
|
1103
|
+
return errorResult('end_work could not verify this session\'s context artifact.');
|
|
1062
1104
|
}
|
|
1063
1105
|
}
|
|
1064
1106
|
const ended = await must(deps.client.rpc('end_agent_run', {
|
|
@@ -1067,7 +1109,6 @@ export async function endAgentRunHandler(deps, session, args = {}, options = {})
|
|
|
1067
1109
|
}));
|
|
1068
1110
|
session.run = null;
|
|
1069
1111
|
session.exploration = null;
|
|
1070
|
-
session.reviewEvidenceArtifactId = null;
|
|
1071
1112
|
return textResult({
|
|
1072
1113
|
ended: Boolean(ended),
|
|
1073
1114
|
agent_run_id: active.runId,
|
|
@@ -1172,7 +1213,7 @@ export async function getTaskHandler(deps, _attribution, args, machineId) {
|
|
|
1172
1213
|
}
|
|
1173
1214
|
const task = parseTaskRow(row);
|
|
1174
1215
|
const topologyMachineId = machineId ?? deps.machineId;
|
|
1175
|
-
const [rawComments, rawArtifacts, rawVersions, rawAgentRuns, rawTaskClaims, collaborationDocument, topology, workflow] = await Promise.all([
|
|
1216
|
+
const [rawComments, rawArtifacts, rawDecisions, rawVersions, rawAgentRuns, rawTaskClaims, collaborationDocument, topology, workflow] = await Promise.all([
|
|
1176
1217
|
must(client
|
|
1177
1218
|
.from('comments')
|
|
1178
1219
|
.select(COMMENT_SELECT)
|
|
@@ -1186,6 +1227,12 @@ export async function getTaskHandler(deps, _attribution, args, machineId) {
|
|
|
1186
1227
|
.is('deleted_at', null)
|
|
1187
1228
|
.order('created_at', { ascending: true })
|
|
1188
1229
|
.order('id', { ascending: true })),
|
|
1230
|
+
must(client
|
|
1231
|
+
.from('decisions')
|
|
1232
|
+
.select('*')
|
|
1233
|
+
.eq('task_id', row.id)
|
|
1234
|
+
.order('asked_at', { ascending: true })
|
|
1235
|
+
.order('id', { ascending: true })),
|
|
1189
1236
|
must(client
|
|
1190
1237
|
.from('task_versions')
|
|
1191
1238
|
.select(TASK_VERSION_SELECT)
|
|
@@ -1222,12 +1269,19 @@ export async function getTaskHandler(deps, _attribution, args, machineId) {
|
|
|
1222
1269
|
]);
|
|
1223
1270
|
const comments = sortRecords((rawComments ?? []).map(normalizeComment), 'created_at', 'id');
|
|
1224
1271
|
const normalizedArtifacts = sortRecords((rawArtifacts ?? []).map(normalizeArtifact), 'created_at', 'id');
|
|
1272
|
+
const hiddenArtifactKinds = new Set([
|
|
1273
|
+
'superseded_context',
|
|
1274
|
+
'legacy_decision_evidence',
|
|
1275
|
+
'legacy_review_evidence',
|
|
1276
|
+
]);
|
|
1277
|
+
const visibleArtifacts = normalizedArtifacts.filter((artifact) => (artifact.id === highlightArtifactId || !hiddenArtifactKinds.has(String(artifact.system_kind ?? ''))));
|
|
1225
1278
|
const artifacts = highlightArtifactId
|
|
1226
1279
|
? [
|
|
1227
|
-
...
|
|
1228
|
-
...
|
|
1280
|
+
...visibleArtifacts.filter((artifact) => artifact.id === highlightArtifactId),
|
|
1281
|
+
...visibleArtifacts.filter((artifact) => artifact.id !== highlightArtifactId),
|
|
1229
1282
|
]
|
|
1230
|
-
:
|
|
1283
|
+
: visibleArtifacts;
|
|
1284
|
+
const decisions = sortRecords(rawDecisions ?? [], 'asked_at', 'id');
|
|
1231
1285
|
const taskVersions = sortRecords(rawVersions ?? [], 'seq', 'id');
|
|
1232
1286
|
const now = Date.now();
|
|
1233
1287
|
const leaseIsLive = (value) => typeof value === 'string' && Number.isFinite(new Date(value).getTime()) && new Date(value).getTime() > now;
|
|
@@ -1270,6 +1324,7 @@ export async function getTaskHandler(deps, _attribution, args, machineId) {
|
|
|
1270
1324
|
dependent_edges: task.dependent_edges,
|
|
1271
1325
|
comments,
|
|
1272
1326
|
artifacts,
|
|
1327
|
+
decisions,
|
|
1273
1328
|
task_versions: taskVersions,
|
|
1274
1329
|
active_agent_runs: activeAgentRuns,
|
|
1275
1330
|
active_task_claims: activeTaskClaims,
|
|
@@ -1408,6 +1463,9 @@ export async function createArtifactHandler(deps, attribution, args, agentRun) {
|
|
|
1408
1463
|
return errorResult('create_artifact requires task_id.');
|
|
1409
1464
|
if (!args.content)
|
|
1410
1465
|
return errorResult('create_artifact requires content.');
|
|
1466
|
+
if (args.purpose_key?.trim().startsWith('context:')) {
|
|
1467
|
+
return errorResult('Context is a reserved work-item artifact. Use record_context_exploration instead.');
|
|
1468
|
+
}
|
|
1411
1469
|
if (agentRun) {
|
|
1412
1470
|
if (agentRun.state !== 'joined') {
|
|
1413
1471
|
return errorResult('create_artifact is blocked while repositories are unavailable. Fetch or acknowledge them, then call begin_work again.');
|
|
@@ -1415,6 +1473,11 @@ export async function createArtifactHandler(deps, attribution, args, agentRun) {
|
|
|
1415
1473
|
if (args.task_id !== agentRun.taskId) {
|
|
1416
1474
|
return errorResult(`create_artifact task_id must match this agent run's task (${agentRun.taskId}).`);
|
|
1417
1475
|
}
|
|
1476
|
+
if (!args.title?.trim())
|
|
1477
|
+
return errorResult('create_artifact requires a title for agent artifacts.');
|
|
1478
|
+
if (!args.purpose_key?.trim()) {
|
|
1479
|
+
return errorResult('create_artifact requires a stable purpose_key for agent artifacts.');
|
|
1480
|
+
}
|
|
1418
1481
|
if (!Array.isArray(args.coverage) || args.coverage.length === 0) {
|
|
1419
1482
|
return errorResult('create_artifact requires repository coverage for every active project scope.');
|
|
1420
1483
|
}
|
|
@@ -1433,6 +1496,8 @@ export async function createArtifactHandler(deps, attribution, args, agentRun) {
|
|
|
1433
1496
|
p_topology_revision: agentRun.topologyRevision,
|
|
1434
1497
|
p_type: args.type,
|
|
1435
1498
|
p_format: args.format ?? 'md',
|
|
1499
|
+
p_title: args.title.trim(),
|
|
1500
|
+
p_purpose_key: args.purpose_key.trim(),
|
|
1436
1501
|
p_content: args.content,
|
|
1437
1502
|
p_storage_path: null,
|
|
1438
1503
|
p_coverage: args.coverage.map((coverage) => ({
|
|
@@ -1459,12 +1524,14 @@ export async function createArtifactHandler(deps, attribution, args, agentRun) {
|
|
|
1459
1524
|
task_id: args.task_id,
|
|
1460
1525
|
type: args.type,
|
|
1461
1526
|
format: args.format ?? 'md',
|
|
1527
|
+
...(args.title?.trim() ? { title: args.title.trim() } : {}),
|
|
1528
|
+
...(args.purpose_key?.trim() ? { purpose_key: args.purpose_key.trim() } : {}),
|
|
1462
1529
|
content: args.content,
|
|
1463
1530
|
created_by: deps.userId,
|
|
1464
1531
|
from_agent: null,
|
|
1465
1532
|
agent_run_id: null,
|
|
1466
1533
|
})
|
|
1467
|
-
.select('id,task_id,type,format,content,storage_path,created_by,from_agent,created_at')
|
|
1534
|
+
.select('id,task_id,type,format,title,purpose_key,system_kind,content,storage_path,created_by,from_agent,created_at')
|
|
1468
1535
|
.single());
|
|
1469
1536
|
if (!row)
|
|
1470
1537
|
throw new Error('Artifact insert returned no row.');
|
|
@@ -1476,6 +1543,63 @@ export async function createArtifactHandler(deps, attribution, args, agentRun) {
|
|
|
1476
1543
|
return errorResult(`create_artifact failed: ${err.message}`);
|
|
1477
1544
|
}
|
|
1478
1545
|
}
|
|
1546
|
+
export async function updateArtifactHandler(deps, args, agentRun) {
|
|
1547
|
+
try {
|
|
1548
|
+
if (agentRun.state !== 'joined') {
|
|
1549
|
+
return errorResult('update_artifact is blocked while repositories are unavailable. Fetch or acknowledge them, then call begin_work again.');
|
|
1550
|
+
}
|
|
1551
|
+
if (!args.id)
|
|
1552
|
+
return errorResult('update_artifact requires an artifact id.');
|
|
1553
|
+
if (!Number.isInteger(args.expected_revision) || args.expected_revision < 1) {
|
|
1554
|
+
return errorResult('update_artifact requires the current positive artifact revision from get_task.');
|
|
1555
|
+
}
|
|
1556
|
+
if (!args.title?.trim())
|
|
1557
|
+
return errorResult('update_artifact requires a title.');
|
|
1558
|
+
if (!args.purpose_key?.trim())
|
|
1559
|
+
return errorResult('update_artifact requires a stable purpose_key.');
|
|
1560
|
+
if (args.purpose_key.trim().startsWith('context:')) {
|
|
1561
|
+
return errorResult('Context is a reserved work-item artifact. Use record_context_exploration instead.');
|
|
1562
|
+
}
|
|
1563
|
+
if (!args.content)
|
|
1564
|
+
return errorResult('update_artifact requires content.');
|
|
1565
|
+
if (!Array.isArray(args.coverage) || args.coverage.length === 0) {
|
|
1566
|
+
return errorResult('update_artifact requires repository coverage for every active project scope.');
|
|
1567
|
+
}
|
|
1568
|
+
for (const coverage of args.coverage) {
|
|
1569
|
+
if (!coverage.repository_scope_id || !coverage.reason?.trim()) {
|
|
1570
|
+
return errorResult('Every update_artifact coverage entry requires repository_scope_id and a non-empty reason.');
|
|
1571
|
+
}
|
|
1572
|
+
const unavailable = coverage.disposition === 'unavailable_acknowledged';
|
|
1573
|
+
if (unavailable !== Boolean(coverage.acknowledgement_id)) {
|
|
1574
|
+
return errorResult('update_artifact coverage acknowledgement_id is required only for unavailable_acknowledged repositories.');
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
const artifact = await must(deps.client.rpc('update_agent_artifact', {
|
|
1578
|
+
p_run: agentRun.runId,
|
|
1579
|
+
p_claim_fencing_token: agentRun.fencingToken,
|
|
1580
|
+
p_topology_revision: agentRun.topologyRevision,
|
|
1581
|
+
p_artifact: args.id,
|
|
1582
|
+
p_expected_revision: args.expected_revision,
|
|
1583
|
+
p_type: args.type,
|
|
1584
|
+
p_format: args.format,
|
|
1585
|
+
p_title: args.title.trim(),
|
|
1586
|
+
p_purpose_key: args.purpose_key.trim(),
|
|
1587
|
+
p_content: args.content,
|
|
1588
|
+
p_coverage: args.coverage.map((coverage) => ({
|
|
1589
|
+
repository_scope_id: coverage.repository_scope_id,
|
|
1590
|
+
disposition: coverage.disposition,
|
|
1591
|
+
reason: coverage.reason.trim(),
|
|
1592
|
+
acknowledgement_id: coverage.acknowledgement_id ?? null,
|
|
1593
|
+
})),
|
|
1594
|
+
}));
|
|
1595
|
+
if (!artifact)
|
|
1596
|
+
throw new Error('update_agent_artifact returned no artifact');
|
|
1597
|
+
return textResult({ artifact, repository_coverage: args.coverage });
|
|
1598
|
+
}
|
|
1599
|
+
catch (err) {
|
|
1600
|
+
return coordinationError('update_artifact', err);
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1479
1603
|
export async function addCommentHandler(deps, attribution, args, agentRunId) {
|
|
1480
1604
|
try {
|
|
1481
1605
|
if (!args.task_id)
|
|
@@ -1648,7 +1772,7 @@ export async function startMcpServer(deps) {
|
|
|
1648
1772
|
return errorResult(`${tool} is blocked because this No workflow item has no instructions. ` +
|
|
1649
1773
|
(run.noWorkflowIntent === 'missing'
|
|
1650
1774
|
? 'Call ask_question with category intent to ask the user what should be done.'
|
|
1651
|
-
: '
|
|
1775
|
+
: 'Wait for the user to answer the decision in the web app, or record their conversational answer with record_user_input.'));
|
|
1652
1776
|
}
|
|
1653
1777
|
function buildServer(session) {
|
|
1654
1778
|
const server = new McpServer({ name: 'ctrl-spc', version: BROKER_VERSION });
|
|
@@ -1783,7 +1907,7 @@ export async function startMcpServer(deps) {
|
|
|
1783
1907
|
reason: z.string().optional(),
|
|
1784
1908
|
persistence_receipt: z.object({
|
|
1785
1909
|
final_task_revision: z.number().int().positive(),
|
|
1786
|
-
artifact_ids: z.array(z.string())
|
|
1910
|
+
artifact_ids: z.array(z.string()),
|
|
1787
1911
|
outcome: z.enum(['completed', 'blocked', 'aborted']).optional(),
|
|
1788
1912
|
}),
|
|
1789
1913
|
},
|
|
@@ -1836,29 +1960,22 @@ export async function startMcpServer(deps) {
|
|
|
1836
1960
|
if (missing)
|
|
1837
1961
|
return missing;
|
|
1838
1962
|
const run = session.context.run;
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
}
|
|
1843
|
-
if (!args.fields.description?.trim() || Object.keys(args.fields).some(field => field !== 'description')) {
|
|
1844
|
-
return errorResult('After the No workflow intent question, update_task must first persist only the user\'s answer in the task description.');
|
|
1845
|
-
}
|
|
1846
|
-
}
|
|
1963
|
+
const intent = requireNoWorkflowIntent(session, 'update_task');
|
|
1964
|
+
if (intent)
|
|
1965
|
+
return intent;
|
|
1847
1966
|
const workflowError = workflowCapabilityError(run, 'update_task', 'update_task');
|
|
1848
1967
|
if (workflowError)
|
|
1849
1968
|
return workflowError;
|
|
1850
|
-
|
|
1851
|
-
if (!result.isError && !run.workflow?.enabled && run.noWorkflowIntent === 'intent_question_asked') {
|
|
1852
|
-
run.noWorkflowIntent = 'provided';
|
|
1853
|
-
}
|
|
1854
|
-
return result;
|
|
1969
|
+
return updateTaskHandler(bindProject(handlerDeps, run.projectId), attribution(), args, run);
|
|
1855
1970
|
});
|
|
1856
1971
|
server.registerTool('create_artifact', {
|
|
1857
|
-
description: '
|
|
1972
|
+
description: 'Create a titled analysis/plan/spec/diagram/mock/wireframe for a new stable purpose. Use update_artifact when that purpose already exists; a different purpose creates a different artifact.',
|
|
1858
1973
|
inputSchema: {
|
|
1859
1974
|
task_id: z.string(),
|
|
1860
1975
|
type: z.enum(['analysis', 'plan', 'spec', 'diagram', 'mock', 'wireframe']),
|
|
1861
1976
|
format: z.enum(['md', 'html', 'json', 'svg']).optional().describe('Defaults to md'),
|
|
1977
|
+
title: z.string().min(1),
|
|
1978
|
+
purpose_key: z.string().min(1).describe('Stable machine-readable identity for this artifact purpose'),
|
|
1862
1979
|
content: z.string(),
|
|
1863
1980
|
coverage: z.array(z.object({
|
|
1864
1981
|
repository_scope_id: z.string(),
|
|
@@ -1880,12 +1997,48 @@ export async function startMcpServer(deps) {
|
|
|
1880
1997
|
return workflowError;
|
|
1881
1998
|
return createArtifactHandler(bindProject(handlerDeps, run.projectId), attribution(), args, run);
|
|
1882
1999
|
});
|
|
2000
|
+
server.registerTool('update_artifact', {
|
|
2001
|
+
description: 'Update the existing artifact for the same purpose. Pass its latest revision; stale updates conflict instead of creating a duplicate or overwriting newer work.',
|
|
2002
|
+
inputSchema: {
|
|
2003
|
+
id: z.string(),
|
|
2004
|
+
expected_revision: z.number().int().positive(),
|
|
2005
|
+
type: z.enum(['analysis', 'plan', 'spec', 'diagram', 'mock', 'wireframe']),
|
|
2006
|
+
format: z.enum(['md', 'html', 'json', 'svg']),
|
|
2007
|
+
title: z.string().min(1),
|
|
2008
|
+
purpose_key: z.string().min(1),
|
|
2009
|
+
content: z.string(),
|
|
2010
|
+
coverage: z.array(z.object({
|
|
2011
|
+
repository_scope_id: z.string(),
|
|
2012
|
+
disposition: z.enum(['affected', 'reviewed_no_change', 'context_only', 'unavailable_acknowledged']),
|
|
2013
|
+
reason: z.string(),
|
|
2014
|
+
acknowledgement_id: z.string().nullable().optional(),
|
|
2015
|
+
})).min(1).describe('Exactly one disposition for every active repository scope returned by get_task'),
|
|
2016
|
+
},
|
|
2017
|
+
}, async (args) => {
|
|
2018
|
+
const missing = requireExploredSessionRun(session, 'update_artifact');
|
|
2019
|
+
if (missing)
|
|
2020
|
+
return missing;
|
|
2021
|
+
const intent = requireNoWorkflowIntent(session, 'update_artifact');
|
|
2022
|
+
if (intent)
|
|
2023
|
+
return intent;
|
|
2024
|
+
const run = session.context.run;
|
|
2025
|
+
const workflowError = workflowCapabilityError(run, 'create_artifact', 'update_artifact');
|
|
2026
|
+
if (workflowError)
|
|
2027
|
+
return workflowError;
|
|
2028
|
+
return updateArtifactHandler(bindProject(handlerDeps, run.projectId), args, run);
|
|
2029
|
+
});
|
|
1883
2030
|
server.registerTool('ask_question', {
|
|
1884
|
-
description: 'Persist a categorized, actionable
|
|
2031
|
+
description: 'Persist a categorized, actionable Decision after complete context exploration. Choose free_text, single_select, or multi_select. Choice modes require 2-12 unique options and always allow optional written context in the web app.',
|
|
1885
2032
|
inputSchema: {
|
|
1886
2033
|
task_id: z.string(),
|
|
1887
2034
|
category: z.enum(['clarification', 'reproduction', 'intent', 'dependency', 'checkout', 'collision', 'split']),
|
|
2035
|
+
context: z.string().min(1).describe('Why this decision is needed and what it affects'),
|
|
1888
2036
|
question: z.string(),
|
|
2037
|
+
answer_mode: z.enum(['free_text', 'single_select', 'multi_select']).optional()
|
|
2038
|
+
.describe('Defaults to free_text. Use single_select for exactly one choice or multi_select for one or more choices.'),
|
|
2039
|
+
options: z.array(z.string()).min(2).max(12).optional()
|
|
2040
|
+
.describe('Required for single_select and multi_select; omit for free_text.'),
|
|
2041
|
+
related_artifact_id: z.string().nullable().optional(),
|
|
1889
2042
|
},
|
|
1890
2043
|
}, async (args) => {
|
|
1891
2044
|
const missing = requireExploredSessionRun(session, 'ask_question');
|
|
@@ -1908,22 +2061,27 @@ export async function startMcpServer(deps) {
|
|
|
1908
2061
|
return result;
|
|
1909
2062
|
});
|
|
1910
2063
|
server.registerTool('record_user_input', {
|
|
1911
|
-
description: 'Persist
|
|
2064
|
+
description: 'Persist answers on first-class Decision records returned by ask_question. Use responses for structured free-text, single-select, or multi-select answers. Legacy decision_ids plus answer remains free-text only. This creates no answer or approval artifact.',
|
|
1912
2065
|
inputSchema: {
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
})).min(1),
|
|
2066
|
+
decision_ids: z.array(z.string()).min(1).optional(),
|
|
2067
|
+
question_ids: z.array(z.string()).min(1).optional().describe('Compatibility alias for decision_ids'),
|
|
2068
|
+
answer: z.string().optional().describe('Legacy free-text answer used with decision_ids or question_ids'),
|
|
2069
|
+
responses: z.array(z.object({
|
|
2070
|
+
decision_id: z.string().min(1),
|
|
2071
|
+
selected_options: z.array(z.string()).optional(),
|
|
2072
|
+
answer_note: z.string().nullable().optional(),
|
|
2073
|
+
})).min(1).optional().describe('Structured answers. Free text uses answer_note; choice modes use selected_options and optional answer_note.'),
|
|
1921
2074
|
},
|
|
1922
2075
|
}, async (args) => {
|
|
1923
2076
|
const missing = requireExploredSessionRun(session, 'record_user_input');
|
|
1924
2077
|
if (missing)
|
|
1925
2078
|
return missing;
|
|
1926
|
-
|
|
2079
|
+
const result = await recordUserInputHandler(handlerDeps, attribution(), session.context, args);
|
|
2080
|
+
const run = session.context.run;
|
|
2081
|
+
if (!result.isError && run?.state === 'joined' && !run.workflow?.enabled && run.noWorkflowIntent === 'intent_question_asked') {
|
|
2082
|
+
run.noWorkflowIntent = 'provided';
|
|
2083
|
+
}
|
|
2084
|
+
return result;
|
|
1927
2085
|
});
|
|
1928
2086
|
server.registerTool('submit_workflow_stage', {
|
|
1929
2087
|
description: 'Submit the current workflow stage. The database validates its stored requirements and either advances or waits for user review.',
|
|
@@ -1972,6 +2130,7 @@ export async function startMcpServer(deps) {
|
|
|
1972
2130
|
return askQuestionHandler(handlerDeps, attribution(), {
|
|
1973
2131
|
task_id: args.task_id,
|
|
1974
2132
|
category: 'clarification',
|
|
2133
|
+
context: 'Legacy question recorded from add_comment.',
|
|
1975
2134
|
question: args.body,
|
|
1976
2135
|
}, run);
|
|
1977
2136
|
});
|
package/dist/protocol.js
CHANGED
|
@@ -25,8 +25,8 @@ in the same stage. Only final user approval moves the item to Done.
|
|
|
25
25
|
\`workflow.enabled\` is false. After \`record_context_exploration\` succeeds,
|
|
26
26
|
if the task does not explicitly request a build or name a deliverable, the immediate
|
|
27
27
|
next tool call is \`ask_question\` with category \`intent\` and the exact question:
|
|
28
|
-
“What should I produce for this feature
|
|
29
|
-
diagram
|
|
28
|
+
“What should I produce for this feature?” using \`answer_mode = multi_select\` and
|
|
29
|
+
options \`build\`, \`plan\`, \`spec\`, \`diagram\`, \`mock\`, and \`wireframe\`. Then call \`end_work\` with reason
|
|
30
30
|
\`pending_user_answer\` and outcome \`blocked\`. Do not call \`get_task\` again,
|
|
31
31
|
\`update_task\`, \`create_artifact\`, \`reserve_work_paths\`, or any intervening tool.
|
|
32
32
|
The context artifact is the only artifact allowed before the answer. A read-only
|
|
@@ -87,7 +87,9 @@ is saved to the work item. I am applying the task's explicit intent gate now.”
|
|
|
87
87
|
run acknowledgement ID. Before the tool succeeds, commentary may state process only:
|
|
88
88
|
never disclose findings, conclusions, likely impact, or decisions. After persistence,
|
|
89
89
|
terminal summaries may reference the persisted artifact/question but must add no
|
|
90
|
-
unpersisted facts.
|
|
90
|
+
unpersisted facts. Context has a stable purpose for the current workflow stage (or work
|
|
91
|
+
item without a workflow): repeat exploration updates that artifact instead of creating
|
|
92
|
+
another card.
|
|
91
93
|
6. **Follow the workflow; otherwise ask exactly what is needed.** For an assigned workflow,
|
|
92
94
|
the stored current-stage instructions and capabilities decide the work; never skip ahead
|
|
93
95
|
based on task wording. Without a workflow, after exploration is recorded, classify intent
|
|
@@ -108,11 +110,15 @@ is saved to the work item. I am applying the task's explicit intent gate now.”
|
|
|
108
110
|
collision handoff. After \`ask_question\` succeeds, do no more work in that run: call
|
|
109
111
|
\`end_work\` with reason \`pending_user_answer\`, a receipt outcome of \`blocked\`, the
|
|
110
112
|
final task revision, and every live artifact ID; continue only in a later pasted run.
|
|
111
|
-
7. **Persist every accepted answer and durable finding.**
|
|
112
|
-
|
|
113
|
-
the
|
|
114
|
-
|
|
115
|
-
|
|
113
|
+
7. **Persist every accepted answer and durable finding.** \`ask_question\` creates a
|
|
114
|
+
first-class decision and returns its decision ID for the web app to display and answer.
|
|
115
|
+
Choose the answer mode deliberately: \`free_text\` for an open response,
|
|
116
|
+
\`single_select\` for exactly one choice, or \`multi_select\` for one or more choices.
|
|
117
|
+
Choice modes require 2–12 concise, unique options and also accept optional written context.
|
|
118
|
+
When the user answers in the agent conversation instead, call \`record_user_input\` with
|
|
119
|
+
structured responses for each decision; the legacy decision IDs plus answer shape is
|
|
120
|
+
free-text only. Decisions
|
|
121
|
+
are not answer artifacts and are not copied into the task description.
|
|
116
122
|
Never leave an accepted answer or finding only in terminal, chat, or a subagent report.
|
|
117
123
|
8. **Reserve the exact write surface only when allowed.** Read-only workflow stages never
|
|
118
124
|
reserve write paths or edit files. When the current stage allows repository writes, call
|
|
@@ -124,16 +130,20 @@ is saved to the work item. I am applying the task's explicit intent gate now.”
|
|
|
124
130
|
9. **Never edit through a collision.** A collision reserves nothing. Narrow to disjoint
|
|
125
131
|
paths, wait, ask for an explicit handoff, or use an isolated Git worktree. Never edit a
|
|
126
132
|
conflicting path, overwrite unexpected local changes, or assume another agent stopped.
|
|
127
|
-
10. **Persist and submit every substantive non-question output.**
|
|
133
|
+
10. **Persist and submit every substantive non-question output.** Give each artifact a clear
|
|
134
|
+
title and stable \`purpose_key\`. Use \`create_artifact\` when that purpose does not exist and
|
|
135
|
+
\`update_artifact\` with the latest revision when the same purpose already exists; a
|
|
136
|
+
different purpose creates a different artifact. Persist every
|
|
128
137
|
plan, specification, research/finding report, schema, diagram, mock, and wireframe before
|
|
129
138
|
presenting it as complete. Use type \`analysis\` for research/findings, \`plan\` for plans,
|
|
130
139
|
\`spec\` for specifications or proposed schemas, and the matching visual type. Never
|
|
131
140
|
write planning documents into a repository. Every artifact has exactly one coverage disposition and non-empty
|
|
132
141
|
reason for every active repository scope at the current topology revision. For workflow
|
|
133
142
|
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.
|
|
143
|
+
database alone decides whether the next stage starts or a review gate opens. Workflow
|
|
144
|
+
approval is displayed on the submitted plan; it is not a decision artifact.
|
|
135
145
|
11. **Keep the work item self-sufficient.** Questions are durable records created through
|
|
136
|
-
\`ask_question\`; accepted answers
|
|
146
|
+
\`ask_question\`; accepted answers live in first-class decisions; substantive
|
|
137
147
|
outputs live in artifacts. No progress comments. Never leave useful
|
|
138
148
|
context only in terminal, chat history, a subagent report, or a repository planning file.
|
|
139
149
|
If work is too large, ask through \`ask_question\`; after approval use \`create_task\` to
|
|
@@ -148,10 +158,10 @@ is saved to the work item. I am applying the task's explicit intent gate now.”
|
|
|
148
158
|
rejected. Ending releases only this session's work.`;
|
|
149
159
|
export const PROTOCOL_SHORT = 'ctrl-spc protocol (compact — full text: MCP prompt `ctrl-spc-protocol`): ' +
|
|
150
160
|
'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
|
-
'
|
|
161
|
+
'No-workflow unclear feature: MUST ask_question, then end blocked; never infer from findings or call update_task/create_artifact/reserve_work_paths first. Improve, coordinate, prepare, support are not deliverables. ' +
|
|
152
162
|
'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. ' +
|
|
153
163
|
'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. ' +
|
|
154
164
|
'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. ' +
|
|
155
165
|
'4) Workflow stage data decides intent. Without a workflow, intent comes only from task, prompt, and accepted answers—never from findings. ' +
|
|
156
|
-
'5)
|
|
166
|
+
'5) record_context_exploration revises one task context across stages/agents. ask_question creates free_text, single_select, or multi_select decisions answered in web; chat answers use structured record_user_input and create no answer artifact. Same purpose: update_artifact; different purpose: create_artifact. Plan approval stays on the plan, not in Decisions. Cover every active scope. No progress comments or terminal-only findings. ' +
|
|
157
167
|
'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
|
@@ -40,7 +40,7 @@ function protocolBody(mcpUrl) {
|
|
|
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
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
|
|
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\`, the exact question “What should I produce for this feature?”, \`answer_mode = multi_select\`, and options \`build\`, \`plan\`, \`spec\`, \`diagram\`, \`mock\`, and \`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.
|
|
44
44
|
|
|
45
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.”
|
|
46
46
|
|
|
@@ -48,13 +48,13 @@ Before the context artifact exists, never state or imply a requested deliverable
|
|
|
48
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.
|
|
49
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.
|
|
50
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.
|
|
51
|
-
5. Persist context before deciding — the parent consolidates every child report and MUST call \`record_context_exploration\` to create the work item's
|
|
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
|
|
53
|
-
7. Persist
|
|
51
|
+
5. Persist context before deciding — the parent consolidates every child report and MUST call \`record_context_exploration\` to create or update the work item's stable-purpose context artifact before deciding what to do, asking a task question, planning, reserving, or editing. Every agent and workflow stage revises the same task-level context artifact instead of creating another card. 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\`. Choose \`free_text\` for an open response, \`single_select\` for exactly one choice, or \`multi_select\` for one or more choices; choice modes require 2–12 concise, unique options. 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 the specified multi-select \`ask_question\`. 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 every accepted answer and finding — \`ask_question\` creates first-class decisions and returns decision IDs for the web app to display and answer. When the user answers in the agent conversation instead, use \`record_user_input\` with one structured response per Decision: \`decision_id\`, \`selected_options\`, and optional \`answer_note\`. The legacy decision IDs plus exact-answer shape is free-text only. Decisions create no answer artifact and are not copied into the task description. Do not use progress comments; do not leave findings only in the terminal.
|
|
54
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 —
|
|
55
|
+
9. Persist every substantive non-question output — give every artifact a clear title and stable \`purpose_key\`. Call \`create_artifact\` for a new purpose and \`update_artifact\` with the latest revision when the same purpose already exists; a different purpose creates a different artifact. 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. Plan approval is displayed on the submitted plan and is not a decision artifact. Never write planning documents into a repository.
|
|
56
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.
|
|
57
|
-
11. Keep the item self-sufficient — questions are persisted through \`ask_question
|
|
57
|
+
11. Keep the item self-sufficient — questions and accepted answers are persisted as first-class decisions through \`ask_question\` and \`record_user_input\`; 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.
|
|
58
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.
|
|
59
59
|
|
|
60
60
|
Full protocol: MCP prompt \`ctrl-spc-protocol\`. Tools live at ${mcpUrl}.
|