@skrr-ai/cli 0.1.107 → 0.1.109
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/README.md +10 -0
- package/dist/base-command.d.ts +59 -0
- package/dist/base-command.js +118 -7
- package/dist/commands/agents/update.d.ts +14 -0
- package/dist/commands/agents/update.js +40 -1
- package/dist/commands/commitments/accounting.js +7 -2
- package/dist/commands/commitments/cost.d.ts +11 -0
- package/dist/commands/commitments/cost.js +26 -3
- package/dist/commands/commitments/create.js +2 -1
- package/dist/commands/commitments/operating-review.d.ts +15 -0
- package/dist/commands/commitments/operating-review.js +103 -50
- package/dist/commands/commitments/update.js +12 -5
- package/dist/commands/doctor.js +9 -0
- package/dist/commands/goals/list.js +6 -1
- package/dist/commands/grants/rotate.js +4 -0
- package/dist/commands/login.js +42 -14
- package/dist/commands/machines/list.js +28 -6
- package/dist/commands/projects/show.js +1 -1
- package/dist/commands/revoke-daemon.js +2 -1
- package/dist/commands/spaces/workflows/archive.d.ts +1 -0
- package/dist/commands/spaces/workflows/archive.js +1 -0
- package/dist/commands/spaces/workflows/artifact.d.ts +1 -0
- package/dist/commands/spaces/workflows/artifact.js +1 -0
- package/dist/commands/spaces/workflows/cancel-node.d.ts +1 -0
- package/dist/commands/spaces/workflows/cancel-node.js +1 -0
- package/dist/commands/spaces/workflows/cancel.d.ts +1 -0
- package/dist/commands/spaces/workflows/cancel.js +6 -2
- package/dist/commands/spaces/workflows/create.d.ts +1 -0
- package/dist/commands/spaces/workflows/create.js +7 -1
- package/dist/commands/spaces/workflows/events.d.ts +1 -0
- package/dist/commands/spaces/workflows/events.js +1 -0
- package/dist/commands/spaces/workflows/fixtures/create.d.ts +1 -0
- package/dist/commands/spaces/workflows/fixtures/create.js +1 -0
- package/dist/commands/spaces/workflows/fixtures/list.d.ts +1 -0
- package/dist/commands/spaces/workflows/fixtures/list.js +1 -0
- package/dist/commands/spaces/workflows/lifecycle-diagnostics.d.ts +1 -0
- package/dist/commands/spaces/workflows/lifecycle-diagnostics.js +1 -0
- package/dist/commands/spaces/workflows/list.d.ts +1 -0
- package/dist/commands/spaces/workflows/list.js +1 -0
- package/dist/commands/spaces/workflows/node-types.d.ts +2 -0
- package/dist/commands/spaces/workflows/node-types.js +24 -6
- package/dist/commands/spaces/workflows/pause.d.ts +1 -0
- package/dist/commands/spaces/workflows/pause.js +1 -0
- package/dist/commands/spaces/workflows/plan/show.d.ts +1 -0
- package/dist/commands/spaces/workflows/plan/show.js +1 -0
- package/dist/commands/spaces/workflows/plan/update.d.ts +1 -0
- package/dist/commands/spaces/workflows/plan/update.js +1 -0
- package/dist/commands/spaces/workflows/proposals/apply.d.ts +1 -0
- package/dist/commands/spaces/workflows/proposals/apply.js +1 -0
- package/dist/commands/spaces/workflows/proposals/confirm.d.ts +1 -0
- package/dist/commands/spaces/workflows/proposals/confirm.js +1 -0
- package/dist/commands/spaces/workflows/proposals/create.d.ts +1 -0
- package/dist/commands/spaces/workflows/proposals/create.js +1 -0
- package/dist/commands/spaces/workflows/proposals/dismiss.d.ts +1 -0
- package/dist/commands/spaces/workflows/proposals/dismiss.js +1 -0
- package/dist/commands/spaces/workflows/proposals/list.d.ts +1 -0
- package/dist/commands/spaces/workflows/proposals/list.js +1 -0
- package/dist/commands/spaces/workflows/publish.d.ts +1 -0
- package/dist/commands/spaces/workflows/publish.js +9 -0
- package/dist/commands/spaces/workflows/reconcile.d.ts +1 -0
- package/dist/commands/spaces/workflows/reconcile.js +1 -0
- package/dist/commands/spaces/workflows/resume.d.ts +1 -0
- package/dist/commands/spaces/workflows/resume.js +1 -0
- package/dist/commands/spaces/workflows/retry.d.ts +1 -0
- package/dist/commands/spaces/workflows/retry.js +1 -0
- package/dist/commands/spaces/workflows/run.d.ts +1 -0
- package/dist/commands/spaces/workflows/run.js +14 -7
- package/dist/commands/spaces/workflows/runs.d.ts +1 -0
- package/dist/commands/spaces/workflows/runs.js +1 -0
- package/dist/commands/spaces/workflows/show.d.ts +25 -0
- package/dist/commands/spaces/workflows/show.js +42 -4
- package/dist/commands/spaces/workflows/signal.d.ts +12 -0
- package/dist/commands/spaces/workflows/signal.js +40 -2
- package/dist/commands/spaces/workflows/start.d.ts +1 -0
- package/dist/commands/spaces/workflows/start.js +1 -0
- package/dist/commands/spaces/workflows/task-execution/create.d.ts +1 -0
- package/dist/commands/spaces/workflows/task-execution/create.js +1 -0
- package/dist/commands/spaces/workflows/test.d.ts +1 -0
- package/dist/commands/spaces/workflows/test.js +1 -0
- package/dist/commands/spaces/workflows/triggers/create.d.ts +1 -0
- package/dist/commands/spaces/workflows/triggers/create.js +1 -0
- package/dist/commands/spaces/workflows/triggers/fire.d.ts +1 -0
- package/dist/commands/spaces/workflows/triggers/fire.js +1 -0
- package/dist/commands/spaces/workflows/triggers/list.d.ts +1 -0
- package/dist/commands/spaces/workflows/triggers/list.js +1 -0
- package/dist/commands/spaces/workflows/triggers/update.d.ts +1 -0
- package/dist/commands/spaces/workflows/triggers/update.js +1 -0
- package/dist/commands/spaces/workflows/update.d.ts +1 -0
- package/dist/commands/spaces/workflows/update.js +4 -0
- package/dist/commands/spaces/workflows/validate.d.ts +1 -0
- package/dist/commands/spaces/workflows/validate.js +15 -0
- package/dist/commands/tasks/activity.js +6 -2
- package/dist/commands/tasks/bulk-update.d.ts +1 -0
- package/dist/commands/tasks/bulk-update.js +7 -0
- package/dist/commands/tasks/complete.js +2 -1
- package/dist/commands/tasks/create-tree.d.ts +69 -0
- package/dist/commands/tasks/create-tree.js +413 -47
- package/dist/commands/tasks/list.d.ts +2 -0
- package/dist/commands/tasks/list.js +19 -1
- package/dist/commands/tasks/rearm-recurrence.d.ts +22 -0
- package/dist/commands/tasks/rearm-recurrence.js +68 -0
- package/dist/commands/tasks/show.d.ts +1 -0
- package/dist/commands/tasks/show.js +17 -3
- package/dist/commands/tasks/timeline.js +4 -0
- package/dist/commands/tasks/update.d.ts +1 -0
- package/dist/commands/tasks/update.js +6 -1
- package/dist/commands/triggers/create.js +8 -2
- package/dist/commands/triggers/parse-intent.d.ts +7 -0
- package/dist/commands/triggers/parse-intent.js +31 -9
- package/dist/commands/workspaces/grants.js +23 -3
- package/dist/commands/workspaces/members.d.ts +20 -0
- package/dist/commands/workspaces/members.js +64 -0
- package/dist/commands/workspaces/stewardship.js +14 -2
- package/dist/help.js +12 -0
- package/dist/hooks/command-not-found.js +27 -1
- package/dist/lib/command-miss.d.ts +52 -2
- package/dist/lib/command-miss.js +187 -18
- package/dist/lib/commitment-loop-status.d.ts +8 -0
- package/dist/lib/commitment-loop-status.js +45 -0
- package/dist/lib/commitment-product.js +13 -1
- package/dist/lib/commitments.d.ts +16 -2
- package/dist/lib/commitments.js +48 -8
- package/dist/lib/daemon-command-hint.d.ts +15 -0
- package/dist/lib/daemon-command-hint.js +21 -0
- package/dist/lib/daemon-setup.js +10 -1
- package/dist/lib/daemonBrokerRefusal.js +3 -2
- package/dist/lib/daemonHandoff.js +2 -1
- package/dist/lib/deployed-build.d.ts +43 -0
- package/dist/lib/deployed-build.js +66 -0
- package/dist/lib/grants.d.ts +27 -1
- package/dist/lib/grants.js +39 -21
- package/dist/lib/json-mode.d.ts +11 -0
- package/dist/lib/json-mode.js +19 -0
- package/dist/lib/json-output.d.ts +25 -0
- package/dist/lib/json-output.js +30 -0
- package/dist/lib/list-output.d.ts +9 -0
- package/dist/lib/list-output.js +20 -1
- package/dist/lib/local-daemon-readiness.d.ts +90 -0
- package/dist/lib/local-daemon-readiness.js +143 -0
- package/dist/lib/machines.d.ts +117 -0
- package/dist/lib/machines.js +139 -0
- package/dist/lib/managed-instruction-local.d.ts +2 -0
- package/dist/lib/managed-instruction-local.js +2 -0
- package/dist/lib/profile-machine-identity-guard.d.ts +18 -0
- package/dist/lib/profile-machine-identity-guard.js +29 -0
- package/dist/lib/request-field-flags.d.ts +20 -0
- package/dist/lib/request-field-flags.js +34 -0
- package/dist/lib/stewardship-output.d.ts +1 -1
- package/dist/lib/stewardship-output.js +26 -30
- package/dist/lib/task-batch.d.ts +61 -4
- package/dist/lib/task-batch.js +316 -80
- package/dist/lib/task-economics.d.ts +5 -0
- package/dist/lib/task-economics.js +15 -0
- package/dist/lib/task-recurrence-outcome.d.ts +6 -6
- package/dist/lib/task-recurrence-outcome.js +27 -3
- package/dist/lib/tasks.d.ts +39 -0
- package/dist/lib/tasks.js +132 -0
- package/dist/lib/triggers.d.ts +31 -1
- package/dist/lib/triggers.js +55 -1
- package/dist/lib/workflow-issues.d.ts +14 -0
- package/dist/lib/workflow-issues.js +32 -0
- package/dist/lib/workflow-run-view.d.ts +55 -0
- package/dist/lib/workflow-run-view.js +105 -0
- package/dist/lib/workspaces.d.ts +31 -4
- package/dist/lib/workspaces.js +43 -0
- package/dist/lib/write-target.d.ts +29 -0
- package/dist/lib/write-target.js +72 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/workspaceIsolationProbeWire.d.ts +51 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/workspaceIsolationProbeWire.js +50 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/workspaceIsolationProbeWire.d.ts +51 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/workspaceIsolationProbeWire.js +46 -0
- package/dist/node_modules/@skrr-ai/auth-core/package.json +11 -1
- package/dist/node_modules/@skrr-ai/data-provider/index.js +7896 -7524
- package/oclif.manifest.json +376 -52
- package/package.json +19 -1
|
@@ -5,6 +5,8 @@ exports.rerunWarning = rerunWarning;
|
|
|
5
5
|
exports.derivedTreePrefix = derivedTreePrefix;
|
|
6
6
|
exports.derivedPrefixWarning = derivedPrefixWarning;
|
|
7
7
|
exports.partialFailureResult = partialFailureResult;
|
|
8
|
+
exports.orderForCreation = orderForCreation;
|
|
9
|
+
exports.describeDefaultAgentAssignment = describeDefaultAgentAssignment;
|
|
8
10
|
exports.normalizeTreeRoots = normalizeTreeRoots;
|
|
9
11
|
exports.planTree = planTree;
|
|
10
12
|
const core_1 = require("@oclif/core");
|
|
@@ -13,6 +15,7 @@ const data_provider_1 = require("@skrr-ai/data-provider");
|
|
|
13
15
|
const base_command_1 = require("../../base-command");
|
|
14
16
|
const prompt_1 = require("../../lib/prompt");
|
|
15
17
|
const task_batch_1 = require("../../lib/task-batch");
|
|
18
|
+
const write_target_1 = require("../../lib/write-target");
|
|
16
19
|
const tasks_1 = require("../../lib/tasks");
|
|
17
20
|
/**
|
|
18
21
|
* Nodes a re-run could not recognize: no externalKey of their own and no
|
|
@@ -52,7 +55,7 @@ function derivedPrefixWarning(prefix) {
|
|
|
52
55
|
return {
|
|
53
56
|
code: 'TREE_PREFIX_DERIVED',
|
|
54
57
|
value: prefix,
|
|
55
|
-
hint: `No --external-key-prefix was given, so nodes are keyed "${prefix}/<tempId>" from the file's content: ` +
|
|
58
|
+
hint: `No --external-key-prefix was given, so nodes without their own externalKey are keyed "${prefix}/<tempId>" from the file's content: ` +
|
|
56
59
|
're-running the same file updates these tasks instead of creating copies. Pass --fresh to create a second copy, ' +
|
|
57
60
|
'or --external-key-prefix to choose your own keys.',
|
|
58
61
|
};
|
|
@@ -60,14 +63,27 @@ function derivedPrefixWarning(prefix) {
|
|
|
60
63
|
/**
|
|
61
64
|
* What a half-applied tree reports (E4). The ids already created are the only
|
|
62
65
|
* way to continue or clean up, and the failing call used to print none of them.
|
|
66
|
+
*
|
|
67
|
+
* Every confirmed id is in `mapping` however the run ended, and `summary` counts
|
|
68
|
+
* the three things a failure can leave: confirmed, UNKNOWN (submitted, outcome
|
|
69
|
+
* not established — it may exist) and not attempted. They are different next
|
|
70
|
+
* steps, and a count that folded them together was the thing the first report
|
|
71
|
+
* of OSK-S-440 could not rely on.
|
|
63
72
|
*/
|
|
64
73
|
function partialFailureResult(args) {
|
|
65
74
|
const { results, mapping, failedAt, total, prefix, warnings } = args;
|
|
75
|
+
const outcomes = (args.failures ?? []).map((f) => String(f.outcome));
|
|
66
76
|
return {
|
|
67
77
|
object: 'task_create_tree_result',
|
|
68
78
|
ok: false,
|
|
69
79
|
count: results.length,
|
|
70
80
|
total,
|
|
81
|
+
summary: {
|
|
82
|
+
confirmed: results.length,
|
|
83
|
+
unknown: outcomes.filter((o) => o === 'unknown').length,
|
|
84
|
+
notAttempted: outcomes.filter((o) => o === 'not_attempted').length,
|
|
85
|
+
failed: outcomes.filter((o) => o === 'failed').length,
|
|
86
|
+
},
|
|
71
87
|
created: results.map((r) => ({
|
|
72
88
|
tempId: r.tempId,
|
|
73
89
|
parentTempId: r.parentTempId,
|
|
@@ -80,18 +96,84 @@ function partialFailureResult(args) {
|
|
|
80
96
|
failedAt,
|
|
81
97
|
...(args.failures ? { failures: args.failures } : {}),
|
|
82
98
|
rerun: prefix || args.keyed
|
|
83
|
-
? 'Re-run the same keyed file with --resume to confirm existing IDs and create only missing nodes. A submitted write may still finish; stable keys prevent duplicates.'
|
|
99
|
+
? 'Re-run the same keyed file with --resume to confirm existing IDs and create only missing nodes; --verify lists what exists and what is missing without writing. A submitted write may still finish; stable keys prevent duplicates.'
|
|
84
100
|
: 'Re-running creates un-keyed nodes again. Use mapping to continue by hand, or re-run with --external-key-prefix.',
|
|
101
|
+
...(args.next && (prefix || args.keyed) ? { next: args.next } : {}),
|
|
85
102
|
...(warnings.length > 0 ? { warnings } : {}),
|
|
86
103
|
};
|
|
87
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* Order nodes so everything a node depends on — its parent and every blocker —
|
|
107
|
+
* comes BEFORE it, keeping input order otherwise. The batch and the serial path
|
|
108
|
+
* both create in this order, so a parent id and a blocker id exist by the time a
|
|
109
|
+
* node that needs them is written.
|
|
110
|
+
*
|
|
111
|
+
* Also the place a bad `blockedByTempIds` is caught: before any request, naming
|
|
112
|
+
* the node, rather than as a per-row refusal after half the tree exists.
|
|
113
|
+
*/
|
|
114
|
+
function orderForCreation(planned) {
|
|
115
|
+
const byKey = new Map(planned.map((node) => [node.key, node]));
|
|
116
|
+
const errors = [];
|
|
117
|
+
const dependenciesOf = (node) => {
|
|
118
|
+
const out = [];
|
|
119
|
+
if (node.parentKey)
|
|
120
|
+
out.push(node.parentKey);
|
|
121
|
+
for (const blocker of node.blockerKeys ?? []) {
|
|
122
|
+
if (blocker === node.key) {
|
|
123
|
+
errors.push(`${node.key}: a node cannot block itself`);
|
|
124
|
+
}
|
|
125
|
+
else if (!byKey.has(blocker)) {
|
|
126
|
+
errors.push(`${node.key}: blockedByTempIds "${blocker}" does not match any node's tempId in this file ` +
|
|
127
|
+
'(to block on a task that already exists, give its id in blockedByIds)');
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
out.push(blocker);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return out;
|
|
134
|
+
};
|
|
135
|
+
const ordered = [];
|
|
136
|
+
const state = new Map();
|
|
137
|
+
const path = [];
|
|
138
|
+
const visit = (node) => {
|
|
139
|
+
const current = state.get(node.key);
|
|
140
|
+
if (current === 'done')
|
|
141
|
+
return;
|
|
142
|
+
if (current === 'visiting') {
|
|
143
|
+
const loop = [...path.slice(path.indexOf(node.key)), node.key];
|
|
144
|
+
errors.push(`dependency cycle: ${loop.join(' -> ')}`);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
state.set(node.key, 'visiting');
|
|
148
|
+
path.push(node.key);
|
|
149
|
+
for (const dependency of dependenciesOf(node)) {
|
|
150
|
+
const next = byKey.get(dependency);
|
|
151
|
+
if (next)
|
|
152
|
+
visit(next);
|
|
153
|
+
}
|
|
154
|
+
path.pop();
|
|
155
|
+
state.set(node.key, 'done');
|
|
156
|
+
ordered.push(node);
|
|
157
|
+
};
|
|
158
|
+
for (const node of planned)
|
|
159
|
+
visit(node);
|
|
160
|
+
return { ordered: errors.length > 0 ? planned : ordered, errors: [...new Set(errors)] };
|
|
161
|
+
}
|
|
88
162
|
class TasksCreateTree extends base_command_1.BaseCommand {
|
|
89
|
-
static description = 'Create a task tree from JSON
|
|
163
|
+
static description = 'Create a task tree from JSON and make every node exist. Recommended shape: ' +
|
|
164
|
+
'{"nodes":[{"tempId":"root","title":"Task","executionMode":"human","assigneeUserIds":["<user-id>"]}]} ' +
|
|
165
|
+
'(a "nodes" array, each node with optional "children"; {"root":{...,"children":[...]}} is accepted too). ' +
|
|
166
|
+
'Put "me" in assigneeUserIds for yourself and "blockedByTempIds":["<tempId>"] for dependencies. ' +
|
|
167
|
+
'Setting executionMode human with an explicit assignee avoids inheriting a Space lead agent. ' +
|
|
168
|
+
'Nodes with an externalKey (or under --external-key-prefix) are created once: a re-run, --resume or --verify finds them again.';
|
|
90
169
|
static examples = [
|
|
91
170
|
'<%= config.bin %> tasks create-tree --from-json tree.json --space <space-id> --json',
|
|
92
171
|
'<%= config.bin %> tasks create-tree --from-json tree.json --space <space-id> --default-agent auto',
|
|
93
172
|
'<%= config.bin %> tasks create-tree --from-json tree.json --external-key-prefix recursive-learning/ws0',
|
|
94
173
|
'<%= config.bin %> tasks create-tree --from-json tree.json --dry-run --json',
|
|
174
|
+
'<%= config.bin %> tasks create-tree --from-json tree.json --external-key-prefix mine/ --verify --json',
|
|
175
|
+
'<%= config.bin %> tasks create-tree --from-json tree.json --external-key-prefix mine/ --resume',
|
|
176
|
+
'<%= config.bin %> tasks create-tree --from-json tree.json --space <space-id> --project OSK-P-1 --expect-target skrr.ai',
|
|
95
177
|
];
|
|
96
178
|
static flags = {
|
|
97
179
|
json: core_1.Flags.boolean({ description: 'Output the create-tree result as JSON' }),
|
|
@@ -99,7 +181,13 @@ class TasksCreateTree extends base_command_1.BaseCommand {
|
|
|
99
181
|
description: 'Path to a JSON file (or "-" for stdin) describing the tree',
|
|
100
182
|
required: true,
|
|
101
183
|
}),
|
|
102
|
-
'dry-run': core_1.Flags.boolean({
|
|
184
|
+
'dry-run': core_1.Flags.boolean({
|
|
185
|
+
description: 'Validate and print the plan without creating tasks. Each node says whether it would be created or already exists.',
|
|
186
|
+
}),
|
|
187
|
+
verify: core_1.Flags.boolean({
|
|
188
|
+
description: 'Read-only: report which nodes of this tree already exist (matched by externalKey) and which are missing, and write nothing. Exits 1 when any is missing or cannot be checked.',
|
|
189
|
+
exclusive: ['fresh', 'resume', 'dry-run', 'no-batch'],
|
|
190
|
+
}),
|
|
103
191
|
'external-key-prefix': core_1.Flags.string({
|
|
104
192
|
description: 'Generate externalKey for nodes that omit one; enables idempotent re-runs. Defaults to a prefix derived from the file content (see --fresh)',
|
|
105
193
|
}),
|
|
@@ -110,11 +198,18 @@ class TasksCreateTree extends base_command_1.BaseCommand {
|
|
|
110
198
|
description: 'Confirm existing stable-key nodes without patching them, then create only missing nodes. Use the unchanged keyed file after an incomplete result.',
|
|
111
199
|
exclusive: ['fresh'],
|
|
112
200
|
}),
|
|
201
|
+
'no-batch': core_1.Flags.boolean({
|
|
202
|
+
description: 'Create one task per request instead of through the batch route. Slower; use it to rule the batch route out. Nothing else needs it: a batch that the server answers in part is continued automatically.',
|
|
203
|
+
}),
|
|
113
204
|
'default-agent': core_1.Flags.string({
|
|
114
205
|
description: 'Override the automatic assignee for nodes with no assignee. Use "auto" to inherit the space lead agent; children inherit parent assignees first.',
|
|
115
206
|
}),
|
|
116
207
|
space: core_1.Flags.string({ description: 'Default space ID for all nodes' }),
|
|
117
208
|
goal: core_1.Flags.string({ description: 'Default goal ID for all nodes' }),
|
|
209
|
+
project: core_1.Flags.string({
|
|
210
|
+
description: "Default project for all nodes: its id, its ref (OSK-P-3) or the externalKey it was filed under. A ref or key is read inside the node's space, so give --space (or a spaceId on the nodes).",
|
|
211
|
+
}),
|
|
212
|
+
...write_target_1.expectTargetFlag,
|
|
118
213
|
};
|
|
119
214
|
async run() {
|
|
120
215
|
this.requireAuth();
|
|
@@ -140,49 +235,82 @@ class TasksCreateTree extends base_command_1.BaseCommand {
|
|
|
140
235
|
? undefined
|
|
141
236
|
: derivedTreePrefix(input, topLevelDefaults);
|
|
142
237
|
const keyPrefix = flags['external-key-prefix'] ?? derivedPrefix;
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
238
|
+
let planned;
|
|
239
|
+
try {
|
|
240
|
+
planned = planTree(roots, {
|
|
241
|
+
defaults: topLevelDefaults,
|
|
242
|
+
externalKeyPrefix: keyPrefix,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
catch (err) {
|
|
246
|
+
this.error(err.message, { exit: 1 });
|
|
247
|
+
}
|
|
248
|
+
// Only a node that actually took its key from the derived prefix makes that
|
|
249
|
+
// prefix worth a warning. A file whose every node names its own key (the
|
|
250
|
+
// shape an agent writes) was told about a prefix it never used.
|
|
251
|
+
const usedDerivedPrefix = Boolean(derivedPrefix) && planned.some((node) => node.keyDerived);
|
|
147
252
|
const defaultAgentErrors = [];
|
|
253
|
+
const ordering = orderForCreation(planned);
|
|
254
|
+
defaultAgentErrors.push(...ordering.errors);
|
|
255
|
+
planned = ordering.ordered;
|
|
148
256
|
const defaultAgentResolutions = {};
|
|
149
257
|
const plannedByKey = new Map(planned.map((item) => [item.key, item]));
|
|
150
258
|
const spaceCache = new Map();
|
|
151
259
|
const parentTaskCache = new Map();
|
|
152
260
|
const projectCache = new Map();
|
|
153
261
|
const existingByKey = new Map();
|
|
262
|
+
// 1. Where does each node land? A parent or a project can decide the Space,
|
|
263
|
+
// and a key means a task only within its Space — so scope comes first.
|
|
264
|
+
const scopes = new Map();
|
|
154
265
|
for (const item of planned) {
|
|
155
|
-
let scope;
|
|
156
266
|
try {
|
|
157
|
-
|
|
158
|
-
parentTaskCache,
|
|
159
|
-
projectCache,
|
|
160
|
-
});
|
|
267
|
+
scopes.set(item.key, await (0, tasks_1.resolveTaskCreateScope)(item.payload, { parentTaskCache, projectCache }));
|
|
161
268
|
}
|
|
162
269
|
catch (err) {
|
|
163
270
|
defaultAgentErrors.push(`${item.key}: ${err.message}`);
|
|
164
|
-
continue;
|
|
165
271
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
272
|
+
}
|
|
273
|
+
// 2. Which nodes already exist? One bulk read, not one request per node.
|
|
274
|
+
const lookups = planned
|
|
275
|
+
.filter((item) => scopes.has(item.key) &&
|
|
276
|
+
typeof item.payload.externalKey === 'string' &&
|
|
277
|
+
item.payload.externalKey.trim())
|
|
278
|
+
.map((item) => ({
|
|
279
|
+
key: item.key,
|
|
280
|
+
lookup: {
|
|
281
|
+
externalKey: item.payload.externalKey,
|
|
282
|
+
spaceId: item.payload.spaceId,
|
|
283
|
+
goalId: item.payload.goalId,
|
|
284
|
+
},
|
|
285
|
+
}));
|
|
286
|
+
const hits = await (0, tasks_1.findTasksByExternalKeys)(lookups.map((entry) => entry.lookup));
|
|
287
|
+
const unverifiable = [];
|
|
288
|
+
for (const { key, lookup } of lookups) {
|
|
289
|
+
const hit = hits.get((0, tasks_1.externalKeyLookupId)(lookup));
|
|
290
|
+
if (hit?.error) {
|
|
291
|
+
defaultAgentErrors.push(`${key}: ${hit.error.message}`);
|
|
292
|
+
unverifiable.push({ tempId: key, reason: hit.error.message });
|
|
184
293
|
}
|
|
185
|
-
|
|
294
|
+
else if (hit && hit.duplicates.length > 0) {
|
|
295
|
+
defaultAgentErrors.push(`${key}: externalKey ${lookup.externalKey} matched multiple tasks`);
|
|
296
|
+
unverifiable.push({ tempId: key, reason: 'externalKey matched multiple tasks' });
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
existingByKey.set(key, hit?.task ?? null);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
if (flags.verify) {
|
|
303
|
+
await this.verifyTree(planned, existingByKey, unverifiable, flags);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
// 3. Who owns each node? `me`, names and emails become ids first, so every
|
|
307
|
+
// decision below sees what will actually be written.
|
|
308
|
+
for (const item of planned) {
|
|
309
|
+
const scope = scopes.get(item.key);
|
|
310
|
+
if (!scope)
|
|
311
|
+
continue;
|
|
312
|
+
await this.resolveAssigneesInPayload(item.payload);
|
|
313
|
+
const existingTask = existingByKey.get(item.key) ?? null;
|
|
186
314
|
const parentPayload = item.parentKey
|
|
187
315
|
? (existingByKey.get(item.parentKey) ?? plannedByKey.get(item.parentKey)?.payload)
|
|
188
316
|
: scope.parentTask;
|
|
@@ -218,7 +346,10 @@ class TasksCreateTree extends base_command_1.BaseCommand {
|
|
|
218
346
|
const errors = [
|
|
219
347
|
...defaultAgentErrors,
|
|
220
348
|
...(flags.resume && unkeyedNodes(planned).length
|
|
221
|
-
? [
|
|
349
|
+
? [
|
|
350
|
+
'--resume requires an externalKey for every node; supply --external-key-prefix and do not use --fresh',
|
|
351
|
+
]
|
|
352
|
+
: []),
|
|
222
353
|
...planned.flatMap(({ key, payload }) => (0, tasks_1.validateTaskCreatePayload)(payload).map((error) => `${key}: ${error}`)),
|
|
223
354
|
];
|
|
224
355
|
if (errors.length > 0) {
|
|
@@ -230,6 +361,12 @@ class TasksCreateTree extends base_command_1.BaseCommand {
|
|
|
230
361
|
this.exit(1);
|
|
231
362
|
return;
|
|
232
363
|
}
|
|
364
|
+
// An agent that would be put in charge by DEFAULT — the session's own agent,
|
|
365
|
+
// a parent's, the space lead — is the one assignment a person did not write
|
|
366
|
+
// and may not expect, and an agent with auto-execution may start work on it.
|
|
367
|
+
// Say so before anything is written, not only inside a dry run (OSK-S-440 #9).
|
|
368
|
+
const agentAssignment = describeDefaultAgentAssignment(defaultAgentResolutions);
|
|
369
|
+
const noteworthyAssignment = agentAssignment ? [agentAssignment] : [];
|
|
233
370
|
if (flags['dry-run']) {
|
|
234
371
|
const result = {
|
|
235
372
|
object: 'task_create_tree_dry_run',
|
|
@@ -238,16 +375,23 @@ class TasksCreateTree extends base_command_1.BaseCommand {
|
|
|
238
375
|
...(() => {
|
|
239
376
|
const dryWarnings = [
|
|
240
377
|
rerunWarning(planned, keyPrefix),
|
|
241
|
-
derivedPrefix ? derivedPrefixWarning(derivedPrefix) : undefined,
|
|
378
|
+
usedDerivedPrefix && derivedPrefix ? derivedPrefixWarning(derivedPrefix) : undefined,
|
|
379
|
+
...noteworthyAssignment,
|
|
242
380
|
].filter((w) => w !== undefined);
|
|
243
381
|
return dryWarnings.length > 0 ? { warnings: dryWarnings } : {};
|
|
244
382
|
})(),
|
|
383
|
+
summary: {
|
|
384
|
+
create: planned.filter((item) => !existingByKey.get(item.key)).length,
|
|
385
|
+
[flags.resume ? 'existing' : 'update']: planned.filter((item) => existingByKey.get(item.key)).length,
|
|
386
|
+
},
|
|
245
387
|
defaultAgents: defaultAgentResolutions,
|
|
246
388
|
nodes: planned.map((item) => ({
|
|
247
389
|
tempId: item.key,
|
|
248
390
|
parentTempId: item.parentKey,
|
|
391
|
+
...(item.blockerKeys?.length ? { blockedByTempIds: item.blockerKeys } : {}),
|
|
249
392
|
title: item.payload.title,
|
|
250
393
|
externalKey: item.payload.externalKey,
|
|
394
|
+
action: plannedAction(existingByKey.get(item.key), Boolean(flags.resume)),
|
|
251
395
|
payload: item.payload,
|
|
252
396
|
})),
|
|
253
397
|
};
|
|
@@ -262,17 +406,27 @@ class TasksCreateTree extends base_command_1.BaseCommand {
|
|
|
262
406
|
return;
|
|
263
407
|
}
|
|
264
408
|
const treeWarning = rerunWarning(planned, keyPrefix);
|
|
265
|
-
const derivedWarning = derivedPrefix ? derivedPrefixWarning(derivedPrefix) : undefined;
|
|
409
|
+
const derivedWarning = usedDerivedPrefix && derivedPrefix ? derivedPrefixWarning(derivedPrefix) : undefined;
|
|
266
410
|
const warnings = [
|
|
267
411
|
...(treeWarning ? [treeWarning] : []),
|
|
268
412
|
...(derivedWarning ? [derivedWarning] : []),
|
|
413
|
+
...noteworthyAssignment,
|
|
269
414
|
];
|
|
270
415
|
if (treeWarning && !flags.json)
|
|
271
416
|
process.stderr.write(`Warning: ${treeWarning.hint}\n`);
|
|
272
417
|
if (derivedWarning && !flags.json)
|
|
273
418
|
process.stderr.write(`Note: ${derivedWarning.hint}\n`);
|
|
419
|
+
if (agentAssignment && process.stderr.isTTY) {
|
|
420
|
+
process.stderr.write(`Warning: ${agentAssignment.hint}\n`);
|
|
421
|
+
}
|
|
422
|
+
// Everything above only read. From here a request writes, so say where, and
|
|
423
|
+
// honour a caller's `--expect-target` before the first one.
|
|
424
|
+
const target = await this.assertWriteTarget(flags['expect-target']);
|
|
274
425
|
const mapping = {};
|
|
275
426
|
const results = [];
|
|
427
|
+
const startedAt = Date.now();
|
|
428
|
+
let rounds = 0;
|
|
429
|
+
const next = this.nextCommand(flags);
|
|
276
430
|
// A half-applied tree reports the ids it did create; the only way back.
|
|
277
431
|
const reportPartial = (item, failure, err, failures) => {
|
|
278
432
|
if (results.length === 0 && !failures)
|
|
@@ -285,6 +439,7 @@ class TasksCreateTree extends base_command_1.BaseCommand {
|
|
|
285
439
|
keyed: unkeyedNodes(planned).length === 0,
|
|
286
440
|
warnings,
|
|
287
441
|
failures,
|
|
442
|
+
next,
|
|
288
443
|
failedAt: {
|
|
289
444
|
tempId: item.key,
|
|
290
445
|
...(item.parentKey ? { parentTempId: item.parentKey } : {}),
|
|
@@ -293,7 +448,7 @@ class TasksCreateTree extends base_command_1.BaseCommand {
|
|
|
293
448
|
},
|
|
294
449
|
});
|
|
295
450
|
if (flags.json) {
|
|
296
|
-
this.log(JSON.stringify(partial, null, 2));
|
|
451
|
+
this.log(JSON.stringify({ ...partial, target }, null, 2));
|
|
297
452
|
process.exitCode = partial.failedAt.error.retryable ? 5 : 1;
|
|
298
453
|
this.exit(partial.failedAt.error.retryable ? 5 : 1);
|
|
299
454
|
}
|
|
@@ -301,6 +456,8 @@ class TasksCreateTree extends base_command_1.BaseCommand {
|
|
|
301
456
|
for (const [tempId, id] of Object.entries(mapping))
|
|
302
457
|
this.log(`- ${tempId} -> ${id}`);
|
|
303
458
|
this.log(partial.rerun);
|
|
459
|
+
if (partial.next)
|
|
460
|
+
this.log(`Next: ${partial.next}`);
|
|
304
461
|
if (failures) {
|
|
305
462
|
for (const row of failures) {
|
|
306
463
|
const detail = row.error;
|
|
@@ -311,8 +468,23 @@ class TasksCreateTree extends base_command_1.BaseCommand {
|
|
|
311
468
|
}
|
|
312
469
|
this.handleApiError(err);
|
|
313
470
|
};
|
|
471
|
+
// Blockers a node names by tempId, as the real ids the serial path needs.
|
|
472
|
+
const blockerIdsFor = (item) => {
|
|
473
|
+
const ids = [];
|
|
474
|
+
for (const blockerKey of item.blockerKeys ?? []) {
|
|
475
|
+
const id = mapping[blockerKey];
|
|
476
|
+
if (!id) {
|
|
477
|
+
this.error(`Blocker tempId "${blockerKey}" was not created before "${item.key}"`, {
|
|
478
|
+
exit: 1,
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
ids.push(id);
|
|
482
|
+
}
|
|
483
|
+
return ids;
|
|
484
|
+
};
|
|
314
485
|
// The serial path: one request per node. Used for updates of existing keyed
|
|
315
|
-
// nodes,
|
|
486
|
+
// nodes, for every create against a server without the batch route, and for
|
|
487
|
+
// everything under --no-batch.
|
|
316
488
|
const applySerial = async (item) => {
|
|
317
489
|
const payload = { ...item.payload };
|
|
318
490
|
if (item.parentKey) {
|
|
@@ -323,6 +495,7 @@ class TasksCreateTree extends base_command_1.BaseCommand {
|
|
|
323
495
|
});
|
|
324
496
|
}
|
|
325
497
|
}
|
|
498
|
+
const blockerIds = blockerIdsFor(item);
|
|
326
499
|
let action;
|
|
327
500
|
let task;
|
|
328
501
|
try {
|
|
@@ -331,6 +504,9 @@ class TasksCreateTree extends base_command_1.BaseCommand {
|
|
|
331
504
|
if (existingTask) {
|
|
332
505
|
action = 'updated';
|
|
333
506
|
const patch = (0, tasks_1.removeUndefinedFields)((0, tasks_1.createPatchFromCreatePayload)(payload));
|
|
507
|
+
// Adding is idempotent, so a re-run never duplicates or drops an edge.
|
|
508
|
+
if (blockerIds.length > 0)
|
|
509
|
+
patch.addBlockedByIds = blockerIds;
|
|
334
510
|
task = await data_provider_1.dataService.updateTask({
|
|
335
511
|
taskId: (0, tasks_1.taskId)(existingTask),
|
|
336
512
|
data: patch,
|
|
@@ -338,11 +514,27 @@ class TasksCreateTree extends base_command_1.BaseCommand {
|
|
|
338
514
|
}
|
|
339
515
|
else {
|
|
340
516
|
action = 'created';
|
|
517
|
+
if (blockerIds.length > 0) {
|
|
518
|
+
payload.blockedByIds = [
|
|
519
|
+
...new Set([
|
|
520
|
+
...(payload.blockedByIds ?? []),
|
|
521
|
+
...blockerIds,
|
|
522
|
+
]),
|
|
523
|
+
];
|
|
524
|
+
}
|
|
341
525
|
task = await data_provider_1.dataService.createTask(payload);
|
|
342
526
|
}
|
|
343
527
|
}
|
|
344
528
|
else {
|
|
345
529
|
action = 'created';
|
|
530
|
+
if (blockerIds.length > 0) {
|
|
531
|
+
payload.blockedByIds = [
|
|
532
|
+
...new Set([
|
|
533
|
+
...(payload.blockedByIds ?? []),
|
|
534
|
+
...blockerIds,
|
|
535
|
+
]),
|
|
536
|
+
];
|
|
537
|
+
}
|
|
346
538
|
task = await data_provider_1.dataService.createTask(payload);
|
|
347
539
|
}
|
|
348
540
|
}
|
|
@@ -362,9 +554,19 @@ class TasksCreateTree extends base_command_1.BaseCommand {
|
|
|
362
554
|
task,
|
|
363
555
|
});
|
|
364
556
|
};
|
|
365
|
-
//
|
|
366
|
-
//
|
|
367
|
-
|
|
557
|
+
// A person watching sees the tree being built; anything capturing output
|
|
558
|
+
// sees nothing extra (the answer is the JSON / summary at the end).
|
|
559
|
+
const onProgress = process.stderr.isTTY
|
|
560
|
+
? (p) => {
|
|
561
|
+
process.stderr.write(p.waiting
|
|
562
|
+
? `Waiting ${Math.max(1, Math.round(p.waiting.ms / 1000))}s before trying again (${p.waiting.reason}); ${p.confirmed} of ${p.total} confirmed.\n`
|
|
563
|
+
: `Confirmed ${p.confirmed} of ${p.total} (request ${p.round}, ${Math.round(p.elapsedMs / 1000)}s).\n`);
|
|
564
|
+
}
|
|
565
|
+
: undefined;
|
|
566
|
+
// New nodes go to POST /api/tasks/batch, and the batch is driven until every
|
|
567
|
+
// one exists (createNodesViaBatch). `batchAvailable` flips off the first time
|
|
568
|
+
// the server answers 404/405, or under --no-batch, and every later node is serial.
|
|
569
|
+
let batchAvailable = !flags['no-batch'];
|
|
368
570
|
let pending = [];
|
|
369
571
|
const flushPending = async () => {
|
|
370
572
|
const nodes = pending;
|
|
@@ -372,8 +574,14 @@ class TasksCreateTree extends base_command_1.BaseCommand {
|
|
|
372
574
|
if (nodes.length === 0)
|
|
373
575
|
return;
|
|
374
576
|
if (batchAvailable) {
|
|
375
|
-
const outcome = await (0, task_batch_1.createNodesViaBatch)(nodes.map((n) => ({
|
|
577
|
+
const outcome = await (0, task_batch_1.createNodesViaBatch)(nodes.map((n) => ({
|
|
578
|
+
key: n.key,
|
|
579
|
+
parentKey: n.parentKey,
|
|
580
|
+
blockerKeys: n.blockerKeys,
|
|
581
|
+
payload: n.payload,
|
|
582
|
+
})), mapping, { onProgress });
|
|
376
583
|
if (outcome.supported) {
|
|
584
|
+
rounds += outcome.rounds;
|
|
377
585
|
const byKey = new Map(nodes.map((n) => [n.key, n]));
|
|
378
586
|
for (const row of outcome.rows) {
|
|
379
587
|
const node = byKey.get(row.key);
|
|
@@ -399,7 +607,8 @@ class TasksCreateTree extends base_command_1.BaseCommand {
|
|
|
399
607
|
...(failure.error.code ? { code: failure.error.code } : {}),
|
|
400
608
|
});
|
|
401
609
|
reportPartial(node, failure.error, err, outcome.failures.map((f) => ({
|
|
402
|
-
tempId: f.key,
|
|
610
|
+
tempId: f.key,
|
|
611
|
+
outcome: f.outcome ?? (f.skipped ? 'not_attempted' : 'failed'),
|
|
403
612
|
externalKey: f.externalKey ?? byKey.get(f.key)?.payload.externalKey,
|
|
404
613
|
error: f.error,
|
|
405
614
|
})));
|
|
@@ -419,8 +628,15 @@ class TasksCreateTree extends base_command_1.BaseCommand {
|
|
|
419
628
|
if (flags.resume) {
|
|
420
629
|
const id = (0, tasks_1.taskId)(existingTask);
|
|
421
630
|
mapping[item.key] = id;
|
|
422
|
-
results.push({
|
|
423
|
-
|
|
631
|
+
results.push({
|
|
632
|
+
tempId: item.key,
|
|
633
|
+
parentTempId: item.parentKey,
|
|
634
|
+
id,
|
|
635
|
+
action: 'existing',
|
|
636
|
+
externalKey: item.payload.externalKey,
|
|
637
|
+
title: existingTask.title ?? item.payload.title,
|
|
638
|
+
task: existingTask,
|
|
639
|
+
});
|
|
424
640
|
}
|
|
425
641
|
else
|
|
426
642
|
await applySerial(item);
|
|
@@ -436,6 +652,8 @@ class TasksCreateTree extends base_command_1.BaseCommand {
|
|
|
436
652
|
...(warnings.length > 0 ? { warnings } : {}),
|
|
437
653
|
count: results.length,
|
|
438
654
|
defaultAgents: defaultAgentResolutions,
|
|
655
|
+
target,
|
|
656
|
+
timing: { elapsedMs: Date.now() - startedAt, requests: rounds },
|
|
439
657
|
mapping,
|
|
440
658
|
results,
|
|
441
659
|
};
|
|
@@ -449,8 +667,135 @@ class TasksCreateTree extends base_command_1.BaseCommand {
|
|
|
449
667
|
this.log(`- ${item.tempId}: ${item.action} ${(0, tasks_1.formatTaskLabel)(task)}`);
|
|
450
668
|
}
|
|
451
669
|
}
|
|
670
|
+
/**
|
|
671
|
+
* `--verify`: which nodes of this tree exist, and which do not. Read-only by
|
|
672
|
+
* construction — it is reached before anything that writes — so it is safe to
|
|
673
|
+
* run at any time, as often as wanted, against a tree of any size.
|
|
674
|
+
*/
|
|
675
|
+
async verifyTree(planned, existingByKey, unverifiable, flags) {
|
|
676
|
+
const cannotCheck = new Map(unverifiable.map((u) => [u.tempId, u.reason]));
|
|
677
|
+
for (const key of unkeyedNodes(planned)) {
|
|
678
|
+
if (!cannotCheck.has(key)) {
|
|
679
|
+
cannotCheck.set(key, 'no externalKey, so nothing can recognise it; give an externalKey or --external-key-prefix');
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
const existing = [];
|
|
683
|
+
const missing = [];
|
|
684
|
+
for (const item of planned) {
|
|
685
|
+
if (cannotCheck.has(item.key))
|
|
686
|
+
continue;
|
|
687
|
+
const task = existingByKey.get(item.key);
|
|
688
|
+
if (task) {
|
|
689
|
+
existing.push({
|
|
690
|
+
tempId: item.key,
|
|
691
|
+
id: (0, tasks_1.taskId)(task),
|
|
692
|
+
...(typeof task.identifier === 'string' ? { identifier: task.identifier } : {}),
|
|
693
|
+
externalKey: item.payload.externalKey,
|
|
694
|
+
title: task.title ?? item.payload.title,
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
else {
|
|
698
|
+
missing.push({
|
|
699
|
+
tempId: item.key,
|
|
700
|
+
...(item.parentKey ? { parentTempId: item.parentKey } : {}),
|
|
701
|
+
externalKey: item.payload.externalKey,
|
|
702
|
+
title: item.payload.title,
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
const ok = missing.length === 0 && cannotCheck.size === 0;
|
|
707
|
+
const result = {
|
|
708
|
+
object: 'task_create_tree_verification',
|
|
709
|
+
ok,
|
|
710
|
+
total: planned.length,
|
|
711
|
+
summary: {
|
|
712
|
+
existing: existing.length,
|
|
713
|
+
missing: missing.length,
|
|
714
|
+
unverifiable: cannotCheck.size,
|
|
715
|
+
},
|
|
716
|
+
existing,
|
|
717
|
+
missing,
|
|
718
|
+
unverifiable: [...cannotCheck].map(([tempId, reason]) => ({ tempId, reason })),
|
|
719
|
+
target: await this.describeWriteTarget(),
|
|
720
|
+
...(missing.length > 0 ? { next: this.nextCommand(flags) } : {}),
|
|
721
|
+
};
|
|
722
|
+
if (flags.json) {
|
|
723
|
+
this.log(JSON.stringify(result, null, 2));
|
|
724
|
+
}
|
|
725
|
+
else {
|
|
726
|
+
this.log(`${existing.length} of ${planned.length} node(s) exist; ${missing.length} missing` +
|
|
727
|
+
(cannotCheck.size ? `; ${cannotCheck.size} cannot be checked` : '') +
|
|
728
|
+
'.');
|
|
729
|
+
for (const row of missing)
|
|
730
|
+
this.log(`- missing: ${String(row.tempId)} (${String(row.externalKey)})`);
|
|
731
|
+
for (const [tempId, reason] of cannotCheck)
|
|
732
|
+
this.log(`- cannot check: ${tempId} — ${reason}`);
|
|
733
|
+
if (result.next)
|
|
734
|
+
this.log(`Next: ${result.next}`);
|
|
735
|
+
}
|
|
736
|
+
if (!ok)
|
|
737
|
+
this.exit(1);
|
|
738
|
+
}
|
|
739
|
+
/**
|
|
740
|
+
* The command that continues this tree, as it should be typed: this
|
|
741
|
+
* invocation's own input and scope, plus `--resume`. Rebuilt from the parsed
|
|
742
|
+
* flags rather than echoed from argv, so it is the same whether the person
|
|
743
|
+
* typed `--space x` or `--space=x`, and so it can say plainly when the input
|
|
744
|
+
* came from stdin and cannot be replayed from a path.
|
|
745
|
+
*/
|
|
746
|
+
nextCommand(flags) {
|
|
747
|
+
const quote = (value) => /^[\w@%+=:,./-]+$/.test(value) ? value : `'${value.replace(/'/g, `'\\''`)}'`;
|
|
748
|
+
const input = flags['from-json'];
|
|
749
|
+
const parts = [
|
|
750
|
+
this.config.bin,
|
|
751
|
+
'tasks',
|
|
752
|
+
'create-tree',
|
|
753
|
+
'--from-json',
|
|
754
|
+
input === '-' ? '<the same JSON on stdin>' : quote(String(input ?? '<file>')),
|
|
755
|
+
];
|
|
756
|
+
for (const name of ['space', 'goal', 'project', 'external-key-prefix', 'default-agent']) {
|
|
757
|
+
const value = flags[name];
|
|
758
|
+
if (typeof value === 'string' && value)
|
|
759
|
+
parts.push(`--${name}`, quote(value));
|
|
760
|
+
}
|
|
761
|
+
for (const value of flags['expect-target'] ?? []) {
|
|
762
|
+
parts.push('--expect-target', quote(value));
|
|
763
|
+
}
|
|
764
|
+
if (flags.json)
|
|
765
|
+
parts.push('--json');
|
|
766
|
+
parts.push('--resume');
|
|
767
|
+
return parts.join(' ');
|
|
768
|
+
}
|
|
452
769
|
}
|
|
453
770
|
exports.default = TasksCreateTree;
|
|
771
|
+
/** What a dry run says will happen to a node: create it, update it, or leave it (`--resume`). */
|
|
772
|
+
function plannedAction(existing, resume) {
|
|
773
|
+
if (!existing)
|
|
774
|
+
return 'create';
|
|
775
|
+
return resume ? 'existing' : 'update';
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* The note that names an Agent put in charge of nodes by DEFAULT, or undefined
|
|
779
|
+
* when no node was. Sources a person wrote themselves (`explicit-assignee`,
|
|
780
|
+
* `provided` — an operator's own `--default-agent`) are not surprises; the
|
|
781
|
+
* session's agent, a parent's, or the space lead are.
|
|
782
|
+
*/
|
|
783
|
+
function describeDefaultAgentAssignment(resolutions) {
|
|
784
|
+
const surprising = Object.values(resolutions).filter((r) => r?.applied &&
|
|
785
|
+
(r.agentIds?.length ?? 0) > 0 &&
|
|
786
|
+
(r.source === 'caller-agent' || r.source === 'parent' || r.source === 'space-lead'));
|
|
787
|
+
if (surprising.length === 0)
|
|
788
|
+
return undefined;
|
|
789
|
+
const agentIds = [...new Set(surprising.flatMap((r) => r.agentIds ?? []))];
|
|
790
|
+
const sources = [...new Set(surprising.map((r) => r.source))];
|
|
791
|
+
return {
|
|
792
|
+
code: 'DEFAULT_AGENT_ASSIGNED',
|
|
793
|
+
value: { count: surprising.length, agentIds, sources },
|
|
794
|
+
hint: `${surprising.length} node(s) will be assigned to agent ${agentIds.join(', ')} by default (${sources.join(', ')}), ` +
|
|
795
|
+
'and a Space with auto-execution may start them. To keep them with you instead, put "executionMode":"human" and ' +
|
|
796
|
+
'"assigneeUserIds":["me"] on the nodes, or pass --default-agent with the assignee you mean.',
|
|
797
|
+
};
|
|
798
|
+
}
|
|
454
799
|
function normalizeTreeRoots(input) {
|
|
455
800
|
const root = input.root;
|
|
456
801
|
const nodes = input.nodes ?? input.tasks;
|
|
@@ -486,6 +831,7 @@ function normalizeDefaults(input, flags) {
|
|
|
486
831
|
...defaults,
|
|
487
832
|
spaceId: flags.space ?? defaults.spaceId,
|
|
488
833
|
goalId: flags.goal ?? defaults.goalId,
|
|
834
|
+
projectId: flags.project ?? defaults.projectId,
|
|
489
835
|
});
|
|
490
836
|
}
|
|
491
837
|
function planTree(roots, opts) {
|
|
@@ -497,17 +843,37 @@ function planTree(roots, opts) {
|
|
|
497
843
|
throw new Error(`Duplicate tree tempId/path key: ${key}`);
|
|
498
844
|
}
|
|
499
845
|
seen.add(key);
|
|
846
|
+
const blockerKeys = readBlockerKeys(node, key);
|
|
500
847
|
const payload = nodePayload(node, opts.defaults);
|
|
848
|
+
let keyDerived = false;
|
|
501
849
|
if (!payload.externalKey && opts.externalKeyPrefix) {
|
|
502
850
|
payload.externalKey = `${opts.externalKeyPrefix.replace(/\/+$/, '')}/${key}`;
|
|
851
|
+
keyDerived = true;
|
|
503
852
|
}
|
|
504
|
-
planned.push({
|
|
853
|
+
planned.push({
|
|
854
|
+
key,
|
|
855
|
+
parentKey,
|
|
856
|
+
...(blockerKeys.length ? { blockerKeys } : {}),
|
|
857
|
+
...(keyDerived ? { keyDerived } : {}),
|
|
858
|
+
node,
|
|
859
|
+
payload,
|
|
860
|
+
});
|
|
505
861
|
const children = Array.isArray(node.children) ? node.children : [];
|
|
506
862
|
children.forEach((child, index) => visit(child, [...path, index], key));
|
|
507
863
|
};
|
|
508
864
|
roots.forEach((root, index) => visit(root, [index]));
|
|
509
865
|
return planned;
|
|
510
866
|
}
|
|
867
|
+
/** `blockedByTempIds` as a clean list of the tempIds a node names, or a precise refusal. */
|
|
868
|
+
function readBlockerKeys(node, key) {
|
|
869
|
+
const raw = node.blockedByTempIds;
|
|
870
|
+
if (raw === undefined)
|
|
871
|
+
return [];
|
|
872
|
+
if (!Array.isArray(raw) || raw.some((entry) => typeof entry !== 'string' || !entry.trim())) {
|
|
873
|
+
throw new Error(`Node "${key}": blockedByTempIds must be an array of tempIds`);
|
|
874
|
+
}
|
|
875
|
+
return [...new Set(raw.map((entry) => entry.trim()))];
|
|
876
|
+
}
|
|
511
877
|
function nodeKey(node, path) {
|
|
512
878
|
const temp = node.tempId ?? node.temp_id ?? node.id;
|
|
513
879
|
if (typeof temp === 'string' && temp.trim())
|
|
@@ -515,6 +881,6 @@ function nodeKey(node, path) {
|
|
|
515
881
|
return path.join('.');
|
|
516
882
|
}
|
|
517
883
|
function nodePayload(node, defaults) {
|
|
518
|
-
const { children: _children, tempId: _tempId, temp_id: _tempIdSnake, id: _id, ...rest } = node;
|
|
884
|
+
const { children: _children, tempId: _tempId, temp_id: _tempIdSnake, id: _id, blockedByTempIds: _blockedByTempIds, ...rest } = node;
|
|
519
885
|
return (0, tasks_1.buildTaskCreatePayload)({ ...defaults, ...rest }, {});
|
|
520
886
|
}
|