@ctrl-spc/cs 0.7.10 → 0.7.11

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.
@@ -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
  *
@@ -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.',
@@ -964,7 +996,7 @@ const TOOLS = [
964
996
  .from('tasks')
965
997
  .select('id, name, description, status, due_date, project_id, created_at, epics(name), sprints(name)')
966
998
  .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)
999
+ const artifacts = await rows(client.from('artifacts').select('id, type, title, created_at, folder_name').eq('task_id', work_item_id)
968
1000
  .is('deleted_at', null).order('created_at'), 'read', `the artifacts on work item ${work_item_id}`);
969
1001
  const feedback = await rows(client.from('artifact_feedback').select('*').eq('task_id', work_item_id).order('created_at'), 'read', 'the artifact feedback');
970
1002
  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');
@@ -983,7 +1015,7 @@ const TOOLS = [
983
1015
  '',
984
1016
  'DECISIONS', JSON.stringify(decisions), 'FEEDBACK', JSON.stringify(feedback), 'COMMENTS', JSON.stringify(comments),
985
1017
  `ARTIFACTS ${artifacts.length}`,
986
- listed(artifacts.map((a) => line(a.id, a.type, a.title ?? '(untitled)')), 'none'),
1018
+ listed(artifacts.map((a) => line(a.id, a.type, a.title ?? '(untitled)', `folder: ${a.folder_name ?? 'Unfiled'}`)), 'none'),
987
1019
  ].join('\n');
988
1020
  },
989
1021
  },
@@ -1681,9 +1713,9 @@ const TOOLS = [
1681
1713
  input: { work_item_id: z.string() },
1682
1714
  handler: async ({ client }, args) => {
1683
1715
  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)
1716
+ const artifacts = await rows(client.from('artifacts').select('id, type, format, title, created_at, folder_name').eq('task_id', work_item_id)
1685
1717
  .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.');
1718
+ 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
1719
  },
1688
1720
  },
