agentplane 0.3.6 → 0.3.7
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/.build-manifest.json +106 -96
- package/dist/adapters/task-backend/task-backend-adapter.d.ts +2 -2
- package/dist/adapters/task-backend/task-backend-adapter.d.ts.map +1 -1
- package/dist/adapters/task-backend/task-backend-adapter.js +2 -2
- package/dist/backends/task-backend/local-backend.d.ts +7 -5
- package/dist/backends/task-backend/local-backend.d.ts.map +1 -1
- package/dist/backends/task-backend/local-backend.js +79 -7
- package/dist/backends/task-backend/redmine/env.d.ts +1 -1
- package/dist/backends/task-backend/redmine/env.d.ts.map +1 -1
- package/dist/backends/task-backend/redmine/env.js +3 -0
- package/dist/backends/task-backend/redmine/inspect.d.ts +11 -0
- package/dist/backends/task-backend/redmine/inspect.d.ts.map +1 -0
- package/dist/backends/task-backend/redmine/inspect.js +75 -0
- package/dist/backends/task-backend/redmine/mapping.d.ts.map +1 -1
- package/dist/backends/task-backend/redmine/mapping.js +21 -2
- package/dist/backends/task-backend/redmine/state.d.ts +17 -0
- package/dist/backends/task-backend/redmine/state.d.ts.map +1 -0
- package/dist/backends/task-backend/redmine/state.js +95 -0
- package/dist/backends/task-backend/redmine-backend.d.ts +10 -16
- package/dist/backends/task-backend/redmine-backend.d.ts.map +1 -1
- package/dist/backends/task-backend/redmine-backend.js +205 -15
- package/dist/backends/task-backend/shared/constants.d.ts +1 -1
- package/dist/backends/task-backend/shared/constants.js +1 -1
- package/dist/backends/task-backend/shared/record.d.ts.map +1 -1
- package/dist/backends/task-backend/shared/record.js +20 -1
- package/dist/backends/task-backend/shared/types.d.ts +42 -4
- package/dist/backends/task-backend/shared/types.d.ts.map +1 -1
- package/dist/backends/task-backend/shared.d.ts +1 -1
- package/dist/backends/task-backend/shared.d.ts.map +1 -1
- package/dist/backends/task-backend.d.ts +1 -1
- package/dist/backends/task-backend.d.ts.map +1 -1
- package/dist/backends/task-index.d.ts.map +1 -1
- package/dist/backends/task-index.js +1 -0
- package/dist/cli/run-cli/command-catalog/project.d.ts +1 -1
- package/dist/cli/run-cli/command-catalog/project.d.ts.map +1 -1
- package/dist/cli/run-cli/command-catalog/project.js +3 -1
- package/dist/cli/run-cli/command-catalog.d.ts +1 -1
- package/dist/cli/run-cli/command-catalog.d.ts.map +1 -1
- package/dist/cli/run-cli/commands/init/write-env.d.ts.map +1 -1
- package/dist/cli/run-cli/commands/init/write-env.js +12 -0
- package/dist/cli/run-cli.test-helpers.d.ts.map +1 -1
- package/dist/cli/run-cli.test-helpers.js +2 -0
- package/dist/commands/backend/sync.command.d.ts +5 -1
- package/dist/commands/backend/sync.command.d.ts.map +1 -1
- package/dist/commands/backend/sync.command.js +67 -3
- package/dist/commands/backend.d.ts +22 -0
- package/dist/commands/backend.d.ts.map +1 -1
- package/dist/commands/backend.js +110 -1
- package/dist/commands/commit.spec.d.ts.map +1 -1
- package/dist/commands/commit.spec.js +30 -6
- package/dist/commands/doctor/workspace.d.ts +8 -0
- package/dist/commands/doctor/workspace.d.ts.map +1 -1
- package/dist/commands/doctor/workspace.js +127 -3
- package/dist/commands/guard/commit.command.d.ts.map +1 -1
- package/dist/commands/guard/commit.command.js +30 -6
- package/dist/commands/guard/impl/allow.d.ts +4 -0
- package/dist/commands/guard/impl/allow.d.ts.map +1 -1
- package/dist/commands/guard/impl/allow.js +14 -3
- package/dist/commands/guard/impl/commands.d.ts.map +1 -1
- package/dist/commands/guard/impl/commands.js +11 -2
- package/dist/commands/shared/task-backend.d.ts +1 -1
- package/dist/commands/shared/task-backend.d.ts.map +1 -1
- package/dist/commands/shared/task-backend.js +9 -0
- package/dist/commands/shared/task-store.d.ts +61 -2
- package/dist/commands/shared/task-store.d.ts.map +1 -1
- package/dist/commands/shared/task-store.js +298 -60
- package/dist/commands/task/block.d.ts.map +1 -1
- package/dist/commands/task/block.js +58 -37
- package/dist/commands/task/close-shared.d.ts.map +1 -1
- package/dist/commands/task/close-shared.js +17 -20
- package/dist/commands/task/comment.d.ts.map +1 -1
- package/dist/commands/task/comment.js +14 -19
- package/dist/commands/task/derive.command.d.ts +1 -0
- package/dist/commands/task/derive.command.d.ts.map +1 -1
- package/dist/commands/task/derive.command.js +15 -2
- package/dist/commands/task/derive.d.ts +1 -0
- package/dist/commands/task/derive.d.ts.map +1 -1
- package/dist/commands/task/derive.js +27 -4
- package/dist/commands/task/doc.d.ts.map +1 -1
- package/dist/commands/task/doc.js +16 -5
- package/dist/commands/task/finish.d.ts.map +1 -1
- package/dist/commands/task/finish.js +41 -41
- package/dist/commands/task/migrate-doc.d.ts +15 -0
- package/dist/commands/task/migrate-doc.d.ts.map +1 -1
- package/dist/commands/task/migrate-doc.js +126 -35
- package/dist/commands/task/new.d.ts.map +1 -1
- package/dist/commands/task/new.js +3 -1
- package/dist/commands/task/plan.js +28 -28
- package/dist/commands/task/set-status.d.ts.map +1 -1
- package/dist/commands/task/set-status.js +104 -61
- package/dist/commands/task/shared/dependencies.d.ts +1 -0
- package/dist/commands/task/shared/dependencies.d.ts.map +1 -1
- package/dist/commands/task/shared/dependencies.js +10 -0
- package/dist/commands/task/shared/docs.js +1 -1
- package/dist/commands/task/shared/transitions.d.ts +17 -0
- package/dist/commands/task/shared/transitions.d.ts.map +1 -1
- package/dist/commands/task/shared/transitions.js +20 -7
- package/dist/commands/task/shared.d.ts +2 -2
- package/dist/commands/task/shared.d.ts.map +1 -1
- package/dist/commands/task/shared.js +2 -2
- package/dist/commands/task/start.d.ts.map +1 -1
- package/dist/commands/task/start.js +33 -28
- package/dist/commands/task/verify-record.d.ts.map +1 -1
- package/dist/commands/task/verify-record.js +32 -32
- package/dist/commands/upgrade/apply.d.ts +2 -0
- package/dist/commands/upgrade/apply.d.ts.map +1 -1
- package/dist/commands/upgrade/apply.js +33 -1
- package/dist/commands/upgrade.command.d.ts.map +1 -1
- package/dist/commands/upgrade.command.js +25 -0
- package/dist/commands/upgrade.d.ts +1 -0
- package/dist/commands/upgrade.d.ts.map +1 -1
- package/dist/commands/upgrade.js +34 -0
- package/dist/policy/rules/allowlist.d.ts.map +1 -1
- package/dist/policy/rules/allowlist.js +12 -9
- package/dist/ports/task-backend-port.d.ts +2 -2
- package/dist/ports/task-backend-port.d.ts.map +1 -1
- package/dist/shared/protected-paths.d.ts +10 -0
- package/dist/shared/protected-paths.d.ts.map +1 -1
- package/dist/shared/protected-paths.js +33 -0
- package/package.json +2 -2
|
@@ -1,41 +1,52 @@
|
|
|
1
1
|
import { readFile, stat } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { docChanged, extractTaskDoc, mergeTaskDoc, parseTaskReadme, renderTaskReadme, renderTaskDocFromSections, taskDocToSectionMap, } from "@agentplaneorg/core";
|
|
4
4
|
import { LocalBackend, taskRecordToData, } from "../../backends/task-backend.js";
|
|
5
5
|
import { exitCodeForError } from "../../cli/exit-codes.js";
|
|
6
6
|
import { CliError } from "../../shared/errors.js";
|
|
7
7
|
import { writeTextIfChanged } from "../../shared/write-if-changed.js";
|
|
8
8
|
import { resolveDocUpdatedBy, taskDataToFrontmatter } from "./task-backend.js";
|
|
9
|
+
export function setTaskFieldsIntent(task) {
|
|
10
|
+
return { kind: "set-task-fields", task };
|
|
11
|
+
}
|
|
12
|
+
export function appendTaskCommentsIntent(comments) {
|
|
13
|
+
return { kind: "append-comments", comments };
|
|
14
|
+
}
|
|
15
|
+
export function appendTaskCommentIntent(comment) {
|
|
16
|
+
return appendTaskCommentsIntent([comment]);
|
|
17
|
+
}
|
|
18
|
+
export function appendTaskEventsIntent(events) {
|
|
19
|
+
return { kind: "append-events", events };
|
|
20
|
+
}
|
|
21
|
+
export function appendTaskEventIntent(event) {
|
|
22
|
+
return appendTaskEventsIntent([event]);
|
|
23
|
+
}
|
|
24
|
+
export function replaceTaskDocIntent(opts) {
|
|
25
|
+
return { kind: "replace-doc", ...opts };
|
|
26
|
+
}
|
|
27
|
+
export function setTaskSectionIntent(opts) {
|
|
28
|
+
return { kind: "set-section", ...opts };
|
|
29
|
+
}
|
|
30
|
+
export function touchTaskDocMetaIntent(opts = {}) {
|
|
31
|
+
return { kind: "touch-doc-meta", ...opts };
|
|
32
|
+
}
|
|
9
33
|
function taskReadmePath(ctx, taskId) {
|
|
10
34
|
return path.join(ctx.resolvedProject.gitRoot, ctx.config.paths.workflow_dir, taskId, "README.md");
|
|
11
35
|
}
|
|
12
|
-
function normalizeTaskDocVersion(value, fallback =
|
|
36
|
+
function normalizeTaskDocVersion(value, fallback = 3) {
|
|
13
37
|
return value === 3 ? 3 : value === 2 ? 2 : fallback;
|
|
14
38
|
}
|
|
39
|
+
function normalizeTaskRevision(value, fallback = 1) {
|
|
40
|
+
return Number.isInteger(value) && Number(value) > 0 ? Number(value) : fallback;
|
|
41
|
+
}
|
|
42
|
+
function readStoredTaskRevision(value) {
|
|
43
|
+
return Number.isInteger(value) && Number(value) > 0 ? Number(value) : null;
|
|
44
|
+
}
|
|
15
45
|
function normalizeDocComparison(text) {
|
|
16
46
|
return String(text ?? "")
|
|
17
47
|
.replaceAll("\r\n", "\n")
|
|
18
48
|
.trim();
|
|
19
49
|
}
|
|
20
|
-
function extractDocSectionText(doc, sectionName) {
|
|
21
|
-
const lines = doc.replaceAll("\r\n", "\n").split("\n");
|
|
22
|
-
let capturing = false;
|
|
23
|
-
const out = [];
|
|
24
|
-
for (const line of lines) {
|
|
25
|
-
const match = /^##\s+(.*)$/.exec(line.trim());
|
|
26
|
-
if (match) {
|
|
27
|
-
if (capturing)
|
|
28
|
-
break;
|
|
29
|
-
capturing = (match[1] ?? "").trim() === sectionName;
|
|
30
|
-
continue;
|
|
31
|
-
}
|
|
32
|
-
if (capturing)
|
|
33
|
-
out.push(line);
|
|
34
|
-
}
|
|
35
|
-
if (!capturing)
|
|
36
|
-
return null;
|
|
37
|
-
return out.join("\n").trimEnd();
|
|
38
|
-
}
|
|
39
50
|
function normalizeComments(task) {
|
|
40
51
|
return Array.isArray(task.comments)
|
|
41
52
|
? task.comments.filter((item) => !!item && typeof item.author === "string" && typeof item.body === "string")
|
|
@@ -79,6 +90,20 @@ function throwTaskDocConflict(opts) {
|
|
|
79
90
|
},
|
|
80
91
|
});
|
|
81
92
|
}
|
|
93
|
+
function throwTaskRevisionConflict(opts) {
|
|
94
|
+
throw new CliError({
|
|
95
|
+
exitCode: exitCodeForError("E_VALIDATION"),
|
|
96
|
+
code: "E_VALIDATION",
|
|
97
|
+
message: `Task revision changed concurrently: ${opts.taskId} ` +
|
|
98
|
+
`(expected revision ${opts.expectedRevision}, current revision ${opts.currentRevision})`,
|
|
99
|
+
context: {
|
|
100
|
+
task_id: opts.taskId,
|
|
101
|
+
expected_revision: opts.expectedRevision,
|
|
102
|
+
current_revision: opts.currentRevision,
|
|
103
|
+
reason_code: "task_revision_conflict",
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
}
|
|
82
107
|
function applyTaskDocPatch(opts) {
|
|
83
108
|
if (opts.patch.kind === "replace-doc") {
|
|
84
109
|
if (opts.patch.expectedCurrentDoc !== undefined) {
|
|
@@ -88,17 +113,209 @@ function applyTaskDocPatch(opts) {
|
|
|
88
113
|
throwTaskDocConflict({ taskId: opts.taskId });
|
|
89
114
|
}
|
|
90
115
|
}
|
|
91
|
-
return opts.patch.doc;
|
|
116
|
+
return renderTaskDocFromSections(taskDocToSectionMap(opts.patch.doc));
|
|
117
|
+
}
|
|
118
|
+
const sections = taskDocToSectionMap(opts.currentDocRaw);
|
|
119
|
+
for (const requiredSection of opts.patch.requiredSections) {
|
|
120
|
+
if (!(requiredSection in sections))
|
|
121
|
+
sections[requiredSection] = "";
|
|
92
122
|
}
|
|
93
|
-
const baseDoc = ensureDocSections(opts.currentDocRaw, opts.patch.requiredSections);
|
|
94
123
|
if (opts.patch.expectedCurrentText !== undefined) {
|
|
95
|
-
const currentSection = normalizeDocComparison(
|
|
124
|
+
const currentSection = normalizeDocComparison(sections[opts.patch.section] ?? null);
|
|
96
125
|
const expectedSection = normalizeDocComparison(opts.patch.expectedCurrentText);
|
|
97
126
|
if (currentSection !== expectedSection) {
|
|
98
127
|
throwTaskSectionConflict({ taskId: opts.taskId, section: opts.patch.section });
|
|
99
128
|
}
|
|
100
129
|
}
|
|
101
|
-
|
|
130
|
+
sections[opts.patch.section] = opts.patch.text.replaceAll("\r\n", "\n").trimEnd();
|
|
131
|
+
return renderTaskDocFromSections(sections);
|
|
132
|
+
}
|
|
133
|
+
function normalizeTaskStoreIntents(intents) {
|
|
134
|
+
if (!intents)
|
|
135
|
+
return [];
|
|
136
|
+
if (Array.isArray(intents)) {
|
|
137
|
+
return intents.filter((intent) => intent != null);
|
|
138
|
+
}
|
|
139
|
+
return [intents];
|
|
140
|
+
}
|
|
141
|
+
function patchToIntents(patch) {
|
|
142
|
+
if (!patch)
|
|
143
|
+
return [];
|
|
144
|
+
const intents = [];
|
|
145
|
+
if (patch.task) {
|
|
146
|
+
intents.push(setTaskFieldsIntent(patch.task));
|
|
147
|
+
}
|
|
148
|
+
if (patch.appendComments && patch.appendComments.length > 0) {
|
|
149
|
+
intents.push(appendTaskCommentsIntent(patch.appendComments));
|
|
150
|
+
}
|
|
151
|
+
if (patch.appendEvents && patch.appendEvents.length > 0) {
|
|
152
|
+
intents.push(appendTaskEventsIntent(patch.appendEvents));
|
|
153
|
+
}
|
|
154
|
+
if (patch.doc) {
|
|
155
|
+
intents.push(patch.doc.kind === "replace-doc"
|
|
156
|
+
? replaceTaskDocIntent({
|
|
157
|
+
doc: patch.doc.doc,
|
|
158
|
+
expectedCurrentDoc: patch.doc.expectedCurrentDoc,
|
|
159
|
+
})
|
|
160
|
+
: setTaskSectionIntent({
|
|
161
|
+
section: patch.doc.section,
|
|
162
|
+
text: patch.doc.text,
|
|
163
|
+
requiredSections: patch.doc.requiredSections,
|
|
164
|
+
expectedCurrentText: patch.doc.expectedCurrentText,
|
|
165
|
+
}));
|
|
166
|
+
}
|
|
167
|
+
if (patch.docMeta && (patch.doc !== undefined || patch.docMeta.touch === true)) {
|
|
168
|
+
intents.push(touchTaskDocMetaIntent({
|
|
169
|
+
updatedBy: patch.docMeta.updatedBy,
|
|
170
|
+
version: patch.docMeta.version,
|
|
171
|
+
}));
|
|
172
|
+
}
|
|
173
|
+
return intents;
|
|
174
|
+
}
|
|
175
|
+
export function taskStorePatchFromIntents(intents) {
|
|
176
|
+
const normalized = normalizeTaskStoreIntents(intents);
|
|
177
|
+
if (normalized.length === 0)
|
|
178
|
+
return null;
|
|
179
|
+
const patch = {};
|
|
180
|
+
for (const intent of normalized) {
|
|
181
|
+
switch (intent.kind) {
|
|
182
|
+
case "set-task-fields": {
|
|
183
|
+
patch.task = patch.task ? { ...patch.task, ...intent.task } : { ...intent.task };
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
case "append-comments": {
|
|
187
|
+
if (intent.comments.length > 0) {
|
|
188
|
+
patch.appendComments = [...(patch.appendComments ?? []), ...intent.comments];
|
|
189
|
+
}
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
case "append-events": {
|
|
193
|
+
if (intent.events.length > 0) {
|
|
194
|
+
patch.appendEvents = [...(patch.appendEvents ?? []), ...intent.events];
|
|
195
|
+
}
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
case "replace-doc": {
|
|
199
|
+
const docPatch = {
|
|
200
|
+
kind: "replace-doc",
|
|
201
|
+
doc: intent.doc,
|
|
202
|
+
};
|
|
203
|
+
if (intent.expectedCurrentDoc === undefined) {
|
|
204
|
+
patch.doc = docPatch;
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
docPatch.expectedCurrentDoc = intent.expectedCurrentDoc;
|
|
208
|
+
patch.doc = docPatch;
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
case "set-section": {
|
|
212
|
+
const sectionPatch = {
|
|
213
|
+
kind: "set-section",
|
|
214
|
+
section: intent.section,
|
|
215
|
+
text: intent.text,
|
|
216
|
+
requiredSections: [...intent.requiredSections],
|
|
217
|
+
};
|
|
218
|
+
if (intent.expectedCurrentText === undefined) {
|
|
219
|
+
patch.doc = sectionPatch;
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
sectionPatch.expectedCurrentText = intent.expectedCurrentText;
|
|
223
|
+
patch.doc = sectionPatch;
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
case "touch-doc-meta": {
|
|
227
|
+
patch.docMeta = {
|
|
228
|
+
touch: true,
|
|
229
|
+
updatedBy: intent.updatedBy ?? patch.docMeta?.updatedBy,
|
|
230
|
+
version: intent.version ?? patch.docMeta?.version,
|
|
231
|
+
};
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return patch;
|
|
237
|
+
}
|
|
238
|
+
export async function mutateTaskStore(store, taskId, builder, opts = {}) {
|
|
239
|
+
if (typeof store.mutate === "function") {
|
|
240
|
+
return await store.mutate(taskId, builder, opts);
|
|
241
|
+
}
|
|
242
|
+
return await store.patch(taskId, async (current) => taskStorePatchFromIntents(await builder(current)), opts);
|
|
243
|
+
}
|
|
244
|
+
function applyTaskStoreIntents(entry, intents) {
|
|
245
|
+
if (intents.length === 0)
|
|
246
|
+
return { ...entry.task };
|
|
247
|
+
const current = entry.task;
|
|
248
|
+
const next = { ...current };
|
|
249
|
+
let touchDoc = false;
|
|
250
|
+
let docMetaUpdatedBy;
|
|
251
|
+
let docMetaVersion;
|
|
252
|
+
for (const intent of intents) {
|
|
253
|
+
switch (intent.kind) {
|
|
254
|
+
case "set-task-fields": {
|
|
255
|
+
Object.assign(next, intent.task);
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
case "append-comments": {
|
|
259
|
+
if (intent.comments.length > 0) {
|
|
260
|
+
next.comments = [...normalizeComments(next), ...intent.comments];
|
|
261
|
+
}
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
case "append-events": {
|
|
265
|
+
if (intent.events.length > 0) {
|
|
266
|
+
next.events = [...normalizeEvents(next), ...intent.events];
|
|
267
|
+
}
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
case "replace-doc": {
|
|
271
|
+
next.doc = applyTaskDocPatch({
|
|
272
|
+
taskId: current.id,
|
|
273
|
+
currentDocRaw: String(next.doc ?? ""),
|
|
274
|
+
patch: {
|
|
275
|
+
kind: "replace-doc",
|
|
276
|
+
doc: intent.doc,
|
|
277
|
+
expectedCurrentDoc: intent.expectedCurrentDoc,
|
|
278
|
+
},
|
|
279
|
+
});
|
|
280
|
+
next.sections = taskDocToSectionMap(String(next.doc ?? ""));
|
|
281
|
+
touchDoc = true;
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
case "set-section": {
|
|
285
|
+
next.doc = applyTaskDocPatch({
|
|
286
|
+
taskId: current.id,
|
|
287
|
+
currentDocRaw: String(next.doc ?? ""),
|
|
288
|
+
patch: {
|
|
289
|
+
kind: "set-section",
|
|
290
|
+
section: intent.section,
|
|
291
|
+
text: intent.text,
|
|
292
|
+
requiredSections: intent.requiredSections,
|
|
293
|
+
expectedCurrentText: intent.expectedCurrentText,
|
|
294
|
+
},
|
|
295
|
+
});
|
|
296
|
+
next.sections = taskDocToSectionMap(String(next.doc ?? ""));
|
|
297
|
+
touchDoc = true;
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
case "touch-doc-meta": {
|
|
301
|
+
touchDoc = true;
|
|
302
|
+
if (intent.updatedBy !== undefined) {
|
|
303
|
+
docMetaUpdatedBy = intent.updatedBy;
|
|
304
|
+
}
|
|
305
|
+
if (intent.version !== undefined) {
|
|
306
|
+
docMetaVersion = intent.version;
|
|
307
|
+
}
|
|
308
|
+
break;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (touchDoc) {
|
|
313
|
+
const currentDocVersion = normalizeTaskDocVersion(entry.parsed.frontmatter.doc_version);
|
|
314
|
+
next.doc_version = normalizeTaskDocVersion(docMetaVersion ?? next.doc_version, currentDocVersion);
|
|
315
|
+
next.doc_updated_at = new Date().toISOString();
|
|
316
|
+
next.doc_updated_by = docMetaUpdatedBy ?? resolveDocUpdatedBy(next);
|
|
317
|
+
}
|
|
318
|
+
return next;
|
|
102
319
|
}
|
|
103
320
|
async function readTaskReadmeCached(opts) {
|
|
104
321
|
const readmePath = taskReadmePath(opts.ctx, opts.taskId);
|
|
@@ -126,7 +343,7 @@ async function readTaskReadmeCached(opts) {
|
|
|
126
343
|
body: parsed.body,
|
|
127
344
|
readmePath,
|
|
128
345
|
});
|
|
129
|
-
return { task, readmePath, mtimeMs: st.mtimeMs, parsed };
|
|
346
|
+
return { task, readmePath, mtimeMs: st.mtimeMs, parsed, rawText: text };
|
|
130
347
|
}
|
|
131
348
|
async function ensureUnchangedOnDisk(opts) {
|
|
132
349
|
const st = await stat(opts.readmePath);
|
|
@@ -138,6 +355,18 @@ async function ensureUnchangedOnDisk(opts) {
|
|
|
138
355
|
});
|
|
139
356
|
}
|
|
140
357
|
}
|
|
358
|
+
async function didReadmeChangeOnDisk(opts) {
|
|
359
|
+
try {
|
|
360
|
+
const st = await stat(opts.readmePath);
|
|
361
|
+
return st.mtimeMs !== opts.expectedMtimeMs;
|
|
362
|
+
}
|
|
363
|
+
catch (err) {
|
|
364
|
+
const code = err?.code;
|
|
365
|
+
if (code === "ENOENT")
|
|
366
|
+
return true;
|
|
367
|
+
throw err;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
141
370
|
export class TaskStore {
|
|
142
371
|
ctx;
|
|
143
372
|
cache = new Map();
|
|
@@ -169,45 +398,47 @@ export class TaskStore {
|
|
|
169
398
|
this.cache.set(key, load);
|
|
170
399
|
return await load;
|
|
171
400
|
}
|
|
172
|
-
async update(taskId, updater) {
|
|
173
|
-
return await this.runWithRetry(taskId, async (entry) => {
|
|
401
|
+
async update(taskId, updater, opts = {}) {
|
|
402
|
+
return await this.runWithRetry(taskId, opts, async (entry) => {
|
|
174
403
|
return await updater({ ...entry.task });
|
|
175
404
|
});
|
|
176
405
|
}
|
|
177
|
-
async patch(taskId, builder) {
|
|
178
|
-
return await this.
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
if (patch.appendComments && patch.appendComments.length > 0) {
|
|
185
|
-
next.comments = [...normalizeComments(current), ...patch.appendComments];
|
|
186
|
-
}
|
|
187
|
-
if (patch.appendEvents && patch.appendEvents.length > 0) {
|
|
188
|
-
next.events = [...normalizeEvents(current), ...patch.appendEvents];
|
|
189
|
-
}
|
|
190
|
-
if (patch.doc) {
|
|
191
|
-
next.doc = applyTaskDocPatch({
|
|
192
|
-
taskId: current.id,
|
|
193
|
-
currentDocRaw: String(current.doc ?? ""),
|
|
194
|
-
patch: patch.doc,
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
|
-
const touchDoc = patch.doc !== undefined || patch.docMeta?.touch === true;
|
|
198
|
-
if (touchDoc) {
|
|
199
|
-
const currentDocVersion = normalizeTaskDocVersion(entry.parsed.frontmatter.doc_version);
|
|
200
|
-
next.doc_version = normalizeTaskDocVersion(patch.docMeta?.version ?? current.doc_version, currentDocVersion);
|
|
201
|
-
next.doc_updated_at = new Date().toISOString();
|
|
202
|
-
next.doc_updated_by = patch.docMeta?.updatedBy ?? resolveDocUpdatedBy(next);
|
|
203
|
-
}
|
|
204
|
-
return next;
|
|
406
|
+
async patch(taskId, builder, opts = {}) {
|
|
407
|
+
return await this.mutate(taskId, async (current) => patchToIntents(await builder(current)), opts);
|
|
408
|
+
}
|
|
409
|
+
async mutate(taskId, builder, opts = {}) {
|
|
410
|
+
return await this.runWithRetry(taskId, opts, async (entry) => {
|
|
411
|
+
const intents = normalizeTaskStoreIntents(await builder({ ...entry.task }));
|
|
412
|
+
return applyTaskStoreIntents(entry, intents);
|
|
205
413
|
});
|
|
206
414
|
}
|
|
207
|
-
async runWithRetry(taskId, computeNext) {
|
|
415
|
+
async runWithRetry(taskId, opts, computeNext) {
|
|
208
416
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
209
417
|
const entry = await this.getCached(taskId);
|
|
210
|
-
|
|
418
|
+
if (opts.expectedRevision !== undefined) {
|
|
419
|
+
const expectedRevision = normalizeTaskRevision(opts.expectedRevision);
|
|
420
|
+
const currentRevision = normalizeTaskRevision(entry.task.revision);
|
|
421
|
+
if (currentRevision !== expectedRevision) {
|
|
422
|
+
throwTaskRevisionConflict({ taskId, expectedRevision, currentRevision });
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
let next;
|
|
426
|
+
try {
|
|
427
|
+
next = await computeNext(entry);
|
|
428
|
+
}
|
|
429
|
+
catch (err) {
|
|
430
|
+
if (attempt === 0 &&
|
|
431
|
+
err instanceof CliError &&
|
|
432
|
+
err.code === "E_VALIDATION" &&
|
|
433
|
+
(await didReadmeChangeOnDisk({
|
|
434
|
+
readmePath: entry.readmePath,
|
|
435
|
+
expectedMtimeMs: entry.mtimeMs,
|
|
436
|
+
}))) {
|
|
437
|
+
this.cache.delete(taskId);
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
throw err;
|
|
441
|
+
}
|
|
211
442
|
try {
|
|
212
443
|
return await this.writeNextTask(taskId, entry, next);
|
|
213
444
|
}
|
|
@@ -250,12 +481,19 @@ export class TaskStore {
|
|
|
250
481
|
frontmatter.doc_updated_by.trim() === "") {
|
|
251
482
|
frontmatter.doc_updated_by = resolveDocUpdatedBy(next);
|
|
252
483
|
}
|
|
253
|
-
const
|
|
484
|
+
const storedRevision = readStoredTaskRevision(entry.parsed.frontmatter.revision);
|
|
485
|
+
frontmatter.revision = storedRevision ?? 1;
|
|
486
|
+
let nextText = renderTaskReadme(frontmatter, body);
|
|
487
|
+
nextText = nextText.endsWith("\n") ? nextText : `${nextText}\n`;
|
|
488
|
+
if (storedRevision !== null && nextText !== entry.rawText) {
|
|
489
|
+
frontmatter.revision = storedRevision + 1;
|
|
490
|
+
nextText = renderTaskReadme(frontmatter, body);
|
|
491
|
+
nextText = nextText.endsWith("\n") ? nextText : `${nextText}\n`;
|
|
492
|
+
}
|
|
254
493
|
await ensureUnchangedOnDisk({
|
|
255
494
|
readmePath: entry.readmePath,
|
|
256
495
|
expectedMtimeMs: entry.mtimeMs,
|
|
257
496
|
});
|
|
258
|
-
const nextText = rendered.endsWith("\n") ? rendered : `${rendered}\n`;
|
|
259
497
|
const changed = await writeTextIfChanged(entry.readmePath, nextText);
|
|
260
498
|
// Refresh cache with latest content on disk.
|
|
261
499
|
this.cache.set(taskId, (async () => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"block.d.ts","sourceRoot":"","sources":["../../../src/commands/task/block.ts"],"names":[],"mappings":"AAOA,OAAO,EAGL,KAAK,cAAc,EACpB,MAAM,2BAA2B,CAAC;
|
|
1
|
+
{"version":3,"file":"block.d.ts","sourceRoot":"","sources":["../../../src/commands/task/block.ts"],"names":[],"mappings":"AAOA,OAAO,EAGL,KAAK,cAAc,EACpB,MAAM,2BAA2B,CAAC;AA0BnC,wBAAsB,QAAQ,CAAC,IAAI,EAAE;IACnC,GAAG,CAAC,EAAE,cAAc,CAAC;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,iBAAiB,EAAE,OAAO,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,eAAe,EAAE,OAAO,CAAC;IACzB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,kBAAkB,EAAE,OAAO,CAAC;IAC5B,mBAAmB,EAAE,OAAO,CAAC;IAC7B,KAAK,EAAE,OAAO,CAAC;IACf,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,OAAO,CAAC;CAChB,GAAG,OAAO,CAAC,MAAM,CAAC,CAgKlB"}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { mapBackendError } from "../../cli/error-map.js";
|
|
2
|
-
import { successMessage } from "../../cli/output.js";
|
|
2
|
+
import { successMessage, warnMessage } from "../../cli/output.js";
|
|
3
3
|
import { formatCommentBodyForCommit } from "../../shared/comment-format.js";
|
|
4
4
|
import { CliError } from "../../shared/errors.js";
|
|
5
5
|
import { commitFromComment } from "../guard/index.js";
|
|
6
6
|
import { ensureActionApproved } from "../shared/approval-requirements.js";
|
|
7
7
|
import { loadCommandContext, loadTaskFromContext, } from "../shared/task-backend.js";
|
|
8
|
-
import { backendIsLocalFileBackend, getTaskStore } from "../shared/task-store.js";
|
|
8
|
+
import { appendTaskCommentIntent, appendTaskEventIntent, backendIsLocalFileBackend, getTaskStore, mutateTaskStore, setTaskFieldsIntent, touchTaskDocMetaIntent, } from "../shared/task-store.js";
|
|
9
9
|
import { readDirectWorkLock } from "../../shared/direct-work-lock.js";
|
|
10
|
-
import { appendTaskEvent, defaultCommitEmojiForStatus, ensureCommentCommitAllowed, ensureStatusTransitionAllowed, normalizeTaskDocVersion, nowIso, requireStructuredComment, resolvePrimaryTag, toStringArray, } from "./shared.js";
|
|
10
|
+
import { appendTaskEvent, defaultCommitEmojiForStatus, ensureCommentCommitAllowed, resolveCommentCommitWarning, ensureStatusTransitionAllowed, normalizeTaskDocVersion, nowIso, requireStructuredComment, resolvePrimaryTag, toStringArray, } from "./shared.js";
|
|
11
11
|
export async function cmdBlock(opts) {
|
|
12
12
|
try {
|
|
13
13
|
const ctx = opts.ctx ??
|
|
@@ -28,20 +28,22 @@ export async function cmdBlock(opts) {
|
|
|
28
28
|
? await store.get(opts.taskId)
|
|
29
29
|
: await loadTaskFromContext({ ctx, taskId: opts.taskId });
|
|
30
30
|
const currentStatus = String(task.status || "TODO").toUpperCase();
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
31
|
+
if (!useStore) {
|
|
32
|
+
ensureStatusTransitionAllowed({
|
|
33
|
+
currentStatus,
|
|
34
|
+
nextStatus: "BLOCKED",
|
|
35
|
+
force: opts.force,
|
|
36
|
+
});
|
|
37
|
+
ensureCommentCommitAllowed({
|
|
38
|
+
enabled: opts.commitFromComment,
|
|
39
|
+
config: ctx.config,
|
|
40
|
+
action: "block",
|
|
41
|
+
confirmed: opts.confirmStatusCommit,
|
|
42
|
+
quiet: opts.quiet,
|
|
43
|
+
statusFrom: currentStatus,
|
|
44
|
+
statusTo: "BLOCKED",
|
|
45
|
+
});
|
|
46
|
+
}
|
|
45
47
|
const formattedComment = opts.commitFromComment
|
|
46
48
|
? formatCommentBodyForCommit(opts.body, ctx.config)
|
|
47
49
|
: null;
|
|
@@ -51,33 +53,47 @@ export async function cmdBlock(opts) {
|
|
|
51
53
|
: [];
|
|
52
54
|
const commentsValue = [...existingComments, { author: opts.author, body: commentBody }];
|
|
53
55
|
const at = nowIso();
|
|
56
|
+
let currentStatusForCommit = currentStatus;
|
|
57
|
+
let primaryTagForCommit = resolvePrimaryTag(toStringArray(task.tags), ctx).primary;
|
|
58
|
+
let deferredWarnings = [];
|
|
54
59
|
await (useStore
|
|
55
|
-
? store
|
|
60
|
+
? mutateTaskStore(store, opts.taskId, (current) => {
|
|
61
|
+
deferredWarnings = [];
|
|
56
62
|
const currentStatus = String(current.status || "TODO").toUpperCase();
|
|
63
|
+
currentStatusForCommit = currentStatus;
|
|
64
|
+
primaryTagForCommit = resolvePrimaryTag(toStringArray(current.tags), ctx).primary;
|
|
57
65
|
ensureStatusTransitionAllowed({
|
|
58
66
|
currentStatus,
|
|
59
67
|
nextStatus: "BLOCKED",
|
|
60
68
|
force: opts.force,
|
|
61
69
|
});
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
70
|
+
const commitWarning = resolveCommentCommitWarning({
|
|
71
|
+
enabled: opts.commitFromComment,
|
|
72
|
+
config: ctx.config,
|
|
73
|
+
action: "block",
|
|
74
|
+
confirmed: opts.confirmStatusCommit,
|
|
75
|
+
quiet: opts.quiet,
|
|
76
|
+
statusFrom: currentStatus,
|
|
77
|
+
statusTo: "BLOCKED",
|
|
78
|
+
});
|
|
79
|
+
if (commitWarning)
|
|
80
|
+
deferredWarnings.push(commitWarning);
|
|
81
|
+
return [
|
|
82
|
+
setTaskFieldsIntent({ status: "BLOCKED" }),
|
|
83
|
+
appendTaskCommentIntent({ author: opts.author, body: commentBody }),
|
|
84
|
+
appendTaskEventIntent({
|
|
85
|
+
type: "status",
|
|
86
|
+
at,
|
|
87
|
+
author: opts.author,
|
|
88
|
+
from: currentStatus,
|
|
89
|
+
to: "BLOCKED",
|
|
90
|
+
note: commentBody,
|
|
91
|
+
}),
|
|
92
|
+
touchTaskDocMetaIntent({
|
|
77
93
|
updatedBy: opts.author,
|
|
78
94
|
version: normalizeTaskDocVersion(current.doc_version),
|
|
79
|
-
},
|
|
80
|
-
|
|
95
|
+
}),
|
|
96
|
+
];
|
|
81
97
|
})
|
|
82
98
|
: ctx.taskBackend.writeTask({
|
|
83
99
|
...task,
|
|
@@ -95,6 +111,11 @@ export async function cmdBlock(opts) {
|
|
|
95
111
|
doc_updated_at: at,
|
|
96
112
|
doc_updated_by: opts.author,
|
|
97
113
|
}));
|
|
114
|
+
if (!opts.quiet) {
|
|
115
|
+
for (const warning of new Set(deferredWarnings)) {
|
|
116
|
+
process.stderr.write(`${warnMessage(warning)}\n`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
98
119
|
let commitInfo = null;
|
|
99
120
|
if (opts.commitFromComment) {
|
|
100
121
|
const mode = ctx.config.workflow_mode;
|
|
@@ -110,10 +131,10 @@ export async function cmdBlock(opts) {
|
|
|
110
131
|
cwd: opts.cwd,
|
|
111
132
|
rootOverride: opts.rootOverride,
|
|
112
133
|
taskId: opts.taskId,
|
|
113
|
-
primaryTag:
|
|
134
|
+
primaryTag: primaryTagForCommit,
|
|
114
135
|
executorAgent,
|
|
115
136
|
author: opts.author,
|
|
116
|
-
statusFrom:
|
|
137
|
+
statusFrom: currentStatusForCommit,
|
|
117
138
|
statusTo: "BLOCKED",
|
|
118
139
|
commentBody: opts.body,
|
|
119
140
|
formattedComment,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"close-shared.d.ts","sourceRoot":"","sources":["../../../src/commands/task/close-shared.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,gCAAgC,CAAC;AAE/D,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,2BAA2B,CAAC;
|
|
1
|
+
{"version":3,"file":"close-shared.d.ts","sourceRoot":"","sources":["../../../src/commands/task/close-shared.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,gCAAgC,CAAC;AAE/D,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAgBhE,wBAAsB,yBAAyB,CAAC,IAAI,EAAE;IACpD,GAAG,EAAE,cAAc,CAAC;IACpB,IAAI,EAAE,QAAQ,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,OAAO,CAAC;IACf,cAAc,EAAE,MAAM,CAAC;IACvB,KAAK,EAAE,OAAO,CAAC;CAChB,GAAG,OAAO,CAAC,IAAI,CAAC,CAwEhB"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CliError } from "../../shared/errors.js";
|
|
2
|
-
import { backendIsLocalFileBackend, getTaskStore } from "../shared/task-store.js";
|
|
2
|
+
import { appendTaskCommentIntent, appendTaskEventIntent, backendIsLocalFileBackend, getTaskStore, setTaskFieldsIntent, touchTaskDocMetaIntent, } from "../shared/task-store.js";
|
|
3
3
|
import { appendTaskEvent, normalizeTaskDocVersion, nowIso, requireStructuredComment, } from "./shared.js";
|
|
4
4
|
export async function recordVerifiedNoopClosure(opts) {
|
|
5
5
|
if (!opts.force && String(opts.task.status || "TODO").toUpperCase() === "DONE") {
|
|
@@ -15,7 +15,7 @@ export async function recordVerifiedNoopClosure(opts) {
|
|
|
15
15
|
const useStore = backendIsLocalFileBackend(opts.ctx);
|
|
16
16
|
const store = useStore ? getTaskStore(opts.ctx) : null;
|
|
17
17
|
await (useStore
|
|
18
|
-
? store.
|
|
18
|
+
? store.mutate(opts.taskId, (current) => {
|
|
19
19
|
if (!opts.force && String(current.status || "TODO").toUpperCase() === "DONE") {
|
|
20
20
|
throw new CliError({
|
|
21
21
|
exitCode: 2,
|
|
@@ -23,30 +23,27 @@ export async function recordVerifiedNoopClosure(opts) {
|
|
|
23
23
|
message: `Task is already DONE: ${opts.taskId} (use --force to override)`,
|
|
24
24
|
});
|
|
25
25
|
}
|
|
26
|
-
return
|
|
27
|
-
|
|
26
|
+
return [
|
|
27
|
+
setTaskFieldsIntent({
|
|
28
28
|
status: "DONE",
|
|
29
29
|
result_summary: opts.resultSummary,
|
|
30
30
|
risk_level: "low",
|
|
31
31
|
breaking: false,
|
|
32
|
-
},
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
],
|
|
44
|
-
docMeta: {
|
|
45
|
-
touch: true,
|
|
32
|
+
}),
|
|
33
|
+
appendTaskCommentIntent({ author: opts.author, body: opts.body }),
|
|
34
|
+
appendTaskEventIntent({
|
|
35
|
+
type: "status",
|
|
36
|
+
at,
|
|
37
|
+
author: opts.author,
|
|
38
|
+
from: String(current.status || "TODO").toUpperCase(),
|
|
39
|
+
to: "DONE",
|
|
40
|
+
note: opts.body,
|
|
41
|
+
}),
|
|
42
|
+
touchTaskDocMetaIntent({
|
|
46
43
|
updatedBy: opts.author,
|
|
47
44
|
version: normalizeTaskDocVersion(current.doc_version),
|
|
48
|
-
},
|
|
49
|
-
|
|
45
|
+
}),
|
|
46
|
+
];
|
|
50
47
|
})
|
|
51
48
|
: opts.ctx.taskBackend.writeTask({
|
|
52
49
|
...opts.task,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"comment.d.ts","sourceRoot":"","sources":["../../../src/commands/task/comment.ts"],"names":[],"mappings":"AAGA,OAAO,EAGL,KAAK,cAAc,EACpB,MAAM,2BAA2B,CAAC;
|
|
1
|
+
{"version":3,"file":"comment.d.ts","sourceRoot":"","sources":["../../../src/commands/task/comment.ts"],"names":[],"mappings":"AAGA,OAAO,EAGL,KAAK,cAAc,EACpB,MAAM,2BAA2B,CAAC;AAUnC,wBAAsB,cAAc,CAAC,IAAI,EAAE;IACzC,GAAG,CAAC,EAAE,cAAc,CAAC;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACd,GAAG,OAAO,CAAC,MAAM,CAAC,CAiDlB"}
|