@clipboard-health/groundcrew 4.26.1 → 4.27.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/README.md CHANGED
@@ -49,7 +49,7 @@ npm install -g @clipboard-health/groundcrew@latest
49
49
 
50
50
  # 2. Scaffold a global config. Agents are sandboxed by default
51
51
  # (Safehouse/Docker Sandboxes); add --runner none to run unsandboxed on the host.
52
- crew init --global --project-dir ~/dev --repo OWNER/REPO --model claude
52
+ crew init --global --project-dir ~/dev --repo OWNER/REPO --agent claude
53
53
 
54
54
  # 3. Run the clone commands printed by `crew init`.
55
55
 
@@ -64,7 +64,7 @@ crew doctor
64
64
  crew run --watch
65
65
  ```
66
66
 
67
- `crew init --global` writes config to `${XDG_CONFIG_HOME:-$HOME/.config}/groundcrew/`. Pass `--repo` more than once for multiple repos. `--model claude` or `--model codex` chooses the single built-in model preset to enable in the generated config.
67
+ `crew init --global` writes config to `${XDG_CONFIG_HOME:-$HOME/.config}/groundcrew/`. Pass `--repo` more than once for multiple repos. `--agent claude` or `--agent codex` chooses the single built-in agent preset to enable in the generated config.
68
68
 
69
69
  ## Task Pickup
70
70
 
@@ -72,8 +72,8 @@ crew run --watch
72
72
 
73
73
  Linear works out of the box: assign tasks to yourself and add an `agent-*` label.
74
74
 
75
- - `agent-claude`, `agent-codex`, or `agent-<name>` routes to that model.
76
- - `agent-any` routes to the enabled model with the most session headroom, after skipping models over their session limit or weekly paced budget.
75
+ - `agent-claude`, `agent-codex`, or `agent-<name>` routes to that agent.
76
+ - `agent-any` routes to the enabled agent with the most session headroom, after skipping agents over their session limit or weekly paced budget.
77
77
  - Tasks without an `agent-*` label are ignored by `crew run`; dispatch one manually with `crew start <TASK>`.
78
78
 
79
79
  Groundcrew scans `workspace.knownRepositories` to infer which repo a task belongs to.
@@ -91,17 +91,19 @@ Write tasks as complete agent instructions: the goal, the context and constraint
91
91
  ```bash
92
92
  crew init [--global | --local] [--force] [--dry-run] # create a crew.config.ts
93
93
  [--project-dir <dir>] [--repo <repo>]...
94
- [--runner <auto|safehouse|sdx|none>] [--model <claude|codex>]
94
+ [--runner <auto|safehouse|sdx|none>] [--agent <claude|codex>]
95
95
  crew doctor # check setup
96
+ crew source list|verify [<source>] # inspect configured task sources
96
97
  crew task list [--source <name>] # list tasks across sources
97
98
  crew task get <TASK> [--source <name>] [--prompt] # inspect one task or its prompt
98
99
  crew task create "Title" --source <name> [--agent <name>] # create a source task
100
+ crew task validate [<source>] # validate task content
99
101
  crew status [<TASK>] # inspect current state or one task
100
102
  crew run [--watch] # one-shot or --watch forever
101
103
  crew start <TASK> # provision + launch one task now
102
104
  crew stop <TASK> [--reason <text>] # stop workspace, keep worktree
103
105
  crew resume <TASK> # reopen a paused task
104
- crew cleanup <TASK> # tear down every worktree for a task
106
+ crew cleanup [--force] <TASK> # tear down every worktree for a task
105
107
  crew upgrade [<version>] # reinstall crew globally through npm
106
108
  ```
107
109
 
@@ -109,7 +111,7 @@ See [command details](./docs/commands.md) for status output, doctor behavior, an
109
111
 
110
112
  ## Configuration
111
113
 
112
- Workspace settings and at least one enabled model are required; everything else has a default.
114
+ Workspace settings and at least one enabled agent are required; everything else has a default.
113
115
 
114
116
  ```ts
115
117
  import type { Config } from "@clipboard-health/groundcrew";
@@ -122,7 +124,7 @@ export default {
122
124
  // Strings live under projectDir; use { name, projectDirOverride } to override per repo.
123
125
  knownRepositories: ["OWNER/REPO"],
124
126
  },
