@astrosheep/keiyaku 2.9.5 → 2.9.6

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.
@@ -2,10 +2,16 @@ import { FlowError } from "../../flow-error.js";
2
2
  import { isCommissionId } from "../ids.js";
3
3
  import { taskUpdatedAt } from "./model.js";
4
4
  import { isTaskId } from "./validation.js";
5
- const FILTER_FIELDS = new Set([
6
- "state", "priority", "parent", "needs", "blocks", "contract", "created", "updated",
7
- ]);
5
+ export const TASK_FILTER_FIELDS = [
6
+ "title", "state", "priority", "parent", "needs", "blocks", "contract", "created", "updated",
7
+ ];
8
+ export const TASK_FILTER_OPERATORS = ["=", "!=", "~", "<", "<=", ">", ">="];
9
+ export const TASK_FILTER_GRAMMAR = "FIELD OP VALUE[,VALUE...] [| FIELD OP VALUE[,VALUE...]]...";
10
+ export const TASK_FILTER_COMPLETION = "FIELD OP VALUE[,VALUE...]|FIELD OP VALUE[,VALUE...]";
11
+ const FILTER_FIELDS = new Set(TASK_FILTER_FIELDS);
8
12
  const ORDERED_FIELDS = new Set(["priority", "created", "updated"]);
13
+ const TITLE_OPERATORS = new Set(["=", "!=", "~"]);
14
+ const ORDERED_OPERATORS = new Set(["=", "!=", "<", "<=", ">", ">="]);
9
15
  const DISPLAY_VALUES = new Set(["ready", "blocked", "in_progress", "done", "dropped", "bound"]);
10
16
  const SORT_KEYS = new Set(["priority", "modified", "created"]);
11
17
  const RELATIVE_TIME = /^now-([1-9][0-9]*)([smhdw])$/;
@@ -21,57 +27,87 @@ export function deriveTaskDisplay(task) {
21
27
  return "in_progress";
22
28
  return task.blocked ? "blocked" : "ready";
23
29
  }
