@ctrl-spc/cs 0.7.7 → 0.7.8
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/design-review.js +242 -0
- package/dist/mcp.js +163 -1053
- package/dist/panel3/tools.js +268 -9
- package/dist/product-tools.js +442 -0
- package/dist/workflow-tool-mentions.js +93 -0
- package/dist/workflows.js +99 -0
- package/package.json +2 -2
package/dist/workflows.js
CHANGED
|
@@ -1,4 +1,53 @@
|
|
|
1
1
|
import { absolutePathToken } from './local-paths.js';
|
|
2
|
+
import { listSteps } from './steps.js';
|
|
3
|
+
import { assertAgentToolMentions, toolMentions, validateToolMentions } from './workflow-tool-mentions.js';
|
|
4
|
+
export { WORKFLOW_TOOL_TEACHING, assertAgentToolMentions } from './workflow-tool-mentions.js';
|
|
5
|
+
/** Resolve permission from the current stage's saved document, never agent
|
|
6
|
+
* prose or a supplied boolean. Both harnesses use the same stage resolver. */
|
|
7
|
+
export async function workflowToolPermission(client, workItemId, tool, instance) {
|
|
8
|
+
if (!workItemId) {
|
|
9
|
+
if (instance)
|
|
10
|
+
throw new Error('A workflow tool mention requires its work item. Nothing was written.');
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
const { data: next, error } = await client.rpc('cliv2_steps_up_next', { p_work_item_id: workItemId });
|
|
14
|
+
if (error)
|
|
15
|
+
throw new Error(`Could not read the active workflow stage: ${error.message}`);
|
|
16
|
+
let stageId = next?.stage_id;
|
|
17
|
+
// Up-next has no runnable step when a stage is blocked. Its permissions
|
|
18
|
+
// still apply while the agent creates or revises the work needed to unblock it.
|
|
19
|
+
if (!stageId) {
|
|
20
|
+
const spine = await listSteps(client, workItemId);
|
|
21
|
+
stageId = spine.stages.find(stage => !stage.steps.length || stage.steps.some(step => step.status !== 'done'))?.id;
|
|
22
|
+
}
|
|
23
|
+
if (!stageId) {
|
|
24
|
+
if (instance)
|
|
25
|
+
throw new Error('There is no active workflow stage for this mention. Nothing was written.');
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
const stages = await read(client.from('cliv2_stages').select('source_ref, source_workflow_ref')
|
|
29
|
+
.eq('id', stageId).eq('work_item_id', workItemId), 'the active workflow stage');
|
|
30
|
+
const stage = stages[0];
|
|
31
|
+
if (!stage?.source_ref || !stage.source_workflow_ref) {
|
|
32
|
+
if (instance)
|
|
33
|
+
throw new Error('The current stage does not own that workflow tool mention. Nothing was written.');
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
const documents = await read(client.from('cliv2_workflow_stages').select('body').eq('id', stage.source_ref), 'the workflow stage document');
|
|
37
|
+
if (!documents[0])
|
|
38
|
+
throw new Error('The active workflow stage document is unavailable. Nothing was written.');
|
|
39
|
+
validateToolMentions(documents[0].body);
|
|
40
|
+
const mentions = toolMentions(documents[0].body);
|
|
41
|
+
if (!instance && !mentions.some(mention => mention.tool === tool))
|
|
42
|
+
return null;
|
|
43
|
+
if (!instance)
|
|
44
|
+
throw new Error('Pass workflow_tool_instance from the current stage for this use of the tool. Read the stage document; do not omit its approval setting.');
|
|
45
|
+
const matching = mentions.filter(mention => mention.id === instance);
|
|
46
|
+
if (matching.length !== 1 || matching[0].tool !== tool) {
|
|
47
|
+
throw new Error('This tool instance is missing, duplicated, belongs to another tool, or is outside the current stage. Nothing was written.');
|
|
48
|
+
}
|
|
49
|
+
return matching[0];
|
|
50
|
+
}
|
|
2
51
|
async function read(query, subject) {
|
|
3
52
|
const { data, error } = await query;
|
|
4
53
|
if (error)
|
|
@@ -91,6 +140,7 @@ export async function readWorkflow(client, workflowId) {
|
|
|
91
140
|
export async function buildWorkflow(client, input) {
|
|
92
141
|
const prose = [['name', input.name], ['description', input.description]];
|
|
93
142
|
input.stages.forEach((stage, i) => {
|
|
143
|
+
assertAgentToolMentions(stage.body);
|
|
94
144
|
prose.push([`stage ${i + 1} name`, stage.name], [`stage ${i + 1} description`, stage.description], [`stage ${i + 1} body`, stage.body]);
|
|
95
145
|
});
|
|
96
146
|
input.exits.forEach((exit, i) => prose.push([`exit ${i + 1} condition`, exit.condition]));
|
|
@@ -147,6 +197,15 @@ export async function rewordStage(client, input) {
|
|
|
147
197
|
+ 'repo-relative. Nothing was written.');
|
|
148
198
|
}
|
|
149
199
|
}
|
|
200
|
+
if (input.body !== undefined) {
|
|
201
|
+
const previous = await read(client.from('cliv2_workflow_stages').select('body').eq('id', input.stageId), 'the saved stage permissions');
|
|
202
|
+
if (!previous[0])
|
|
203
|
+
throw new Error('The stage is unavailable. Nothing was written.');
|
|
204
|
+
if (toolMentions(previous[0].body).length && input.body !== previous[0].body) {
|
|
205
|
+
throw new Error('Only the user can change a stage containing tool permissions. Ask them to edit it in Workflows.');
|
|
206
|
+
}
|
|
207
|
+
assertAgentToolMentions(input.body, previous[0].body);
|
|
208
|
+
}
|
|
150
209
|
const patch = {};
|
|
151
210
|
if (input.name !== undefined)
|
|
152
211
|
patch.name = input.name;
|
|
@@ -194,6 +253,7 @@ export async function editWorkflow(client, input) {
|
|
|
194
253
|
input.stages.forEach((stage, i) => {
|
|
195
254
|
if ('stage_id' in stage)
|
|
196
255
|
return;
|
|
256
|
+
assertAgentToolMentions(stage.body);
|
|
197
257
|
prose.push([`stage ${i + 1} name`, stage.name], [`stage ${i + 1} description`, stage.description], [`stage ${i + 1} body`, stage.body]);
|
|
198
258
|
});
|
|
199
259
|
input.exits.forEach((exit, i) => prose.push([`exit ${i + 1} condition`, exit.condition]));
|
|
@@ -254,3 +314,42 @@ export async function duplicateWorkflow(client, input) {
|
|
|
254
314
|
}
|
|
255
315
|
return data;
|
|
256
316
|
}
|
|
317
|
+
/** A stage grant is scoped to its work item and project, even when a planning
|
|
318
|
+
* action creates or organizes another item in that same project. */
|
|
319
|
+
export async function validateWorkflowToolTarget(client, workItemId, tool, args) {
|
|
320
|
+
const targetId = args.work_item_id ?? args.task_id ?? (tool === 'update_task' ? args.id : undefined);
|
|
321
|
+
if (tool === 'create_artifact' || tool === 'attach_screenshot' || tool === 'record_context_exploration'
|
|
322
|
+
|| tool === 'present_mocks' || tool === 'present_wireframes' || tool === 'propose_scope_change') {
|
|
323
|
+
if (targetId && targetId !== workItemId)
|
|
324
|
+
throw new Error('The tool mention belongs to the current workflow work item. Nothing was written.');
|
|
325
|
+
}
|
|
326
|
+
if (tool === 'update_artifact') {
|
|
327
|
+
const artifacts = await read(client.from('artifacts').select('task_id')
|
|
328
|
+
.eq('id', args.artifact_id ?? args.id), 'the artifact work item');
|
|
329
|
+
if (artifacts.length !== 1 || artifacts[0].task_id !== workItemId)
|
|
330
|
+
throw new Error('This artifact is outside the workflow work item. Nothing was written.');
|
|
331
|
+
}
|
|
332
|
+
if (tool === 'create_step') {
|
|
333
|
+
const stages = await read(client.from('cliv2_stages').select('work_item_id')
|
|
334
|
+
.eq('id', args.stage_id), 'the execution stage');
|
|
335
|
+
if (stages.length !== 1 || stages[0].work_item_id !== workItemId)
|
|
336
|
+
throw new Error('This execution stage is outside the workflow work item. Nothing was written.');
|
|
337
|
+
}
|
|
338
|
+
if (tool === 'resolve_feedback') {
|
|
339
|
+
const ids = args.feedback_ids;
|
|
340
|
+
const feedback = await read(client.from('artifact_feedback').select('id, task_id')
|
|
341
|
+
.in('id', ids), 'the feedback work items');
|
|
342
|
+
if (new Set(feedback.map(row => row.id)).size !== new Set(ids).size || feedback.some(row => row.task_id !== workItemId)) {
|
|
343
|
+
throw new Error('Feedback is missing or outside the workflow work item. Nothing was written.');
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
if (args.project_id || (targetId && targetId !== workItemId)) {
|
|
347
|
+
const items = await read(client.from('tasks').select('id, project_id')
|
|
348
|
+
.in('id', [...new Set([workItemId, ...(targetId ? [String(targetId)] : [])])]), 'the workflow project');
|
|
349
|
+
const source = items.find(item => item.id === workItemId);
|
|
350
|
+
if (!source || (args.project_id && args.project_id !== source.project_id)
|
|
351
|
+
|| (targetId && !items.some(item => item.id === targetId && item.project_id === source.project_id))) {
|
|
352
|
+
throw new Error('This action is outside the workflow project. Nothing was written.');
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ctrl-spc/cs",
|
|
3
|
-
"version": "0.7.
|
|
4
|
-
"description": "CTRL+SPC
|
|
3
|
+
"version": "0.7.8",
|
|
4
|
+
"description": "CTRL+SPC — minimal, reliable per-machine agent presence. Sign-in, auto-start, agent detection, heartbeat presence, and ping acknowledgement.",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=22"
|
|
7
7
|
},
|