125
- models: {
127
+ agents: {
126
128
  default: "claude",
127
129
  definitions: {
128
130
  claude: {},
@@ -27,9 +27,9 @@ export default {
27
27
  // { name: "other-org/other-repo", projectDirOverride: "~/work" }
28
28
  knownRepositories: ["your-org/your-repo"],
29
29
  },
30
- models: {
30
+ agents: {
31
31
  default: "claude",
32
- // `definitions` is the enabled model set. Built-in keys can use `{}` to
32
+ // `definitions` is the enabled agent set. Built-in keys can use `{}` to
33
33
  // opt into the shipped command/color/usage preset. Add `codex: {}` if you
34
34
  // want both shipped agents, or add a custom entry and tag tasks with
35
35
  // `agent-<name>`.
@@ -99,7 +99,7 @@ export default {
99
99
  // // it into the agent. Chain with `&&` so a failed mint aborts launch.
100
100
  // preLaunch: "SESSION_TOKEN=$(your-mint-command) && export SESSION_TOKEN",
101
101
  // preLaunchEnv: ["SESSION_TOKEN"],
102
- // // Required for this model when `local.runner` resolves to `sdx`.
102
+ // // Required for this agent when `local.runner` resolves to `sdx`.
103
103
  // sandbox: { agent: "claude" },
104
104
  // },
105
105
  //
@@ -110,7 +110,7 @@ export default {
110
110
  // local: { runner: "auto" },
111
111
  //
112
112
  // // Groundcrew does not create or authenticate sdx sandboxes. For an sdx
113
- // // model, create the matching sandbox yourself before first launch:
113
+ // // agent, create the matching sandbox yourself before first launch:
114
114
  // // sbx create --name groundcrew-claude claude ~/dev/groundcrew
115
115
  // // sbx exec -it groundcrew-claude claude auth login
116
116
  // // sbx exec -it groundcrew-claude gh auth login
@@ -18,5 +18,5 @@ export interface NormalizeOptions {
18
18
  updatedAt: string;
19
19
  }
20
20
  export declare function normalizeToIssue(options: NormalizeOptions): Issue | undefined;
21
- export declare function isActiveForFetch(parsed: ParsedTodoLine): boolean;
21
+ export declare function isActiveForFetch(parsed: ParsedTodoLine, todayIsoDate: string): boolean;
22
22
  //# sourceMappingURL=normalizer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"normalizer.d.ts","sourceRoot":"","sources":["../../../../src/lib/adapters/todo-txt/normalizer.ts"],"names":[],"mappings":"AAGA,OAAO,EAAsC,KAAK,KAAK,EAAiB,MAAM,qBAAqB,CAAC;AACpG,OAAO,EAA8C,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AAE9F,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,EAAE,EAAE,MAAM,CAAC;IACX,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;CACpB;AAkED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,cAAc,CAAC;IACvB,SAAS,EAAE,CAAC,cAAc,GAAG,IAAI,CAAC,EAAE,CAAC;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,iBAAiB,EAAE,MAAM,GAAG,SAAS,CAAC;IACtC,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,gBAAgB,GAAG,KAAK,GAAG,SAAS,CAqD7E;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAShE"}
1
+ {"version":3,"file":"normalizer.d.ts","sourceRoot":"","sources":["../../../../src/lib/adapters/todo-txt/normalizer.ts"],"names":[],"mappings":"AAGA,OAAO,EAAsC,KAAK,KAAK,EAAiB,MAAM,qBAAqB,CAAC;AACpG,OAAO,EAKL,KAAK,cAAc,EACpB,MAAM,aAAa,CAAC;AAErB,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,EAAE,EAAE,MAAM,CAAC;IACX,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;CACpB;AAkED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,cAAc,CAAC;IACvB,SAAS,EAAE,CAAC,cAAc,GAAG,IAAI,CAAC,EAAE,CAAC;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,iBAAiB,EAAE,MAAM,GAAG,SAAS,CAAC;IACtC,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,gBAAgB,GAAG,KAAK,GAAG,SAAS,CAqD7E;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAYtF"}
@@ -1,7 +1,7 @@
1
1
  import path from "node:path";
2
2
  import { AGENT_ANY } from "../../config.js";
3
3
  import { toCanonicalId } from "../../taskSource.js";
4
- import { getMetadataAll, getMetadataFirst, hashLine } from "./parser.js";
4
+ import { DATE_RE, getMetadataAll, getMetadataFirst, hashLine, } from "./parser.js";
5
5
  function derivedCanonicalStatus(parsed) {
6
6
  if (parsed.completed) {
7
7
  return "done";
@@ -90,7 +90,7 @@ export function normalizeToIssue(options) {
90
90
  sourceRef,
91
91
  };
92
92
  }
93
- export function isActiveForFetch(parsed) {
93
+ export function isActiveForFetch(parsed, todayIsoDate) {
94
94
  if (parsed.completed) {
95
95
  return false;
96
96
  }
@@ -98,5 +98,28 @@ export function isActiveForFetch(parsed) {
98
98
  return false;
99
99
  }
100
100
  const statusValue = getMetadataFirst(parsed, "status");
101
- return statusValue === "todo" || statusValue === "in-progress" || statusValue === "in-review";
101
+ if (statusValue === "todo") {
102
+ return !isDeferredByThreshold(parsed, todayIsoDate);
103
+ }
104
+ return statusValue === "in-progress" || statusValue === "in-review";
105
+ }
106
+ // Per todo.txt convention, t: (threshold) hides a task until that date.
107
+ // Only not-yet-started tasks defer; in-progress/in-review work must stay
108
+ // visible so the orchestrator keeps tracking it. Malformed t: values are
109
+ // surfaced by validate() and do not block dispatch.
110
+ function isDeferredByThreshold(parsed, todayIsoDate) {
111
+ const threshold = getMetadataFirst(parsed, "t");
112
+ if (threshold === undefined || !DATE_RE.test(threshold) || !isCalendarDate(threshold)) {
113
+ return false;
114
+ }
115
+ // ISO YYYY-MM-DD dates order lexicographically
116
+ return threshold > todayIsoDate;
117
+ }
118
+ // DATE_RE is format-only; non-calendar values like 2026-99-99 would compare
119
+ // greater than any real date and defer the task forever.
120
+ function isCalendarDate(value) {
121
+ const monthIndex = Number(value.slice(5, 7)) - 1;
122
+ const day = Number(value.slice(8, 10));
123
+ const date = new Date(Date.UTC(Number(value.slice(0, 4)), monthIndex, day));
124
+ return date.getUTCMonth() === monthIndex && date.getUTCDate() === day;
102
125
  }
@@ -12,6 +12,7 @@ export interface ParsedTodoLine {
12
12
  /** True when the final meaningful whitespace-delimited token is a `status:X` field. */
13
13
  readonly isStatusFinalToken: boolean;
14
14
  }
15
+ export declare const DATE_RE: RegExp;
15
16
  export declare function hashLine(raw: string): string;
16
17
  export declare function parseAllLines(fileContent: string): (ParsedTodoLine | null)[];
17
18
  export declare function getMetadataFirst(parsed: ParsedTodoLine, key: string): string | undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../../../../src/lib/adapters/todo-txt/parser.ts"],"names":[],"mappings":"AAEA,KAAK,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC;AAEzD,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAC;IAChC,uFAAuF;IACvF,QAAQ,CAAC,kBAAkB,EAAE,OAAO,CAAC;CACtC;AAQD,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE5C;AA6ED,wBAAgB,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,CAAC,cAAc,GAAG,IAAI,CAAC,EAAE,CAQ5E;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,cAAc,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAExF;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,cAAc,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAE5E"}
1
+ {"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../../../../src/lib/adapters/todo-txt/parser.ts"],"names":[],"mappings":"AAEA,KAAK,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC;AAEzD,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAC;IAChC,uFAAuF;IACvF,QAAQ,CAAC,kBAAkB,EAAE,OAAO,CAAC;CACtC;AAED,eAAO,MAAM,OAAO,QAAwB,CAAC;AAM7C,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE5C;AA6ED,wBAAgB,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,CAAC,cAAc,GAAG,IAAI,CAAC,EAAE,CAQ5E;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,cAAc,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAExF;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,cAAc,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAE5E"}
@@ -1,5 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
- const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
2
+ export const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
3
3
  const KEY_VALUE_RE = /^(?<key>[a-zA-Z][a-zA-Z0-9-]*):(?<value>\S+)$/;
4
4
  const PRIORITY_RE = /^\((?<priority>[A-Z])\) /;
5
5
  const PROJECT_RE = /^\+\S+$/;
@@ -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;AA0RxD,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,oBAAoB,EAC5B,OAAO,EAAE,cAAc,GACtB,UAAU,CAqJZ"}
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;AA6RxD,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,oBAAoB,EAC5B,OAAO,EAAE,cAAc,GACtB,UAAU,CAsJZ"}
@@ -5,9 +5,8 @@ import { AGENT_ANY } from "../../config.js";
5
5
  import { toCanonicalId, } from "../../taskSource.js";
6
6
  import { readEnvironmentVariable } from "../../util.js";
7
7
  import { isActiveForFetch, normalizeToIssue } from "./normalizer.js";
8
- import { getMetadataFirst, parseAllLines } from "./parser.js";
8
+ import { DATE_RE, getMetadataFirst, parseAllLines } from "./parser.js";
9
9
  import { copyPromptFile, updateTaskStatus, validateTodoFile, withLock } from "./writeback.js";
10
- const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
11
10
  const RECURRENCE_RE = /^\+?\d+[dwmy]$/;
12
11
  function readPromptFile(promptPath) {
13
12
  try {
@@ -95,7 +94,7 @@ function metadataToken(key, value) {
95
94
  assertToken(`${key}: value`, value);
96
95
  return `${key}:${value}`;
97
96
  }
98
- function datePartFor(timeZone, now) {
97
+ function isoDateFor(timeZone, now) {
99
98
  const parts = new Intl.DateTimeFormat("en-CA", {
100
99
  timeZone,
101
100
  year: "numeric",
@@ -109,7 +108,10 @@ function datePartFor(timeZone, now) {
109
108
  if (year === undefined || month === undefined || day === undefined) {
110
109
  throw new Error(`todo-txt: could not format date in timezone "${timeZone}"`);
111
110
  }
112
- return `${year}${month}${day}`;
111
+ return `${year}-${month}-${day}`;
112
+ }
113
+ function datePartFor(timeZone, now) {
114
+ return isoDateFor(timeZone, now).replaceAll("-", "");
113
115
  }
114
116
  /* v8 ignore next @preserve -- Covered in source tests; full-suite V8 coverage remaps this helper inconsistently. */
115
117
  function nextGeneratedId(config, parsedAll) {
@@ -256,6 +258,7 @@ export function createTodoTxtTaskSource(config, context) {
256
258
  ]);
257
259
  function listTasks() {
258
260
  const updatedAt = fileUpdatedAt(todoPath);
261
+ const todayIsoDate = isoDateFor(config.timezone, new Date());
259
262
  const { parsedAll } = readAndParseTodo(todoPath);
260
263
  const issues = [];
261
264
  for (let i = 0; i < parsedAll.length; i++) {
@@ -263,7 +266,7 @@ export function createTodoTxtTaskSource(config, context) {
263
266
  if (parsed === null || parsed === undefined) {
264
267
  continue;
265
268
  }
266
- if (!isActiveForFetch(parsed)) {
269
+ if (!isActiveForFetch(parsed, todayIsoDate)) {
267
270
  continue;
268
271
  }
269
272
  const issue = buildIssue({
@@ -1 +1 @@
1
- {"version":3,"file":"writeback.d.ts","sourceRoot":"","sources":["../../../../src/lib/adapters/todo-txt/writeback.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAExD,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;CACvB;AAoKD,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,gBAAgB,CAAC;IACtB,GAAG,CAAC,EAAE,IAAI,CAAC;CACZ;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;AA6E3D,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,aAAa,EACtB,SAAS,EAAE,cAAc,GACxB,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC,CAsElC;AAED,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAQrE;AAgGD,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,WAAW,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,GAChC,MAAM,EAAE,CA0CV"}
1
+ {"version":3,"file":"writeback.d.ts","sourceRoot":"","sources":["../../../../src/lib/adapters/todo-txt/writeback.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAExD,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;CACvB;AAkKD,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,gBAAgB,CAAC;IACtB,GAAG,CAAC,EAAE,IAAI,CAAC;CACZ;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;AAgF3D,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,aAAa,EACtB,SAAS,EAAE,cAAc,GACxB,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC,CAsElC;AAED,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAQrE;AAgGD,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,WAAW,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,GAChC,MAAM,EAAE,CA0CV"}
@@ -1,7 +1,6 @@
1
1
  import { closeSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync, } from "node:fs";
2
2
  import path from "node:path";
3
- import { hashLine, parseAllLines } from "./parser.js";
4
- const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
3
+ import { DATE_RE, hashLine, parseAllLines } from "./parser.js";
5
4
  function isoDate(date) {
6
5
  return date.toISOString().slice(0, 10);
7
6
  }
@@ -181,9 +180,11 @@ function buildRecurResult(parsed, parsedAll, originalLine, ref, completionDateSt
181
180
  const newDue = oldDue === undefined ? undefined : advanceDate(dueBase, rec);
182
181
  // t: always advances from its own current value by the same period
183
182
  const newT = oldT === undefined ? undefined : advanceDate(oldT, rec);
184
- // Compute new date for id advancement
185
- /* v8 ignore next @preserve -- newDue undefined when no due: field; rare edge case */
186
- const newDateForId = newDue === undefined ? now : new Date(`${newDue}T00:00:00Z`);
183
+ // Compute new date for id advancement: prefer due:, then t:, so ids stay
184
+ // schedule-aligned for t:-only recurring tasks
185
+ const newScheduleDate = newDue ?? newT;
186
+ /* v8 ignore next @preserve -- rec: without due: or t: is unusual; id falls back to completion date */
187
+ const newDateForId = newScheduleDate === undefined ? now : new Date(`${newScheduleDate}T00:00:00Z`);
187
188
  const baseNewId = advanceId(ref.id, newDateForId);
188
189
  const newId = buildUniqueId(baseNewId, existingIds);
189
190
  const newTodoLine = buildRecurringLine(originalLine, ref.id, newId, oldDue, newDue, oldT, newT);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clipboard-health/groundcrew",
3
- "version": "4.26.1",
3
+ "version": "4.27.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",