@clipboard-health/groundcrew 4.33.0 → 4.34.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/commands/resumeWorkspace.d.ts.map +1 -1
- package/dist/commands/resumeWorkspace.js +2 -1
- package/dist/lib/adapters/todo-txt/source.d.ts.map +1 -1
- package/dist/lib/adapters/todo-txt/source.js +3 -3
- package/dist/lib/adapters/todo-txt/writeback.d.ts +2 -0
- package/dist/lib/adapters/todo-txt/writeback.d.ts.map +1 -1
- package/dist/lib/adapters/todo-txt/writeback.js +79 -14
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resumeWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/resumeWorkspace.ts"],"names":[],"mappings":"AAGA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"resumeWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/resumeWorkspace.ts"],"names":[],"mappings":"AAGA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAcnE,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,MAAM,CAAC;CACd;AAuID,wBAAsB,eAAe,CACnC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,sBAAsB,GAC9B,OAAO,CAAC,IAAI,CAAC,CA2Ef;AAED,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAGtE"}
|
|
@@ -5,6 +5,7 @@ import { loadConfig } from "../lib/config.js";
|
|
|
5
5
|
import { composeAgentLaunch, openAgentWorkspace, prepareAgentLaunch } from "../lib/agentLaunch.js";
|
|
6
6
|
import { readRunState, recordRunState } from "../lib/runState.js";
|
|
7
7
|
import { removeStagedPrompt, stageBuildSecrets, stagePromptText, stageWorkspaceLaunchCommand, } from "../lib/stagedLaunch.js";
|
|
8
|
+
import { naturalIdFromCanonical } from "../lib/taskSource.js";
|
|
8
9
|
import { errorMessage, log } from "../lib/util.js";
|
|
9
10
|
import { workspaces } from "../lib/workspaces.js";
|
|
10
11
|
import { resolveLaunchDir, worktrees } from "../lib/worktrees.js";
|
|
@@ -13,7 +14,7 @@ function parseArguments(argv) {
|
|
|
13
14
|
if (task === undefined || task.length === 0 || extras.length > 0 || task.startsWith("-")) {
|
|
14
15
|
throw new Error("Usage: crew resume <task>");
|
|
15
16
|
}
|
|
16
|
-
return { task: task.toLowerCase() };
|
|
17
|
+
return { task: naturalIdFromCanonical(task).toLowerCase() };
|
|
17
18
|
}
|
|
18
19
|
async function fetchTaskDetails(task) {
|
|
19
20
|
try {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"source.d.ts","sourceRoot":"","sources":["../../../../src/lib/adapters/todo-txt/source.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAEjE,OAAO,EAKL,KAAK,UAAU,EAEhB,MAAM,qBAAqB,CAAC;AAI7B,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AA+SxD,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,oBAAoB,EAC5B,OAAO,EAAE,cAAc,GACtB,UAAU,
|
|
1
|
+
{"version":3,"file":"source.d.ts","sourceRoot":"","sources":["../../../../src/lib/adapters/todo-txt/source.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAEjE,OAAO,EAKL,KAAK,UAAU,EAEhB,MAAM,qBAAqB,CAAC;AAI7B,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AA+SxD,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,oBAAoB,EAC5B,OAAO,EAAE,cAAc,GACtB,UAAU,CAyJZ"}
|
|
@@ -7,7 +7,7 @@ import { readEnvironmentVariable } from "../../util.js";
|
|
|
7
7
|
import { isActiveForFetch, normalizeToIssue } from "./normalizer.js";
|
|
8
8
|
import { DATE_RE, getMetadataFirst, parseAllLines } from "./parser.js";
|
|
9
9
|
import { copyPromptFile, updateTaskStatus, validateTodoFile, withLock } from "./writeback.js";
|
|
10
|
-
const RECURRENCE_RE = /^\+?\d+[
|
|
10
|
+
const RECURRENCE_RE = /^\+?\d+[dwmyh]$/;
|
|
11
11
|
function readPromptFile(promptPath) {
|
|
12
12
|
try {
|
|
13
13
|
return readFileSync(promptPath, "utf8");
|
|
@@ -197,7 +197,7 @@ function buildTodoLine(id, input) {
|
|
|
197
197
|
}
|
|
198
198
|
if (input.recurrence !== undefined) {
|
|
199
199
|
if (!RECURRENCE_RE.test(input.recurrence)) {
|
|
200
|
-
throw new Error("todo-txt: recurrence must look like 1d, 1w, 1m, 1y, or +1m");
|
|
200
|
+
throw new Error("todo-txt: recurrence must look like 1d, 1w, 1m, 1y, 2h, or +1m");
|
|
201
201
|
}
|
|
202
202
|
tokens.push(metadataToken("rec", input.recurrence));
|
|
203
203
|
}
|
|
@@ -380,7 +380,7 @@ export function createTodoTxtTaskSource(config, context) {
|
|
|
380
380
|
async markDone(issue) {
|
|
381
381
|
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- TodoTxtTaskSource always writes TodoTxtSourceRef
|
|
382
382
|
const ref = issue.sourceRef;
|
|
383
|
-
const recurResult = await updateTaskStatus({ todoPath, ref }, "done");
|
|
383
|
+
const recurResult = await updateTaskStatus({ todoPath, ref, timezone: config.timezone }, "done");
|
|
384
384
|
if (recurResult !== undefined) {
|
|
385
385
|
copyPromptFile(recurResult.oldPromptPath, recurResult.newPromptPath);
|
|
386
386
|
}
|
|
@@ -9,6 +9,8 @@ export interface UpdateOptions {
|
|
|
9
9
|
todoPath: string;
|
|
10
10
|
ref: TodoTxtSourceRef;
|
|
11
11
|
now?: Date;
|
|
12
|
+
/** Source timezone for hour-unit recurrence wall-clock math. Defaults to UTC. */
|
|
13
|
+
timezone?: string;
|
|
12
14
|
}
|
|
13
15
|
export declare function withLock<T>(lockPath: string, fn: () => T | Promise<T>): Promise<T>;
|
|
14
16
|
type StatusMutation = "in-progress" | "in-review" | "done";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"writeback.d.ts","sourceRoot":"","sources":["../../../../src/lib/adapters/todo-txt/writeback.ts"],"names":[],"mappings":"AAYA,OAAO,EAAyB,KAAK,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAE/E,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;CACvB;
|
|
1
|
+
{"version":3,"file":"writeback.d.ts","sourceRoot":"","sources":["../../../../src/lib/adapters/todo-txt/writeback.ts"],"names":[],"mappings":"AAYA,OAAO,EAAyB,KAAK,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAE/E,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;CACvB;AAqOD,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,gBAAgB,CAAC;IACtB,GAAG,CAAC,EAAE,IAAI,CAAC;IACX,iFAAiF;IACjF,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,wBAAsB,QAAQ,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAQxF;AAED,KAAK,cAAc,GAAG,aAAa,GAAG,WAAW,GAAG,MAAM,CAAC;AAwF3D,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,aAAa,EACtB,SAAS,EAAE,cAAc,GACxB,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC,CA+ElC;AAED,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAQrE;AA8GD,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,WAAW,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,GAChC,MAAM,EAAE,CA0CV"}
|
|
@@ -23,15 +23,15 @@ function addMonths(dateStr, months) {
|
|
|
23
23
|
const d = new Date(Date.UTC(year, month - 1 + months, day));
|
|
24
24
|
return isoDate(d);
|
|
25
25
|
}
|
|
26
|
-
const REC_RE = /^(?<strict>\+?)(?<amount>\d+)(?<unit>[
|
|
26
|
+
const REC_RE = /^(?<strict>\+?)(?<amount>\d+)(?<unit>[dwmyh])$/;
|
|
27
27
|
function parseRecurrence(rec) {
|
|
28
28
|
const m = REC_RE.exec(rec);
|
|
29
29
|
if (m === null) {
|
|
30
30
|
return undefined;
|
|
31
31
|
}
|
|
32
32
|
const [, strictStr, amountStr, unit] = m;
|
|
33
|
-
/* v8 ignore next @preserve -- regex [
|
|
34
|
-
if (unit !== "d" && unit !== "w" && unit !== "m" && unit !== "y") {
|
|
33
|
+
/* v8 ignore next @preserve -- regex [dwmyh] guarantees unit is always one of d/w/m/y/h */
|
|
34
|
+
if (unit !== "d" && unit !== "w" && unit !== "m" && unit !== "y" && unit !== "h") {
|
|
35
35
|
return undefined;
|
|
36
36
|
}
|
|
37
37
|
return {
|
|
@@ -54,19 +54,73 @@ function advanceDate(dateStr, rec) {
|
|
|
54
54
|
}
|
|
55
55
|
return addMonths(dateStr, amount * 12);
|
|
56
56
|
}
|
|
57
|
+
// Add hours to a (timezone-naive wall-clock) datetime, minute precision.
|
|
58
|
+
function addHours(dateTime, hours) {
|
|
59
|
+
const padded = dateTime.length === 16 ? `${dateTime}:00` : dateTime;
|
|
60
|
+
const ms = Date.parse(`${padded}Z`);
|
|
61
|
+
return new Date(ms + hours * 60 * 60 * 1000).toISOString().slice(0, 16);
|
|
62
|
+
}
|
|
57
63
|
// t: may carry a datetime threshold; advance its date part and keep the time
|
|
58
64
|
// component so a recurring task stays scheduled at the same instant of day.
|
|
59
|
-
|
|
65
|
+
//
|
|
66
|
+
// Hour units advance differently: non-strict rec:Nh advances from the
|
|
67
|
+
// completion instant (the source-timezone wall clock), so a task that sat
|
|
68
|
+
// through daemon downtime re-arms N hours from now instead of stampeding
|
|
69
|
+
// through every missed slot. Strict rec:+Nh keeps schedule-aligned
|
|
70
|
+
// advancement from the previous threshold, matching due:'s strict semantics.
|
|
71
|
+
function advanceThreshold(threshold, rec, completionWall) {
|
|
72
|
+
if (rec.unit === "h") {
|
|
73
|
+
let base = completionWall;
|
|
74
|
+
if (rec.strict) {
|
|
75
|
+
base = threshold.length === 10 ? `${threshold}T00:00` : threshold;
|
|
76
|
+
}
|
|
77
|
+
return addHours(base, rec.amount);
|
|
78
|
+
}
|
|
60
79
|
const [datePart, timePart] = threshold.split("T");
|
|
61
80
|
/* v8 ignore next @preserve -- split always yields a first element */
|
|
62
81
|
const nextDate = advanceDate(datePart ?? threshold, rec);
|
|
63
82
|
return timePart === undefined ? nextDate : `${nextDate}T${timePart}`;
|
|
64
83
|
}
|
|
84
|
+
// "YYYY-MM-DDTHH:MM" wall-clock time for `now` in the given timezone.
|
|
85
|
+
function wallClockDateTime(timeZone, now) {
|
|
86
|
+
const parts = new Intl.DateTimeFormat("en-CA", {
|
|
87
|
+
timeZone,
|
|
88
|
+
year: "numeric",
|
|
89
|
+
month: "2-digit",
|
|
90
|
+
day: "2-digit",
|
|
91
|
+
hour: "2-digit",
|
|
92
|
+
minute: "2-digit",
|
|
93
|
+
hourCycle: "h23",
|
|
94
|
+
}).formatToParts(now);
|
|
95
|
+
const get = (type) => parts.find((part) => part.type === type)?.value;
|
|
96
|
+
const year = get("year");
|
|
97
|
+
const month = get("month");
|
|
98
|
+
const day = get("day");
|
|
99
|
+
const hour = get("hour");
|
|
100
|
+
const minute = get("minute");
|
|
101
|
+
/* v8 ignore next 3 @preserve -- Intl.DateTimeFormat with these options always returns the parts */
|
|
102
|
+
if (year === undefined ||
|
|
103
|
+
month === undefined ||
|
|
104
|
+
day === undefined ||
|
|
105
|
+
hour === undefined ||
|
|
106
|
+
minute === undefined) {
|
|
107
|
+
throw new Error(`todo-txt: could not format datetime in timezone "${timeZone}"`);
|
|
108
|
+
}
|
|
109
|
+
return `${year}-${month}-${day}T${hour}:${minute}`;
|
|
110
|
+
}
|
|
65
111
|
function advanceId(id, newDate) {
|
|
66
112
|
const dateCompact = compactDate(newDate);
|
|
67
113
|
// Replace the first 8-digit run (compact date) in the id
|
|
68
114
|
const replaced = id.replace(/\d{8}/, dateCompact);
|
|
69
|
-
|
|
115
|
+
if (replaced !== id) {
|
|
116
|
+
return replaced;
|
|
117
|
+
}
|
|
118
|
+
// Unchanged: either the id has no date run (append one), or the date run
|
|
119
|
+
// already equals the new date — same-day hourly recurrence. For the latter,
|
|
120
|
+
// strip any prior collision suffixes and let buildUniqueId number this
|
|
121
|
+
// cycle (-002, -003, …) instead of growing a suffix chain.
|
|
122
|
+
const base = id.replace(/(?:-\d{3})+$/, "");
|
|
123
|
+
return base.includes(dateCompact) ? base : `${id}-${dateCompact}`;
|
|
70
124
|
}
|
|
71
125
|
function buildUniqueId(baseNewId, existingIds) {
|
|
72
126
|
if (existingIds.has(baseNewId.toLowerCase())) {
|
|
@@ -166,7 +220,7 @@ function assertValidTransition(newStatus, currentStatus, id) {
|
|
|
166
220
|
throw new Error(`todo-txt: cannot mark done: task "${id}" has status "${s}"`);
|
|
167
221
|
}
|
|
168
222
|
}
|
|
169
|
-
function buildRecurResult(parsed, parsedAll, originalLine, ref, completionDateStr, now) {
|
|
223
|
+
function buildRecurResult(parsed, parsedAll, originalLine, ref, completionDateStr, completionWallStr, now) {
|
|
170
224
|
const recStr = parsed.metadata["rec"]?.[0];
|
|
171
225
|
if (recStr === undefined) {
|
|
172
226
|
return undefined;
|
|
@@ -182,13 +236,19 @@ function buildRecurResult(parsed, parsedAll, originalLine, ref, completionDateSt
|
|
|
182
236
|
.filter((id) => id !== undefined));
|
|
183
237
|
const oldDue = parsed.metadata["due"]?.[0];
|
|
184
238
|
const oldT = parsed.metadata["t"]?.[0];
|
|
185
|
-
// due: advances from old due (strict) or completion date (normal)
|
|
239
|
+
// due: advances from old due (strict) or completion date (normal). Hour
|
|
240
|
+
// units never advance due: — validate() rejects the combination, and a line
|
|
241
|
+
// that bypassed verify carries its due forward unchanged rather than
|
|
242
|
+
// feeding an hour recurrence into date-only math.
|
|
186
243
|
/* v8 ignore next @preserve -- oldDue undefined with rec: is unusual; callers typically pair rec: with due: */
|
|
187
244
|
const dueBase = rec.strict ? (oldDue ?? completionDateStr) : completionDateStr;
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
245
|
+
let newDue;
|
|
246
|
+
if (oldDue !== undefined) {
|
|
247
|
+
newDue = rec.unit === "h" ? oldDue : advanceDate(dueBase, rec);
|
|
248
|
+
}
|
|
249
|
+
// t: advances from its own current value by the same period (hour units:
|
|
250
|
+
// see advanceThreshold for strict vs non-strict base)
|
|
251
|
+
const newT = oldT === undefined ? undefined : advanceThreshold(oldT, rec, completionWallStr);
|
|
192
252
|
// Compute new date for id advancement: prefer due:, then t:, so ids stay
|
|
193
253
|
// schedule-aligned for t:-only recurring tasks. Slice to the date part —
|
|
194
254
|
// t: may carry a datetime.
|
|
@@ -247,8 +307,9 @@ export async function updateTaskStatus(options, newStatus) {
|
|
|
247
307
|
let updatedLine;
|
|
248
308
|
if (newStatus === "done") {
|
|
249
309
|
const completionDateStr = isoDate(now);
|
|
310
|
+
const completionWallStr = wallClockDateTime(options.timezone ?? "UTC", now);
|
|
250
311
|
updatedLine = buildDoneLine(originalLine, completionDateStr);
|
|
251
|
-
recurResult = buildRecurResult(parsed, parsedAll, originalLine, ref, completionDateStr, now);
|
|
312
|
+
recurResult = buildRecurResult(parsed, parsedAll, originalLine, ref, completionDateStr, completionWallStr, now);
|
|
252
313
|
}
|
|
253
314
|
else {
|
|
254
315
|
updatedLine = replaceStatusToken(originalLine, newStatus);
|
|
@@ -308,8 +369,12 @@ function validateDepsAndDates(parsed, parsedAll, id, prefix, errors) {
|
|
|
308
369
|
errors.push(`${prefix}: malformed t: date "${tVal}" for task "${id}" (expected YYYY-MM-DD or YYYY-MM-DDTHH:MM[:SS])`);
|
|
309
370
|
}
|
|
310
371
|
const recVal = parsed.metadata["rec"]?.[0];
|
|
311
|
-
|
|
312
|
-
|
|
372
|
+
const recParsed = recVal === undefined ? undefined : parseRecurrence(recVal);
|
|
373
|
+
if (recParsed?.unit === "h" && (tVal === undefined || dueVal !== undefined)) {
|
|
374
|
+
errors.push(`${prefix}: hourly rec: "${recVal}" for task "${id}" requires a t: threshold and is incompatible with due:`);
|
|
375
|
+
}
|
|
376
|
+
if (recVal !== undefined && recParsed === undefined) {
|
|
377
|
+
errors.push(`${prefix}: malformed rec: "${recVal}" for task "${id}" (expected e.g. 1d, 1w, +1m, 2h)`);
|
|
313
378
|
}
|
|
314
379
|
}
|
|
315
380
|
function validateActiveTaskLine(parsed, parsedAll, tasksDir, id, prefix, errors, knownAgents) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clipboard-health/groundcrew",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.34.1",
|
|
4
4
|
"description": "Linear-driven orchestrator that launches AI coding agents in git worktrees, with workspace lifecycle and usage tracking.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent",
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
"zod": "4.4.3"
|
|
77
77
|
},
|
|
78
78
|
"devDependencies": {
|
|
79
|
-
"@clipboard-health/ai-rules": "2.
|
|
79
|
+
"@clipboard-health/ai-rules": "2.27.0",
|
|
80
80
|
"@clipboard-health/oxlint-config": "1.10.14",
|
|
81
81
|
"@nx/js": "22.7.5",
|
|
82
82
|
"@tsconfig/node24": "24.0.4",
|