24
- function invalidFilter(detail) {
25
- throw new FlowError("INVALID_TARGET", `invalid task filter: ${detail}`);
30
+ function invalidFilter(detail, recoveredTerms = []) {
31
+ throw new FlowError("INVALID_TARGET", `invalid task filter: ${detail}`, {
32
+ facts: {
33
+ kind: "invalid_task_filter",
34
+ detail,
35
+ grammar: TASK_FILTER_GRAMMAR,
36
+ fields: [...TASK_FILTER_FIELDS],
37
+ operators: [...TASK_FILTER_OPERATORS],
38
+ recoveredTerms: recoveredTerms.map((term) => ({
39
+ field: term.field,
40
+ operator: term.operator,
41
+ values: [...term.values],
42
+ })),
43
+ },
44
+ });
26
45
  }
27
- export function parseTaskFilter(expression) {
28
- const match = /^([a-z][a-z0-9_-]*)(!=|<=|>=|=|<|>)(.*)$/.exec(expression);
46
+ function parseTaskFilterTerm(term, expression, recoveredTerms) {
47
+ const match = /^([a-z][a-z0-9_-]*)(!=|<=|>=|~|=|<|>)(.*)$/.exec(term);
29
48
  if (!match)
30
- invalidFilter(`'${expression}' must use FIELD OP VALUE`);
49
+ invalidFilter(`'${expression}' must use ${TASK_FILTER_GRAMMAR}`, recoveredTerms);
31
50
  const field = match[1];
32
51
  const operator = match[2];
33
52
  if (!FILTER_FIELDS.has(field))
34
- invalidFilter(`unknown field '${field}'`);
35
- if (!ORDERED_FIELDS.has(field) && operator !== "=" && operator !== "!=") {
36
- invalidFilter(`field '${field}' does not support operator '${operator}'`);
53
+ invalidFilter(`unknown field '${field}'`, recoveredTerms);
54
+ if (field === "title" && !TITLE_OPERATORS.has(operator)) {
55
+ invalidFilter(`field '${field}' does not support operator '${operator}'`, recoveredTerms);
56
+ }
57
+ if (field !== "title" && (ORDERED_FIELDS.has(field)
58
+ ? !ORDERED_OPERATORS.has(operator)
59
+ : operator !== "=" && operator !== "!=")) {
60
+ invalidFilter(`field '${field}' does not support operator '${operator}'`, recoveredTerms);
37
61
  }
38
62
  const values = match[3].split(",");
39
63
  if (values.some((value) => value.length === 0))
40
- invalidFilter(`field '${field}' has an empty value`);
41
- validateFilterValues(field, values);
64
+ invalidFilter(`field '${field}' has an empty value`, recoveredTerms);
65
+ validateFilterValues(field, values, recoveredTerms);
42
66
  return { field, operator, values };
43
67
  }
44
- function parseTimestamp(value, nowMs) {
68
+ export function parseTaskFilter(expression) {
69
+ const terms = expression.split("|");
70
+ const parsed = [];
71
+ for (const term of terms) {
72
+ if (term.length === 0)
73
+ invalidFilter(`'${expression}' must use ${TASK_FILTER_GRAMMAR}`, parsed);
74
+ parsed.push(parseTaskFilterTerm(term, expression, parsed));
75
+ }
76
+ return parsed;
77
+ }
78
+ function parseTimestamp(value, nowMs, recoveredTerms = []) {
45
79
  const relative = RELATIVE_TIME.exec(value);
46
80
  if (relative) {
47
81
  const amount = Number(relative[1]);
48
82
  const offset = amount * UNIT_MS[relative[2]];
49
83
  if (!Number.isSafeInteger(offset))
50
- invalidFilter(`timestamp '${value}' is outside the supported range`);
84
+ invalidFilter(`timestamp '${value}' is outside the supported range`, recoveredTerms);
51
85
  return nowMs - offset;
52
86
  }
53
87
  const timestamp = Date.parse(value);
54
88
  if (!Number.isFinite(timestamp) || new Date(timestamp).toISOString() !== value) {
55
- invalidFilter(`timestamp '${value}' must be canonical ISO-8601 or now-<duration>`);
89
+ invalidFilter(`timestamp '${value}' must be canonical ISO-8601 or now-<duration>`, recoveredTerms);
56
90
  }
57
91
  return timestamp;
58
92
  }
59
- function validateFilterValues(field, values) {
93
+ function validateFilterValues(field, values, recoveredTerms) {
94
+ if (field === "title")
95
+ return;
60
96
  if (field === "state") {
61
97
  for (const value of values)
62
98
  if (!DISPLAY_VALUES.has(value))
63
- invalidFilter(`unknown state '${value}'`);
99
+ invalidFilter(`unknown state '${value}'`, recoveredTerms);
64
100
  return;
65
101
  }
66
102
  if (field === "priority") {
67
103
  for (const value of values)
68
104
  if (!/^[0-3]$/.test(value))
69
- invalidFilter(`priority '${value}' must be 0 through 3`);
105
+ invalidFilter(`priority '${value}' must be 0 through 3`, recoveredTerms);
70
106
  return;
71
107
  }
72
108
  if (field === "created" || field === "updated") {
73
109
  for (const value of values)
74
- parseTimestamp(value, 0);
110
+ parseTimestamp(value, 0, recoveredTerms);
75
111
  return;
76
112
  }
77
113
  if (field === "parent" || field === "needs" || field === "blocks") {
@@ -79,14 +115,14 @@ function validateFilterValues(field, values) {
79
115
  if (field === "parent" && value === "none")
80
116
  continue;
81
117
  if (!isTaskId(value))
82
- invalidFilter(`value '${value}' must be a canonical full task id`);
118
+ invalidFilter(`value '${value}' must be a canonical full task id`, recoveredTerms);
83
119
  }
84
120
  return;
85
121
  }
86
122
  if (field === "contract") {
87
123
  for (const value of values) {
88
124
  if (value !== "none" && !isCommissionId(value))
89
- invalidFilter(`value '${value}' must be a full commission id or none`);
125
+ invalidFilter(`value '${value}' must be a full commission id or none`, recoveredTerms);
90
126
  }
91
127
  }
92
128
  }
@@ -114,10 +150,16 @@ function compareValue(left, right, operator) {
114
150
  case "<=": return left <= right;
115
151
  case ">": return left > right;
116
152
  case ">=": return left >= right;
153
+ case "~": return false;
117
154
  }
118
155
  }
119
156
  function matchesFilter(task, filter, nowMs) {
120
157
  return filter.values.some((raw) => {
158
+ if (filter.field === "title") {
159
+ if (filter.operator === "~")
160
+ return task.title.toLowerCase().includes(raw.toLowerCase());
161
+ return compareValue(task.title, raw, filter.operator);
162
+ }
121
163
  if (filter.field === "state")
122
164
  return compareValue(deriveTaskDisplay(task), raw, filter.operator);
123
165
  if (filter.field === "priority")
@@ -167,6 +209,6 @@ export function selectTaskList(tasks, options = {}) {
167
209
  const nowMs = options.nowMs ?? Date.now();
168
210
  return tasks
169
211
  .filter((task) => selected(task, options.selection ?? "default"))
170
- .filter((task) => filters.every((filter) => matchesFilter(task, filter, nowMs)))
212
+ .filter((task) => filters.every((filter) => filter.some((term) => matchesFilter(task, term, nowMs))))
171
213
  .sort(sort === undefined ? defaultCompare : (left, right) => explicitCompare(sort, left, right));
172
214
  }
@@ -1,3 +1,3 @@
1
1
  // Generated into build output by scripts/build.mjs
2
- export const VERSION = "2.9.5";
3
- export const GIT_HASH = "ade1510";
2
+ export const VERSION = "2.9.6";
3
+ export const GIT_HASH = "cb7f9ef";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrosheep/keiyaku",
3
- "version": "2.9.5",
3
+ "version": "2.9.6",
4
4
  "description": "CLI for running iterative keiyaku workflows with Codex subagents.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -67,6 +67,12 @@ means it constructed the report, not that the candidate is acceptable. Use
67
67
  Commissioned workers never write Git metadata. The host alone reviews and
68
68
  commits accepted bytes before `arc`, `renew`, or `petition`.
69
69
 
70
+ Dispatch rule: every time a dispatcher sends a worker, whether through
71
+ `keiyaku call` or any other agent tool, the prompt must explicitly tell that
72
+ worker to read the worktree-root `.keiyaku/KEIYAKU.md` first and follow its
73
+ objective, scope, and checks. The file is the contract-body carrier; do not
74
+ copy its rendered body into a dispatch prompt.
75
+
70
76
  ## Mid-flight commands (use when needed, skip otherwise)
71
77
 
72
78
  ```bash