@ctrl-spc/cs 0.7.10 → 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/mcp.js +94 -357
- 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 +207 -58
- package/dist/product-tools.js +533 -0
- package/dist/workflow-tool-mentions.js +12 -0
- package/dist/workflows.js +21 -7
- package/package.json +2 -2
package/dist/panel3/tools.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createEpicHandler, createSprintHandler, listStructureHandler, updateStructureHandler, listStructureArtifactsHandler, getStructureArtifactHandler, searchAgentCardsHandler, createStructureArtifactHandler, updateStructureArtifactHandler } from '../product-tools.js';
|
|
2
|
+
import { listArtifactFoldersHandler, setArtifactFolderHandler, workItemDependencyHandler } from '../product-tools.js';
|
|
1
3
|
/**
|
|
2
4
|
* ═══ AGENT PANEL v3: the tools server, and the per-level allowlist. ═══
|
|
3
5
|
*
|
|
@@ -125,11 +127,12 @@ import { returned } from './client.js';
|
|
|
125
127
|
import { readableWriteError, FIREWALL_WRITING_RULE } from '../firewall.js';
|
|
126
128
|
import { rememberSecret, redactArgs } from './secrets.js';
|
|
127
129
|
import { workBrief } from './prompt.js';
|
|
130
|
+
import { listCodexModels } from './codex-models.js';
|
|
128
131
|
/* The harness this daemon spawns with, which is the process this server runs
|
|
129
132
|
in. It is what `panel3_runs.harness` is written from at spawn, so reading it
|
|
130
133
|
here is the same fact without a second round trip. */
|
|
131
134
|
import { harness } from './spawn.js';
|
|
132
|
-
import { listCodebases } from '../codebases.js';
|
|
135
|
+
import { listCodebases, listWorkItemCodebaseTargets, editWorkItemCodebaseTarget } from '../codebases.js';
|
|
133
136
|
import { buildWorkflow, duplicateWorkflow, editWorkflow, readWorkflow, rewordStage, workflowToolPermission, validateWorkflowToolTarget, WORKFLOW_TOOL_TEACHING, WORKFLOW_AUTHORING_TEACHING } from '../workflows.js';
|
|
134
137
|
/* ═══ 38-panel3-steps: THE FIFTH NEUTRAL MODULE, AND A DECISION LIKE THE OTHERS.
|
|
135
138
|
═══ Stages and steps are the work item's own record, shared by both
|
|
@@ -298,10 +301,9 @@ async function asked(caller, args, askPerson) {
|
|
|
298
301
|
p_question: question,
|
|
299
302
|
p_category: category,
|
|
300
303
|
p_context: context,
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
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 ?? [],
|
|
305
307
|
p_question_id: question_id ?? null,
|
|
306
308
|
p_ask_person: askPerson,
|
|
307
309
|
/* ═══ ONLY `ask_question` OFFERS THIS, AND THE DATABASE REFUSES IT ON AN
|
|
@@ -789,6 +791,36 @@ const TOOLS = [
|
|
|
789
791
|
return `Created Product Idea ${a.title}, id ${id}. No work has started. Only a human can promote it.`;
|
|
790
792
|
},
|
|
791
793
|
},
|
|
794
|
+
{
|
|
795
|
+
name: 'list_artifact_folders', levels: ALL,
|
|
796
|
+
description: 'Read existing artifact folder names and counts before choosing a folder.',
|
|
797
|
+
input: { work_item_id: z.string().uuid() },
|
|
798
|
+
handler: async (caller, args) => productResult(await listArtifactFoldersHandler(caller.client, args.work_item_id)),
|
|
799
|
+
},
|
|
800
|
+
{
|
|
801
|
+
name: 'set_artifact_folder', levels: ALL,
|
|
802
|
+
description: 'Move an artifact into one folder, replacing its previous folder. Null clears it to Unfiled. Content and approval stay unchanged.',
|
|
803
|
+
input: { artifact_id: z.string().uuid(), folder_name: z.string().max(80).nullable() },
|
|
804
|
+
handler: async (caller, args) => productResult(await setArtifactFolderHandler(caller.client, args.artifact_id, args.folder_name)),
|
|
805
|
+
},
|
|
806
|
+
{
|
|
807
|
+
name: 'list_work_item_dependencies', levels: ALL,
|
|
808
|
+
description: 'Read blockers, dependents, and whether this work item is waiting.',
|
|
809
|
+
input: { work_item_id: z.string().uuid(), },
|
|
810
|
+
handler: async (caller, args) => productResult(await workItemDependencyHandler(caller.client, 'list', args)),
|
|
811
|
+
},
|
|
812
|
+
{
|
|
813
|
+
name: 'add_work_item_dependency', levels: [1, 2],
|
|
814
|
+
description: 'Make work_item_id wait for depends_on_work_item_id to reach Done. Same-project work items only; self-links and cycles are refused.',
|
|
815
|
+
input: { work_item_id: z.string().uuid(), depends_on_work_item_id: z.string().uuid(), },
|
|
816
|
+
handler: async (caller, args) => productResult(await workItemDependencyHandler(caller.client, 'add', args)),
|
|
817
|
+
},
|
|
818
|
+
{
|
|
819
|
+
name: 'remove_work_item_dependency', levels: [1, 2],
|
|
820
|
+
description: 'Remove exactly this dependency edge without changing either work item status.',
|
|
821
|
+
input: { work_item_id: z.string().uuid(), depends_on_work_item_id: z.string().uuid(), },
|
|
822
|
+
handler: async (caller, args) => productResult(await workItemDependencyHandler(caller.client, 'remove', args)),
|
|
823
|
+
},
|
|
792
824
|
{
|
|
793
825
|
name: 'place_work_item', levels: [1],
|
|
794
826
|
description: 'Move a work item into an epic or sprint in its project. Null removes placement. Its status stays the same.',
|
|
@@ -797,13 +829,14 @@ const TOOLS = [
|
|
|
797
829
|
handler: async (caller, args) => productResult(await placeWorkItemHandler(caller.client, args)),
|
|
798
830
|
},
|
|
799
831
|
{
|
|
800
|
-
name: 'reorder_backlog', levels: [1],
|
|
832
|
+
name: 'reorder_backlog', levels: [1, 2],
|
|
801
833
|
description: 'Move one backlog work item before another, or to the bottom. Read the current order first and explain the reason.',
|
|
802
834
|
input: { project_id: z.string().uuid(), task_id: z.string().uuid(), before_task_id: z.string().uuid().optional(), reason: z.string().min(1) },
|
|
803
835
|
handler: async (caller, args) => {
|
|
804
836
|
const a = args;
|
|
805
837
|
const items = await rows(caller.client.from('tasks').select('id, revision')
|
|
806
|
-
.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');
|
|
807
840
|
const moving = items.find(item => item.id === a.task_id);
|
|
808
841
|
if (!moving || (a.before_task_id && !items.some(item => item.id === a.before_task_id)))
|
|
809
842
|
throw new Error('Both items must be live backlog work in this project. Nothing was moved.');
|
|
@@ -931,32 +964,44 @@ const TOOLS = [
|
|
|
931
964
|
{
|
|
932
965
|
name: 'list_work_items',
|
|
933
966
|
levels: ALL,
|
|
934
|
-
description: '
|
|
935
|
-
+ '
|
|
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.',
|
|
936
970
|
input: {
|
|
937
971
|
project_id: z.string().optional().describe('Only items in this project.'),
|
|
938
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.'),
|
|
939
974
|
},
|
|
940
975
|
handler: async ({ client }, args) => {
|
|
941
|
-
const { project_id, status } = args;
|
|
976
|
+
const { project_id, status, offset = 0 } = args;
|
|
942
977
|
let query = client
|
|
943
978
|
.from('tasks')
|
|
944
|
-
.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' })
|
|
945
980
|
.eq('is_idea', false)
|
|
946
981
|
.is('archived_at', null)
|
|
947
|
-
.order('created_at'
|
|
982
|
+
.order('project_id').order('status').order('position').order('created_at').order('id');
|
|
948
983
|
if (project_id)
|
|
949
984
|
query = query.eq('project_id', project_id);
|
|
950
985
|
if (status)
|
|
951
986
|
query = query.eq('status', status);
|
|
952
|
-
const
|
|
953
|
-
|
|
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
|
+
});
|
|
954
999
|
},
|
|
955
1000
|
},
|
|
956
1001
|
{
|
|
957
1002
|
name: 'get_work_item',
|
|
958
1003
|
levels: ALL,
|
|
959
|
-
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.',
|
|
960
1005
|
input: { work_item_id: z.string() },
|
|
961
1006
|
handler: async ({ client }, args) => {
|
|
962
1007
|
const { work_item_id } = args;
|
|
@@ -964,11 +1009,15 @@ const TOOLS = [
|
|
|
964
1009
|
.from('tasks')
|
|
965
1010
|
.select('id, name, description, status, due_date, project_id, created_at, epics(name), sprints(name)')
|
|
966
1011
|
.eq('id', work_item_id), 'read', `work item ${work_item_id}`);
|
|
967
|
-
const artifacts = await rows(client.from('artifacts').select('id, type, title, created_at').eq('task_id', work_item_id)
|
|
1012
|
+
const artifacts = await rows(client.from('artifacts').select('id, type, title, created_at, folder_name').eq('task_id', work_item_id)
|
|
968
1013
|
.is('deleted_at', null).order('created_at'), 'read', `the artifacts on work item ${work_item_id}`);
|
|
969
1014
|
const feedback = await rows(client.from('artifact_feedback').select('*').eq('task_id', work_item_id).order('created_at'), 'read', 'the artifact feedback');
|
|
970
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');
|
|
971
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);
|
|
972
1021
|
return [
|
|
973
1022
|
`${item.name}`,
|
|
974
1023
|
`id ${item.id}`,
|
|
@@ -977,13 +1026,15 @@ const TOOLS = [
|
|
|
977
1026
|
`epic ${item.epics?.name ?? 'none'}`,
|
|
978
1027
|
`sprint ${item.sprints?.name ?? 'none'}`,
|
|
979
1028
|
`due ${item.due_date ?? 'no date'}`,
|
|
1029
|
+
'PROJECT CODEBASES', JSON.stringify(codebases),
|
|
1030
|
+
'SAVED CODEBASE TARGETS', JSON.stringify(targets),
|
|
980
1031
|
'',
|
|
981
1032
|
'DESCRIPTION',
|
|
982
1033
|
item.description.trim() === '' ? '(empty)' : item.description,
|
|
983
1034
|
'',
|
|
984
1035
|
'DECISIONS', JSON.stringify(decisions), 'FEEDBACK', JSON.stringify(feedback), 'COMMENTS', JSON.stringify(comments),
|
|
985
1036
|
`ARTIFACTS ${artifacts.length}`,
|
|
986
|
-
listed(artifacts.map((a) => line(a.id, a.type, a.title ?? '(untitled)')), 'none'),
|
|
1037
|
+
listed(artifacts.map((a) => line(a.id, a.type, a.title ?? '(untitled)', `folder: ${a.folder_name ?? 'Unfiled'}`)), 'none'),
|
|
987
1038
|
].join('\n');
|
|
988
1039
|
},
|
|
989
1040
|
},
|
|
@@ -1133,10 +1184,15 @@ const TOOLS = [
|
|
|
1133
1184
|
`id ${workflow.id}`,
|
|
1134
1185
|
`about ${workflow.description.trim() === '' ? 'no description' : workflow.description}`,
|
|
1135
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
|
+
'',
|
|
1136
1191
|
`STAGES ${workflow.stages.length}`,
|
|
1137
1192
|
...workflow.stages.flatMap((stage, i) => [
|
|
1138
1193
|
'',
|
|
1139
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)`,
|
|
1140
1196
|
'',
|
|
1141
1197
|
/* THE WHOLE BODY. The body IS the stage document, so truncating it is
|
|
1142
1198
|
truncating the process. An empty one is a stage nobody has written
|
|
@@ -1159,8 +1215,9 @@ const TOOLS = [
|
|
|
1159
1215
|
'',
|
|
1160
1216
|
]
|
|
1161
1217
|
: []),
|
|
1162
|
-
'
|
|
1163
|
-
+ '
|
|
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.',
|
|
1164
1221
|
].join('\n');
|
|
1165
1222
|
},
|
|
1166
1223
|
},
|
|
@@ -1178,7 +1235,9 @@ const TOOLS = [
|
|
|
1178
1235
|
+ 'is earlier than N or the same stage; a forward jump is refused and nothing is written. '
|
|
1179
1236
|
+ 'Takes no organisation or project: the workflow goes into the organisation this '
|
|
1180
1237
|
+ "conversation's project belongs to. It appears on /workflows at once. Never put an "
|
|
1181
|
-
+ '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.',
|
|
1182
1241
|
input: {
|
|
1183
1242
|
name: z.string(),
|
|
1184
1243
|
description: z.string().optional(),
|
|
@@ -1192,9 +1251,14 @@ const TOOLS = [
|
|
|
1192
1251
|
to_stage: z.number().int(),
|
|
1193
1252
|
condition: z.string(),
|
|
1194
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(),
|
|
1195
1259
|
},
|
|
1196
1260
|
handler: async (caller, args) => {
|
|
1197
|
-
const { name, description, stages, exits } = args;
|
|
1261
|
+
const { name, description, stages, exits, branches, ending } = args;
|
|
1198
1262
|
/* ═══ CHECKED HERE IN THE NUMBERING THE AGENT SENT. ═══
|
|
1199
1263
|
`cliv2_build_workflow` enforces every one of these itself, and its
|
|
1200
1264
|
messages count stages from 0, a numbering this tool never showed the
|
|
@@ -1237,6 +1301,8 @@ const TOOLS = [
|
|
|
1237
1301
|
body: stage.body,
|
|
1238
1302
|
})),
|
|
1239
1303
|
exits: wanted,
|
|
1304
|
+
branches: branches?.map(branch => ({ when: branch.when, runsWorkflowId: branch.runs_workflow_id })),
|
|
1305
|
+
ending,
|
|
1240
1306
|
fromAgent: harness(),
|
|
1241
1307
|
});
|
|
1242
1308
|
await receipt(caller, 'workflow', id, workflowName);
|
|
@@ -1332,8 +1398,12 @@ const TOOLS = [
|
|
|
1332
1398
|
+ 'must be earlier than from_stage or the same, and exits off one stage are tried in the order '
|
|
1333
1399
|
+ 'sent. name and description left out are left as they are; an empty description clears it. '
|
|
1334
1400
|
+ 'To change the wording of an '
|
|
1335
|
-
+ 'existing stage use reword_stage
|
|
1336
|
-
+ '
|
|
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.',
|
|
1337
1407
|
input: {
|
|
1338
1408
|
workflow_id: z.string(),
|
|
1339
1409
|
name: z.string().optional(),
|
|
@@ -1351,9 +1421,14 @@ const TOOLS = [
|
|
|
1351
1421
|
to_stage: z.number().int(),
|
|
1352
1422
|
condition: z.string(),
|
|
1353
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(),
|
|
1354
1429
|
},
|
|
1355
1430
|
handler: async (caller, args) => {
|
|
1356
|
-
const { workflow_id, name, description, stages, exits } = args;
|
|
1431
|
+
const { workflow_id, name, description, stages, exits, branches, ending } = args;
|
|
1357
1432
|
const workflowName = name?.trim();
|
|
1358
1433
|
if (workflowName === '')
|
|
1359
1434
|
throw new Error('a workflow needs a name. Nothing was written.');
|
|
@@ -1404,10 +1479,8 @@ const TOOLS = [
|
|
|
1404
1479
|
description: description?.trim() ?? null,
|
|
1405
1480
|
stages: entries,
|
|
1406
1481
|
exits: zeroBasedExits(exits, entries.length),
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
delete what a person set on `/workflows`. */
|
|
1410
|
-
branches: workflow.branches,
|
|
1482
|
+
branches: branches?.map(branch => ({ when: branch.when, runsWorkflowId: branch.runs_workflow_id })) ?? workflow.branches,
|
|
1483
|
+
ending,
|
|
1411
1484
|
fromAgent: harness(),
|
|
1412
1485
|
});
|
|
1413
1486
|
/* THE LABEL IS THE NAME THAT SURVIVED THE EDIT, which is the reply's own:
|
|
@@ -1681,9 +1754,9 @@ const TOOLS = [
|
|
|
1681
1754
|
input: { work_item_id: z.string() },
|
|
1682
1755
|
handler: async ({ client }, args) => {
|
|
1683
1756
|
const { work_item_id } = args;
|
|
1684
|
-
const artifacts = await rows(client.from('artifacts').select('id, type, format, title, created_at').eq('task_id', work_item_id)
|
|
1757
|
+
const artifacts = await rows(client.from('artifacts').select('id, type, format, title, created_at, folder_name').eq('task_id', work_item_id)
|
|
1685
1758
|
.is('deleted_at', null).order('created_at'), 'read', `the artifacts on work item ${work_item_id}`);
|
|
1686
|
-
return listed(artifacts.map((a) => line(a.id, a.type, a.format, a.title ?? '(untitled)')), 'That work item has no artifacts.');
|
|
1759
|
+
return listed(artifacts.map((a) => line(a.id, a.type, a.format, a.title ?? '(untitled)', `folder: ${a.folder_name ?? 'Unfiled'}`)), 'That work item has no artifacts.');
|
|
1687
1760
|
},
|
|
1688
1761
|
},
|
|
1689
1762
|
{
|
|
@@ -1694,7 +1767,7 @@ const TOOLS = [
|
|
|
1694
1767
|
handler: async ({ client }, args) => {
|
|
1695
1768
|
const { artifact_id } = args;
|
|
1696
1769
|
const artifact = await only(client.from('artifacts')
|
|
1697
|
-
.select('id, task_id, type, format, title, content, storage_path, revision, created_at')
|
|
1770
|
+
.select('id, task_id, type, format, title, content, storage_path, revision, created_at, folder_name')
|
|
1698
1771
|
.eq('id', artifact_id), 'read', `artifact ${artifact_id}`);
|
|
1699
1772
|
return [
|
|
1700
1773
|
`${artifact.title ?? '(untitled)'}`,
|
|
@@ -1702,6 +1775,7 @@ const TOOLS = [
|
|
|
1702
1775
|
`work item ${artifact.task_id}`,
|
|
1703
1776
|
`type ${artifact.type} (${artifact.format})`,
|
|
1704
1777
|
`revision ${artifact.revision}`,
|
|
1778
|
+
`folder ${artifact.folder_name ?? 'Unfiled'}`,
|
|
1705
1779
|
'',
|
|
1706
1780
|
/* A stored file and an empty body are different facts and are said
|
|
1707
1781
|
differently. Returning '' for a PNG would read as an artifact with
|
|
@@ -1977,7 +2051,8 @@ const TOOLS = [
|
|
|
1977
2051
|
levels: [2],
|
|
1978
2052
|
description: 'Offer the person the ending for this card: put the finished work onto the main branch, or '
|
|
1979
2053
|
+ 'leave it on its branch. Call this when the card\'s work is DONE and the codebase lands on '
|
|
1980
|
-
+ '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: '
|
|
1981
2056
|
+ 'the product composes both, and it performs the merge itself if they choose to put the work '
|
|
1982
2057
|
+ 'back. Say in one sentence what was done, in their words. After this call, stop immediately: '
|
|
1983
2058
|
+ 'you are started again with what the product did with their answer.',
|
|
@@ -1997,8 +2072,13 @@ const TOOLS = [
|
|
|
1997
2072
|
+ 'sentence saying what changed.');
|
|
1998
2073
|
}
|
|
1999
2074
|
const run = await only(client.from('panel3_runs')
|
|
2000
|
-
.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)')
|
|
2001
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
|
+
}
|
|
2002
2082
|
if (!run.codebase_id || !run.branch || !run.base || !run.card?.project_id) {
|
|
2003
2083
|
throw new Error('NOTHING WAS WRITTEN. This card has no codebase, branch and base recorded, so there is no '
|
|
2004
2084
|
+ 'ending to offer. Say what you did and finish the card.');
|
|
@@ -2048,34 +2128,65 @@ const TOOLS = [
|
|
|
2048
2128
|
},
|
|
2049
2129
|
},
|
|
2050
2130
|
{
|
|
2051
|
-
name: 'create_epic',
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
input: { project_id: z.string(), name: z.string().min(1) },
|
|
2131
|
+
name: 'create_epic', levels: [1, 2],
|
|
2132
|
+
description: 'Create an epic with optional description and dates. Read list_structure first to avoid duplicates.',
|
|
2133
|
+
input: { project_id: z.string().uuid(), name: z.string().min(1), description: z.string().optional(), start_date: z.string().optional(), target_date: z.string().optional() },
|
|
2055
2134
|
handler: async (caller, args) => {
|
|
2056
|
-
const
|
|
2057
|
-
const
|
|
2058
|
-
await receipt(caller, 'epic',
|
|
2059
|
-
return
|
|
2135
|
+
const text = productResult(await createEpicHandler(caller.client, args));
|
|
2136
|
+
const row = JSON.parse(text).epic;
|
|
2137
|
+
await receipt(caller, 'epic', row.id, row.name);
|
|
2138
|
+
return text;
|
|
2060
2139
|
},
|
|
2061
2140
|
},
|
|
2062
2141
|
{
|
|
2063
|
-
name: 'create_sprint',
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
input: {
|
|
2067
|
-
project_id: z.string(),
|
|
2068
|
-
name: z.string().min(1),
|
|
2069
|
-
start_date: z.string().optional().describe('YYYY-MM-DD'),
|
|
2070
|
-
end_date: z.string().optional().describe('YYYY-MM-DD'),
|
|
2071
|
-
},
|
|
2142
|
+
name: 'create_sprint', levels: [1, 2],
|
|
2143
|
+
description: 'Create an sprint with optional description and dates. Read list_structure first to avoid duplicates.',
|
|
2144
|
+
input: { project_id: z.string().uuid(), name: z.string().min(1), description: z.string().optional(), start_date: z.string().optional(), end_date: z.string().optional() },
|
|
2072
2145
|
handler: async (caller, args) => {
|
|
2073
|
-
const
|
|
2074
|
-
const
|
|
2075
|
-
await receipt(caller, 'sprint',
|
|
2076
|
-
return
|
|
2146
|
+
const text = productResult(await createSprintHandler(caller.client, args));
|
|
2147
|
+
const row = JSON.parse(text).sprint;
|
|
2148
|
+
await receipt(caller, 'sprint', row.id, row.name);
|
|
2149
|
+
return text;
|
|
2077
2150
|
},
|
|
2078
2151
|
},
|
|
2152
|
+
{
|
|
2153
|
+
name: 'list_structure', levels: ALL,
|
|
2154
|
+
description: 'Read project epics and sprints with their descriptions and dates.',
|
|
2155
|
+
input: { project_id: z.string().uuid() },
|
|
2156
|
+
handler: async (caller, args) => productResult(await listStructureHandler(caller.client, { project_id: args.project_id })),
|
|
2157
|
+
},
|
|
2158
|
+
{
|
|
2159
|
+
name: 'update_structure', levels: [1, 2],
|
|
2160
|
+
description: 'Edit an epic or sprint name, description, dates, or archive state. Null clears a date. Epics use target_date; sprints use end_date.',
|
|
2161
|
+
input: { kind: z.enum(['epic', 'sprint']), id: z.string().uuid(), name: z.string().min(1).optional(), description: z.string().optional(), start_date: z.string().nullable().optional(), end_date: z.string().nullable().optional(), target_date: z.string().nullable().optional(), archived: z.boolean().optional() },
|
|
2162
|
+
handler: async (caller, args) => productResult(await updateStructureHandler(caller.client, args)),
|
|
2163
|
+
},
|
|
2164
|
+
{
|
|
2165
|
+
name: 'search_agent_cards', levels: ALL,
|
|
2166
|
+
description: 'Find your agent conversations by literal title or message text, or by work item. Includes archived cards unless specified. Follow next_offset for remaining results. Read-only.',
|
|
2167
|
+
input: { query: z.string().max(500).optional(), work_item_id: z.string().uuid().optional(), archived: z.boolean().optional(), limit: z.number().int().min(1).max(1000).optional(), offset: z.number().int().min(0).optional() },
|
|
2168
|
+
handler: async (caller, args) => productResult(await searchAgentCardsHandler(caller.client, args)),
|
|
2169
|
+
},
|
|
2170
|
+
{
|
|
2171
|
+
name: 'list_structure_artifacts', levels: ALL, description: 'Read active artifacts attached directly to an epic or sprint.',
|
|
2172
|
+
input: { kind: z.enum(['epic', 'sprint']), structure_id: z.string().uuid() },
|
|
2173
|
+
handler: async (caller, args) => productResult(await listStructureArtifactsHandler(caller.client, args.kind, args.structure_id)),
|
|
2174
|
+
},
|
|
2175
|
+
{
|
|
2176
|
+
name: 'get_structure_artifact', levels: ALL, description: 'Read a full epic or sprint artifact and its current revision.',
|
|
2177
|
+
input: { artifact_id: z.string().uuid() },
|
|
2178
|
+
handler: async (caller, args) => productResult(await getStructureArtifactHandler(caller.client, args.artifact_id)),
|
|
2179
|
+
},
|
|
2180
|
+
{
|
|
2181
|
+
name: 'create_structure_artifact', levels: ALL, description: 'Create a titled artifact on an epic or sprint. It appears on its management page.',
|
|
2182
|
+
input: { kind: z.enum(['epic', 'sprint']), structure_id: z.string().uuid(), title: z.string().min(1).max(200), type: z.enum(['analysis', 'plan', 'spec', 'user_story', 'diagram', 'mock', 'wireframe']), format: z.enum(['md', 'html', 'json', 'svg']).optional(), content: z.string().min(1) },
|
|
2183
|
+
handler: async (caller, args) => { const text = productResult(await createStructureArtifactHandler(caller.client, caller.userId, args)); const result = JSON.parse(text); await receipt(caller, result.kind, result.structure.id, result.structure.name); return text; },
|
|
2184
|
+
},
|
|
2185
|
+
{
|
|
2186
|
+
name: 'update_structure_artifact', levels: ALL, description: 'Update an epic or sprint artifact using the revision you read. Concurrent edits refuse without overwriting. archived=true removes it; false restores it.',
|
|
2187
|
+
input: { artifact_id: z.string().uuid(), expected_revision: z.number().int().positive(), title: z.string().min(1).max(200).optional(), type: z.enum(['analysis', 'plan', 'spec', 'user_story', 'diagram', 'mock', 'wireframe']).optional(), format: z.enum(['md', 'html', 'json', 'svg']).optional(), content: z.string().optional(), archived: z.boolean().optional() },
|
|
2188
|
+
handler: async (caller, args) => productResult(await updateStructureArtifactHandler(caller.client, args)),
|
|
2189
|
+
},
|
|
2079
2190
|
{
|
|
2080
2191
|
name: 'create_work_item',
|
|
2081
2192
|
levels: [1],
|
|
@@ -2110,6 +2221,20 @@ const TOOLS = [
|
|
|
2110
2221
|
return `Created work item ${item.name}, id ${item.id}.`;
|
|
2111
2222
|
},
|
|
2112
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
|
+
},
|
|
2113
2238
|
// ── Do the work, and produce objects ─────────────────────────────────────
|
|
2114
2239
|
//
|
|
2115
2240
|
// ═══ EVERY LEVEL, AND THAT IS ux.md's FOURTH RULE. ═══ "Producing an object
|
|
@@ -2178,6 +2303,7 @@ const TOOLS = [
|
|
|
2178
2303
|
+ FIREWALL_WRITING_RULE,
|
|
2179
2304
|
input: {
|
|
2180
2305
|
work_item_id: z.string(),
|
|
2306
|
+
folder_name: z.string().max(80).nullable().optional().describe('Read list_artifact_folders first to reuse a folder.'),
|
|
2181
2307
|
title: z.string().min(1),
|
|
2182
2308
|
content: z.string().min(1),
|
|
2183
2309
|
type: z.enum(['plan', 'spec', 'analysis', 'diagram', 'mock', 'wireframe', 'user_story']).optional(),
|
|
@@ -2187,6 +2313,7 @@ const TOOLS = [
|
|
|
2187
2313
|
const a = args;
|
|
2188
2314
|
const artifact = await only(caller.client.from('artifacts').insert({
|
|
2189
2315
|
task_id: a.work_item_id,
|
|
2316
|
+
folder_name: a.folder_name ?? null,
|
|
2190
2317
|
title: a.title,
|
|
2191
2318
|
content: a.content,
|
|
2192
2319
|
type: a.type ?? 'plan',
|
|
@@ -2697,7 +2824,7 @@ const TOOLS = [
|
|
|
2697
2824
|
await whileRunning(client.rpc('panel3_request_completion', {
|
|
2698
2825
|
p_run_id: runId, p_process_token: processToken, p_summary: report,
|
|
2699
2826
|
}), runId, 'declare completion of');
|
|
2700
|
-
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.';
|
|
2701
2828
|
}
|
|
2702
2829
|
await whileRunning(processToken === undefined
|
|
2703
2830
|
? client.from('panel3_runs').update({ report }).eq('id', runId)
|
|
@@ -2863,6 +2990,13 @@ const TOOLS = [
|
|
|
2863
2990
|
},
|
|
2864
2991
|
},
|
|
2865
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
|
+
},
|
|
2866
3000
|
{
|
|
2867
3001
|
name: 'dispatch',
|
|
2868
3002
|
/* ═══ ABSENT AT LEVEL 3, WHICH IS HOW "DEPTH STOPS AT THREE" IS ENFORCED.
|
|
@@ -2896,7 +3030,7 @@ const TOOLS = [
|
|
|
2896
3030
|
+ 'context: what to find out or change, and in which part of the codebase.'),
|
|
2897
3031
|
boundary: z.string().min(1).describe('What it must not touch, and where its work stops.'),
|
|
2898
3032
|
work_item_id: z.string().optional().describe('The work item it is working, if there is one.'),
|
|
2899
|
-
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.'),
|
|
2900
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.'),
|
|
2901
3035
|
work_name: z.string().optional().describe(`A few words, ${WORK_NAME_WORDS} at most, naming the WORK this conversation is doing, such `
|
|
2902
3036
|
+ 'as "Fix the sign-out checklist bug". Pass it when the conversation has NO work item '
|
|
@@ -2926,6 +3060,9 @@ const TOOLS = [
|
|
|
2926
3060
|
+ 'the work, do not describe it.');
|
|
2927
3061
|
}
|
|
2928
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);
|
|
2929
3066
|
let codebase = null;
|
|
2930
3067
|
if (codebase_id) {
|
|
2931
3068
|
const projectId = await projectOfCard(caller);
|
|
@@ -2937,6 +3074,19 @@ const TOOLS = [
|
|
|
2937
3074
|
if (!codebase) {
|
|
2938
3075
|
throw new Error('That codebase is not registered on this project. Read the current project codebases and choose one of them.');
|
|
2939
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
|
+
}
|
|
2940
3090
|
}
|
|
2941
3091
|
/* ONE LEVEL DOWN, AND THE SAME ARITHMETIC THE DATABASE DOES. This decides
|
|
2942
3092
|
the words in the brief; `panel3_dispatch` decides the level on the row,
|
|
@@ -2951,7 +3101,6 @@ const TOOLS = [
|
|
|
2951
3101
|
three hops from level 1 still knows which item it is working, without
|
|
2952
3102
|
asking. See `workBrief`'s own doc for why the two are different things
|
|
2953
3103
|
carried the same way. */
|
|
2954
|
-
const attached = await loadAttachments(caller.client, caller.cardId);
|
|
2955
3104
|
/* ═══ THE CONVERSATION IS NAMED HERE OR IT IS NEVER NAMED. ═══ Sending
|
|
2956
3105
|
somebody resolves the working copy, and resolving it cuts the branch
|
|
2957
3106
|
from whatever the conversation is called at that moment and stamps it
|
|
@@ -2966,7 +3115,7 @@ const TOOLS = [
|
|
|
2966
3115
|
.select('id'), 'name', `card ${caller.cardId}`);
|
|
2967
3116
|
}
|
|
2968
3117
|
const attachments = attached.map(attachmentLine);
|
|
2969
|
-
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 : {
|
|
2970
3119
|
id: codebase.id, name: codebase.name, identity: codebase.gitRemoteUrl,
|
|
2971
3120
|
}), codebase, caller.processToken, { model, effort });
|
|
2972
3121
|
/* ═══ WHERE ITS ANSWER GOES DEPENDS ON WHICH LEVEL THIS IS, AND THAT IS
|