orizu 0.4.0 → 0.4.1
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/help.js +5 -5
- package/dist/index.js +93 -24
- package/dist/task-assignment-manifest.js +59 -0
- package/package.json +1 -1
- package/src/help.ts +5 -5
- package/src/index.ts +119 -25
- package/src/task-assignment-manifest.ts +96 -0
- package/vendor/skills/orizu-cli/SKILL.md +5 -3
- package/vendor/skills/orizu-cli/references/cli-reference.md +9 -1
package/dist/help.js
CHANGED
|
@@ -450,20 +450,20 @@ export const COMMAND_DOCS = [
|
|
|
450
450
|
},
|
|
451
451
|
{
|
|
452
452
|
path: ['tasks', 'create'],
|
|
453
|
-
usage: 'orizu tasks create --project <team/project> --dataset <datasetId> --app <appId> --title <title> [--assignees <userIdOrEmail1,userIdOrEmail2>] [--publish] [--version <n>] [--instructions <text>] [--labels-per-item <n>] [--json]',
|
|
454
|
-
summary: 'Create a draft review task by default, or
|
|
453
|
+
usage: 'orizu tasks create --project <team/project> --dataset <datasetId> --app <appId> --title <title> [--assignees <userIdOrEmail1,userIdOrEmail2> | --assignment-file <path>] [--publish] [--version <n>] [--instructions <text>] [--labels-per-item <n>] [--json]',
|
|
454
|
+
summary: 'Create a draft review task by default, with auto distribution or an explicit row-assignment JSONL file.',
|
|
455
455
|
group: 'Tasks',
|
|
456
456
|
},
|
|
457
457
|
{
|
|
458
458
|
path: ['tasks', 'publish'],
|
|
459
|
-
usage: 'orizu tasks publish --task <taskId> --assignees <userId1,userId2> [--json]',
|
|
459
|
+
usage: 'orizu tasks publish --task <taskId> (--assignees <userId1,userId2> | --assignment-file <path>) [--json]',
|
|
460
460
|
summary: 'Publish an approved draft task and assign reviewers.',
|
|
461
461
|
group: 'Tasks',
|
|
462
462
|
},
|
|
463
463
|
{
|
|
464
464
|
path: ['tasks', 'assign'],
|
|
465
|
-
usage: 'orizu tasks assign --task <taskId> --assignees <userId1,userId2>',
|
|
466
|
-
summary: 'Assign a task
|
|
465
|
+
usage: 'orizu tasks assign --task <taskId> (--assignees <userId1,userId2> | --assignment-file <path>) [--replace-existing] [--json]',
|
|
466
|
+
summary: 'Assign a task with auto distribution or an explicit row-assignment JSONL file.',
|
|
467
467
|
group: 'Tasks',
|
|
468
468
|
},
|
|
469
469
|
{
|
package/dist/index.js
CHANGED
|
@@ -17,6 +17,7 @@ import { extractErrorMessage } from './error-response.js';
|
|
|
17
17
|
import { parseGlobalFlags } from './global-flags.js';
|
|
18
18
|
import { getCapabilities, renderHelpForArgs, renderRootHelp } from './help.js';
|
|
19
19
|
import { runLocalAppPreview } from './preview-runtime.js';
|
|
20
|
+
import { parseAssignmentManifestJsonl } from './task-assignment-manifest.js';
|
|
20
21
|
import { assertSecureTokenTransport, authedFetch, getBaseUrl, resolveLoginBaseUrl, setGlobalFlags, } from './http.js';
|
|
21
22
|
import { computeSkillContentHash, getSkillInstallPath, getSkillTargetStatus, getTargetForAgent, installSkillTarget, isSkillInstallAgent, isSkillInstallTarget, resolveSkillSource, SKILL_INSTALL_AGENTS, SKILL_INSTALL_TARGETS, targetNeedsOverwrite, } from './skill-installer.js';
|
|
22
23
|
import { renderBanner } from './banner.js';
|
|
@@ -2986,39 +2987,65 @@ function parseCommaSeparated(value) {
|
|
|
2986
2987
|
.map(item => item.trim())
|
|
2987
2988
|
.filter(Boolean);
|
|
2988
2989
|
}
|
|
2990
|
+
function readAssignmentManifest(path) {
|
|
2991
|
+
if (!path) {
|
|
2992
|
+
return null;
|
|
2993
|
+
}
|
|
2994
|
+
const content = readFileSync(expandHomePath(path), 'utf8');
|
|
2995
|
+
return parseAssignmentManifestJsonl(content);
|
|
2996
|
+
}
|
|
2989
2997
|
async function createTask() {
|
|
2990
2998
|
const projectSlug = getArg('--project');
|
|
2991
2999
|
const datasetId = getArg('--dataset');
|
|
2992
3000
|
const appId = getArg('--app');
|
|
2993
3001
|
const title = getArg('--title');
|
|
2994
3002
|
const assignees = parseCommaSeparated(getArg('--assignees'));
|
|
3003
|
+
const assignmentFile = getArg('--assignment-file');
|
|
2995
3004
|
const publish = hasArg('--publish');
|
|
2996
3005
|
const versionArg = getArg('--version');
|
|
2997
3006
|
const instructions = getArg('--instructions');
|
|
2998
3007
|
const labelsPerItemArg = getArg('--labels-per-item');
|
|
2999
|
-
const
|
|
3008
|
+
const parsedLabelsPerItem = labelsPerItemArg ? Number(labelsPerItemArg) : 1;
|
|
3009
|
+
const labelsPerItem = Number.isInteger(parsedLabelsPerItem) && parsedLabelsPerItem > 0
|
|
3010
|
+
? parsedLabelsPerItem
|
|
3011
|
+
: null;
|
|
3000
3012
|
const parsedVersionNum = versionArg ? Number(versionArg) : Number.NaN;
|
|
3001
3013
|
const versionNum = Number.isInteger(parsedVersionNum) && parsedVersionNum > 0 ? parsedVersionNum : null;
|
|
3002
|
-
if (
|
|
3003
|
-
throw new Error('
|
|
3014
|
+
if (assignmentFile && assignees.length > 0) {
|
|
3015
|
+
throw new Error('Use either --assignment-file or --assignees, not both.');
|
|
3016
|
+
}
|
|
3017
|
+
const explicitAssignments = readAssignmentManifest(assignmentFile);
|
|
3018
|
+
if (!projectSlug || !datasetId || !appId || !title || (publish && assignees.length === 0 && !explicitAssignments)) {
|
|
3019
|
+
throw new Error('Usage: orizu tasks create --project <team/project> --dataset <datasetId> --app <appId> --title <title> [--assignees <userIdOrEmail1,userIdOrEmail2> | --assignment-file <path>] [--publish] [--version <n>] [--instructions <text>] [--labels-per-item <n>] [--json]');
|
|
3004
3020
|
}
|
|
3005
3021
|
if (versionArg && versionNum === null) {
|
|
3006
3022
|
throw new Error('--version must be a positive integer');
|
|
3007
3023
|
}
|
|
3024
|
+
if (labelsPerItemArg && labelsPerItem === null) {
|
|
3025
|
+
throw new Error('--labels-per-item must be a positive integer');
|
|
3026
|
+
}
|
|
3027
|
+
const requestBody = {
|
|
3028
|
+
projectSlug,
|
|
3029
|
+
datasetId,
|
|
3030
|
+
appId,
|
|
3031
|
+
versionNum,
|
|
3032
|
+
title,
|
|
3033
|
+
publish,
|
|
3034
|
+
instructions,
|
|
3035
|
+
};
|
|
3036
|
+
if (explicitAssignments) {
|
|
3037
|
+
requestBody.explicitAssignments = explicitAssignments;
|
|
3038
|
+
}
|
|
3039
|
+
else {
|
|
3040
|
+
requestBody.memberIds = assignees;
|
|
3041
|
+
}
|
|
3042
|
+
if (!explicitAssignments || labelsPerItemArg) {
|
|
3043
|
+
requestBody.requiredAssignmentsPerRow = labelsPerItem;
|
|
3044
|
+
}
|
|
3008
3045
|
const response = await authedFetch('/api/cli/tasks', {
|
|
3009
3046
|
method: 'POST',
|
|
3010
3047
|
headers: { 'Content-Type': 'application/json' },
|
|
3011
|
-
body: JSON.stringify(
|
|
3012
|
-
projectSlug,
|
|
3013
|
-
datasetId,
|
|
3014
|
-
appId,
|
|
3015
|
-
versionNum,
|
|
3016
|
-
title,
|
|
3017
|
-
memberIds: assignees,
|
|
3018
|
-
publish,
|
|
3019
|
-
instructions,
|
|
3020
|
-
requiredAssignmentsPerRow: labelsPerItem,
|
|
3021
|
-
}),
|
|
3048
|
+
body: JSON.stringify(requestBody),
|
|
3022
3049
|
});
|
|
3023
3050
|
if (!response.ok) {
|
|
3024
3051
|
const cliError = await formatTaskCreateError(response);
|
|
@@ -3041,13 +3068,16 @@ async function createTask() {
|
|
|
3041
3068
|
taskUrl: `${getBaseUrl()}/d/${projectSlug}/tasks/${data.task.id}`,
|
|
3042
3069
|
title: data.task.title,
|
|
3043
3070
|
status: data.task.status,
|
|
3071
|
+
assignmentMode: explicitAssignments ? 'custom' : 'auto',
|
|
3044
3072
|
requiredAssignmentsPerRow: data.task.requiredAssignmentsPerRow,
|
|
3045
3073
|
assignmentsCreated: data.assignmentsCreated,
|
|
3046
3074
|
draft: data.task.status === 'draft',
|
|
3047
3075
|
...(data.task.status === 'draft'
|
|
3048
3076
|
? {
|
|
3049
3077
|
message: 'Draft created. Test it manually before assigning.',
|
|
3050
|
-
publishCommand:
|
|
3078
|
+
publishCommand: explicitAssignments
|
|
3079
|
+
? `orizu tasks publish --task ${data.task.id} --assignment-file ${assignmentFile ? shellQuote(assignmentFile) : '<path>'}`
|
|
3080
|
+
: `orizu tasks publish --task ${data.task.id} --assignees <userId1,userId2>`,
|
|
3051
3081
|
}
|
|
3052
3082
|
: {}),
|
|
3053
3083
|
...(data.assignmentShortfall !== undefined ? { assignmentShortfall: data.assignmentShortfall } : {}),
|
|
@@ -3062,24 +3092,34 @@ async function createTask() {
|
|
|
3062
3092
|
`\n Task ID: ${sanitizeTerminalText(data.task.id)}` +
|
|
3063
3093
|
`\n Dataset ID: ${sanitizeTerminalText(datasetId)}` +
|
|
3064
3094
|
`\n Version: v${data.task.versionNum} (${sanitizeTerminalText(data.task.versionId)})` +
|
|
3095
|
+
`\n Mode: ${explicitAssignments ? 'custom row map' : 'auto distribute'}` +
|
|
3065
3096
|
`\n Labels/row: ${data.task.requiredAssignmentsPerRow}` +
|
|
3066
3097
|
`\n Assignments: ${data.assignmentsCreated}` +
|
|
3067
3098
|
(data.warning ? `\n Warning: ${sanitizeTerminalText(data.warning)}` : '') +
|
|
3068
3099
|
`\n URL: ${sanitizeTerminalText(taskUrl)}` +
|
|
3069
3100
|
(isDraft
|
|
3070
|
-
? `\n\nThis task is a draft. Test it manually before assigning.\nPublish after approval:
|
|
3101
|
+
? `\n\nThis task is a draft. Test it manually before assigning.\nPublish after approval: ${explicitAssignments
|
|
3102
|
+
? `orizu tasks publish --task ${sanitizeTerminalText(data.task.id)} --assignment-file ${sanitizeTerminalText(assignmentFile || '<path>')}`
|
|
3103
|
+
: `orizu tasks publish --task ${sanitizeTerminalText(data.task.id)} --assignees <userId1,userId2>`}`
|
|
3071
3104
|
: ''));
|
|
3072
3105
|
}
|
|
3073
3106
|
async function publishTask() {
|
|
3074
3107
|
const taskId = getArg('--task');
|
|
3075
3108
|
const assignees = parseCommaSeparated(getArg('--assignees'));
|
|
3076
|
-
|
|
3077
|
-
|
|
3109
|
+
const assignmentFile = getArg('--assignment-file');
|
|
3110
|
+
if (assignmentFile && assignees.length > 0) {
|
|
3111
|
+
throw new Error('Use either --assignment-file or --assignees, not both.');
|
|
3112
|
+
}
|
|
3113
|
+
const explicitAssignments = readAssignmentManifest(assignmentFile);
|
|
3114
|
+
if (!taskId || (assignees.length === 0 && !explicitAssignments)) {
|
|
3115
|
+
throw new Error('Usage: orizu tasks publish --task <taskId> (--assignees <userId1,userId2> | --assignment-file <path>) [--json]');
|
|
3078
3116
|
}
|
|
3079
3117
|
const response = await authedFetch(`/api/cli/tasks/${encodeURIComponent(taskId)}/publish`, {
|
|
3080
3118
|
method: 'POST',
|
|
3081
3119
|
headers: { 'Content-Type': 'application/json' },
|
|
3082
|
-
body: JSON.stringify(
|
|
3120
|
+
body: JSON.stringify(explicitAssignments
|
|
3121
|
+
? { explicitAssignments }
|
|
3122
|
+
: { memberIds: assignees }),
|
|
3083
3123
|
});
|
|
3084
3124
|
if (!response.ok) {
|
|
3085
3125
|
throw new Error(`Failed to publish task: ${await extractErrorMessage(response)}`);
|
|
@@ -3089,6 +3129,7 @@ async function publishTask() {
|
|
|
3089
3129
|
printLine(JSON.stringify({
|
|
3090
3130
|
taskId: data.task.id,
|
|
3091
3131
|
status: data.task.status,
|
|
3132
|
+
assignmentMode: explicitAssignments ? 'custom' : 'auto',
|
|
3092
3133
|
assignmentsCreated: data.assignmentsCreated,
|
|
3093
3134
|
assignmentsProcessed: data.assignmentsProcessed,
|
|
3094
3135
|
...(data.assignmentsRemoved !== undefined
|
|
@@ -3102,6 +3143,7 @@ async function publishTask() {
|
|
|
3102
3143
|
return;
|
|
3103
3144
|
}
|
|
3104
3145
|
printLine(`Published task ${sanitizeTerminalText(data.task.id)} [${sanitizeTerminalText(data.task.status)}]` +
|
|
3146
|
+
`\n Mode: ${explicitAssignments ? 'custom row map' : 'auto distribute'}` +
|
|
3105
3147
|
`\n Assignments: ${data.assignmentsCreated}` +
|
|
3106
3148
|
(data.assignmentsRemoved !== undefined
|
|
3107
3149
|
? `\n Replaced: ${data.assignmentsRemoved} old assignments removed`
|
|
@@ -3111,23 +3153,50 @@ async function publishTask() {
|
|
|
3111
3153
|
async function assignTask() {
|
|
3112
3154
|
const taskId = getArg('--task');
|
|
3113
3155
|
const assignees = parseCommaSeparated(getArg('--assignees'));
|
|
3114
|
-
|
|
3115
|
-
|
|
3156
|
+
const assignmentFile = getArg('--assignment-file');
|
|
3157
|
+
if (assignmentFile && assignees.length > 0) {
|
|
3158
|
+
throw new Error('Use either --assignment-file or --assignees, not both.');
|
|
3159
|
+
}
|
|
3160
|
+
const explicitAssignments = readAssignmentManifest(assignmentFile);
|
|
3161
|
+
if (!taskId || (assignees.length === 0 && !explicitAssignments)) {
|
|
3162
|
+
throw new Error('Usage: orizu tasks assign --task <taskId> (--assignees <userId1,userId2> | --assignment-file <path>) [--replace-existing] [--json]');
|
|
3163
|
+
}
|
|
3164
|
+
const requestBody = explicitAssignments
|
|
3165
|
+
? { explicitAssignments }
|
|
3166
|
+
: { memberIds: assignees };
|
|
3167
|
+
if (hasArg('--replace-existing')) {
|
|
3168
|
+
requestBody.replaceExisting = true;
|
|
3116
3169
|
}
|
|
3117
3170
|
const response = await authedFetch(`/api/cli/tasks/${encodeURIComponent(taskId)}/assign`, {
|
|
3118
3171
|
method: 'POST',
|
|
3119
3172
|
headers: { 'Content-Type': 'application/json' },
|
|
3120
|
-
body: JSON.stringify(
|
|
3173
|
+
body: JSON.stringify(requestBody),
|
|
3121
3174
|
});
|
|
3122
3175
|
if (!response.ok) {
|
|
3123
3176
|
throw new Error(`Failed to assign task: ${await response.text()}`);
|
|
3124
3177
|
}
|
|
3125
3178
|
const data = await parseJsonResponse(response, 'Task assign');
|
|
3126
3179
|
if (hasJsonFlag()) {
|
|
3127
|
-
printJson({
|
|
3180
|
+
printJson({
|
|
3181
|
+
assignmentMode: explicitAssignments ? 'custom' : 'auto',
|
|
3182
|
+
assignmentsProcessed: data.assignmentsProcessed ?? data.assignmentsCreated,
|
|
3183
|
+
assignmentsCreated: data.assignmentsCreated,
|
|
3184
|
+
...(data.assignmentsRemoved !== undefined
|
|
3185
|
+
? { assignmentsRemoved: data.assignmentsRemoved }
|
|
3186
|
+
: {}),
|
|
3187
|
+
...(data.assignmentShortfall !== undefined
|
|
3188
|
+
? { assignmentShortfall: data.assignmentShortfall }
|
|
3189
|
+
: {}),
|
|
3190
|
+
...(data.warning ? { warning: data.warning } : {}),
|
|
3191
|
+
});
|
|
3128
3192
|
return;
|
|
3129
3193
|
}
|
|
3130
|
-
printLine(`Created ${data.assignmentsCreated} assignments.`
|
|
3194
|
+
printLine(`Created ${data.assignmentsCreated} assignments.` +
|
|
3195
|
+
`\n Mode: ${explicitAssignments ? 'custom row map' : 'auto distribute'}` +
|
|
3196
|
+
(data.assignmentsRemoved !== undefined
|
|
3197
|
+
? `\n Replaced: ${data.assignmentsRemoved} old assignments removed`
|
|
3198
|
+
: '') +
|
|
3199
|
+
(data.warning ? `\n Warning: ${sanitizeTerminalText(data.warning)}` : ''));
|
|
3131
3200
|
}
|
|
3132
3201
|
async function taskStatus() {
|
|
3133
3202
|
const taskId = getArg('--task');
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
function isRecord(value) {
|
|
2
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
3
|
+
}
|
|
4
|
+
export function parseAssignmentManifestJsonl(content) {
|
|
5
|
+
// Keep these JSONL field rules in sync with the server's explicitAssignments parser.
|
|
6
|
+
const entries = [];
|
|
7
|
+
const lines = content.split(/\r?\n/);
|
|
8
|
+
lines.forEach((line, lineIndex) => {
|
|
9
|
+
const trimmed = line.trim();
|
|
10
|
+
if (!trimmed) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
let parsed;
|
|
14
|
+
try {
|
|
15
|
+
parsed = JSON.parse(trimmed);
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
throw new Error(`Assignment manifest line ${lineIndex + 1} is not valid JSON`);
|
|
19
|
+
}
|
|
20
|
+
if (!isRecord(parsed)) {
|
|
21
|
+
throw new Error(`Assignment manifest line ${lineIndex + 1} must be a JSON object`);
|
|
22
|
+
}
|
|
23
|
+
const rowId = typeof parsed.rowId === 'string' ? parsed.rowId.trim() : '';
|
|
24
|
+
if (!rowId) {
|
|
25
|
+
throw new Error(`Assignment manifest line ${lineIndex + 1} must include a non-empty rowId`);
|
|
26
|
+
}
|
|
27
|
+
const hasAssignee = Object.hasOwn(parsed, 'assignee');
|
|
28
|
+
const hasAssignees = Object.hasOwn(parsed, 'assignees');
|
|
29
|
+
if (hasAssignee && hasAssignees) {
|
|
30
|
+
throw new Error(`Assignment manifest line ${lineIndex + 1} must provide either assignee or assignees, not both`);
|
|
31
|
+
}
|
|
32
|
+
if (hasAssignee) {
|
|
33
|
+
const assignee = typeof parsed.assignee === 'string' ? parsed.assignee.trim() : '';
|
|
34
|
+
if (!assignee) {
|
|
35
|
+
throw new Error(`Assignment manifest line ${lineIndex + 1} assignee must be a non-empty string`);
|
|
36
|
+
}
|
|
37
|
+
entries.push({ rowId, assignee });
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (hasAssignees) {
|
|
41
|
+
if (!Array.isArray(parsed.assignees) || parsed.assignees.length === 0) {
|
|
42
|
+
throw new Error(`Assignment manifest line ${lineIndex + 1} assignees must be a non-empty string array`);
|
|
43
|
+
}
|
|
44
|
+
parsed.assignees.forEach((assigneeValue, assigneeIndex) => {
|
|
45
|
+
const assignee = typeof assigneeValue === 'string' ? assigneeValue.trim() : '';
|
|
46
|
+
if (!assignee) {
|
|
47
|
+
throw new Error(`Assignment manifest line ${lineIndex + 1} assignees[${assigneeIndex}] must be a non-empty string`);
|
|
48
|
+
}
|
|
49
|
+
entries.push({ rowId, assignee });
|
|
50
|
+
});
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
throw new Error(`Assignment manifest line ${lineIndex + 1} must include assignee or assignees`);
|
|
54
|
+
});
|
|
55
|
+
if (entries.length === 0) {
|
|
56
|
+
throw new Error('Assignment manifest must contain at least one assignment');
|
|
57
|
+
}
|
|
58
|
+
return entries;
|
|
59
|
+
}
|
package/package.json
CHANGED
package/src/help.ts
CHANGED
|
@@ -475,20 +475,20 @@ export const COMMAND_DOCS: CliCommandDoc[] = [
|
|
|
475
475
|
},
|
|
476
476
|
{
|
|
477
477
|
path: ['tasks', 'create'],
|
|
478
|
-
usage: 'orizu tasks create --project <team/project> --dataset <datasetId> --app <appId> --title <title> [--assignees <userIdOrEmail1,userIdOrEmail2>] [--publish] [--version <n>] [--instructions <text>] [--labels-per-item <n>] [--json]',
|
|
479
|
-
summary: 'Create a draft review task by default, or
|
|
478
|
+
usage: 'orizu tasks create --project <team/project> --dataset <datasetId> --app <appId> --title <title> [--assignees <userIdOrEmail1,userIdOrEmail2> | --assignment-file <path>] [--publish] [--version <n>] [--instructions <text>] [--labels-per-item <n>] [--json]',
|
|
479
|
+
summary: 'Create a draft review task by default, with auto distribution or an explicit row-assignment JSONL file.',
|
|
480
480
|
group: 'Tasks',
|
|
481
481
|
},
|
|
482
482
|
{
|
|
483
483
|
path: ['tasks', 'publish'],
|
|
484
|
-
usage: 'orizu tasks publish --task <taskId> --assignees <userId1,userId2> [--json]',
|
|
484
|
+
usage: 'orizu tasks publish --task <taskId> (--assignees <userId1,userId2> | --assignment-file <path>) [--json]',
|
|
485
485
|
summary: 'Publish an approved draft task and assign reviewers.',
|
|
486
486
|
group: 'Tasks',
|
|
487
487
|
},
|
|
488
488
|
{
|
|
489
489
|
path: ['tasks', 'assign'],
|
|
490
|
-
usage: 'orizu tasks assign --task <taskId> --assignees <userId1,userId2>',
|
|
491
|
-
summary: 'Assign a task
|
|
490
|
+
usage: 'orizu tasks assign --task <taskId> (--assignees <userId1,userId2> | --assignment-file <path>) [--replace-existing] [--json]',
|
|
491
|
+
summary: 'Assign a task with auto distribution or an explicit row-assignment JSONL file.',
|
|
492
492
|
group: 'Tasks',
|
|
493
493
|
},
|
|
494
494
|
{
|
package/src/index.ts
CHANGED
|
@@ -27,6 +27,7 @@ import { extractErrorMessage } from './error-response.js'
|
|
|
27
27
|
import { parseGlobalFlags } from './global-flags.js'
|
|
28
28
|
import { getCapabilities, renderHelpForArgs, renderRootHelp } from './help.js'
|
|
29
29
|
import { runLocalAppPreview } from './preview-runtime.js'
|
|
30
|
+
import { parseAssignmentManifestJsonl } from './task-assignment-manifest.js'
|
|
30
31
|
import {
|
|
31
32
|
assertSecureTokenTransport,
|
|
32
33
|
authedFetch,
|
|
@@ -3908,43 +3909,77 @@ function parseCommaSeparated(value: string | null): string[] {
|
|
|
3908
3909
|
.filter(Boolean)
|
|
3909
3910
|
}
|
|
3910
3911
|
|
|
3912
|
+
function readAssignmentManifest(path: string | null): ReturnType<typeof parseAssignmentManifestJsonl> | null {
|
|
3913
|
+
if (!path) {
|
|
3914
|
+
return null
|
|
3915
|
+
}
|
|
3916
|
+
|
|
3917
|
+
const content = readFileSync(expandHomePath(path), 'utf8')
|
|
3918
|
+
return parseAssignmentManifestJsonl(content)
|
|
3919
|
+
}
|
|
3920
|
+
|
|
3911
3921
|
async function createTask() {
|
|
3912
3922
|
const projectSlug = getArg('--project')
|
|
3913
3923
|
const datasetId = getArg('--dataset')
|
|
3914
3924
|
const appId = getArg('--app')
|
|
3915
3925
|
const title = getArg('--title')
|
|
3916
3926
|
const assignees = parseCommaSeparated(getArg('--assignees'))
|
|
3927
|
+
const assignmentFile = getArg('--assignment-file')
|
|
3917
3928
|
const publish = hasArg('--publish')
|
|
3918
3929
|
const versionArg = getArg('--version')
|
|
3919
3930
|
const instructions = getArg('--instructions')
|
|
3920
3931
|
const labelsPerItemArg = getArg('--labels-per-item')
|
|
3921
|
-
const
|
|
3932
|
+
const parsedLabelsPerItem = labelsPerItemArg ? Number(labelsPerItemArg) : 1
|
|
3933
|
+
const labelsPerItem =
|
|
3934
|
+
Number.isInteger(parsedLabelsPerItem) && parsedLabelsPerItem > 0
|
|
3935
|
+
? parsedLabelsPerItem
|
|
3936
|
+
: null
|
|
3922
3937
|
const parsedVersionNum = versionArg ? Number(versionArg) : Number.NaN
|
|
3923
3938
|
const versionNum =
|
|
3924
3939
|
Number.isInteger(parsedVersionNum) && parsedVersionNum > 0 ? parsedVersionNum : null
|
|
3925
3940
|
|
|
3926
|
-
if (
|
|
3927
|
-
throw new Error('
|
|
3941
|
+
if (assignmentFile && assignees.length > 0) {
|
|
3942
|
+
throw new Error('Use either --assignment-file or --assignees, not both.')
|
|
3943
|
+
}
|
|
3944
|
+
|
|
3945
|
+
const explicitAssignments = readAssignmentManifest(assignmentFile)
|
|
3946
|
+
|
|
3947
|
+
if (!projectSlug || !datasetId || !appId || !title || (publish && assignees.length === 0 && !explicitAssignments)) {
|
|
3948
|
+
throw new Error('Usage: orizu tasks create --project <team/project> --dataset <datasetId> --app <appId> --title <title> [--assignees <userIdOrEmail1,userIdOrEmail2> | --assignment-file <path>] [--publish] [--version <n>] [--instructions <text>] [--labels-per-item <n>] [--json]')
|
|
3928
3949
|
}
|
|
3929
3950
|
|
|
3930
3951
|
if (versionArg && versionNum === null) {
|
|
3931
3952
|
throw new Error('--version must be a positive integer')
|
|
3932
3953
|
}
|
|
3933
3954
|
|
|
3955
|
+
if (labelsPerItemArg && labelsPerItem === null) {
|
|
3956
|
+
throw new Error('--labels-per-item must be a positive integer')
|
|
3957
|
+
}
|
|
3958
|
+
|
|
3959
|
+
const requestBody: Record<string, unknown> = {
|
|
3960
|
+
projectSlug,
|
|
3961
|
+
datasetId,
|
|
3962
|
+
appId,
|
|
3963
|
+
versionNum,
|
|
3964
|
+
title,
|
|
3965
|
+
publish,
|
|
3966
|
+
instructions,
|
|
3967
|
+
}
|
|
3968
|
+
|
|
3969
|
+
if (explicitAssignments) {
|
|
3970
|
+
requestBody.explicitAssignments = explicitAssignments
|
|
3971
|
+
} else {
|
|
3972
|
+
requestBody.memberIds = assignees
|
|
3973
|
+
}
|
|
3974
|
+
|
|
3975
|
+
if (!explicitAssignments || labelsPerItemArg) {
|
|
3976
|
+
requestBody.requiredAssignmentsPerRow = labelsPerItem
|
|
3977
|
+
}
|
|
3978
|
+
|
|
3934
3979
|
const response = await authedFetch('/api/cli/tasks', {
|
|
3935
3980
|
method: 'POST',
|
|
3936
3981
|
headers: { 'Content-Type': 'application/json' },
|
|
3937
|
-
body: JSON.stringify(
|
|
3938
|
-
projectSlug,
|
|
3939
|
-
datasetId,
|
|
3940
|
-
appId,
|
|
3941
|
-
versionNum,
|
|
3942
|
-
title,
|
|
3943
|
-
memberIds: assignees,
|
|
3944
|
-
publish,
|
|
3945
|
-
instructions,
|
|
3946
|
-
requiredAssignmentsPerRow: labelsPerItem,
|
|
3947
|
-
}),
|
|
3982
|
+
body: JSON.stringify(requestBody),
|
|
3948
3983
|
})
|
|
3949
3984
|
|
|
3950
3985
|
if (!response.ok) {
|
|
@@ -3982,13 +4017,16 @@ async function createTask() {
|
|
|
3982
4017
|
taskUrl: `${getBaseUrl()}/d/${projectSlug}/tasks/${data.task.id}`,
|
|
3983
4018
|
title: data.task.title,
|
|
3984
4019
|
status: data.task.status,
|
|
4020
|
+
assignmentMode: explicitAssignments ? 'custom' : 'auto',
|
|
3985
4021
|
requiredAssignmentsPerRow: data.task.requiredAssignmentsPerRow,
|
|
3986
4022
|
assignmentsCreated: data.assignmentsCreated,
|
|
3987
4023
|
draft: data.task.status === 'draft',
|
|
3988
4024
|
...(data.task.status === 'draft'
|
|
3989
4025
|
? {
|
|
3990
4026
|
message: 'Draft created. Test it manually before assigning.',
|
|
3991
|
-
publishCommand:
|
|
4027
|
+
publishCommand: explicitAssignments
|
|
4028
|
+
? `orizu tasks publish --task ${data.task.id} --assignment-file ${assignmentFile ? shellQuote(assignmentFile) : '<path>'}`
|
|
4029
|
+
: `orizu tasks publish --task ${data.task.id} --assignees <userId1,userId2>`,
|
|
3992
4030
|
}
|
|
3993
4031
|
: {}),
|
|
3994
4032
|
...(data.assignmentShortfall !== undefined ? { assignmentShortfall: data.assignmentShortfall } : {}),
|
|
@@ -4005,12 +4043,17 @@ async function createTask() {
|
|
|
4005
4043
|
`\n Task ID: ${sanitizeTerminalText(data.task.id)}` +
|
|
4006
4044
|
`\n Dataset ID: ${sanitizeTerminalText(datasetId)}` +
|
|
4007
4045
|
`\n Version: v${data.task.versionNum} (${sanitizeTerminalText(data.task.versionId)})` +
|
|
4046
|
+
`\n Mode: ${explicitAssignments ? 'custom row map' : 'auto distribute'}` +
|
|
4008
4047
|
`\n Labels/row: ${data.task.requiredAssignmentsPerRow}` +
|
|
4009
4048
|
`\n Assignments: ${data.assignmentsCreated}` +
|
|
4010
4049
|
(data.warning ? `\n Warning: ${sanitizeTerminalText(data.warning)}` : '') +
|
|
4011
4050
|
`\n URL: ${sanitizeTerminalText(taskUrl)}` +
|
|
4012
4051
|
(isDraft
|
|
4013
|
-
? `\n\nThis task is a draft. Test it manually before assigning.\nPublish after approval:
|
|
4052
|
+
? `\n\nThis task is a draft. Test it manually before assigning.\nPublish after approval: ${
|
|
4053
|
+
explicitAssignments
|
|
4054
|
+
? `orizu tasks publish --task ${sanitizeTerminalText(data.task.id)} --assignment-file ${sanitizeTerminalText(assignmentFile || '<path>')}`
|
|
4055
|
+
: `orizu tasks publish --task ${sanitizeTerminalText(data.task.id)} --assignees <userId1,userId2>`
|
|
4056
|
+
}`
|
|
4014
4057
|
: '')
|
|
4015
4058
|
)
|
|
4016
4059
|
}
|
|
@@ -4018,15 +4061,26 @@ async function createTask() {
|
|
|
4018
4061
|
async function publishTask() {
|
|
4019
4062
|
const taskId = getArg('--task')
|
|
4020
4063
|
const assignees = parseCommaSeparated(getArg('--assignees'))
|
|
4064
|
+
const assignmentFile = getArg('--assignment-file')
|
|
4065
|
+
|
|
4066
|
+
if (assignmentFile && assignees.length > 0) {
|
|
4067
|
+
throw new Error('Use either --assignment-file or --assignees, not both.')
|
|
4068
|
+
}
|
|
4069
|
+
|
|
4070
|
+
const explicitAssignments = readAssignmentManifest(assignmentFile)
|
|
4021
4071
|
|
|
4022
|
-
if (!taskId || assignees.length === 0) {
|
|
4023
|
-
throw new Error('Usage: orizu tasks publish --task <taskId> --assignees <userId1,userId2> [--json]')
|
|
4072
|
+
if (!taskId || (assignees.length === 0 && !explicitAssignments)) {
|
|
4073
|
+
throw new Error('Usage: orizu tasks publish --task <taskId> (--assignees <userId1,userId2> | --assignment-file <path>) [--json]')
|
|
4024
4074
|
}
|
|
4025
4075
|
|
|
4026
4076
|
const response = await authedFetch(`/api/cli/tasks/${encodeURIComponent(taskId)}/publish`, {
|
|
4027
4077
|
method: 'POST',
|
|
4028
4078
|
headers: { 'Content-Type': 'application/json' },
|
|
4029
|
-
body: JSON.stringify(
|
|
4079
|
+
body: JSON.stringify(
|
|
4080
|
+
explicitAssignments
|
|
4081
|
+
? { explicitAssignments }
|
|
4082
|
+
: { memberIds: assignees }
|
|
4083
|
+
),
|
|
4030
4084
|
})
|
|
4031
4085
|
|
|
4032
4086
|
if (!response.ok) {
|
|
@@ -4049,6 +4103,7 @@ async function publishTask() {
|
|
|
4049
4103
|
printLine(JSON.stringify({
|
|
4050
4104
|
taskId: data.task.id,
|
|
4051
4105
|
status: data.task.status,
|
|
4106
|
+
assignmentMode: explicitAssignments ? 'custom' : 'auto',
|
|
4052
4107
|
assignmentsCreated: data.assignmentsCreated,
|
|
4053
4108
|
assignmentsProcessed: data.assignmentsProcessed,
|
|
4054
4109
|
...(data.assignmentsRemoved !== undefined
|
|
@@ -4064,6 +4119,7 @@ async function publishTask() {
|
|
|
4064
4119
|
|
|
4065
4120
|
printLine(
|
|
4066
4121
|
`Published task ${sanitizeTerminalText(data.task.id)} [${sanitizeTerminalText(data.task.status)}]` +
|
|
4122
|
+
`\n Mode: ${explicitAssignments ? 'custom row map' : 'auto distribute'}` +
|
|
4067
4123
|
`\n Assignments: ${data.assignmentsCreated}` +
|
|
4068
4124
|
(data.assignmentsRemoved !== undefined
|
|
4069
4125
|
? `\n Replaced: ${data.assignmentsRemoved} old assignments removed`
|
|
@@ -4075,27 +4131,65 @@ async function publishTask() {
|
|
|
4075
4131
|
async function assignTask() {
|
|
4076
4132
|
const taskId = getArg('--task')
|
|
4077
4133
|
const assignees = parseCommaSeparated(getArg('--assignees'))
|
|
4134
|
+
const assignmentFile = getArg('--assignment-file')
|
|
4078
4135
|
|
|
4079
|
-
if (
|
|
4080
|
-
throw new Error('
|
|
4136
|
+
if (assignmentFile && assignees.length > 0) {
|
|
4137
|
+
throw new Error('Use either --assignment-file or --assignees, not both.')
|
|
4138
|
+
}
|
|
4139
|
+
|
|
4140
|
+
const explicitAssignments = readAssignmentManifest(assignmentFile)
|
|
4141
|
+
|
|
4142
|
+
if (!taskId || (assignees.length === 0 && !explicitAssignments)) {
|
|
4143
|
+
throw new Error('Usage: orizu tasks assign --task <taskId> (--assignees <userId1,userId2> | --assignment-file <path>) [--replace-existing] [--json]')
|
|
4144
|
+
}
|
|
4145
|
+
|
|
4146
|
+
const requestBody: Record<string, unknown> = explicitAssignments
|
|
4147
|
+
? { explicitAssignments }
|
|
4148
|
+
: { memberIds: assignees }
|
|
4149
|
+
if (hasArg('--replace-existing')) {
|
|
4150
|
+
requestBody.replaceExisting = true
|
|
4081
4151
|
}
|
|
4082
4152
|
|
|
4083
4153
|
const response = await authedFetch(`/api/cli/tasks/${encodeURIComponent(taskId)}/assign`, {
|
|
4084
4154
|
method: 'POST',
|
|
4085
4155
|
headers: { 'Content-Type': 'application/json' },
|
|
4086
|
-
body: JSON.stringify(
|
|
4156
|
+
body: JSON.stringify(requestBody),
|
|
4087
4157
|
})
|
|
4088
4158
|
|
|
4089
4159
|
if (!response.ok) {
|
|
4090
4160
|
throw new Error(`Failed to assign task: ${await response.text()}`)
|
|
4091
4161
|
}
|
|
4092
4162
|
|
|
4093
|
-
const data = await parseJsonResponse<{
|
|
4163
|
+
const data = await parseJsonResponse<{
|
|
4164
|
+
assignmentsProcessed?: number
|
|
4165
|
+
assignmentsCreated: number
|
|
4166
|
+
assignmentsRemoved?: number
|
|
4167
|
+
assignmentShortfall?: number
|
|
4168
|
+
warning?: string
|
|
4169
|
+
}>(response, 'Task assign')
|
|
4094
4170
|
if (hasJsonFlag()) {
|
|
4095
|
-
printJson({
|
|
4171
|
+
printJson({
|
|
4172
|
+
assignmentMode: explicitAssignments ? 'custom' : 'auto',
|
|
4173
|
+
assignmentsProcessed: data.assignmentsProcessed ?? data.assignmentsCreated,
|
|
4174
|
+
assignmentsCreated: data.assignmentsCreated,
|
|
4175
|
+
...(data.assignmentsRemoved !== undefined
|
|
4176
|
+
? { assignmentsRemoved: data.assignmentsRemoved }
|
|
4177
|
+
: {}),
|
|
4178
|
+
...(data.assignmentShortfall !== undefined
|
|
4179
|
+
? { assignmentShortfall: data.assignmentShortfall }
|
|
4180
|
+
: {}),
|
|
4181
|
+
...(data.warning ? { warning: data.warning } : {}),
|
|
4182
|
+
})
|
|
4096
4183
|
return
|
|
4097
4184
|
}
|
|
4098
|
-
printLine(
|
|
4185
|
+
printLine(
|
|
4186
|
+
`Created ${data.assignmentsCreated} assignments.` +
|
|
4187
|
+
`\n Mode: ${explicitAssignments ? 'custom row map' : 'auto distribute'}` +
|
|
4188
|
+
(data.assignmentsRemoved !== undefined
|
|
4189
|
+
? `\n Replaced: ${data.assignmentsRemoved} old assignments removed`
|
|
4190
|
+
: '') +
|
|
4191
|
+
(data.warning ? `\n Warning: ${sanitizeTerminalText(data.warning)}` : '')
|
|
4192
|
+
)
|
|
4099
4193
|
}
|
|
4100
4194
|
|
|
4101
4195
|
async function taskStatus() {
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
export interface ParsedAssignmentManifestEntry {
|
|
2
|
+
rowId: string
|
|
3
|
+
assignee: string
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
7
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function parseAssignmentManifestJsonl(
|
|
11
|
+
content: string
|
|
12
|
+
): ParsedAssignmentManifestEntry[] {
|
|
13
|
+
// Keep these JSONL field rules in sync with the server's explicitAssignments parser.
|
|
14
|
+
const entries: ParsedAssignmentManifestEntry[] = []
|
|
15
|
+
const lines = content.split(/\r?\n/)
|
|
16
|
+
|
|
17
|
+
lines.forEach((line, lineIndex) => {
|
|
18
|
+
const trimmed = line.trim()
|
|
19
|
+
if (!trimmed) {
|
|
20
|
+
return
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let parsed: unknown
|
|
24
|
+
try {
|
|
25
|
+
parsed = JSON.parse(trimmed)
|
|
26
|
+
} catch (error) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
`Assignment manifest line ${lineIndex + 1} is not valid JSON`
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!isRecord(parsed)) {
|
|
33
|
+
throw new Error(
|
|
34
|
+
`Assignment manifest line ${lineIndex + 1} must be a JSON object`
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const rowId = typeof parsed.rowId === 'string' ? parsed.rowId.trim() : ''
|
|
39
|
+
if (!rowId) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`Assignment manifest line ${lineIndex + 1} must include a non-empty rowId`
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const hasAssignee = Object.hasOwn(parsed, 'assignee')
|
|
46
|
+
const hasAssignees = Object.hasOwn(parsed, 'assignees')
|
|
47
|
+
|
|
48
|
+
if (hasAssignee && hasAssignees) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`Assignment manifest line ${lineIndex + 1} must provide either assignee or assignees, not both`
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (hasAssignee) {
|
|
55
|
+
const assignee =
|
|
56
|
+
typeof parsed.assignee === 'string' ? parsed.assignee.trim() : ''
|
|
57
|
+
if (!assignee) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
`Assignment manifest line ${lineIndex + 1} assignee must be a non-empty string`
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
entries.push({ rowId, assignee })
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (hasAssignees) {
|
|
67
|
+
if (!Array.isArray(parsed.assignees) || parsed.assignees.length === 0) {
|
|
68
|
+
throw new Error(
|
|
69
|
+
`Assignment manifest line ${lineIndex + 1} assignees must be a non-empty string array`
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
parsed.assignees.forEach((assigneeValue, assigneeIndex) => {
|
|
74
|
+
const assignee =
|
|
75
|
+
typeof assigneeValue === 'string' ? assigneeValue.trim() : ''
|
|
76
|
+
if (!assignee) {
|
|
77
|
+
throw new Error(
|
|
78
|
+
`Assignment manifest line ${lineIndex + 1} assignees[${assigneeIndex}] must be a non-empty string`
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
entries.push({ rowId, assignee })
|
|
82
|
+
})
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
throw new Error(
|
|
87
|
+
`Assignment manifest line ${lineIndex + 1} must include assignee or assignees`
|
|
88
|
+
)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
if (entries.length === 0) {
|
|
92
|
+
throw new Error('Assignment manifest must contain at least one assignment')
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return entries
|
|
96
|
+
}
|
|
@@ -89,6 +89,8 @@ orizu tasks create --project <teamSlug>/<projectSlug> --dataset <datasetId> --ap
|
|
|
89
89
|
|
|
90
90
|
# Open the returned task URL and test the draft manually before assigning.
|
|
91
91
|
orizu tasks publish --task <taskId> --assignees <userId1,userId2>
|
|
92
|
+
# Or publish exact row assignments:
|
|
93
|
+
orizu tasks publish --task <taskId> --assignment-file ./assignments.jsonl
|
|
92
94
|
|
|
93
95
|
orizu tasks status --task <taskId>
|
|
94
96
|
orizu tasks report set --task <taskId> --report-file ./task-report.md
|
|
@@ -175,9 +177,9 @@ Detailed walkthrough — GEPA mechanics, Orizu-tracked optimization, optional DS
|
|
|
175
177
|
# Execution rules
|
|
176
178
|
|
|
177
179
|
- Prefer explicit flags in non-TTY contexts; reserve interactive fallback for TTY.
|
|
178
|
-
- Canonical selectors: `--team`, `--project`, `--app`, `--task`, `--dataset`, `--assignees`.
|
|
179
|
-
- `tasks create` creates drafts by default. Test the returned task URL manually, then use `tasks publish --task <taskId> --assignees <userId1,userId2>` after approval.
|
|
180
|
-
- `tasks assign` and `tasks publish` take user IDs, not emails.
|
|
180
|
+
- Canonical selectors: `--team`, `--project`, `--app`, `--task`, `--dataset`, `--assignees`, `--assignment-file`.
|
|
181
|
+
- `tasks create` creates drafts by default. Test the returned task URL manually, then use `tasks publish --task <taskId> --assignees <userId1,userId2>` or `tasks publish --task <taskId> --assignment-file <path>` after approval.
|
|
182
|
+
- `tasks assign --assignees` and `tasks publish --assignees` take user IDs, not emails. `--assignment-file` accepts emails or user IDs in JSONL rows shaped as `{ "rowId": "...", "assignee": "..." }` or `{ "rowId": "...", "assignees": ["..."] }`.
|
|
181
183
|
- `datasets edit-rows` requires a non-empty string `id` on each row in `--file`.
|
|
182
184
|
- `datasets delete-rows` uses `--row-ids` as the canonical selector.
|
|
183
185
|
- `datasets delete` requires interactive terminal confirmation; there is no non-interactive confirmation option.
|
|
@@ -186,23 +186,28 @@ orizu tasks create \
|
|
|
186
186
|
Behavior:
|
|
187
187
|
- task creation creates a draft by default and returns a task URL to test manually before assigning
|
|
188
188
|
- use `--publish --assignees <userIdOrEmail1,userIdOrEmail2>` only when you intentionally want to create and ship immediately
|
|
189
|
+
- use `--assignment-file <path>` instead of `--assignees` when specific rows should go to specific labellers
|
|
189
190
|
- after manually approving a draft, run `orizu tasks publish --task <taskId> --assignees <userId1,userId2>`
|
|
191
|
+
- or run `orizu tasks publish --task <taskId> --assignment-file <path>` to publish the exact row map
|
|
190
192
|
- task creation resolves and stores the app's pinned current `version_id` at create time
|
|
191
193
|
- downstream consumers (exports, judges, optimization) should trust the task's pinned `version_id`, not the app's current pointer
|
|
192
194
|
- dataset compatibility is validated against that pinned app version before the task is inserted
|
|
193
195
|
- malformed JSON and mixed-type assignee arrays fail with deterministic `400` responses
|
|
194
196
|
- assignment fanout enforces unique `(assignee, row)` pairs; `--labels-per-item` cannot exceed the number of unique assignees, and the backend shortfalls instead of duplicating
|
|
197
|
+
- explicit assignment manifests are JSONL with `rowId` plus `assignee` or `assignees`; row IDs are canonical dataset row `id` values, assignees may be emails or user IDs, and V1 publish requires whole-dataset uniform coverage
|
|
195
198
|
|
|
196
199
|
Publish:
|
|
197
200
|
|
|
198
201
|
```bash
|
|
199
202
|
orizu tasks publish --task <taskId> --assignees <userId1,userId2>
|
|
203
|
+
orizu tasks publish --task <taskId> --assignment-file ./assignments.jsonl
|
|
200
204
|
```
|
|
201
205
|
|
|
202
206
|
Assign:
|
|
203
207
|
|
|
204
208
|
```bash
|
|
205
209
|
orizu tasks assign --task <taskId> --assignees <userId1,userId2>
|
|
210
|
+
orizu tasks assign --task <taskId> --assignment-file ./assignments.jsonl --replace-existing
|
|
206
211
|
```
|
|
207
212
|
|
|
208
213
|
Status:
|
|
@@ -301,6 +306,8 @@ orizu tasks create \
|
|
|
301
306
|
|
|
302
307
|
# Open the returned task URL and test the draft manually before assigning.
|
|
303
308
|
orizu tasks publish --task <taskId> --assignees <userId1,userId2>
|
|
309
|
+
# Or publish a deterministic row map:
|
|
310
|
+
orizu tasks publish --task <taskId> --assignment-file ./assignments.jsonl
|
|
304
311
|
|
|
305
312
|
orizu tasks status --task <taskId>
|
|
306
313
|
orizu tasks export --task <taskId> --format csv --out ./support-round1.csv
|
|
@@ -321,7 +328,8 @@ Use these shortcuts only in TTY environments where prompts can run.
|
|
|
321
328
|
|
|
322
329
|
- `tasks create` creates a draft by default, does not require `--assignees`, and pins the app's current version when the task is created.
|
|
323
330
|
- `tasks create --publish --assignees <...>` intentionally creates and ships immediately.
|
|
324
|
-
- `tasks assign` and
|
|
331
|
+
- `tasks create|publish|assign --assignment-file <path>` is mutually exclusive with `--assignees` and accepts emails or user IDs in the JSONL manifest.
|
|
332
|
+
- `tasks assign --assignees` and `tasks publish --assignees` expect user IDs, not emails.
|
|
325
333
|
- Assignment queue reads are assignee-self-only; use task status/export as the operator summary path.
|
|
326
334
|
- Assignment completion payloads are validated against the pinned app-version `output_json_schema`.
|
|
327
335
|
- `datasets delete-rows` requires `--row-ids`.
|