bullswarm 0.10.0 → 0.10.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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # bullswarm changelog
2
2
 
3
+ ## 0.10.1 — initiated-time workflow history search
4
+
5
+ - Added `workflow runs --since <time> --until <time>` filtering against the
6
+ workflow's initiation timestamp (`startedAt`), with an inclusive lower bound
7
+ and exclusive upper bound.
8
+ - Added `--from`/`--to` and `--started-after`/`--started-before` aliases plus
9
+ ISO timestamps, local dates, calendar keywords, and relative durations such
10
+ as `7d`.
11
+ - Historical listing now falls back to `report.startedAt` when an older state
12
+ record lacks its initiation timestamp, and JSON output reports the normalized
13
+ range used for the audit.
14
+
3
15
  ## 0.10.0 — battle-tested advisory orchestration and agent activity
4
16
 
5
17
  - Made `maxAgents` and `maxWorkflowSeconds` advisory planning targets instead
package/README.md CHANGED
@@ -173,6 +173,9 @@ bullswarm workflow runs # ongoing only (default)
173
173
  bullswarm workflow runs --all # ongoing + historical
174
174
  bullswarm workflow runs --historical # only historical
175
175
  bullswarm workflow runs --name audit-code # filter by workflow
176
+ bullswarm workflow runs --all --since 7d # initiated in the last 7 days
177
+ bullswarm workflow runs --historical --since yesterday --until today
178
+ bullswarm workflow runs --all --from 2026-08-20 --to 2026-08-27
176
179
  bullswarm workflow runs --limit 20 # cap the result count
177
180
  bullswarm workflow runs show <shortId> # state + report + summary
178
181
  bullswarm runs show <shortId> # top-level shorthand
@@ -182,6 +185,14 @@ bullswarm workflow runs delete <shortId> --yes # remove the run dir
182
185
  bullswarm workflow run audit-code --resume <shortId>
183
186
  ```
184
187
 
188
+ Run-history time filters always compare when the workflow was initiated
189
+ (`startedAt`), never when it finished. `--since` is inclusive and `--until` is
190
+ exclusive; `--started-after`/`--from` and `--started-before`/`--to` are aliases.
191
+ Values accept ISO timestamps, local `YYYY-MM-DD` dates, `today`, `yesterday`,
192
+ `tomorrow`, `now`, or relative durations such as `30m`, `24h`, `7d`, and `2w`.
193
+ Time filters preserve the existing scope, so use `--all` or `--historical` when
194
+ auditing completed runs.
195
+
185
196
  ### Live workflow dashboard
186
197
 
187
198
  For ordinary observation, use the non-interactive watcher. It prints only when
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bullswarm",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
4
4
  "description": "Route work across coding-agent CLI subscriptions — paced by live quota meters, verified by content, never trusting exit codes.",
5
5
  "type": "module",
6
6
  "bin": {
package/skill/SKILL.md CHANGED
@@ -166,10 +166,19 @@ Every run gets a 6-character shortId (Crockford-style alphabet, no
166
166
  bullswarm workflow runs # ongoing only (default)
167
167
  bullswarm workflow runs --all # ongoing + historical
168
168
  bullswarm workflow runs --name <workflow> # filter by workflow name
169
+ bullswarm workflow runs --all --since 7d # initiated in the last 7 days
170
+ bullswarm workflow runs --historical --since yesterday --until today
169
171
  bullswarm workflow runs show <shortId> # state + report + summary
170
172
  bullswarm workflow runs delete <shortId> --yes
171
173
  ```
172
174
 
175
+ Historical time ranges filter the workflow's initiation time (`startedAt`),
176
+ not completion time. `--since` is inclusive and `--until` is exclusive, with
177
+ `--from`/`--to` and `--started-after`/`--started-before` aliases. Bounds accept
178
+ ISO timestamps, local dates, today/yesterday/tomorrow/now, or durations such as
179
+ `7d`. Add `--all` or `--historical`; time filters do not silently change the
180
+ normal ongoing-only scope.
181
+
173
182
  Before authoring or choosing a workflow, agents can inspect the live execution
174
183
  fabric and the workflow document itself:
175
184
 
@@ -4,6 +4,8 @@
4
4
  // bullswarm workflow runs --all # list ongoing + historical
5
5
  // bullswarm workflow runs --historical # only historical
6
6
  // bullswarm workflow runs --name <workflow> # filter by workflow name
7
+ // bullswarm workflow runs --all --since 7d # initiated in the last 7 days
8
+ // bullswarm workflow runs --all --since yesterday --until today
7
9
  // bullswarm workflow runs --limit N # cap result count
8
10
  // bullswarm workflow runs show <id> # dump state + report
9
11
  // bullswarm workflow runs delete <id> --yes # remove the run directory