1689
1721
  {
@@ -1694,7 +1726,7 @@ const TOOLS = [
1694
1726
  handler: async ({ client }, args) => {
1695
1727
  const { artifact_id } = args;
1696
1728
  const artifact = await only(client.from('artifacts')
1697
- .select('id, task_id, type, format, title, content, storage_path, revision, created_at')
1729
+ .select('id, task_id, type, format, title, content, storage_path, revision, created_at, folder_name')
1698
1730
  .eq('id', artifact_id), 'read', `artifact ${artifact_id}`);
1699
1731
  return [
1700
1732
  `${artifact.title ?? '(untitled)'}`,
@@ -1702,6 +1734,7 @@ const TOOLS = [
1702
1734
  `work item ${artifact.task_id}`,
1703
1735
  `type ${artifact.type} (${artifact.format})`,
1704
1736
  `revision ${artifact.revision}`,
1737
+ `folder ${artifact.folder_name ?? 'Unfiled'}`,
1705
1738
  '',
1706
1739
  /* A stored file and an empty body are different facts and are said
1707
1740
  differently. Returning '' for a PNG would read as an artifact with
@@ -2048,34 +2081,65 @@ const TOOLS = [
2048
2081
  },
2049
2082
  },
2050
2083
  {
2051
- name: 'create_epic',
2052
- levels: [1],
2053
- description: 'Create an epic in a project. An epic groups work items that belong to one body of work.',
2054
- input: { project_id: z.string(), name: z.string().min(1) },
2084
+ name: 'create_epic', levels: [1, 2],
2085
+ description: 'Create an epic with optional description and dates. Read list_structure first to avoid duplicates.',
2086
+ 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
2087
  handler: async (caller, args) => {
2056
- const { project_id, name } = args;
2057
- const epic = await only(caller.client.from('epics').insert({ project_id, name }).select('id, name'), 'create', `an epic called ${name}`);
2058
- await receipt(caller, 'epic', epic.id, epic.name);
2059
- return `Created epic ${epic.name}, id ${epic.id}.`;
2088
+ const text = productResult(await createEpicHandler(caller.client, args));
2089
+ const row = JSON.parse(text).epic;
2090
+ await receipt(caller, 'epic', row.id, row.name);
2091
+ return text;
2060
2092
  },
2061
2093
  },
2062
2094
  {
2063
- name: 'create_sprint',
2064
- levels: [1],
2065
- description: 'Create an ordered batch of work in a project. Dates are optional; use them only for a stated calendar constraint.',
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
- },
2095
+ name: 'create_sprint', levels: [1, 2],
2096
+ description: 'Create an sprint with optional description and dates. Read list_structure first to avoid duplicates.',
2097
+ 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
2098
  handler: async (caller, args) => {
2073
- const { project_id, name, start_date, end_date } = args;
2074
- const sprint = await only(caller.client.from('sprints').insert({ project_id, name, start_date, end_date }).select('id, name'), 'create', `a sprint called ${name}`);
2075
- await receipt(caller, 'sprint', sprint.id, sprint.name);
2076
- return `Created sprint ${sprint.name}, id ${sprint.id}.`;
2099
+ const text = productResult(await createSprintHandler(caller.client, args));
2100
+ const row = JSON.parse(text).sprint;
2101
+ await receipt(caller, 'sprint', row.id, row.name);
2102
+ return text;
2077
2103
  },
2078
2104
  },
2105
+ {
2106
+ name: 'list_structure', levels: ALL,
2107
+ description: 'Read project epics and sprints with their descriptions and dates.',
2108
+ input: { project_id: z.string().uuid() },
2109
+ handler: async (caller, args) => productResult(await listStructureHandler(caller.client, { project_id: args.project_id })),
2110
+ },
2111
+ {
2112
+ name: 'update_structure', levels: [1, 2],
2113
+ description: 'Edit an epic or sprint name, description, dates, or archive state. Null clears a date. Epics use target_date; sprints use end_date.',
2114
+ 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() },
2115
+ handler: async (caller, args) => productResult(await updateStructureHandler(caller.client, args)),
2116
+ },
2117
+ {
2118
+ name: 'search_agent_cards', levels: ALL,
2119
+ 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.',
2120
+ 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() },
2121
+ handler: async (caller, args) => productResult(await searchAgentCardsHandler(caller.client, args)),
2122
+ },
2123
+ {
2124
+ name: 'list_structure_artifacts', levels: ALL, description: 'Read active artifacts attached directly to an epic or sprint.',
2125
+ input: { kind: z.enum(['epic', 'sprint']), structure_id: z.string().uuid() },
2126
+ handler: async (caller, args) => productResult(await listStructureArtifactsHandler(caller.client, args.kind, args.structure_id)),
2127
+ },
2128
+ {
2129
+ name: 'get_structure_artifact', levels: ALL, description: 'Read a full epic or sprint artifact and its current revision.',
2130
+ input: { artifact_id: z.string().uuid() },
2131
+ handler: async (caller, args) => productResult(await getStructureArtifactHandler(caller.client, args.artifact_id)),
2132
+ },
2133
+ {
2134
+ name: 'create_structure_artifact', levels: ALL, description: 'Create a titled artifact on an epic or sprint. It appears on its management page.',
2135
+ 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) },
2136
+ 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; },
2137
+ },
2138
+ {
2139
+ 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.',
2140
+ 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() },
2141
+ handler: async (caller, args) => productResult(await updateStructureArtifactHandler(caller.client, args)),
2142
+ },
2079
2143
  {
2080
2144
  name: 'create_work_item',
2081
2145
  levels: [1],
@@ -2178,6 +2242,7 @@ const TOOLS = [
2178
2242
  + FIREWALL_WRITING_RULE,
2179
2243
  input: {
2180
2244
  work_item_id: z.string(),
2245
+ folder_name: z.string().max(80).nullable().optional().describe('Read list_artifact_folders first to reuse a folder.'),
2181
2246
  title: z.string().min(1),
2182
2247
  content: z.string().min(1),
2183
2248
  type: z.enum(['plan', 'spec', 'analysis', 'diagram', 'mock', 'wireframe', 'user_story']).optional(),
@@ -2187,6 +2252,7 @@ const TOOLS = [
2187
2252
  const a = args;
2188
2253
  const artifact = await only(caller.client.from('artifacts').insert({
2189
2254
  task_id: a.work_item_id,
2255
+ folder_name: a.folder_name ?? null,
2190
2256
  title: a.title,
2191
2257
  content: a.content,
2192
2258
  type: a.type ?? 'plan',