@ctrl-spc/cs 0.7.11 → 0.7.12
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/codebases.js +26 -0
- package/dist/panel3/checkout.js +9 -32
- package/dist/panel3/codex-models.js +96 -0
- package/dist/panel3/coordinator.js +15 -0
- package/dist/panel3/prompt.js +28 -8
- package/dist/panel3/run.js +163 -253
- package/dist/panel3/spawn.js +24 -15
- package/dist/panel3/tools.js +115 -32
- package/dist/workflow-tool-mentions.js +1 -0
- package/dist/workflows.js +7 -6
- package/package.json +2 -2
package/dist/panel3/tools.js
CHANGED
|
@@ -127,11 +127,12 @@ import { returned } from './client.js';
|
|
|
127
127
|
import { readableWriteError, FIREWALL_WRITING_RULE } from '../firewall.js';
|
|
128
128
|
import { rememberSecret, redactArgs } from './secrets.js';
|
|
129
129
|
import { workBrief } from './prompt.js';
|
|
130
|
+
import { listCodexModels } from './codex-models.js';
|
|
130
131
|
/* The harness this daemon spawns with, which is the process this server runs
|
|
131
132
|
in. It is what `panel3_runs.harness` is written from at spawn, so reading it
|
|
132
133
|
here is the same fact without a second round trip. */
|
|
133
134
|
import { harness } from './spawn.js';
|
|
134
|
-
import { listCodebases } from '../codebases.js';
|
|
135
|
+
import { listCodebases, listWorkItemCodebaseTargets, editWorkItemCodebaseTarget } from '../codebases.js';
|
|
135
136
|
import { buildWorkflow, duplicateWorkflow, editWorkflow, readWorkflow, rewordStage, workflowToolPermission, validateWorkflowToolTarget, WORKFLOW_TOOL_TEACHING, WORKFLOW_AUTHORING_TEACHING } from '../workflows.js';
|
|
136
137
|
/* ═══ 38-panel3-steps: THE FIFTH NEUTRAL MODULE, AND A DECISION LIKE THE OTHERS.
|
|
137
138
|
═══ Stages and steps are the work item's own record, shared by both
|
|
@@ -300,10 +301,9 @@ async function asked(caller, args, askPerson) {
|
|
|
300
301
|
p_question: question,
|
|
301
302
|
p_category: category,
|
|
302
303
|
p_context: context,
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
p_options: options,
|
|
304
|
+
// Token-aware RPC overloads require these arguments, even for a free-text question.
|
|
305
|
+
p_answer_mode: answer_mode ?? 'free_text',
|
|
306
|
+
p_options: options ?? [],
|
|
307
307
|
p_question_id: question_id ?? null,
|
|
308
308
|
p_ask_person: askPerson,
|
|
309
309
|
/* ═══ ONLY `ask_question` OFFERS THIS, AND THE DATABASE REFUSES IT ON AN
|
|
@@ -829,13 +829,14 @@ const TOOLS = [
|
|
|
829
829
|
handler: async (caller, args) => productResult(await placeWorkItemHandler(caller.client, args)),
|
|
830
830
|
},
|
|
831
831
|
{
|
|
832
|
-
name: 'reorder_backlog', levels: [1],
|
|
832
|
+
name: 'reorder_backlog', levels: [1, 2],
|
|
833
833
|
description: 'Move one backlog work item before another, or to the bottom. Read the current order first and explain the reason.',
|
|
834
834
|
input: { project_id: z.string().uuid(), task_id: z.string().uuid(), before_task_id: z.string().uuid().optional(), reason: z.string().min(1) },
|
|
835
835
|
handler: async (caller, args) => {
|
|
836
836
|
const a = args;
|
|
837
837
|
const items = await rows(caller.client.from('tasks').select('id, revision')
|
|
838
|
-
.eq('project_id', a.project_id).eq('status', 'backlog').is('archived_at', null).eq('is_idea', false)
|
|
838
|
+
.eq('project_id', a.project_id).eq('status', 'backlog').is('archived_at', null).eq('is_idea', false)
|
|
839
|
+
.in('id', a.before_task_id ? [a.task_id, a.before_task_id] : [a.task_id]), 'read', 'the backlog');
|
|
839
840
|
const moving = items.find(item => item.id === a.task_id);
|
|
840
841
|
if (!moving || (a.before_task_id && !items.some(item => item.id === a.before_task_id)))
|
|
841
842
|
throw new Error('Both items must be live backlog work in this project. Nothing was moved.');
|
|
@@ -963,32 +964,44 @@ const TOOLS = [
|
|
|
963
964
|
{
|
|
964
965
|
name: 'list_work_items',
|
|
965
966
|
levels: ALL,
|
|
966
|
-
description: '
|
|
967
|
-
+ '
|
|
967
|
+
description: 'Live work items in saved manual board order, top to bottom within each project and status, '
|
|
968
|
+
+ 'not creation order or a temporary UI sort. For backlog priority, pass project_id and status backlog. '
|
|
969
|
+
+ 'Read all pages using next_offset until it is null before reporting the full order. Re-read after a reorder or a human change.',
|
|
968
970
|
input: {
|
|
969
971
|
project_id: z.string().optional().describe('Only items in this project.'),
|
|
970
972
|
status: z.enum(['backlog', 'in_progress', 'done']).optional().describe('Only items in this status.'),
|
|
973
|
+
offset: z.number().int().min(0).default(0).describe('Use next_offset from the previous page; start at zero.'),
|
|
971
974
|
},
|
|
972
975
|
handler: async ({ client }, args) => {
|
|
973
|
-
const { project_id, status } = args;
|
|
976
|
+
const { project_id, status, offset = 0 } = args;
|
|
974
977
|
let query = client
|
|
975
978
|
.from('tasks')
|
|
976
|
-
.select('id, name, status, project_id, epic_id, sprint_id, due_date, created_at')
|
|
979
|
+
.select('id, name, status, project_id, epic_id, sprint_id, due_date, position, created_at', { count: 'exact' })
|
|
977
980
|
.eq('is_idea', false)
|
|
978
981
|
.is('archived_at', null)
|
|
979
|
-
.order('created_at'
|
|
982
|
+
.order('project_id').order('status').order('position').order('created_at').order('id');
|
|
980
983
|
if (project_id)
|
|
981
984
|
query = query.eq('project_id', project_id);
|
|
982
985
|
if (status)
|
|
983
986
|
query = query.eq('status', status);
|
|
984
|
-
const
|
|
985
|
-
|
|
987
|
+
const result = await query.range(offset, offset + 99);
|
|
988
|
+
const items = await rows(Promise.resolve(result), 'read', 'work items');
|
|
989
|
+
if (result.count === null)
|
|
990
|
+
throw new Error('Could not read the work-item total; the full order is unavailable.');
|
|
991
|
+
if (items.length === 0 && offset < result.count)
|
|
992
|
+
throw new Error('Work items changed while reading this page. Restart from offset zero.');
|
|
993
|
+
return JSON.stringify({
|
|
994
|
+
order: 'Saved manual board order, top to bottom within each project and status',
|
|
995
|
+
total: result.count,
|
|
996
|
+
next_offset: offset + items.length < result.count ? offset + items.length : null,
|
|
997
|
+
items,
|
|
998
|
+
});
|
|
986
999
|
},
|
|
987
1000
|
},
|
|
988
1001
|
{
|
|
989
1002
|
name: 'get_work_item',
|
|
990
1003
|
levels: ALL,
|
|
991
|
-
description: 'One work item in full:
|
|
1004
|
+
description: 'One work item in full: description, status, placement, saved codebase targets, available project codebases, and artifacts. Saved targets belong to the work item, not just this conversation.',
|
|
992
1005
|
input: { work_item_id: z.string() },
|
|
993
1006
|
handler: async ({ client }, args) => {
|
|
994
1007
|
const { work_item_id } = args;
|
|
@@ -1001,6 +1014,10 @@ const TOOLS = [
|
|
|
1001
1014
|
const feedback = await rows(client.from('artifact_feedback').select('*').eq('task_id', work_item_id).order('created_at'), 'read', 'the artifact feedback');
|
|
1002
1015
|
const decisions = await rows(client.from('decisions').select('id, category, question, state, selected_options, answer_note, related_artifact_id').eq('task_id', work_item_id).order('asked_at'), 'read', 'the work item decisions');
|
|
1003
1016
|
const comments = await rows(client.from('comments').select('id, body, created_at').eq('task_id', work_item_id).order('created_at'), 'read', 'the work item comments');
|
|
1017
|
+
const codebases = (await listCodebases(client, item.project_id))
|
|
1018
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
1019
|
+
.map(({ id, name, gitRemoteUrl }) => ({ id, name, git_remote_url: gitRemoteUrl }));
|
|
1020
|
+
const targets = await listWorkItemCodebaseTargets(client, work_item_id);
|
|
1004
1021
|
return [
|
|
1005
1022
|
`${item.name}`,
|
|
1006
1023
|
`id ${item.id}`,
|
|
@@ -1009,6 +1026,8 @@ const TOOLS = [
|
|
|
1009
1026
|
`epic ${item.epics?.name ?? 'none'}`,
|
|
1010
1027
|
`sprint ${item.sprints?.name ?? 'none'}`,
|
|
1011
1028
|
`due ${item.due_date ?? 'no date'}`,
|
|
1029
|
+
'PROJECT CODEBASES', JSON.stringify(codebases),
|
|
1030
|
+
'SAVED CODEBASE TARGETS', JSON.stringify(targets),
|
|
1012
1031
|
'',
|
|
1013
1032
|
'DESCRIPTION',
|
|
1014
1033
|
item.description.trim() === '' ? '(empty)' : item.description,
|
|
@@ -1165,10 +1184,15 @@ const TOOLS = [
|
|
|
1165
1184
|
`id ${workflow.id}`,
|
|
1166
1185
|
`about ${workflow.description.trim() === '' ? 'no description' : workflow.description}`,
|
|
1167
1186
|
'',
|
|
1187
|
+
`ending ${workflow.ending}`,
|
|
1188
|
+
`BRANCH CONDITIONS ${workflow.branches.length}`,
|
|
1189
|
+
...workflow.branches.map(branch => `if ${branch.when}, run workflow ${branch.runsWorkflowId}, then resume this workflow`),
|
|
1190
|
+
'',
|
|
1168
1191
|
`STAGES ${workflow.stages.length}`,
|
|
1169
1192
|
...workflow.stages.flatMap((stage, i) => [
|
|
1170
1193
|
'',
|
|
1171
1194
|
line(`${i + 1}. ${stage.name}`, stage.description.trim() === '' ? null : stage.description),
|
|
1195
|
+
`stage_id ${stage.id} (use stage number ${i + 1} for reword_stage and keep)`,
|
|
1172
1196
|
'',
|
|
1173
1197
|
/* THE WHOLE BODY. The body IS the stage document, so truncating it is
|
|
1174
1198
|
truncating the process. An empty one is a stage nobody has written
|
|
@@ -1191,8 +1215,9 @@ const TOOLS = [
|
|
|
1191
1215
|
'',
|
|
1192
1216
|
]
|
|
1193
1217
|
: []),
|
|
1194
|
-
'
|
|
1195
|
-
+ '
|
|
1218
|
+
'When assigned execution, follow this workflow stage by stage. When assigned authoring '
|
|
1219
|
+
+ 'or review only, edit or inspect it without starting work. Stage IDs above identify '
|
|
1220
|
+
+ 'library stages; IDs inside Markdown tool links identify tool occurrences, not stages.',
|
|
1196
1221
|
].join('\n');
|
|
1197
1222
|
},
|
|
1198
1223
|
},
|
|
@@ -1210,7 +1235,9 @@ const TOOLS = [
|
|
|
1210
1235
|
+ 'is earlier than N or the same stage; a forward jump is refused and nothing is written. '
|
|
1211
1236
|
+ 'Takes no organisation or project: the workflow goes into the organisation this '
|
|
1212
1237
|
+ "conversation's project belongs to. It appears on /workflows at once. Never put an "
|
|
1213
|
-
+ 'absolute local path in any field.'
|
|
1238
|
+
+ 'absolute local path in any field. '
|
|
1239
|
+
+ 'branches are global conditions: when a condition holds, run runs_workflow_id in the same organisation and resume. '
|
|
1240
|
+
+ 'ending is end or next-in-backlog. Omitted settings are preserved on edit; branches: [] clears branches.',
|
|
1214
1241
|
input: {
|
|
1215
1242
|
name: z.string(),
|
|
1216
1243
|
description: z.string().optional(),
|
|
@@ -1224,9 +1251,14 @@ const TOOLS = [
|
|
|
1224
1251
|
to_stage: z.number().int(),
|
|
1225
1252
|
condition: z.string(),
|
|
1226
1253
|
})).optional(),
|
|
1254
|
+
branches: z.array(z.object({
|
|
1255
|
+
when: z.string().trim().min(1),
|
|
1256
|
+
runs_workflow_id: z.string().uuid(),
|
|
1257
|
+
})).optional().describe('the whole new branch list; omitted preserves current branches, [] clears them'),
|
|
1258
|
+
ending: z.enum(['end', 'next-in-backlog']).optional(),
|
|
1227
1259
|
},
|
|
1228
1260
|
handler: async (caller, args) => {
|
|
1229
|
-
const { name, description, stages, exits } = args;
|
|
1261
|
+
const { name, description, stages, exits, branches, ending } = args;
|
|
1230
1262
|
/* ═══ CHECKED HERE IN THE NUMBERING THE AGENT SENT. ═══
|
|
1231
1263
|
`cliv2_build_workflow` enforces every one of these itself, and its
|
|
1232
1264
|
messages count stages from 0, a numbering this tool never showed the
|
|
@@ -1269,6 +1301,8 @@ const TOOLS = [
|
|
|
1269
1301
|
body: stage.body,
|
|
1270
1302
|
})),
|
|
1271
1303
|
exits: wanted,
|
|
1304
|
+
branches: branches?.map(branch => ({ when: branch.when, runsWorkflowId: branch.runs_workflow_id })),
|
|
1305
|
+
ending,
|
|
1272
1306
|
fromAgent: harness(),
|
|
1273
1307
|
});
|
|
1274
1308
|
await receipt(caller, 'workflow', id, workflowName);
|
|
@@ -1364,8 +1398,12 @@ const TOOLS = [
|
|
|
1364
1398
|
+ 'must be earlier than from_stage or the same, and exits off one stage are tried in the order '
|
|
1365
1399
|
+ 'sent. name and description left out are left as they are; an empty description clears it. '
|
|
1366
1400
|
+ 'To change the wording of an '
|
|
1367
|
-
+ 'existing stage use reword_stage
|
|
1368
|
-
+ '
|
|
1401
|
+
+ 'existing stage use reword_stage first, then keep its stage number here. Read the current '
|
|
1402
|
+
+ 'workflow before editing. Never replace a retained stage with a new name/body entry just '
|
|
1403
|
+
+ 'to rename it, copy its wording, or bypass a protected-body refusal. Refused, with nothing written, while any work item is '
|
|
1404
|
+
+ 'running the workflow. Never put an absolute local path in any field. '
|
|
1405
|
+
+ 'branches are global conditions: when a condition holds, run runs_workflow_id in the same organisation and resume. '
|
|
1406
|
+
+ 'ending is end or next-in-backlog. Omitted settings are preserved on edit; branches: [] clears branches.',
|
|
1369
1407
|
input: {
|
|
1370
1408
|
workflow_id: z.string(),
|
|
1371
1409
|
name: z.string().optional(),
|
|
@@ -1383,9 +1421,14 @@ const TOOLS = [
|
|
|
1383
1421
|
to_stage: z.number().int(),
|
|
1384
1422
|
condition: z.string(),
|
|
1385
1423
|
})).optional().describe('the whole new list of conditional ways back; send [] for none'),
|
|
1424
|
+
branches: z.array(z.object({
|
|
1425
|
+
when: z.string().trim().min(1),
|
|
1426
|
+
runs_workflow_id: z.string().uuid(),
|
|
1427
|
+
})).optional().describe('the whole new branch list; omitted preserves current branches, [] clears them'),
|
|
1428
|
+
ending: z.enum(['end', 'next-in-backlog']).optional(),
|
|
1386
1429
|
},
|
|
1387
1430
|
handler: async (caller, args) => {
|
|
1388
|
-
const { workflow_id, name, description, stages, exits } = args;
|
|
1431
|
+
const { workflow_id, name, description, stages, exits, branches, ending } = args;
|
|
1389
1432
|
const workflowName = name?.trim();
|
|
1390
1433
|
if (workflowName === '')
|
|
1391
1434
|
throw new Error('a workflow needs a name. Nothing was written.');
|
|
@@ -1436,10 +1479,8 @@ const TOOLS = [
|
|
|
1436
1479
|
description: description?.trim() ?? null,
|
|
1437
1480
|
stages: entries,
|
|
1438
1481
|
exits: zeroBasedExits(exits, entries.length),
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
delete what a person set on `/workflows`. */
|
|
1442
|
-
branches: workflow.branches,
|
|
1482
|
+
branches: branches?.map(branch => ({ when: branch.when, runsWorkflowId: branch.runs_workflow_id })) ?? workflow.branches,
|
|
1483
|
+
ending,
|
|
1443
1484
|
fromAgent: harness(),
|
|
1444
1485
|
});
|
|
1445
1486
|
/* THE LABEL IS THE NAME THAT SURVIVED THE EDIT, which is the reply's own:
|
|
@@ -2010,7 +2051,8 @@ const TOOLS = [
|
|
|
2010
2051
|
levels: [2],
|
|
2011
2052
|
description: 'Offer the person the ending for this card: put the finished work onto the main branch, or '
|
|
2012
2053
|
+ 'leave it on its branch. Call this when the card\'s work is DONE and the codebase lands on '
|
|
2013
|
-
+ 'the main branch.
|
|
2054
|
+
+ 'the main branch. First record verified completion with write_report(work_complete=true). '
|
|
2055
|
+
+ 'Use say for progress; this tool cannot announce work you intend to do. You do not write the question or the answers and you never merge anything: '
|
|
2014
2056
|
+ 'the product composes both, and it performs the merge itself if they choose to put the work '
|
|
2015
2057
|
+ 'back. Say in one sentence what was done, in their words. After this call, stop immediately: '
|
|
2016
2058
|
+ 'you are started again with what the product did with their answer.',
|
|
@@ -2030,8 +2072,13 @@ const TOOLS = [
|
|
|
2030
2072
|
+ 'sentence saying what changed.');
|
|
2031
2073
|
}
|
|
2032
2074
|
const run = await only(client.from('panel3_runs')
|
|
2033
|
-
.select('codebase_id, branch, base, card:panel3_cards!panel3_runs_card_id_fkey(project_id)')
|
|
2075
|
+
.select('codebase_id, branch, base, completion_requested_token, card:panel3_cards!panel3_runs_card_id_fkey(project_id)')
|
|
2034
2076
|
.eq('id', runId), 'read', 'which branch this card\'s work is on');
|
|
2077
|
+
if (run.completion_requested_token !== processToken) {
|
|
2078
|
+
throw new Error('NOTHING WAS WRITTEN and nobody was asked. The assignment has not been declared complete in this turn. '
|
|
2079
|
+
+ 'Finish and verify every requested codebase first, then call write_report with work_complete=true before offer_ending. '
|
|
2080
|
+
+ 'Use say for a progress update, not offer_ending.');
|
|
2081
|
+
}
|
|
2035
2082
|
if (!run.codebase_id || !run.branch || !run.base || !run.card?.project_id) {
|
|
2036
2083
|
throw new Error('NOTHING WAS WRITTEN. This card has no codebase, branch and base recorded, so there is no '
|
|
2037
2084
|
+ 'ending to offer. Say what you did and finish the card.');
|
|
@@ -2174,6 +2221,20 @@ const TOOLS = [
|
|
|
2174
2221
|
return `Created work item ${item.name}, id ${item.id}.`;
|
|
2175
2222
|
},
|
|
2176
2223
|
},
|
|
2224
|
+
{
|
|
2225
|
+
name: 'edit_work_item_codebase',
|
|
2226
|
+
levels: [1, 2],
|
|
2227
|
+
description: 'Add or remove one SAVED work-item codebase target, preserving all other targets and attributes. Read get_work_item first for current targets and project codebase IDs. To select multiple codebases, add each; to replace a target, add its replacement before removing it. Duplicate additions are harmless. Re-read after edits or human changes. This edits the work item itself and provides an Open work item receipt; it does not attach to the conversation or execute files.',
|
|
2228
|
+
input: { work_item_id: z.string().uuid(), codebase_id: z.string().uuid(), action: z.enum(['add', 'remove']) },
|
|
2229
|
+
handler: async (caller, args) => {
|
|
2230
|
+
const a = args;
|
|
2231
|
+
const item = await only(caller.client.from('tasks')
|
|
2232
|
+
.select('id, name, project_id').eq('id', a.work_item_id), 'read', 'the work item');
|
|
2233
|
+
const targets = await editWorkItemCodebaseTarget(caller.client, item.id, item.project_id, a.codebase_id, a.action === 'add');
|
|
2234
|
+
await receiptOnce(caller, 'work_item', item.id, item.name);
|
|
2235
|
+
return JSON.stringify({ work_item_id: item.id, saved_codebase_targets: targets });
|
|
2236
|
+
},
|
|
2237
|
+
},
|
|
2177
2238
|
// ── Do the work, and produce objects ─────────────────────────────────────
|
|
2178
2239
|
//
|
|
2179
2240
|
// ═══ EVERY LEVEL, AND THAT IS ux.md's FOURTH RULE. ═══ "Producing an object
|
|
@@ -2763,7 +2824,7 @@ const TOOLS = [
|
|
|
2763
2824
|
await whileRunning(client.rpc('panel3_request_completion', {
|
|
2764
2825
|
p_run_id: runId, p_process_token: processToken, p_summary: report,
|
|
2765
2826
|
}), runId, 'declare completion of');
|
|
2766
|
-
return 'Completion recorded.
|
|
2827
|
+
return 'Completion recorded. If the finished work needs an ending choice, call offer_ending; otherwise give your final answer and exit. Done waits until all processes have exited.';
|
|
2767
2828
|
}
|
|
2768
2829
|
await whileRunning(processToken === undefined
|
|
2769
2830
|
? client.from('panel3_runs').update({ report }).eq('id', runId)
|
|
@@ -2929,6 +2990,13 @@ const TOOLS = [
|
|
|
2929
2990
|
},
|
|
2930
2991
|
},
|
|
2931
2992
|
// ── Dispatch and stop ────────────────────────────────────────────────────
|
|
2993
|
+
{
|
|
2994
|
+
name: 'list_codex_models',
|
|
2995
|
+
levels: [1, 2],
|
|
2996
|
+
description: 'Read the models and reasoning efforts currently offered by the installed Codex harness on this machine. Call before choosing a Codex child model yourself. This is discovery, not an allowlist for explicit user model IDs or aliases. If discovery fails, omit autonomous model/effort choices to use the installed defaults; never guess an identifier.',
|
|
2997
|
+
input: {},
|
|
2998
|
+
handler: async () => JSON.stringify(await listCodexModels()),
|
|
2999
|
+
},
|
|
2932
3000
|
{
|
|
2933
3001
|
name: 'dispatch',
|
|
2934
3002
|
/* ═══ ABSENT AT LEVEL 3, WHICH IS HOW "DEPTH STOPS AT THREE" IS ENFORCED.
|
|
@@ -2962,7 +3030,7 @@ const TOOLS = [
|
|
|
2962
3030
|
+ 'context: what to find out or change, and in which part of the codebase.'),
|
|
2963
3031
|
boundary: z.string().min(1).describe('What it must not touch, and where its work stops.'),
|
|
2964
3032
|
work_item_id: z.string().optional().describe('The work item it is working, if there is one.'),
|
|
2965
|
-
model: z.string().min(1).optional().describe('Exact model for this child. Copy the user\'s applicable value verbatim, including aliases; never expand or normalize it. Otherwise choose for the task.
|
|
3033
|
+
model: z.string().min(1).refine(value => !/^(?:codex|claude(?: code)?)(?:\s|$)/i.test(value.trim()), 'Use only the model ID, without the Claude or Codex harness label. Omit model to use the installed default.').optional().describe('Exact model for this child. Copy the user\'s applicable value verbatim, including aliases; never expand or normalize it. Otherwise choose for the task: for Claude, prefer supported unversioned aliases such as sonnet or opus; do not invent version-suffixed aliases. For Codex, first call list_codex_models and choose from its result. If uncertain, omit for the harness or machine default, never parent inheritance.'),
|
|
2966
3034
|
effort: z.string().min(1).optional().describe('Exact effort for this child. Honor the user\'s applicable wish; otherwise choose independently. Passed unchanged to the harness.'),
|
|
2967
3035
|
work_name: z.string().optional().describe(`A few words, ${WORK_NAME_WORDS} at most, naming the WORK this conversation is doing, such `
|
|
2968
3036
|
+ 'as "Fix the sign-out checklist bug". Pass it when the conversation has NO work item '
|
|
@@ -2992,6 +3060,9 @@ const TOOLS = [
|
|
|
2992
3060
|
+ 'the work, do not describe it.');
|
|
2993
3061
|
}
|
|
2994
3062
|
}
|
|
3063
|
+
const attached = await loadAttachments(caller.client, caller.cardId);
|
|
3064
|
+
const workItems = attached.filter(attachment => attachment.kind === 'work_item');
|
|
3065
|
+
const scopedWorkItem = work_item_id ?? (workItems.length === 1 ? workItems[0].ref_id : undefined);
|
|
2995
3066
|
let codebase = null;
|
|
2996
3067
|
if (codebase_id) {
|
|
2997
3068
|
const projectId = await projectOfCard(caller);
|
|
@@ -3003,6 +3074,19 @@ const TOOLS = [
|
|
|
3003
3074
|
if (!codebase) {
|
|
3004
3075
|
throw new Error('That codebase is not registered on this project. Read the current project codebases and choose one of them.');
|
|
3005
3076
|
}
|
|
3077
|
+
if (!scopedWorkItem && workItems.length > 1) {
|
|
3078
|
+
throw new Error('No agent was started. Identify the work item for this piece before choosing its codebase.');
|
|
3079
|
+
}
|
|
3080
|
+
if (scopedWorkItem) {
|
|
3081
|
+
await only(caller.client.from('tasks').select('id').eq('id', scopedWorkItem)
|
|
3082
|
+
.eq('project_id', projectId).is('archived_at', null).eq('is_idea', false), 'read', 'the live work item in this conversation\'s project');
|
|
3083
|
+
const targets = await listWorkItemCodebaseTargets(caller.client, scopedWorkItem);
|
|
3084
|
+
if (!targets.includes(codebase.gitRemoteUrl)) {
|
|
3085
|
+
throw new Error('No agent was started. This codebase is not a saved target of the work item. '
|
|
3086
|
+
+ 'Read get_work_item for its current saved targets; if none are saved, ask the person to select one. '
|
|
3087
|
+
+ 'Do not fall back to another project codebase or change targets to bypass this check.');
|
|
3088
|
+
}
|
|
3089
|
+
}
|
|
3006
3090
|
}
|
|
3007
3091
|
/* ONE LEVEL DOWN, AND THE SAME ARITHMETIC THE DATABASE DOES. This decides
|
|
3008
3092
|
the words in the brief; `panel3_dispatch` decides the level on the row,
|
|
@@ -3017,7 +3101,6 @@ const TOOLS = [
|
|
|
3017
3101
|
three hops from level 1 still knows which item it is working, without
|
|
3018
3102
|
asking. See `workBrief`'s own doc for why the two are different things
|
|
3019
3103
|
carried the same way. */
|
|
3020
|
-
const attached = await loadAttachments(caller.client, caller.cardId);
|
|
3021
3104
|
/* ═══ THE CONVERSATION IS NAMED HERE OR IT IS NEVER NAMED. ═══ Sending
|
|
3022
3105
|
somebody resolves the working copy, and resolving it cuts the branch
|
|
3023
3106
|
from whatever the conversation is called at that moment and stamps it
|
|
@@ -3032,7 +3115,7 @@ const TOOLS = [
|
|
|
3032
3115
|
.select('id'), 'name', `card ${caller.cardId}`);
|
|
3033
3116
|
}
|
|
3034
3117
|
const attachments = attached.map(attachmentLine);
|
|
3035
|
-
const { runId } = await caller.dispatch(caller.runId, workBrief(childLevel, responsibility, boundary,
|
|
3118
|
+
const { runId } = await caller.dispatch(caller.runId, workBrief(childLevel, responsibility, boundary, scopedWorkItem, attachments, codebase === null ? undefined : {
|
|
3036
3119
|
id: codebase.id, name: codebase.name, identity: codebase.gitRemoteUrl,
|
|
3037
3120
|
}), codebase, caller.processToken, { model, effort });
|
|
3038
3121
|
/* ═══ WHERE ITS ANSWER GOES DEPENDS ON WHICH LEVEL THIS IS, AND THAT IS
|
|
@@ -123,6 +123,7 @@ export function authorToolMentions(body) {
|
|
|
123
123
|
}
|
|
124
124
|
export const WORKFLOW_AUTHORING_TEACHING = 'When the user asks you to create a workflow, build it in the library; do not merely describe it or execute it. ' +
|
|
125
125
|
'Use ask_question for missing requirements that materially change the workflow, then continue after the answer. ' +
|
|
126
|
+
'If the user requests plain prose or no tool mentions, write plain Markdown only: do not add tool links, permissions, or an approval template. ' +
|
|
126
127
|
'Encode every requested tool use and permission as a Markdown mention in the stage body. ' +
|
|
127
128
|
'Use compact links: [@Create Artifact](ctrl-spc://tool/create_artifact?approval=not-required) to write a plan without asking; ' +
|
|
128
129
|
'[@Ask Question](ctrl-spc://tool/ask_question?approval=not-required) to review its output; ' +
|
package/dist/workflows.js
CHANGED
|
@@ -144,6 +144,7 @@ export async function buildWorkflow(client, input) {
|
|
|
144
144
|
prose.push([`stage ${i + 1} name`, stage.name], [`stage ${i + 1} description`, stage.description], [`stage ${i + 1} body`, stage.body]);
|
|
145
145
|
});
|
|
146
146
|
input.exits.forEach((exit, i) => prose.push([`exit ${i + 1} condition`, exit.condition]));
|
|
147
|
+
input.branches?.forEach((branch, i) => prose.push([`branch ${i + 1} condition`, branch.when]));
|
|
147
148
|
for (const [field, value] of prose) {
|
|
148
149
|
const token = absolutePathToken(value);
|
|
149
150
|
if (token) {
|
|
@@ -158,8 +159,9 @@ export async function buildWorkflow(client, input) {
|
|
|
158
159
|
p_description: input.description,
|
|
159
160
|
p_stages: input.stages,
|
|
160
161
|
p_exits: input.exits,
|
|
161
|
-
p_branches: [],
|
|
162
|
+
p_branches: (input.branches ?? []).map(branch => ({ when: branch.when, runs_workflow_id: branch.runsWorkflowId })),
|
|
162
163
|
p_from_agent: input.fromAgent,
|
|
164
|
+
...(input.ending === undefined ? {} : { p_ending: input.ending }),
|
|
163
165
|
});
|
|
164
166
|
if (error)
|
|
165
167
|
throw new Error(`could not create workflow ${input.name}: ${error.message}`);
|
|
@@ -239,11 +241,8 @@ export async function rewordStage(client, input) {
|
|
|
239
241
|
* behalf. Its refusals come back verbatim behind one sentence naming what was
|
|
240
242
|
* being edited.
|
|
241
243
|
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
* `readWorkflow` read a moment before. A branch written between that read and
|
|
245
|
-
* this call is lost, which is the posture the web's `replaceStructure` records
|
|
246
|
-
* for itself.
|
|
244
|
+
* Branches are the complete replacement list. Callers preserve the current
|
|
245
|
+
* list when no branch change was requested, matching the web editor.
|
|
247
246
|
*
|
|
248
247
|
* ═══ NULL NAME OR DESCRIPTION MEANS UNCHANGED, ═══ which is the RPC's own
|
|
249
248
|
* contract: an agent adding a stage does not restate a name it is not touching.
|
|
@@ -257,6 +256,7 @@ export async function editWorkflow(client, input) {
|
|
|
257
256
|
prose.push([`stage ${i + 1} name`, stage.name], [`stage ${i + 1} description`, stage.description], [`stage ${i + 1} body`, stage.body]);
|
|
258
257
|
});
|
|
259
258
|
input.exits.forEach((exit, i) => prose.push([`exit ${i + 1} condition`, exit.condition]));
|
|
259
|
+
input.branches.forEach((branch, i) => prose.push([`branch ${i + 1} condition`, branch.when]));
|
|
260
260
|
for (const [field, value] of prose) {
|
|
261
261
|
const token = value === null ? null : absolutePathToken(value);
|
|
262
262
|
if (token) {
|
|
@@ -273,6 +273,7 @@ export async function editWorkflow(client, input) {
|
|
|
273
273
|
p_exits: input.exits,
|
|
274
274
|
p_branches: input.branches.map((branch) => ({ when: branch.when, runs_workflow_id: branch.runsWorkflowId })),
|
|
275
275
|
p_from_agent: input.fromAgent,
|
|
276
|
+
...(input.ending === undefined ? {} : { p_ending: input.ending }),
|
|
276
277
|
});
|
|
277
278
|
if (error)
|
|
278
279
|
throw new Error(`could not edit the workflow: ${error.message}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ctrl-spc/cs",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.12",
|
|
4
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"
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"scripts": {
|
|
19
19
|
"build": "tsc",
|
|
20
20
|
"start": "npm run build && node dist/index.js",
|
|
21
|
-
"test": "npm run build && node --test test/*.test.mjs"
|
|
21
|
+
"test": "npm run build && node --test --test-concurrency=1 test/*.test.mjs"
|
|
22
22
|
},
|
|
23
23
|
"license": "UNLICENSED",
|
|
24
24
|
"dependencies": {
|