@@ -32,29 +34,60 @@ export function cmdRuns(args) {
32
34
  export function runsUsage() {
33
35
  return `usage:
34
36
  bullswarm workflow runs # list ongoing runs
35
- bullswarm workflow runs [--all | --historical] [--name <workflow>] [--limit N] [--json]
37
+ bullswarm workflow runs [--all | --historical] [--name <workflow>]
38
+ [--since <time>] [--until <time>] [--limit N] [--json]
36
39
  bullswarm workflow runs show <id> # <id> = shortId (6 chars) or full runId
37
- bullswarm workflow runs delete <id> --yes # remove the run directory`;
40
+ bullswarm workflow runs delete <id> --yes # remove the run directory
41
+
42
+ Time filters compare the workflow initiation timestamp (startedAt):
43
+ --since / --started-after / --from inclusive lower bound
44
+ --until / --started-before / --to exclusive upper bound
45
+ Values accept ISO timestamps, local dates, today/yesterday/tomorrow/now,
46
+ or relative durations such as 30m, 24h, 7d, and 2w.`;
38
47
  }
39
48
 
40
49
  function parseRunsFlags(argv) {
41
50
  const out = { _positional: [] };
51
+ const valueFlags = new Map([
52
+ ['name', 'name'],
53
+ ['limit', 'limit'],
54
+ ['since', 'since'],
55
+ ['started-after', 'since'],
56
+ ['from', 'since'],
57
+ ['until', 'until'],
58
+ ['started-before', 'until'],
59
+ ['to', 'until'],
60
+ ]);
42
61
  for (let i = 0; i < argv.length; i++) {
43
62
  const a = argv[i];
44
63
  if (a === '--json') out.json = true;
45
64
  else if (a === '--all') out.all = true;
46
65
  else if (a === '--historical') out.historical = true;
47
66
  else if (a === '--yes' || a === '-y') out.yes = true;
48
- else if (a === '--name') out.name = argv[++i];
49
- else if (a === '--limit') out.limit = Number(argv[++i]);
50
67
  else if (a === '--force') out.force = true;
51
- else if (a.startsWith('--')) out[a.slice(2)] = true;
68
+ else if (a.startsWith('--')) {
69
+ const eq = a.indexOf('=');
70
+ const key = a.slice(2, eq > 0 ? eq : undefined);
71
+ const target = valueFlags.get(key);
72
+ if (target) {
73
+ const value = eq > 0 ? a.slice(eq + 1) : argv[++i];
74
+ out[target] = target === 'limit' ? Number(value) : value;
75
+ } else {
76
+ out[key] = eq > 0 ? a.slice(eq + 1) : true;
77
+ }
78
+ }
52
79
  else out._positional.push(a);
53
80
  }
54
81
  return out;
55
82
  }
56
83
 
57
84
  function runsList(opts) {
85
+ let initiatedRange;
86
+ try {
87
+ initiatedRange = resolveInitiatedRange(opts);
88
+ } catch (error) {
89
+ return err(error.message);
90
+ }
58
91
  const all = listRuns(BULLSWARM_DIR());
59
92
  let filtered = all;
60
93
  if (opts.name) filtered = filtered.filter((r) => r.state?.workflow === opts.name);
@@ -64,10 +97,22 @@ function runsList(opts) {
64
97
  } else if (opts.historical) {
65
98
  filtered = filtered.filter((r) => !r.ongoing);
66
99
  }
100
+ if (initiatedRange.sinceMs != null) {
101
+ filtered = filtered.filter((r) => {
102
+ const startedMs = Date.parse(runStartedAt(r) ?? '');
103
+ return Number.isFinite(startedMs) && startedMs >= initiatedRange.sinceMs;
104
+ });
105
+ }
106
+ if (initiatedRange.untilMs != null) {
107
+ filtered = filtered.filter((r) => {
108
+ const startedMs = Date.parse(runStartedAt(r) ?? '');
109
+ return Number.isFinite(startedMs) && startedMs < initiatedRange.untilMs;
110
+ });
111
+ }
67
112
  // Newest first.
68
113
  filtered.sort((a, b) => {
69
- const ta = a.state?.startedAt ?? '';
70
- const tb = b.state?.startedAt ?? '';
114
+ const ta = runStartedAt(a) ?? '';
115
+ const tb = runStartedAt(b) ?? '';
71
116
  return tb.localeCompare(ta);
72
117
  });
73
118
  if (opts.limit && opts.limit > 0) filtered = filtered.slice(0, opts.limit);
@@ -77,6 +122,15 @@ function runsList(opts) {
77
122
  ongoing: opts.all || !opts.historical,
78
123
  historical: opts.all || opts.historical,
79
124
  name: opts.name ?? null,
125
+ initiatedRange: {
126
+ field: 'startedAt',
127
+ sinceInclusive: initiatedRange.sinceMs == null
128
+ ? null
129
+ : new Date(initiatedRange.sinceMs).toISOString(),
130
+ untilExclusive: initiatedRange.untilMs == null
131
+ ? null
132
+ : new Date(initiatedRange.untilMs).toISOString(),
133
+ },
80
134
  count: filtered.length,
81
135
  runs: filtered.map(summarize),
82
136
  }, opts);
@@ -96,7 +150,7 @@ function runsList(opts) {
96
150
  for (const r of filtered) {
97
151
  const wf = r.state?.workflow ?? '?';
98
152
  const status = r.state?.status ?? (r.ongoing ? 'running' : 'unknown');
99
- const age = humanAge(r.state?.startedAt);
153
+ const age = humanAge(runStartedAt(r));
100
154
  const phases = `${r.state?.steps?.filter((s) => s.ok).length ?? 0}/${r.state?.steps?.length ?? 0}`;
101
155
  console.log(
102
156
  `${r.ongoing ? '●' : '○'} ${(r.shortId ?? '------').padEnd(8)} ` +
@@ -167,8 +221,8 @@ function summarize(r) {
167
221
  shortId: r.shortId,
168
222
  workflow: r.state?.workflow ?? null,
169
223
  status: r.state?.status ?? null,
170
- startedAt: r.state?.startedAt ?? null,
171
- finishedAt: r.state?.finishedAt ?? null,
224
+ startedAt: runStartedAt(r),
225
+ finishedAt: r.state?.finishedAt ?? r.report?.finishedAt ?? null,
172
226
  ongoing: r.ongoing,
173
227
  stepsOk: r.state?.steps?.filter((s) => s.ok).length ?? 0,
174
228
  stepsFailed: r.state?.steps?.filter((s) => s.ok === false).length ?? 0,
@@ -177,6 +231,58 @@ function summarize(r) {
177
231
  };
178
232
  }
179
233
 
234
+ function runStartedAt(run) {
235
+ return run.state?.startedAt ?? run.report?.startedAt ?? null;
236
+ }
237
+
238
+ function resolveInitiatedRange(opts, nowMs = Date.now()) {
239
+ const sinceMs = opts.since == null ? null : parseTimeBound(opts.since, nowMs, '--since');
240
+ const untilMs = opts.until == null ? null : parseTimeBound(opts.until, nowMs, '--until');
241
+ if (sinceMs != null && untilMs != null && sinceMs >= untilMs) {
242
+ throw new Error('initiated-time range is empty: --since must be earlier than --until');
243
+ }
244
+ return { sinceMs, untilMs };
245
+ }
246
+
247
+ function parseTimeBound(value, nowMs, flag) {
248
+ const raw = String(value ?? '').trim();
249
+ if (!raw) throw new Error(`${flag} requires a time value`);
250
+ const normalized = raw.toLowerCase();
251
+
252
+ if (normalized === 'now') return nowMs;
253
+ if (['yesterday', 'today', 'tomorrow'].includes(normalized)) {
254
+ const date = new Date(nowMs);
255
+ date.setHours(0, 0, 0, 0);
256
+ if (normalized === 'yesterday') date.setDate(date.getDate() - 1);
257
+ if (normalized === 'tomorrow') date.setDate(date.getDate() + 1);
258
+ return date.getTime();
259
+ }
260
+
261
+ const duration = normalized.match(/^(\d+(?:\.\d+)?)(m|h|d|w)$/);
262
+ if (duration) {
263
+ const unitMs = { m: 60_000, h: 3_600_000, d: 86_400_000, w: 604_800_000 }[duration[2]];
264
+ return nowMs - Number(duration[1]) * unitMs;
265
+ }
266
+
267
+ const localDate = normalized.match(/^(\d{4})-(\d{2})-(\d{2})$/);
268
+ if (localDate) {
269
+ const [, year, month, day] = localDate;
270
+ const date = new Date(Number(year), Number(month) - 1, Number(day));
271
+ if (date.getFullYear() !== Number(year)
272
+ || date.getMonth() !== Number(month) - 1
273
+ || date.getDate() !== Number(day)) {
274
+ throw new Error(`${flag} has an invalid calendar date: "${raw}"`);
275
+ }
276
+ return date.getTime();
277
+ }
278
+
279
+ const parsed = Date.parse(raw);
280
+ if (!Number.isFinite(parsed)) {
281
+ throw new Error(`${flag} has an invalid time: "${raw}"`);
282
+ }
283
+ return parsed;
284
+ }
285
+
180
286
  function humanAge(iso) {
181
287
  if (!iso) return '—';
182
288
  const ms = Date.now() - Date.parse(iso);