@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/codebases.js
CHANGED
|
@@ -101,6 +101,32 @@ export async function removeCodebase(client, codebaseId) {
|
|
|
101
101
|
if (error)
|
|
102
102
|
throw new Error(error.message);
|
|
103
103
|
}
|
|
104
|
+
/** Saved work-item scope is shared by the reader and the dispatch guard. */
|
|
105
|
+
export async function listWorkItemCodebaseTargets(client, workItemId) {
|
|
106
|
+
const { data, error } = await client.from('cliv2_task_codebase_targets')
|
|
107
|
+
.select('git_remote_url').eq('task_id', workItemId).order('git_remote_url');
|
|
108
|
+
if (error)
|
|
109
|
+
throw new Error(`Could not read the saved work-item codebase targets: ${error.message}`);
|
|
110
|
+
return (data ?? []).map(row => row.git_remote_url);
|
|
111
|
+
}
|
|
112
|
+
/** The existing database transaction owns membership and final-target guards. */
|
|
113
|
+
export async function editWorkItemCodebaseTarget(client, workItemId, projectId, codebaseId, add) {
|
|
114
|
+
const codebases = await client.from('cliv2_codebases').select('git_remote_url')
|
|
115
|
+
.eq('id', codebaseId).eq('project_id', projectId);
|
|
116
|
+
if (codebases.error)
|
|
117
|
+
throw new Error(`Could not read the project codebase: ${codebases.error.message}`);
|
|
118
|
+
if (codebases.data?.length !== 1)
|
|
119
|
+
throw new Error("Could not read a codebase belonging to this work item's project.");
|
|
120
|
+
const { data, error } = await client.rpc('cliv2_edit_task_codebase_target', {
|
|
121
|
+
p_task_id: workItemId, p_git_remote_url: codebases.data[0].git_remote_url, p_add: add,
|
|
122
|
+
});
|
|
123
|
+
if (error)
|
|
124
|
+
throw new Error(`Could not edit the saved work-item codebase targets: ${error.message}`);
|
|
125
|
+
if (!Array.isArray(data) || !data.every(target => typeof target === 'string')) {
|
|
126
|
+
throw new Error('The saved work-item codebase targets could not be read back after editing.');
|
|
127
|
+
}
|
|
128
|
+
return data;
|
|
129
|
+
}
|
|
104
130
|
/**
|
|
105
131
|
* Record that THIS machine has a codebase's folder checked out — Phase 2
|
|
106
132
|
* per-machine availability. Upserts one path-free row into the owner-scoped
|
package/dist/mcp.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { listStructureArtifactsHandler, getStructureArtifactHandler, searchAgentCardsHandler, createStructureArtifactHandler, updateStructureArtifactHandler } from './product-tools.js';
|
|
2
|
+
import { createEpicHandler, createSprintHandler, listStructureHandler, updateStructureHandler } from './product-tools.js';
|
|
3
|
+
export { createEpicHandler, createSprintHandler, listStructureHandler, updateStructureHandler } from './product-tools.js';
|
|
4
|
+
import { listArtifactFoldersHandler, setArtifactFolderHandler, workItemDependencyHandler } from './product-tools.js';
|
|
1
5
|
import { presentDesign } from './design-review.js';
|
|
2
6
|
import { must, errorMessage, textResult, errorResult, canonicalArgsHash, TASK_PLACEMENT, placeWorkItemHandler, UUID_RE, resolveFeedbackHandler, PROJECT_DOCUMENT_TYPES, countCharacters, resolveContextProject, resolveContextCodebase, getDocumentHandler, PROPOSAL_SCOPES, MAX_PROPOSALS, MAX_PROPOSAL_BATCH_CHARS, proposeProjectContextHandler } from './product-tools.js';
|
|
3
7
|
export { canonicalArgsHash, placeWorkItemHandler, resolveFeedbackHandler, getDocumentHandler, proposeProjectContextHandler } from './product-tools.js';
|
|
@@ -248,7 +252,7 @@ function validateGrounding(value) {
|
|
|
248
252
|
* reused verbatim so get_task and create_artifact hand back the same shape, plus
|
|
249
253
|
* `revision` so the agent has the optimistic-concurrency token update_artifact
|
|
250
254
|
* needs for `expected_revision` (mirrors how get_task returns tasks.revision). */
|
|
251
|
-
const ARTIFACT_COLUMNS = 'id,task_id,type,format,title,purpose_key,system_kind,content,storage_path,created_by,from_agent,created_at,revision';
|
|
255
|
+
const ARTIFACT_COLUMNS = 'id,task_id,type,format,title,purpose_key,system_kind,content,storage_path,created_by,from_agent,created_at,revision,folder_name';
|
|
252
256
|
/** Columns a decision row exposes to the agent (D3) — reused verbatim so get_task
|
|
253
257
|
* and ask_question hand back the same shape. RLS `decisions_select` grants the
|
|
254
258
|
* signed-in user read via `app.can_access_task`. */
|
|
@@ -835,6 +839,7 @@ connectionTodoId = null) {
|
|
|
835
839
|
.from('artifacts')
|
|
836
840
|
.insert({
|
|
837
841
|
task_id: args.task_id,
|
|
842
|
+
folder_name: args.folder_name ?? null,
|
|
838
843
|
type: args.type,
|
|
839
844
|
format: args.format ?? 'md',
|
|
840
845
|
...(args.title?.trim()
|
|
@@ -2105,357 +2110,6 @@ connectionTodoId = null) {
|
|
|
2105
2110
|
return errorResult(`create_task failed: ${err.message}`);
|
|
2106
2111
|
}
|
|
2107
2112
|
}
|
|
2108
|
-
/* ---------------------------------------------------------------------------
|
|
2109
|
-
* Agile skeleton (feature 32, Slice 1) — `create_epic` / `create_sprint`,
|
|
2110
|
-
* beside the grown `create_task` above.
|
|
2111
|
-
*
|
|
2112
|
-
* The agent lays out the WHOLE skeleton from one feature ask: an epic (what
|
|
2113
|
-
* for), work items under it, sprints (ordered batches). Step −1 rulings
|
|
2114
|
-
* (ux.md, 2026-07-30): DIRECT WRITE as the user under RLS — a skeleton of
|
|
2115
|
-
* backlog items is un-started work, closer to an idea than an edit, so no
|
|
2116
|
-
* propose queue; SMALL ORTHOGONAL TOOLS, not a composite draft pass;
|
|
2117
|
-
* IDEMPOTENCE = read-before-write plus a duplicate-name refusal that teaches
|
|
2118
|
-
* extending the existing structure instead of minting a parallel one.
|
|
2119
|
-
*
|
|
2120
|
-
* THE CALIBRATION RULE LIVES IN THE DESCRIPTIONS (teach-in-the-description
|
|
2121
|
-
* doctrine): agents dramatically overestimate how long building takes because
|
|
2122
|
-
* their training is saturated with human engineering timelines. The
|
|
2123
|
-
* descriptions state the bias outright, size sprints as ordered batches of
|
|
2124
|
-
* agent-executable work, and forbid duration estimates on items unless the
|
|
2125
|
-
* user asked. Nothing here stores an estimate — there is no column for one,
|
|
2126
|
-
* deliberately.
|
|
2127
|
-
*
|
|
2128
|
-
* GRANTS, verified against the seeded rows rather than assumed: members hold
|
|
2129
|
-
* ('member','epic','create') (20260727080000 §4) and ('member','sprint',
|
|
2130
|
-
* 'create') (20260708000000 init.sql §9), and no later migration revokes
|
|
2131
|
-
* either — so the epics_insert / sprints_insert RLS policies already admit
|
|
2132
|
-
* every org member and NO new grants migration is needed. A denial for some
|
|
2133
|
-
* future role surfaces through the normal error path.
|
|
2134
|
-
*
|
|
2135
|
-
* NO cliv2_agent_outputs attribution row is written for an epic or a sprint:
|
|
2136
|
-
* the kind check is ('artifact','comment','task','decision') and widening it
|
|
2137
|
-
* is a migration this slice does not need — the skeleton's provenance is
|
|
2138
|
-
* legible through its work items, which create_task already attributes.
|
|
2139
|
-
* ------------------------------------------------------------------------- */
|
|
2140
|
-
/** Date args are refused by shape HERE (the resolveClient idiom) so a
|
|
2141
|
-
* malformed date gets a clean tool error instead of a Postgres cast failure
|
|
2142
|
-
* after other work happened. */
|
|
2143
|
-
const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
2144
|
-
/** The project a skeleton tool writes into, resolved by id: UUID-shape-checked
|
|
2145
|
-
* before the read, and a miss (no such project, or one outside the caller's
|
|
2146
|
-
* orgs — RLS makes those indistinguishable on purpose) refused by id. */
|
|
2147
|
-
async function resolveSkeletonProject(client, projectIdArg, tool) {
|
|
2148
|
-
const projectId = typeof projectIdArg === 'string' ? projectIdArg.trim() : '';
|
|
2149
|
-
if (!projectId || !UUID_RE.test(projectId)) {
|
|
2150
|
-
return {
|
|
2151
|
-
ok: false,
|
|
2152
|
-
error: errorResult(`${tool}: "${projectId}" is not a project id. Call list_tasks to see your projects and their ids. ` +
|
|
2153
|
-
'Nothing was created.'),
|
|
2154
|
-
};
|
|
2155
|
-
}
|
|
2156
|
-
const row = await must(client.from('projects').select('id, name').eq('id', projectId).maybeSingle());
|
|
2157
|
-
if (!row) {
|
|
2158
|
-
return {
|
|
2159
|
-
ok: false,
|
|
2160
|
-
error: errorResult(`${tool}: no project found for id "${projectId}". Nothing was created.`),
|
|
2161
|
-
};
|
|
2162
|
-
}
|
|
2163
|
-
return { ok: true, value: row };
|
|
2164
|
-
}
|
|
2165
|
-
/** Refuse a malformed date arg by name; `undefined` passes (the arg is
|
|
2166
|
-
* optional everywhere it appears). */
|
|
2167
|
-
function validDateArg(tool, field, value) {
|
|
2168
|
-
if (value === undefined)
|
|
2169
|
-
return null;
|
|
2170
|
-
if (!ISO_DATE_RE.test(value)) {
|
|
2171
|
-
return errorResult(`${tool}: ${field} must be an ISO date (YYYY-MM-DD), got "${value}". Nothing was created.`);
|
|
2172
|
-
}
|
|
2173
|
-
return null;
|
|
2174
|
-
}
|
|
2175
|
-
/**
|
|
2176
|
-
* Create an Epic in a project — writes public.epics as the user (RLS
|
|
2177
|
-
* epics_insert, the 'epic'/'create' grant every member holds).
|
|
2178
|
-
*
|
|
2179
|
-
* THE DUPLICATE-NAME REFUSAL IS THE IDEMPOTENCE MECHANISM (Step −1 ruling 4):
|
|
2180
|
-
* a re-run, or a "feature ABC v2" ask, must EXTEND the existing epic, not
|
|
2181
|
-
* mint a second one — 24c's updating-beats-creating rule applied to
|
|
2182
|
-
* structure. The match is case-insensitive on the trimmed name, against LIVE
|
|
2183
|
-
* epics only: an archived epic has left the picker and can take no new work,
|
|
2184
|
-
* so a fresh epic re-using its name is a legitimate new start, not a
|
|
2185
|
-
* duplicate. The refusal lists the project's live epics so it doubles as the
|
|
2186
|
-
* read surface the skeleton pass otherwise lacks (no epic-listing tool ships
|
|
2187
|
-
* in this slice).
|
|
2188
|
-
*/
|
|
2189
|
-
export async function createEpicHandler(client, args) {
|
|
2190
|
-
try {
|
|
2191
|
-
const project = await resolveSkeletonProject(client, args.project_id, 'create_epic');
|
|
2192
|
-
if (!project.ok)
|
|
2193
|
-
return project.error;
|
|
2194
|
-
const name = typeof args.name === 'string' ? args.name.trim() : '';
|
|
2195
|
-
if (!name)
|
|
2196
|
-
return errorResult('create_epic requires a non-empty name. Nothing was created.');
|
|
2197
|
-
for (const [field, value] of [
|
|
2198
|
-
['start_date', args.start_date],
|
|
2199
|
-
['target_date', args.target_date],
|
|
2200
|
-
]) {
|
|
2201
|
-
const refused = validDateArg('create_epic', field, value);
|
|
2202
|
-
if (refused)
|
|
2203
|
-
return refused;
|
|
2204
|
-
}
|
|
2205
|
-
// Mirrors the DB's epics_target_not_before_start check, refused where the
|
|
2206
|
-
// agent can fix it instead of as a constraint violation.
|
|
2207
|
-
if (args.start_date && args.target_date && args.target_date < args.start_date) {
|
|
2208
|
-
return errorResult(`create_epic: target_date (${args.target_date}) is before start_date (${args.start_date}). ` +
|
|
2209
|
-
'Nothing was created.');
|
|
2210
|
-
}
|
|
2211
|
-
const epics = (await must(client.from('epics').select('id, name, archived_at').eq('project_id', project.value.id))) ?? [];
|
|
2212
|
-
const live = epics.filter((epic) => epic.archived_at === null);
|
|
2213
|
-
const duplicate = live.find((epic) => epic.name.trim().toLowerCase() === name.toLowerCase());
|
|
2214
|
-
if (duplicate) {
|
|
2215
|
-
const listing = live.map((epic) => `"${epic.name}" (${epic.id})`).join(', ');
|
|
2216
|
-
return errorResult(`create_epic: an epic named "${duplicate.name}" already exists in "${project.value.name}" ` +
|
|
2217
|
-
`(id ${duplicate.id}). Do not create a duplicate — EXTEND the existing epic: create the new ` +
|
|
2218
|
-
`work items with epic_id ${duplicate.id}, and reshape what already hangs under it. A re-run ` +
|
|
2219
|
-
'or a "v2" ask extends the existing skeleton; it never mints a second one. Live epics in ' +
|
|
2220
|
-
`this project: ${listing}. Nothing was created.`);
|
|
2221
|
-
}
|
|
2222
|
-
const epic = await must(client
|
|
2223
|
-
.from('epics')
|
|
2224
|
-
.insert({
|
|
2225
|
-
project_id: project.value.id,
|
|
2226
|
-
name,
|
|
2227
|
-
...(args.description?.trim() ? { description: args.description.trim() } : {}),
|
|
2228
|
-
...(args.start_date ? { start_date: args.start_date } : {}),
|
|
2229
|
-
...(args.target_date ? { target_date: args.target_date } : {}),
|
|
2230
|
-
})
|
|
2231
|
-
.select('id, project_id, name, description, start_date, target_date, created_at')
|
|
2232
|
-
.single());
|
|
2233
|
-
if (!epic)
|
|
2234
|
-
throw new Error('Epic insert returned no row.');
|
|
2235
|
-
return textResult({
|
|
2236
|
-
epic,
|
|
2237
|
-
note: 'Created as an empty epic — no work has started and none is declared. Place work items under ' +
|
|
2238
|
-
"it with create_task's epic_id, sequence them with create_sprint + sprint_id, and add NO " +
|
|
2239
|
-
'duration estimates to anything unless the user asked: sequencing is the value.',
|
|
2240
|
-
});
|
|
2241
|
-
}
|
|
2242
|
-
catch (err) {
|
|
2243
|
-
return errorResult(`create_epic failed: ${errorMessage(err)}`);
|
|
2244
|
-
}
|
|
2245
|
-
}
|
|
2246
|
-
/**
|
|
2247
|
-
* Create a Sprint in a project — writes public.sprints as the user (RLS
|
|
2248
|
-
* sprints_insert, the 'sprint'/'create' grant every member holds). Dates are
|
|
2249
|
-
* optional (28 made the columns nullable): for agent-executed work a sprint is
|
|
2250
|
-
* an ORDERED BATCH, not a time-box, and an invented fortnight is exactly the
|
|
2251
|
-
* human-timeline bias this feature exists to counter.
|
|
2252
|
-
*
|
|
2253
|
-
* Same duplicate-name posture as create_epic, for the same reason: a re-run
|
|
2254
|
-
* must extend the existing sprint sequence, never mint a parallel one.
|
|
2255
|
-
*/
|
|
2256
|
-
export async function createSprintHandler(client, args) {
|
|
2257
|
-
try {
|
|
2258
|
-
const project = await resolveSkeletonProject(client, args.project_id, 'create_sprint');
|
|
2259
|
-
if (!project.ok)
|
|
2260
|
-
return project.error;
|
|
2261
|
-
const name = typeof args.name === 'string' ? args.name.trim() : '';
|
|
2262
|
-
if (!name)
|
|
2263
|
-
return errorResult('create_sprint requires a non-empty name. Nothing was created.');
|
|
2264
|
-
for (const [field, value] of [
|
|
2265
|
-
['start_date', args.start_date],
|
|
2266
|
-
['end_date', args.end_date],
|
|
2267
|
-
]) {
|
|
2268
|
-
const refused = validDateArg('create_sprint', field, value);
|
|
2269
|
-
if (refused)
|
|
2270
|
-
return refused;
|
|
2271
|
-
}
|
|
2272
|
-
// Mirrors the sprints table's own start <= end check (init.sql), refused
|
|
2273
|
-
// where the agent can fix it.
|
|
2274
|
-
if (args.start_date && args.end_date && args.end_date < args.start_date) {
|
|
2275
|
-
return errorResult(`create_sprint: end_date (${args.end_date}) is before start_date (${args.start_date}). ` +
|
|
2276
|
-
'Nothing was created.');
|
|
2277
|
-
}
|
|
2278
|
-
const sprints = (await must(client.from('sprints').select('id, name, archived_at').eq('project_id', project.value.id))) ?? [];
|
|
2279
|
-
const live = sprints.filter((sprint) => sprint.archived_at === null);
|
|
2280
|
-
const duplicate = live.find((sprint) => sprint.name.trim().toLowerCase() === name.toLowerCase());
|
|
2281
|
-
if (duplicate) {
|
|
2282
|
-
const listing = live.map((sprint) => `"${sprint.name}" (${sprint.id})`).join(', ');
|
|
2283
|
-
return errorResult(`create_sprint: a sprint named "${duplicate.name}" already exists in "${project.value.name}" ` +
|
|
2284
|
-
`(id ${duplicate.id}). Reuse it — place items into it with create_task's sprint_id — or name ` +
|
|
2285
|
-
'the NEXT batch in the sequence; a re-run extends the existing sprint sequence, it never ' +
|
|
2286
|
-
`mints a parallel one. Live sprints in this project: ${listing}. Nothing was created.`);
|
|
2287
|
-
}
|
|
2288
|
-
const sprint = await must(client
|
|
2289
|
-
.from('sprints')
|
|
2290
|
-
.insert({
|
|
2291
|
-
project_id: project.value.id,
|
|
2292
|
-
name,
|
|
2293
|
-
...(args.start_date ? { start_date: args.start_date } : {}),
|
|
2294
|
-
...(args.end_date ? { end_date: args.end_date } : {}),
|
|
2295
|
-
})
|
|
2296
|
-
.select('id, project_id, name, start_date, end_date, created_at')
|
|
2297
|
-
.single());
|
|
2298
|
-
if (!sprint)
|
|
2299
|
-
throw new Error('Sprint insert returned no row.');
|
|
2300
|
-
return textResult({
|
|
2301
|
-
sprint,
|
|
2302
|
-
note: 'Created. Sprints for agent-executed work are ordered batches: the first holds what unblocks ' +
|
|
2303
|
-
"everything else, each sized to what agents deliver in a session or a day. Place items with " +
|
|
2304
|
-
"create_task's sprint_id, and add NO duration estimates unless the user asked.",
|
|
2305
|
-
});
|
|
2306
|
-
}
|
|
2307
|
-
catch (err) {
|
|
2308
|
-
return errorResult(`create_sprint failed: ${errorMessage(err)}`);
|
|
2309
|
-
}
|
|
2310
|
-
}
|
|
2311
|
-
const STRUCTURE_TABLE = { epic: 'epics', sprint: 'sprints' };
|
|
2312
|
-
/**
|
|
2313
|
-
* WHAT STRUCTURE THIS PROJECT HAS — the read that made "read before you write"
|
|
2314
|
-
* possible (I13).
|
|
2315
|
-
*
|
|
2316
|
-
* LIVE ONES BY DEFAULT, because that is what "what exists" means to an agent
|
|
2317
|
-
* about to add to it: an archived epic is not something to extend, and listing
|
|
2318
|
-
* it invites a duplicate-name refusal the agent cannot act on. `include_archived`
|
|
2319
|
-
* is deliberately NOT offered — nothing in this phase's Gherkin needs it, and
|
|
2320
|
-
* YAGNI beats a flag with no caller.
|
|
2321
|
-
*
|
|
2322
|
-
* BOTH KINDS IN ONE CALL. They are always wanted together, and two tools would
|
|
2323
|
-
* mean two round trips for the one question an agent actually has.
|
|
2324
|
-
*/
|
|
2325
|
-
export async function listStructureHandler(client, args) {
|
|
2326
|
-
try {
|
|
2327
|
-
const project = await resolveSkeletonProject(client, args.project_id, 'list_structure');
|
|
2328
|
-
if (!project.ok)
|
|
2329
|
-
return project.error;
|
|
2330
|
-
const [epics, sprints] = await Promise.all([
|
|
2331
|
-
must(client
|
|
2332
|
-
.from('epics')
|
|
2333
|
-
.select('id, name, description, start_date, target_date, created_at')
|
|
2334
|
-
.eq('project_id', project.value.id)
|
|
2335
|
-
.is('archived_at', null)
|
|
2336
|
-
.order('created_at', { ascending: true })),
|
|
2337
|
-
must(client
|
|
2338
|
-
.from('sprints')
|
|
2339
|
-
.select('id, name, start_date, end_date, created_at')
|
|
2340
|
-
.eq('project_id', project.value.id)
|
|
2341
|
-
.is('archived_at', null)
|
|
2342
|
-
.order('created_at', { ascending: true })),
|
|
2343
|
-
]);
|
|
2344
|
-
return textResult({
|
|
2345
|
-
project: project.value,
|
|
2346
|
-
epics: epics ?? [],
|
|
2347
|
-
sprints: sprints ?? [],
|
|
2348
|
-
note: 'Live epics and sprints only. Extend these rather than creating a near-duplicate — a ' +
|
|
2349
|
-
'case-insensitive name match is refused. Place a work item with place_work_item, and see ' +
|
|
2350
|
-
"where items already sit in list_tasks's epic and sprint fields.",
|
|
2351
|
-
});
|
|
2352
|
-
}
|
|
2353
|
-
catch (err) {
|
|
2354
|
-
return errorResult(`list_structure failed: ${errorMessage(err)}`);
|
|
2355
|
-
}
|
|
2356
|
-
}
|
|
2357
|
-
/**
|
|
2358
|
-
* RENAME OR ARCHIVE an epic or a sprint (I13).
|
|
2359
|
-
*
|
|
2360
|
-
* THE ARCHIVE RULE IS THE DATABASE'S, NOT THIS FUNCTION'S. A trigger refuses
|
|
2361
|
-
* archiving anything that still has unfinished work
|
|
2362
|
-
* (`app.guard_epic_archive` / `app.guard_sprint_archive`, SQLSTATE PT423). This
|
|
2363
|
-
* does not pre-check it — a client-side check would read a snapshot of a table
|
|
2364
|
-
* this write does not touch, so it can be stale, and duplicating the rule means
|
|
2365
|
-
* two places to keep in step. It translates the refusal instead.
|
|
2366
|
-
*
|
|
2367
|
-
* THE MACHINE TOKEN NEVER REACHES THE AGENT. The raise's message is
|
|
2368
|
-
* `epic_archive_blocked`, which the shipped web UI is explicit must never be
|
|
2369
|
-
* rendered; the SAME rule applies to a tool result, which is read by an agent
|
|
2370
|
-
* that will repeat it to the user. Keyed off SQLSTATE, exactly as
|
|
2371
|
-
* `archiveErrorMessage` in ProjectSettings.tsx does, and the sentence says what
|
|
2372
|
-
* would unblock it — the Gherkin asserts all three.
|
|
2373
|
-
*/
|
|
2374
|
-
export async function updateStructureHandler(client, args) {
|
|
2375
|
-
try {
|
|
2376
|
-
const kind = typeof args.kind === 'string' ? args.kind.trim().toLowerCase() : '';
|
|
2377
|
-
if (kind !== 'epic' && kind !== 'sprint') {
|
|
2378
|
-
return errorResult(`update_structure: kind must be "epic" or "sprint", got "${args.kind ?? ''}". Nothing was changed.`);
|
|
2379
|
-
}
|
|
2380
|
-
const table = STRUCTURE_TABLE[kind];
|
|
2381
|
-
const id = typeof args.id === 'string' ? args.id.trim() : '';
|
|
2382
|
-
if (!id || !UUID_RE.test(id)) {
|
|
2383
|
-
return errorResult(`update_structure: "${id}" is not a ${kind} id. Call list_structure to see them. Nothing was changed.`);
|
|
2384
|
-
}
|
|
2385
|
-
const name = typeof args.name === 'string' ? args.name.trim() : undefined;
|
|
2386
|
-
if (args.name !== undefined && !name) {
|
|
2387
|
-
return errorResult(`update_structure: name cannot be blank. Nothing was changed.`);
|
|
2388
|
-
}
|
|
2389
|
-
if (name === undefined && args.archived === undefined) {
|
|
2390
|
-
return errorResult('update_structure: pass a new name, archived, or both — there is nothing to change. ' +
|
|
2391
|
-
'Nothing was changed.');
|
|
2392
|
-
}
|
|
2393
|
-
const current = await must(client.from(table).select('id, project_id, name, archived_at').eq('id', id).maybeSingle());
|
|
2394
|
-
if (!current) {
|
|
2395
|
-
return errorResult(`update_structure: no ${kind} found for id "${id}". Nothing was changed.`);
|
|
2396
|
-
}
|
|
2397
|
-
/* THE SAME DUPLICATE-NAME RULE THE CREATE TOOLS ENFORCE. Without it, rename
|
|
2398
|
-
is a way around the refusal `create_epic` gives — two live epics with one
|
|
2399
|
-
name, reached by creating under a throwaway name and renaming. Scoped to
|
|
2400
|
-
LIVE rows in the same project, and it ignores the row being renamed so
|
|
2401
|
-
that setting a name to itself is not a conflict. */
|
|
2402
|
-
if (name !== undefined && name.toLowerCase() !== current.name.trim().toLowerCase()) {
|
|
2403
|
-
const siblings = (await must(client
|
|
2404
|
-
.from(table)
|
|
2405
|
-
.select('id, name')
|
|
2406
|
-
.eq('project_id', current.project_id)
|
|
2407
|
-
.is('archived_at', null))) ?? [];
|
|
2408
|
-
const clash = siblings.find((row) => row.id !== id && row.name.trim().toLowerCase() === name.toLowerCase());
|
|
2409
|
-
if (clash) {
|
|
2410
|
-
return errorResult(`update_structure: a live ${kind} named "${clash.name}" already exists in this project ` +
|
|
2411
|
-
`(id ${clash.id}). Use that one, or pick a different name. Nothing was changed.`);
|
|
2412
|
-
}
|
|
2413
|
-
}
|
|
2414
|
-
const patch = {};
|
|
2415
|
-
if (name !== undefined)
|
|
2416
|
-
patch.name = name;
|
|
2417
|
-
/* THE DATABASE OWNS THE TIMESTAMP. The guard trigger coalesces whatever is
|
|
2418
|
-
sent on the archiving transition to `now()`, so this sends a marker
|
|
2419
|
-
instant rather than pretending to choose one; restoring sends null, which
|
|
2420
|
-
the trigger leaves alone. */
|
|
2421
|
-
if (args.archived !== undefined)
|
|
2422
|
-
patch.archived_at = args.archived ? new Date().toISOString() : null;
|
|
2423
|
-
const { data, error } = await client
|
|
2424
|
-
.from(table)
|
|
2425
|
-
.update(patch)
|
|
2426
|
-
.eq('id', id)
|
|
2427
|
-
.select('id, project_id, name, archived_at')
|
|
2428
|
-
.maybeSingle();
|
|
2429
|
-
if (error) {
|
|
2430
|
-
/* PT423 — the archive guard. The sentence is OURS: the raise's message is
|
|
2431
|
-
a machine token (`epic_archive_blocked`) and must never be repeated to
|
|
2432
|
-
the user. The database's own `hint` says the same thing, but keying off
|
|
2433
|
-
the stable SQLSTATE rather than parsing prose is what the web does. */
|
|
2434
|
-
if (error.code === 'PT423') {
|
|
2435
|
-
return errorResult(`update_structure: this ${kind} still has work items that are not done, so it cannot be ` +
|
|
2436
|
-
`archived. Finish them or move them to another ${kind} first, then archive it. ` +
|
|
2437
|
-
'Nothing was changed.');
|
|
2438
|
-
}
|
|
2439
|
-
throw new Error(error.message);
|
|
2440
|
-
}
|
|
2441
|
-
/* RLS REFUSING A WRITE RETURNS NO ERROR AND NO ROW — the silent-failure
|
|
2442
|
-
shape this repo has been bitten by before. Reported as a refusal rather
|
|
2443
|
-
than as success. */
|
|
2444
|
-
if (!data) {
|
|
2445
|
-
return errorResult(`update_structure: the ${kind} could not be changed — it may have been removed, or you may ` +
|
|
2446
|
-
'not have permission. Nothing was changed.');
|
|
2447
|
-
}
|
|
2448
|
-
return textResult({
|
|
2449
|
-
[kind]: data,
|
|
2450
|
-
note: args.archived
|
|
2451
|
-
? `Archived. Its work items survive and keep their other placements.`
|
|
2452
|
-
: 'Updated.',
|
|
2453
|
-
});
|
|
2454
|
-
}
|
|
2455
|
-
catch (err) {
|
|
2456
|
-
return errorResult(`update_structure failed: ${errorMessage(err)}`);
|
|
2457
|
-
}
|
|
2458
|
-
}
|
|
2459
2113
|
/* ═══════════════════════ 16d SLICE 3a — THE PERMISSION GATE ═══════════════
|
|
2460
2114
|
`.implementations/z-done-16d-inbox-and-permission/ux.md` § "3a — One tool asks
|
|
2461
2115
|
first, and waits".
|
|
@@ -8680,9 +8334,18 @@ export const TOOL_NAMES = [
|
|
|
8680
8334
|
'create_epic',
|
|
8681
8335
|
'create_sprint',
|
|
8682
8336
|
// !Cleanup Phase 5 (I13, I14): the structure was write-only until these.
|
|
8337
|
+
'search_agent_cards', 'list_structure_artifacts',
|
|
8338
|
+
'get_structure_artifact',
|
|
8339
|
+
'create_structure_artifact',
|
|
8340
|
+
'update_structure_artifact',
|
|
8683
8341
|
'list_structure',
|
|
8684
8342
|
'update_structure',
|
|
8685
8343
|
'place_work_item',
|
|
8344
|
+
'list_artifact_folders',
|
|
8345
|
+
'set_artifact_folder',
|
|
8346
|
+
'list_work_item_dependencies',
|
|
8347
|
+
'add_work_item_dependency',
|
|
8348
|
+
'remove_work_item_dependency',
|
|
8686
8349
|
'begin_work',
|
|
8687
8350
|
'end_work',
|
|
8688
8351
|
// 18c Slice 1: the run's own activity, unasked, on the request's card.
|
|
@@ -8935,6 +8598,7 @@ runTodoIdSource = null) {
|
|
|
8935
8598
|
inputSchema: {
|
|
8936
8599
|
workflow_tool_instance: z.string().uuid().optional().describe('The id of this tool mention in the active workflow stage. ' + WORKFLOW_TOOL_TEACHING),
|
|
8937
8600
|
task_id: z.string(),
|
|
8601
|
+
folder_name: z.string().max(80).nullable().optional().describe('Optional folder. Read list_artifact_folders first to reuse an existing name.'),
|
|
8938
8602
|
type: z.enum(['analysis', 'plan', 'spec', 'diagram', 'mock', 'wireframe', 'user_story']),
|
|
8939
8603
|
format: z.enum(['md', 'html', 'json', 'svg']).optional().describe('Defaults to md'),
|
|
8940
8604
|
title: z.string().min(1).optional(),
|
|
@@ -9259,6 +8923,7 @@ runTodoIdSource = null) {
|
|
|
9259
8923
|
"create_task's sprint_id.",
|
|
9260
8924
|
inputSchema: {
|
|
9261
8925
|
project_id: z.string().describe('Project id the sprint belongs to'),
|
|
8926
|
+
description: z.string().optional(),
|
|
9262
8927
|
name: z.string().min(1).describe('The batch name — refused if a live sprint already carries it'),
|
|
9263
8928
|
start_date: z.string().optional().describe('ISO date (YYYY-MM-DD) — only for a real calendar constraint'),
|
|
9264
8929
|
end_date: z.string().optional().describe('ISO date (YYYY-MM-DD) — only for a real calendar constraint'),
|
|
@@ -9283,6 +8948,7 @@ runTodoIdSource = null) {
|
|
|
9283
8948
|
'because they are not something to extend. To see where each work item already sits, read ' +
|
|
9284
8949
|
"list_tasks's epic and sprint fields. Read-only.",
|
|
9285
8950
|
inputSchema: {
|
|
8951
|
+
workflow_tool_instance: z.string().uuid().optional().describe(WORKFLOW_TOOL_TEACHING),
|
|
9286
8952
|
project_id: z.string().describe('Project whose epics and sprints to list'),
|
|
9287
8953
|
},
|
|
9288
8954
|
}, async (args) => {
|
|
@@ -9290,7 +8956,7 @@ runTodoIdSource = null) {
|
|
|
9290
8956
|
return listStructureHandler(client, args);
|
|
9291
8957
|
});
|
|
9292
8958
|
server.registerTool('update_structure', {
|
|
9293
|
-
description: '
|
|
8959
|
+
description: 'Edit an epic or sprint name, description, dates, or archive state. Null clears a date; use target_date for epics and end_date for sprints. Follow workflow tool permission when provided. Otherwise nothing changes until ' +
|
|
9294
8960
|
'they approve, and you will be asked to call again with identical arguments once they have. ' +
|
|
9295
8961
|
'NEEDS AN OPEN WORK SESSION (begin_work) because the approval is recorded against it: if you ' +
|
|
9296
8962
|
'have no work item to open one on, say what you would rename or archive and let the user do ' +
|
|
@@ -9303,8 +8969,13 @@ runTodoIdSource = null) {
|
|
|
9303
8969
|
'rename is refused if another live epic or sprint of that project already carries the name, ' +
|
|
9304
8970
|
'the same rule create_epic and create_sprint enforce.',
|
|
9305
8971
|
inputSchema: {
|
|
8972
|
+
workflow_tool_instance: z.string().uuid().optional().describe(WORKFLOW_TOOL_TEACHING),
|
|
9306
8973
|
kind: z.enum(['epic', 'sprint']).describe('Which kind of thing to change'),
|
|
9307
8974
|
id: z.string().describe('Its id, from list_structure'),
|
|
8975
|
+
description: z.string().optional(),
|
|
8976
|
+
start_date: z.string().nullable().optional(),
|
|
8977
|
+
end_date: z.string().nullable().optional(),
|
|
8978
|
+
target_date: z.string().nullable().optional(),
|
|
9308
8979
|
name: z.string().optional().describe('A new name. Omit to leave it alone'),
|
|
9309
8980
|
archived: z
|
|
9310
8981
|
.boolean()
|
|
@@ -9320,10 +8991,76 @@ runTodoIdSource = null) {
|
|
|
9320
8991
|
creating it, so it sits in the same class as `create_epic` and
|
|
9321
8992
|
`create_sprint` rather than with `create_task`. */
|
|
9322
8993
|
const structureArgs = args;
|
|
9323
|
-
const
|
|
9324
|
-
|
|
9325
|
-
|
|
9326
|
-
|
|
8994
|
+
const write = () => updateStructureHandler(client, structureArgs);
|
|
8995
|
+
const summary = `${structureArgs.archived === true ? 'Archive' : structureArgs.archived === false ? 'Restore' : 'Update'} the ${structureArgs.kind} details`;
|
|
8996
|
+
return runWorkflowTool(client, userId, openSessions.get(connectionId) ?? null, 'update_structure', args, write, async () => runGated(client, userId, openSessions.get(connectionId) ?? null, 'update_structure', args, summary, write, await runTodoIdOf()), await runTodoIdOf());
|
|
8997
|
+
});
|
|
8998
|
+
server.registerTool('search_agent_cards', {
|
|
8999
|
+
description: 'Find your agent conversations by literal text in titles or messages, or by work item they worked on. Includes archived cards unless archived is specified. Follow next_offset to read every page. Read-only.',
|
|
9000
|
+
inputSchema: { 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() },
|
|
9001
|
+
}, async (args) => searchAgentCardsHandler(client, args));
|
|
9002
|
+
server.registerTool('list_structure_artifacts', {
|
|
9003
|
+
description: 'Read active artifacts attached directly to an epic or sprint.',
|
|
9004
|
+
inputSchema: { kind: z.enum(['epic', 'sprint']), structure_id: z.string().uuid(), workflow_tool_instance: z.string().uuid().optional().describe(WORKFLOW_TOOL_TEACHING) },
|
|
9005
|
+
}, async (args) => {
|
|
9006
|
+
touchSession(connectionId);
|
|
9007
|
+
return runWorkflowTool(client, userId, openSessions.get(connectionId) ?? null, 'list_structure_artifacts', args, () => listStructureArtifactsHandler(client, args.kind, args.structure_id), undefined, await runTodoIdOf());
|
|
9008
|
+
});
|
|
9009
|
+
server.registerTool('get_structure_artifact', {
|
|
9010
|
+
description: 'Read a full epic or sprint artifact and its current revision.',
|
|
9011
|
+
inputSchema: { artifact_id: z.string().uuid(), workflow_tool_instance: z.string().uuid().optional().describe(WORKFLOW_TOOL_TEACHING) },
|
|
9012
|
+
}, async (args) => {
|
|
9013
|
+
touchSession(connectionId);
|
|
9014
|
+
return runWorkflowTool(client, userId, openSessions.get(connectionId) ?? null, 'get_structure_artifact', args, () => getStructureArtifactHandler(client, args.artifact_id), undefined, await runTodoIdOf());
|
|
9015
|
+
});
|
|
9016
|
+
server.registerTool('create_structure_artifact', {
|
|
9017
|
+
description: 'Create a titled artifact on an epic or sprint. It appears on its management page.',
|
|
9018
|
+
inputSchema: { 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), workflow_tool_instance: z.string().uuid().optional().describe(WORKFLOW_TOOL_TEACHING) },
|
|
9019
|
+
}, async (args) => {
|
|
9020
|
+
touchSession(connectionId);
|
|
9021
|
+
return runWorkflowTool(client, userId, openSessions.get(connectionId) ?? null, 'create_structure_artifact', args, () => createStructureArtifactHandler(client, userId, args), undefined, await runTodoIdOf());
|
|
9022
|
+
});
|
|
9023
|
+
server.registerTool('update_structure_artifact', {
|
|
9024
|
+
description: 'Update an epic or sprint artifact using the revision you read. Concurrent edits refuse without overwriting. archived=true removes it; false restores it.',
|
|
9025
|
+
inputSchema: { 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(), workflow_tool_instance: z.string().uuid().optional().describe(WORKFLOW_TOOL_TEACHING) },
|
|
9026
|
+
}, async (args) => {
|
|
9027
|
+
touchSession(connectionId);
|
|
9028
|
+
return runWorkflowTool(client, userId, openSessions.get(connectionId) ?? null, 'update_structure_artifact', args, () => updateStructureArtifactHandler(client, args), undefined, await runTodoIdOf());
|
|
9029
|
+
});
|
|
9030
|
+
server.registerTool('list_artifact_folders', {
|
|
9031
|
+
description: 'Read existing artifact folder names and counts on a work item before choosing a folder.',
|
|
9032
|
+
inputSchema: { work_item_id: z.string().uuid(), workflow_tool_instance: z.string().uuid().optional().describe(WORKFLOW_TOOL_TEACHING) },
|
|
9033
|
+
}, async (args) => {
|
|
9034
|
+
touchSession(connectionId);
|
|
9035
|
+
return runWorkflowTool(client, userId, openSessions.get(connectionId) ?? null, 'list_artifact_folders', args, () => listArtifactFoldersHandler(client, args.work_item_id), undefined, await runTodoIdOf());
|
|
9036
|
+
});
|
|
9037
|
+
server.registerTool('set_artifact_folder', {
|
|
9038
|
+
description: 'Move an artifact into one folder, replacing its previous folder. Null clears it to Unfiled. Content and approval stay unchanged.',
|
|
9039
|
+
inputSchema: { artifact_id: z.string().uuid(), folder_name: z.string().max(80).nullable(), workflow_tool_instance: z.string().uuid().optional().describe(WORKFLOW_TOOL_TEACHING) },
|
|
9040
|
+
}, async (args) => {
|
|
9041
|
+
touchSession(connectionId);
|
|
9042
|
+
return runWorkflowTool(client, userId, openSessions.get(connectionId) ?? null, 'set_artifact_folder', args, () => setArtifactFolderHandler(client, args.artifact_id, args.folder_name), undefined, await runTodoIdOf());
|
|
9043
|
+
});
|
|
9044
|
+
server.registerTool('list_work_item_dependencies', {
|
|
9045
|
+
description: 'Read blockers, dependents, and whether this work item is waiting.',
|
|
9046
|
+
inputSchema: { work_item_id: z.string().uuid(), workflow_tool_instance: z.string().uuid().optional().describe(WORKFLOW_TOOL_TEACHING) },
|
|
9047
|
+
}, async (args) => {
|
|
9048
|
+
touchSession(connectionId);
|
|
9049
|
+
return runWorkflowTool(client, userId, openSessions.get(connectionId) ?? null, 'list_work_item_dependencies', args, () => workItemDependencyHandler(client, 'list', args), undefined, await runTodoIdOf());
|
|
9050
|
+
});
|
|
9051
|
+
server.registerTool('add_work_item_dependency', {
|
|
9052
|
+
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.',
|
|
9053
|
+
inputSchema: { work_item_id: z.string().uuid(), depends_on_work_item_id: z.string().uuid(), workflow_tool_instance: z.string().uuid().optional().describe(WORKFLOW_TOOL_TEACHING) },
|
|
9054
|
+
}, async (args) => {
|
|
9055
|
+
touchSession(connectionId);
|
|
9056
|
+
return runWorkflowTool(client, userId, openSessions.get(connectionId) ?? null, 'add_work_item_dependency', args, () => workItemDependencyHandler(client, 'add', args), undefined, await runTodoIdOf());
|
|
9057
|
+
});
|
|
9058
|
+
server.registerTool('remove_work_item_dependency', {
|
|
9059
|
+
description: 'Remove exactly this dependency edge without changing either work item status.',
|
|
9060
|
+
inputSchema: { work_item_id: z.string().uuid(), depends_on_work_item_id: z.string().uuid(), workflow_tool_instance: z.string().uuid().optional().describe(WORKFLOW_TOOL_TEACHING) },
|
|
9061
|
+
}, async (args) => {
|
|
9062
|
+
touchSession(connectionId);
|
|
9063
|
+
return runWorkflowTool(client, userId, openSessions.get(connectionId) ?? null, 'remove_work_item_dependency', args, () => workItemDependencyHandler(client, 'remove', args), undefined, await runTodoIdOf());
|
|
9327
9064
|
});
|
|
9328
9065
|
server.registerTool('place_work_item', {
|
|
9329
9066
|
description: 'Move a work item into an epic and/or a sprint, after it was created. Pass work_item_id and ' +
|
package/dist/panel3/checkout.js
CHANGED
|
@@ -407,35 +407,10 @@ settings = {}) {
|
|
|
407
407
|
+ `${branch}. Check that ${codebase.name} is a git repository on ${machineName}.`);
|
|
408
408
|
}
|
|
409
409
|
}
|
|
410
|
-
/**
|
|
411
|
-
*
|
|
412
|
-
*
|
|
413
|
-
* The commit comes first and is unconditional on there being something to
|
|
414
|
-
* commit, so removing the copy cannot lose work. The BRANCH is never touched:
|
|
415
|
-
* work that never landed survives until a person deals with it.
|
|
416
|
-
*
|
|
417
|
-
* It takes the folder alone, and that is deliberate: a worktree knows its own
|
|
418
|
-
* repository, so the sweep needs neither the located checkout nor the codebase
|
|
419
|
-
* row to clean one up.
|
|
420
|
-
*/
|
|
421
|
-
/**
|
|
422
|
-
* ═══ EVERYTHING IN A CARD'S COPY, ON ITS BRANCH. ═══
|
|
423
|
-
*
|
|
424
|
-
* `settleCardWorktree`'s own first three lines, extracted rather than rewritten,
|
|
425
|
-
* because the landing needs the commit BEFORE the merge and the sweep needs it
|
|
426
|
-
* before the removal. Committing in two places with two messages is how one card
|
|
427
|
-
* comes to have two ideas of what its work is.
|
|
428
|
-
*
|
|
429
|
-
* ═══ ITS FAILURE IS COMPOSED, WHICH THE SWEEP NEVER NEEDED. ═══ The sweep
|
|
430
|
-
* writes to stderr, so git's own text was harmless there. On the landing path a
|
|
431
|
-
* failure reaches a person through an agent, and `execFileSync`'s message begins
|
|
432
|
-
* `Command failed: git -C <absolute path>`. So this wraps in `worktreeForCard`'s
|
|
433
|
-
* shape: what was composed here passes through, anything else is replaced.
|
|
434
|
-
*/
|
|
410
|
+
/** Commit only for an explicitly selected landing action. Cleanup never commits.
|
|
411
|
+
* Failures are path-free because the landing result reaches the hosted panel. */
|
|
435
412
|
export function commitCardWork(folder,
|
|
436
|
-
|
|
437
|
-
folder and says so; the landing has the codebase and the branch and names
|
|
438
|
-
both, which is what a person reading the failure needs. */
|
|
413
|
+
// Name the codebase and branch in failures, without exposing a local path.
|
|
439
414
|
describe) {
|
|
440
415
|
try {
|
|
441
416
|
git(folder, ['add', '-A']);
|
|
@@ -491,11 +466,13 @@ export function mergeIntoBase(source, branch, base, codebaseName) {
|
|
|
491
466
|
}
|
|
492
467
|
}
|
|
493
468
|
export function settleCardWorktree(folder) {
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
469
|
+
// Completion is not permission to commit. Preserve unfinished Git work,
|
|
470
|
+
// including staged and untracked files, for the person to review or resume.
|
|
471
|
+
if (git(folder, ['status', '--porcelain']).length > 0)
|
|
472
|
+
return false;
|
|
473
|
+
// Only ignored build/dependency files can remain in a clean copy.
|
|
498
474
|
removeWorktree(folder);
|
|
475
|
+
return true;
|
|
499
476
|
}
|
|
500
477
|
/**
|
|
501
478
|
* Every card copy this machine currently holds, as `<codebase id>/<folder>`
|