@hasna/todos 0.11.59 → 0.11.61

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
@@ -412,7 +412,14 @@ polling:
412
412
  todos webhooks add loops \
413
413
  --id openloops-task-created \
414
414
  --transport command \
415
+ --source todos \
415
416
  --type task.created \
417
+ --metadata 'project_path=/home/hasna/workspace/hasna/opensource/*' \
418
+ --metadata-json 'route_enabled=true' \
419
+ --metadata-json 'automation.no_auto!=true' \
420
+ --metadata-json 'automation.manual_required!=true' \
421
+ --metadata-json 'automation.requires_approval!=true' \
422
+ --metadata-json 'automation.approval_required!=true' \
416
423
  --arg=events \
417
424
  --arg=handle \
418
425
  --arg=todos-task \
@@ -432,8 +439,27 @@ When a task is created, `@hasna/events` sends the event JSON on stdin and in
432
439
  `HASNA_EVENT_JSON`. OpenLoops uses that event to create a deduped one-shot
433
440
  worker/verifier workflow for the task. The event data includes task identity,
434
441
  title, description, project/list ids, working directory, tags, metadata, status,
435
- priority, and timestamps. Local event hooks remain available for local-only
436
- JSONL/socket/script integrations.
442
+ priority, approval state, and timestamps. Event metadata includes routing-safe
443
+ project/list/path fields, `route_enabled` when the task metadata opts in, and an
444
+ `automation` object containing only boolean routing gates such as `no_auto`,
445
+ `manual_required`, `requires_approval`, and `approval_required`.
446
+
447
+ Production task-created routes should fail closed:
448
+
449
+ - Require one explicit opt-in, either task metadata `route_enabled=true` or an
450
+ approved routing tag such as `auto:route`.
451
+ - Add negative automation predicates so `no_auto`, manual, and approval-gated
452
+ tasks do not invoke the route.
453
+ - Scope by project path, task list, tags, or repo metadata before invoking
454
+ OpenLoops.
455
+ - Avoid overlapping opt-in channels for the same task family unless the target
456
+ handler is idempotent. `loops events handle todos-task` dedupes by task id and
457
+ event type, but a narrower subscription still avoids wasted invocations.
458
+
459
+ For tag opt-in, use a second route with the same deny predicates and
460
+ `--data 'tags=auto:route'` instead of `--metadata-json 'route_enabled=true'`.
461
+ Tasks without one of those opt-ins are intentionally no-route. Local event hooks
462
+ remain available for local-only JSONL/socket/script integrations.
437
463
 
438
464
  ## Local Terminal Notifications
439
465
 
package/dist/cli/index.js CHANGED
@@ -6239,7 +6239,7 @@ var init_event_hooks = __esm(() => {
6239
6239
  VALID_TARGETS = new Set(["stdout", "file", "socket", "script"]);
6240
6240
  });
6241
6241
 
6242
- // node_modules/.bun/@hasna+events@0.1.9/node_modules/@hasna/events/dist/index.js
6242
+ // node_modules/.bun/@hasna+events@0.1.11/node_modules/@hasna/events/dist/index.js
6243
6243
  import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
6244
6244
  import { existsSync as existsSync6 } from "fs";
6245
6245
  import { homedir } from "os";
@@ -6256,6 +6256,19 @@ function getPathValue(input, path) {
6256
6256
  return;
6257
6257
  }, input);
6258
6258
  }
6259
+ function getFieldValues(input, path) {
6260
+ const values = [];
6261
+ const push = (value) => {
6262
+ if (!values.some((item) => Object.is(item, value)))
6263
+ values.push(value);
6264
+ };
6265
+ if (path.includes(".") && path in input)
6266
+ push(input[path]);
6267
+ const nestedValue = getPathValue(input, path);
6268
+ if (nestedValue !== undefined || !path.includes("."))
6269
+ push(nestedValue);
6270
+ return values;
6271
+ }
6259
6272
  function wildcardToRegExp(pattern, options = {}) {
6260
6273
  let body = "";
6261
6274
  for (let index = 0;index < pattern.length; index += 1) {
@@ -6285,15 +6298,41 @@ function matchRecord(input, matcher) {
6285
6298
  if (!matcher)
6286
6299
  return true;
6287
6300
  return Object.entries(matcher).every(([path, expected]) => {
6288
- const actual = getPathValue(input, path);
6289
- if (typeof expected === "string" || Array.isArray(expected)) {
6290
- return matchString(actual === undefined ? undefined : String(actual), expected, {
6291
- segmentSafe: path.endsWith("_path") || path.endsWith(".path")
6292
- });
6293
- }
6294
- return actual === expected;
6301
+ const actualValues = getFieldValues(input, path);
6302
+ return matchField(actualValues, expected, path);
6295
6303
  });
6296
6304
  }
6305
+ function matchField(actualValues, expected, path) {
6306
+ if (isNegativeMatcher(expected)) {
6307
+ return !actualValues.some((actual) => matchPositiveField(actual, expected.not, path));
6308
+ }
6309
+ return actualValues.some((actual) => matchPositiveField(actual, expected, path));
6310
+ }
6311
+ function matchPositiveField(actual, expected, path) {
6312
+ if (typeof expected === "string" || Array.isArray(expected)) {
6313
+ return stringCandidates(actual).some((candidate) => matchString(candidate, expected, {
6314
+ segmentSafe: path.endsWith("_path") || path.endsWith(".path")
6315
+ }));
6316
+ }
6317
+ if (Array.isArray(actual)) {
6318
+ return actual.some((item) => item === expected);
6319
+ }
6320
+ return actual === expected;
6321
+ }
6322
+ function stringCandidates(actual) {
6323
+ if (actual === undefined)
6324
+ return [];
6325
+ if (Array.isArray(actual)) {
6326
+ return actual.flatMap((item) => isPrimitiveFieldValue(item) ? [String(item)] : []);
6327
+ }
6328
+ return [String(actual)];
6329
+ }
6330
+ function isPrimitiveFieldValue(value) {
6331
+ return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
6332
+ }
6333
+ function isNegativeMatcher(value) {
6334
+ return Boolean(value && typeof value === "object" && !Array.isArray(value) && "not" in value);
6335
+ }
6297
6336
  function eventMatchesFilter(event, filter) {
6298
6337
  return matchString(event.source, filter.source) && matchString(event.type, filter.type) && matchString(event.subject, filter.subject) && matchString(event.severity, filter.severity) && matchRecord(event.data, filter.data) && matchRecord(event.metadata, filter.metadata);
6299
6338
  }
@@ -6901,9 +6940,66 @@ function taskEventData(task, extra = {}) {
6901
6940
  started_at: task.started_at,
6902
6941
  completed_at: task.completed_at,
6903
6942
  due_at: task.due_at,
6943
+ requires_approval: task.requires_approval,
6944
+ approved_by: task.approved_by,
6945
+ approved_at: task.approved_at,
6904
6946
  ...extra
6905
6947
  };
6906
6948
  }
6949
+ function booleanField(value) {
6950
+ if (typeof value === "boolean")
6951
+ return value;
6952
+ if (typeof value === "number") {
6953
+ if (value === 1)
6954
+ return true;
6955
+ if (value === 0)
6956
+ return false;
6957
+ }
6958
+ if (typeof value === "string") {
6959
+ const normalized = value.trim().toLowerCase();
6960
+ if (["true", "1", "yes", "on"].includes(normalized))
6961
+ return true;
6962
+ if (["false", "0", "no", "off"].includes(normalized))
6963
+ return false;
6964
+ }
6965
+ return;
6966
+ }
6967
+ function objectField(value) {
6968
+ return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
6969
+ }
6970
+ function firstBoolean(records, keys) {
6971
+ for (const record of records) {
6972
+ for (const key of keys) {
6973
+ const value = booleanField(record[key]);
6974
+ if (value !== undefined)
6975
+ return value;
6976
+ }
6977
+ }
6978
+ return;
6979
+ }
6980
+ function routingAutomationMetadata(task) {
6981
+ const automation = objectField(task.metadata.automation);
6982
+ const records = [task.metadata];
6983
+ if (automation)
6984
+ records.push(automation);
6985
+ const result = {};
6986
+ const aliases = [
6987
+ ["allowed", ["allowed", "automation_allowed", "automationAllowed"]],
6988
+ ["no_auto", ["no_auto", "noAuto"]],
6989
+ ["manual", ["manual"]],
6990
+ ["manual_required", ["manual_required", "manualRequired"]],
6991
+ ["requires_approval", ["requires_approval", "requiresApproval"]],
6992
+ ["approval_required", ["approval_required", "approvalRequired"]]
6993
+ ];
6994
+ for (const [canonical, keys] of aliases) {
6995
+ const value = firstBoolean(records, keys);
6996
+ if (value !== undefined)
6997
+ result[canonical] = value;
6998
+ }
6999
+ if (task.requires_approval)
7000
+ result.requires_approval = true;
7001
+ return Object.keys(result).length > 0 ? result : undefined;
7002
+ }
6907
7003
  function taskEventMetadata(task) {
6908
7004
  const metadata = {
6909
7005
  package: "@hasna/todos",
@@ -6914,6 +7010,14 @@ function taskEventMetadata(task) {
6914
7010
  task_list_id: task.task_list_id,
6915
7011
  working_dir: task.working_dir
6916
7012
  };
7013
+ const routeEnabled = booleanField(task.metadata.route_enabled);
7014
+ if (routeEnabled !== undefined) {
7015
+ metadata.route_enabled = routeEnabled;
7016
+ }
7017
+ const automation = routingAutomationMetadata(task);
7018
+ if (automation) {
7019
+ metadata.automation = automation;
7020
+ }
6917
7021
  try {
6918
7022
  const project = task.project_id ? getProject(task.project_id) : null;
6919
7023
  const projectPath = project ? readMachineLocalPath(project) ?? project.path : task.working_dir;
@@ -6931,9 +7035,6 @@ function taskEventMetadata(task) {
6931
7035
  if (projectPath) {
6932
7036
  metadata.project_kind = classifyProjectKind(projectPath);
6933
7037
  metadata.project_is_worktree = isWorktreePath(projectPath);
6934
- if (typeof task.metadata.route_enabled === "boolean") {
6935
- metadata.route_enabled = task.metadata.route_enabled;
6936
- }
6937
7038
  metadata.working_dir = task.working_dir ?? projectPath;
6938
7039
  }
6939
7040
  const taskList = task.task_list_id ? getTaskList(task.task_list_id) ?? (project ? getTaskListBySlug(task.task_list_id, project.id) : null) : project?.task_list_id ? getTaskListBySlug(project.task_list_id, project.id) : null;
package/dist/contracts.js CHANGED
@@ -6955,7 +6955,7 @@ async function testLocalEventHook(name, input) {
6955
6955
  return emitLocalEventHooks({ ...input, hooks: [hook] });
6956
6956
  }
6957
6957
 
6958
- // node_modules/.bun/@hasna+events@0.1.9/node_modules/@hasna/events/dist/index.js
6958
+ // node_modules/.bun/@hasna+events@0.1.11/node_modules/@hasna/events/dist/index.js
6959
6959
  import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
6960
6960
  import { existsSync as existsSync6 } from "fs";
6961
6961
  import { homedir } from "os";
@@ -6972,6 +6972,19 @@ function getPathValue(input, path) {
6972
6972
  return;
6973
6973
  }, input);
6974
6974
  }
6975
+ function getFieldValues(input, path) {
6976
+ const values = [];
6977
+ const push = (value) => {
6978
+ if (!values.some((item) => Object.is(item, value)))
6979
+ values.push(value);
6980
+ };
6981
+ if (path.includes(".") && path in input)
6982
+ push(input[path]);
6983
+ const nestedValue = getPathValue(input, path);
6984
+ if (nestedValue !== undefined || !path.includes("."))
6985
+ push(nestedValue);
6986
+ return values;
6987
+ }
6975
6988
  function wildcardToRegExp(pattern, options = {}) {
6976
6989
  let body = "";
6977
6990
  for (let index = 0;index < pattern.length; index += 1) {
@@ -7001,15 +7014,41 @@ function matchRecord(input, matcher) {
7001
7014
  if (!matcher)
7002
7015
  return true;
7003
7016
  return Object.entries(matcher).every(([path, expected]) => {
7004
- const actual = getPathValue(input, path);
7005
- if (typeof expected === "string" || Array.isArray(expected)) {
7006
- return matchString(actual === undefined ? undefined : String(actual), expected, {
7007
- segmentSafe: path.endsWith("_path") || path.endsWith(".path")
7008
- });
7009
- }
7010
- return actual === expected;
7017
+ const actualValues = getFieldValues(input, path);
7018
+ return matchField(actualValues, expected, path);
7011
7019
  });
7012
7020
  }
7021
+ function matchField(actualValues, expected, path) {
7022
+ if (isNegativeMatcher(expected)) {
7023
+ return !actualValues.some((actual) => matchPositiveField(actual, expected.not, path));
7024
+ }
7025
+ return actualValues.some((actual) => matchPositiveField(actual, expected, path));
7026
+ }
7027
+ function matchPositiveField(actual, expected, path) {
7028
+ if (typeof expected === "string" || Array.isArray(expected)) {
7029
+ return stringCandidates(actual).some((candidate) => matchString(candidate, expected, {
7030
+ segmentSafe: path.endsWith("_path") || path.endsWith(".path")
7031
+ }));
7032
+ }
7033
+ if (Array.isArray(actual)) {
7034
+ return actual.some((item) => item === expected);
7035
+ }
7036
+ return actual === expected;
7037
+ }
7038
+ function stringCandidates(actual) {
7039
+ if (actual === undefined)
7040
+ return [];
7041
+ if (Array.isArray(actual)) {
7042
+ return actual.flatMap((item) => isPrimitiveFieldValue(item) ? [String(item)] : []);
7043
+ }
7044
+ return [String(actual)];
7045
+ }
7046
+ function isPrimitiveFieldValue(value) {
7047
+ return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
7048
+ }
7049
+ function isNegativeMatcher(value) {
7050
+ return Boolean(value && typeof value === "object" && !Array.isArray(value) && "not" in value);
7051
+ }
7013
7052
  function eventMatchesFilter(event, filter) {
7014
7053
  return matchString(event.source, filter.source) && matchString(event.type, filter.type) && matchString(event.subject, filter.subject) && matchString(event.severity, filter.severity) && matchRecord(event.data, filter.data) && matchRecord(event.metadata, filter.metadata);
7015
7054
  }
@@ -7616,9 +7655,66 @@ function taskEventData(task, extra = {}) {
7616
7655
  started_at: task.started_at,
7617
7656
  completed_at: task.completed_at,
7618
7657
  due_at: task.due_at,
7658
+ requires_approval: task.requires_approval,
7659
+ approved_by: task.approved_by,
7660
+ approved_at: task.approved_at,
7619
7661
  ...extra
7620
7662
  };
7621
7663
  }
7664
+ function booleanField(value) {
7665
+ if (typeof value === "boolean")
7666
+ return value;
7667
+ if (typeof value === "number") {
7668
+ if (value === 1)
7669
+ return true;
7670
+ if (value === 0)
7671
+ return false;
7672
+ }
7673
+ if (typeof value === "string") {
7674
+ const normalized = value.trim().toLowerCase();
7675
+ if (["true", "1", "yes", "on"].includes(normalized))
7676
+ return true;
7677
+ if (["false", "0", "no", "off"].includes(normalized))
7678
+ return false;
7679
+ }
7680
+ return;
7681
+ }
7682
+ function objectField(value) {
7683
+ return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
7684
+ }
7685
+ function firstBoolean(records, keys) {
7686
+ for (const record of records) {
7687
+ for (const key of keys) {
7688
+ const value = booleanField(record[key]);
7689
+ if (value !== undefined)
7690
+ return value;
7691
+ }
7692
+ }
7693
+ return;
7694
+ }
7695
+ function routingAutomationMetadata(task) {
7696
+ const automation = objectField(task.metadata.automation);
7697
+ const records = [task.metadata];
7698
+ if (automation)
7699
+ records.push(automation);
7700
+ const result = {};
7701
+ const aliases = [
7702
+ ["allowed", ["allowed", "automation_allowed", "automationAllowed"]],
7703
+ ["no_auto", ["no_auto", "noAuto"]],
7704
+ ["manual", ["manual"]],
7705
+ ["manual_required", ["manual_required", "manualRequired"]],
7706
+ ["requires_approval", ["requires_approval", "requiresApproval"]],
7707
+ ["approval_required", ["approval_required", "approvalRequired"]]
7708
+ ];
7709
+ for (const [canonical, keys] of aliases) {
7710
+ const value = firstBoolean(records, keys);
7711
+ if (value !== undefined)
7712
+ result[canonical] = value;
7713
+ }
7714
+ if (task.requires_approval)
7715
+ result.requires_approval = true;
7716
+ return Object.keys(result).length > 0 ? result : undefined;
7717
+ }
7622
7718
  function taskEventMetadata(task) {
7623
7719
  const metadata = {
7624
7720
  package: "@hasna/todos",
@@ -7629,6 +7725,14 @@ function taskEventMetadata(task) {
7629
7725
  task_list_id: task.task_list_id,
7630
7726
  working_dir: task.working_dir
7631
7727
  };
7728
+ const routeEnabled = booleanField(task.metadata.route_enabled);
7729
+ if (routeEnabled !== undefined) {
7730
+ metadata.route_enabled = routeEnabled;
7731
+ }
7732
+ const automation = routingAutomationMetadata(task);
7733
+ if (automation) {
7734
+ metadata.automation = automation;
7735
+ }
7632
7736
  try {
7633
7737
  const project = task.project_id ? getProject(task.project_id) : null;
7634
7738
  const projectPath = project ? readMachineLocalPath(project) ?? project.path : task.working_dir;
@@ -7646,9 +7750,6 @@ function taskEventMetadata(task) {
7646
7750
  if (projectPath) {
7647
7751
  metadata.project_kind = classifyProjectKind(projectPath);
7648
7752
  metadata.project_is_worktree = isWorktreePath(projectPath);
7649
- if (typeof task.metadata.route_enabled === "boolean") {
7650
- metadata.route_enabled = task.metadata.route_enabled;
7651
- }
7652
7753
  metadata.working_dir = task.working_dir ?? projectPath;
7653
7754
  }
7654
7755
  const taskList = task.task_list_id ? getTaskList(task.task_list_id) ?? (project ? getTaskListBySlug(task.task_list_id, project.id) : null) : project?.task_list_id ? getTaskListBySlug(project.task_list_id, project.id) : null;
package/dist/index.js CHANGED
@@ -7060,7 +7060,7 @@ async function testLocalEventHook(name, input) {
7060
7060
  return emitLocalEventHooks({ ...input, hooks: [hook] });
7061
7061
  }
7062
7062
 
7063
- // node_modules/.bun/@hasna+events@0.1.9/node_modules/@hasna/events/dist/index.js
7063
+ // node_modules/.bun/@hasna+events@0.1.11/node_modules/@hasna/events/dist/index.js
7064
7064
  import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
7065
7065
  import { existsSync as existsSync6 } from "fs";
7066
7066
  import { homedir } from "os";
@@ -7077,6 +7077,19 @@ function getPathValue(input, path) {
7077
7077
  return;
7078
7078
  }, input);
7079
7079
  }
7080
+ function getFieldValues(input, path) {
7081
+ const values = [];
7082
+ const push = (value) => {
7083
+ if (!values.some((item) => Object.is(item, value)))
7084
+ values.push(value);
7085
+ };
7086
+ if (path.includes(".") && path in input)
7087
+ push(input[path]);
7088
+ const nestedValue = getPathValue(input, path);
7089
+ if (nestedValue !== undefined || !path.includes("."))
7090
+ push(nestedValue);
7091
+ return values;
7092
+ }
7080
7093
  function wildcardToRegExp(pattern, options = {}) {
7081
7094
  let body = "";
7082
7095
  for (let index = 0;index < pattern.length; index += 1) {
@@ -7106,15 +7119,41 @@ function matchRecord(input, matcher) {
7106
7119
  if (!matcher)
7107
7120
  return true;
7108
7121
  return Object.entries(matcher).every(([path, expected]) => {
7109
- const actual = getPathValue(input, path);
7110
- if (typeof expected === "string" || Array.isArray(expected)) {
7111
- return matchString(actual === undefined ? undefined : String(actual), expected, {
7112
- segmentSafe: path.endsWith("_path") || path.endsWith(".path")
7113
- });
7114
- }
7115
- return actual === expected;
7122
+ const actualValues = getFieldValues(input, path);
7123
+ return matchField(actualValues, expected, path);
7116
7124
  });
7117
7125
  }
7126
+ function matchField(actualValues, expected, path) {
7127
+ if (isNegativeMatcher(expected)) {
7128
+ return !actualValues.some((actual) => matchPositiveField(actual, expected.not, path));
7129
+ }
7130
+ return actualValues.some((actual) => matchPositiveField(actual, expected, path));
7131
+ }
7132
+ function matchPositiveField(actual, expected, path) {
7133
+ if (typeof expected === "string" || Array.isArray(expected)) {
7134
+ return stringCandidates(actual).some((candidate) => matchString(candidate, expected, {
7135
+ segmentSafe: path.endsWith("_path") || path.endsWith(".path")
7136
+ }));
7137
+ }
7138
+ if (Array.isArray(actual)) {
7139
+ return actual.some((item) => item === expected);
7140
+ }
7141
+ return actual === expected;
7142
+ }
7143
+ function stringCandidates(actual) {
7144
+ if (actual === undefined)
7145
+ return [];
7146
+ if (Array.isArray(actual)) {
7147
+ return actual.flatMap((item) => isPrimitiveFieldValue(item) ? [String(item)] : []);
7148
+ }
7149
+ return [String(actual)];
7150
+ }
7151
+ function isPrimitiveFieldValue(value) {
7152
+ return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
7153
+ }
7154
+ function isNegativeMatcher(value) {
7155
+ return Boolean(value && typeof value === "object" && !Array.isArray(value) && "not" in value);
7156
+ }
7118
7157
  function eventMatchesFilter(event, filter) {
7119
7158
  return matchString(event.source, filter.source) && matchString(event.type, filter.type) && matchString(event.subject, filter.subject) && matchString(event.severity, filter.severity) && matchRecord(event.data, filter.data) && matchRecord(event.metadata, filter.metadata);
7120
7159
  }
@@ -7721,9 +7760,66 @@ function taskEventData(task, extra = {}) {
7721
7760
  started_at: task.started_at,
7722
7761
  completed_at: task.completed_at,
7723
7762
  due_at: task.due_at,
7763
+ requires_approval: task.requires_approval,
7764
+ approved_by: task.approved_by,
7765
+ approved_at: task.approved_at,
7724
7766
  ...extra
7725
7767
  };
7726
7768
  }
7769
+ function booleanField(value) {
7770
+ if (typeof value === "boolean")
7771
+ return value;
7772
+ if (typeof value === "number") {
7773
+ if (value === 1)
7774
+ return true;
7775
+ if (value === 0)
7776
+ return false;
7777
+ }
7778
+ if (typeof value === "string") {
7779
+ const normalized = value.trim().toLowerCase();
7780
+ if (["true", "1", "yes", "on"].includes(normalized))
7781
+ return true;
7782
+ if (["false", "0", "no", "off"].includes(normalized))
7783
+ return false;
7784
+ }
7785
+ return;
7786
+ }
7787
+ function objectField(value) {
7788
+ return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
7789
+ }
7790
+ function firstBoolean(records, keys) {
7791
+ for (const record of records) {
7792
+ for (const key of keys) {
7793
+ const value = booleanField(record[key]);
7794
+ if (value !== undefined)
7795
+ return value;
7796
+ }
7797
+ }
7798
+ return;
7799
+ }
7800
+ function routingAutomationMetadata(task) {
7801
+ const automation = objectField(task.metadata.automation);
7802
+ const records = [task.metadata];
7803
+ if (automation)
7804
+ records.push(automation);
7805
+ const result = {};
7806
+ const aliases = [
7807
+ ["allowed", ["allowed", "automation_allowed", "automationAllowed"]],
7808
+ ["no_auto", ["no_auto", "noAuto"]],
7809
+ ["manual", ["manual"]],
7810
+ ["manual_required", ["manual_required", "manualRequired"]],
7811
+ ["requires_approval", ["requires_approval", "requiresApproval"]],
7812
+ ["approval_required", ["approval_required", "approvalRequired"]]
7813
+ ];
7814
+ for (const [canonical, keys] of aliases) {
7815
+ const value = firstBoolean(records, keys);
7816
+ if (value !== undefined)
7817
+ result[canonical] = value;
7818
+ }
7819
+ if (task.requires_approval)
7820
+ result.requires_approval = true;
7821
+ return Object.keys(result).length > 0 ? result : undefined;
7822
+ }
7727
7823
  function taskEventMetadata(task) {
7728
7824
  const metadata = {
7729
7825
  package: "@hasna/todos",
@@ -7734,6 +7830,14 @@ function taskEventMetadata(task) {
7734
7830
  task_list_id: task.task_list_id,
7735
7831
  working_dir: task.working_dir
7736
7832
  };
7833
+ const routeEnabled = booleanField(task.metadata.route_enabled);
7834
+ if (routeEnabled !== undefined) {
7835
+ metadata.route_enabled = routeEnabled;
7836
+ }
7837
+ const automation = routingAutomationMetadata(task);
7838
+ if (automation) {
7839
+ metadata.automation = automation;
7840
+ }
7737
7841
  try {
7738
7842
  const project = task.project_id ? getProject(task.project_id) : null;
7739
7843
  const projectPath = project ? readMachineLocalPath(project) ?? project.path : task.working_dir;
@@ -7751,9 +7855,6 @@ function taskEventMetadata(task) {
7751
7855
  if (projectPath) {
7752
7856
  metadata.project_kind = classifyProjectKind(projectPath);
7753
7857
  metadata.project_is_worktree = isWorktreePath(projectPath);
7754
- if (typeof task.metadata.route_enabled === "boolean") {
7755
- metadata.route_enabled = task.metadata.route_enabled;
7756
- }
7757
7858
  metadata.working_dir = task.working_dir ?? projectPath;
7758
7859
  }
7759
7860
  const taskList = task.task_list_id ? getTaskList(task.task_list_id) ?? (project ? getTaskListBySlug(task.task_list_id, project.id) : null) : project?.task_list_id ? getTaskListBySlug(project.task_list_id, project.id) : null;
@@ -1 +1 @@
1
- {"version":3,"file":"shared-events.d.ts","sourceRoot":"","sources":["../../src/lib/shared-events.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAKnD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AAI9C,MAAM,MAAM,oBAAoB,GAC5B,cAAc,GACd,cAAc,GACd,gBAAgB,GAChB,aAAa,GACb,cAAc,GACd,eAAe,GACf,qBAAqB,GACrB,gBAAgB,CAAC;AAErB,wBAAgB,aAAa,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CA2BtG;AAgFD,wBAAsB,mBAAmB,CAAC,KAAK,EAAE;IAC/C,IAAI,EAAE,oBAAoB,CAAC;IAC3B,IAAI,EAAE,IAAI,CAAC;IACX,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,GAAG,OAAO,CAAC,IAAI,CAAC,CAehB;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,UAAU,CAAC,OAAO,mBAAmB,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,CAE/F"}
1
+ {"version":3,"file":"shared-events.d.ts","sourceRoot":"","sources":["../../src/lib/shared-events.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAKnD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AAI9C,MAAM,MAAM,oBAAoB,GAC5B,cAAc,GACd,cAAc,GACd,gBAAgB,GAChB,aAAa,GACb,cAAc,GACd,eAAe,GACf,qBAAqB,GACrB,gBAAgB,CAAC;AAErB,wBAAgB,aAAa,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CA8BtG;AA0ID,wBAAsB,mBAAmB,CAAC,KAAK,EAAE;IAC/C,IAAI,EAAE,oBAAoB,CAAC;IAC3B,IAAI,EAAE,IAAI,CAAC;IACX,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,GAAG,OAAO,CAAC,IAAI,CAAC,CAehB;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,UAAU,CAAC,OAAO,mBAAmB,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,CAE/F"}
package/dist/mcp/index.js CHANGED
@@ -8738,7 +8738,7 @@ var init_event_hooks = __esm(() => {
8738
8738
  VALID_TARGETS = new Set(["stdout", "file", "socket", "script"]);
8739
8739
  });
8740
8740
 
8741
- // node_modules/.bun/@hasna+events@0.1.9/node_modules/@hasna/events/dist/index.js
8741
+ // node_modules/.bun/@hasna+events@0.1.11/node_modules/@hasna/events/dist/index.js
8742
8742
  import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
8743
8743
  import { existsSync as existsSync5 } from "fs";
8744
8744
  import { homedir } from "os";
@@ -8755,6 +8755,19 @@ function getPathValue(input, path) {
8755
8755
  return;
8756
8756
  }, input);
8757
8757
  }
8758
+ function getFieldValues(input, path) {
8759
+ const values = [];
8760
+ const push = (value) => {
8761
+ if (!values.some((item) => Object.is(item, value)))
8762
+ values.push(value);
8763
+ };
8764
+ if (path.includes(".") && path in input)
8765
+ push(input[path]);
8766
+ const nestedValue = getPathValue(input, path);
8767
+ if (nestedValue !== undefined || !path.includes("."))
8768
+ push(nestedValue);
8769
+ return values;
8770
+ }
8758
8771
  function wildcardToRegExp(pattern, options = {}) {
8759
8772
  let body = "";
8760
8773
  for (let index = 0;index < pattern.length; index += 1) {
@@ -8784,15 +8797,41 @@ function matchRecord(input, matcher) {
8784
8797
  if (!matcher)
8785
8798
  return true;
8786
8799
  return Object.entries(matcher).every(([path, expected]) => {
8787
- const actual = getPathValue(input, path);
8788
- if (typeof expected === "string" || Array.isArray(expected)) {
8789
- return matchString(actual === undefined ? undefined : String(actual), expected, {
8790
- segmentSafe: path.endsWith("_path") || path.endsWith(".path")
8791
- });
8792
- }
8793
- return actual === expected;
8800
+ const actualValues = getFieldValues(input, path);
8801
+ return matchField(actualValues, expected, path);
8794
8802
  });
8795
8803
  }
8804
+ function matchField(actualValues, expected, path) {
8805
+ if (isNegativeMatcher(expected)) {
8806
+ return !actualValues.some((actual) => matchPositiveField(actual, expected.not, path));
8807
+ }
8808
+ return actualValues.some((actual) => matchPositiveField(actual, expected, path));
8809
+ }
8810
+ function matchPositiveField(actual, expected, path) {
8811
+ if (typeof expected === "string" || Array.isArray(expected)) {
8812
+ return stringCandidates(actual).some((candidate) => matchString(candidate, expected, {
8813
+ segmentSafe: path.endsWith("_path") || path.endsWith(".path")
8814
+ }));
8815
+ }
8816
+ if (Array.isArray(actual)) {
8817
+ return actual.some((item) => item === expected);
8818
+ }
8819
+ return actual === expected;
8820
+ }
8821
+ function stringCandidates(actual) {
8822
+ if (actual === undefined)
8823
+ return [];
8824
+ if (Array.isArray(actual)) {
8825
+ return actual.flatMap((item) => isPrimitiveFieldValue(item) ? [String(item)] : []);
8826
+ }
8827
+ return [String(actual)];
8828
+ }
8829
+ function isPrimitiveFieldValue(value) {
8830
+ return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
8831
+ }
8832
+ function isNegativeMatcher(value) {
8833
+ return Boolean(value && typeof value === "object" && !Array.isArray(value) && "not" in value);
8834
+ }
8796
8835
  function eventMatchesFilter(event, filter) {
8797
8836
  return matchString(event.source, filter.source) && matchString(event.type, filter.type) && matchString(event.subject, filter.subject) && matchString(event.severity, filter.severity) && matchRecord(event.data, filter.data) && matchRecord(event.metadata, filter.metadata);
8798
8837
  }
@@ -9400,9 +9439,66 @@ function taskEventData(task, extra = {}) {
9400
9439
  started_at: task.started_at,
9401
9440
  completed_at: task.completed_at,
9402
9441
  due_at: task.due_at,
9442
+ requires_approval: task.requires_approval,
9443
+ approved_by: task.approved_by,
9444
+ approved_at: task.approved_at,
9403
9445
  ...extra
9404
9446
  };
9405
9447
  }
9448
+ function booleanField(value) {
9449
+ if (typeof value === "boolean")
9450
+ return value;
9451
+ if (typeof value === "number") {
9452
+ if (value === 1)
9453
+ return true;
9454
+ if (value === 0)
9455
+ return false;
9456
+ }
9457
+ if (typeof value === "string") {
9458
+ const normalized = value.trim().toLowerCase();
9459
+ if (["true", "1", "yes", "on"].includes(normalized))
9460
+ return true;
9461
+ if (["false", "0", "no", "off"].includes(normalized))
9462
+ return false;
9463
+ }
9464
+ return;
9465
+ }
9466
+ function objectField(value) {
9467
+ return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
9468
+ }
9469
+ function firstBoolean(records, keys) {
9470
+ for (const record of records) {
9471
+ for (const key of keys) {
9472
+ const value = booleanField(record[key]);
9473
+ if (value !== undefined)
9474
+ return value;
9475
+ }
9476
+ }
9477
+ return;
9478
+ }
9479
+ function routingAutomationMetadata(task) {
9480
+ const automation = objectField(task.metadata.automation);
9481
+ const records = [task.metadata];
9482
+ if (automation)
9483
+ records.push(automation);
9484
+ const result = {};
9485
+ const aliases = [
9486
+ ["allowed", ["allowed", "automation_allowed", "automationAllowed"]],
9487
+ ["no_auto", ["no_auto", "noAuto"]],
9488
+ ["manual", ["manual"]],
9489
+ ["manual_required", ["manual_required", "manualRequired"]],
9490
+ ["requires_approval", ["requires_approval", "requiresApproval"]],
9491
+ ["approval_required", ["approval_required", "approvalRequired"]]
9492
+ ];
9493
+ for (const [canonical, keys] of aliases) {
9494
+ const value = firstBoolean(records, keys);
9495
+ if (value !== undefined)
9496
+ result[canonical] = value;
9497
+ }
9498
+ if (task.requires_approval)
9499
+ result.requires_approval = true;
9500
+ return Object.keys(result).length > 0 ? result : undefined;
9501
+ }
9406
9502
  function taskEventMetadata(task) {
9407
9503
  const metadata = {
9408
9504
  package: "@hasna/todos",
@@ -9413,6 +9509,14 @@ function taskEventMetadata(task) {
9413
9509
  task_list_id: task.task_list_id,
9414
9510
  working_dir: task.working_dir
9415
9511
  };
9512
+ const routeEnabled = booleanField(task.metadata.route_enabled);
9513
+ if (routeEnabled !== undefined) {
9514
+ metadata.route_enabled = routeEnabled;
9515
+ }
9516
+ const automation = routingAutomationMetadata(task);
9517
+ if (automation) {
9518
+ metadata.automation = automation;
9519
+ }
9416
9520
  try {
9417
9521
  const project = task.project_id ? getProject(task.project_id) : null;
9418
9522
  const projectPath = project ? readMachineLocalPath(project) ?? project.path : task.working_dir;
@@ -9430,9 +9534,6 @@ function taskEventMetadata(task) {
9430
9534
  if (projectPath) {
9431
9535
  metadata.project_kind = classifyProjectKind(projectPath);
9432
9536
  metadata.project_is_worktree = isWorktreePath(projectPath);
9433
- if (typeof task.metadata.route_enabled === "boolean") {
9434
- metadata.route_enabled = task.metadata.route_enabled;
9435
- }
9436
9537
  metadata.working_dir = task.working_dir ?? projectPath;
9437
9538
  }
9438
9539
  const taskList = task.task_list_id ? getTaskList(task.task_list_id) ?? (project ? getTaskListBySlug(task.task_list_id, project.id) : null) : project?.task_list_id ? getTaskListBySlug(project.task_list_id, project.id) : null;
package/dist/registry.js CHANGED
@@ -6955,7 +6955,7 @@ async function testLocalEventHook(name, input) {
6955
6955
  return emitLocalEventHooks({ ...input, hooks: [hook] });
6956
6956
  }
6957
6957
 
6958
- // node_modules/.bun/@hasna+events@0.1.9/node_modules/@hasna/events/dist/index.js
6958
+ // node_modules/.bun/@hasna+events@0.1.11/node_modules/@hasna/events/dist/index.js
6959
6959
  import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
6960
6960
  import { existsSync as existsSync6 } from "fs";
6961
6961
  import { homedir } from "os";
@@ -6972,6 +6972,19 @@ function getPathValue(input, path) {
6972
6972
  return;
6973
6973
  }, input);
6974
6974
  }
6975
+ function getFieldValues(input, path) {
6976
+ const values = [];
6977
+ const push = (value) => {
6978
+ if (!values.some((item) => Object.is(item, value)))
6979
+ values.push(value);
6980
+ };
6981
+ if (path.includes(".") && path in input)
6982
+ push(input[path]);
6983
+ const nestedValue = getPathValue(input, path);
6984
+ if (nestedValue !== undefined || !path.includes("."))
6985
+ push(nestedValue);
6986
+ return values;
6987
+ }
6975
6988
  function wildcardToRegExp(pattern, options = {}) {
6976
6989
  let body = "";
6977
6990
  for (let index = 0;index < pattern.length; index += 1) {
@@ -7001,15 +7014,41 @@ function matchRecord(input, matcher) {
7001
7014
  if (!matcher)
7002
7015
  return true;
7003
7016
  return Object.entries(matcher).every(([path, expected]) => {
7004
- const actual = getPathValue(input, path);
7005
- if (typeof expected === "string" || Array.isArray(expected)) {
7006
- return matchString(actual === undefined ? undefined : String(actual), expected, {
7007
- segmentSafe: path.endsWith("_path") || path.endsWith(".path")
7008
- });
7009
- }
7010
- return actual === expected;
7017
+ const actualValues = getFieldValues(input, path);
7018
+ return matchField(actualValues, expected, path);
7011
7019
  });
7012
7020
  }
7021
+ function matchField(actualValues, expected, path) {
7022
+ if (isNegativeMatcher(expected)) {
7023
+ return !actualValues.some((actual) => matchPositiveField(actual, expected.not, path));
7024
+ }
7025
+ return actualValues.some((actual) => matchPositiveField(actual, expected, path));
7026
+ }
7027
+ function matchPositiveField(actual, expected, path) {
7028
+ if (typeof expected === "string" || Array.isArray(expected)) {
7029
+ return stringCandidates(actual).some((candidate) => matchString(candidate, expected, {
7030
+ segmentSafe: path.endsWith("_path") || path.endsWith(".path")
7031
+ }));
7032
+ }
7033
+ if (Array.isArray(actual)) {
7034
+ return actual.some((item) => item === expected);
7035
+ }
7036
+ return actual === expected;
7037
+ }
7038
+ function stringCandidates(actual) {
7039
+ if (actual === undefined)
7040
+ return [];
7041
+ if (Array.isArray(actual)) {
7042
+ return actual.flatMap((item) => isPrimitiveFieldValue(item) ? [String(item)] : []);
7043
+ }
7044
+ return [String(actual)];
7045
+ }
7046
+ function isPrimitiveFieldValue(value) {
7047
+ return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
7048
+ }
7049
+ function isNegativeMatcher(value) {
7050
+ return Boolean(value && typeof value === "object" && !Array.isArray(value) && "not" in value);
7051
+ }
7013
7052
  function eventMatchesFilter(event, filter) {
7014
7053
  return matchString(event.source, filter.source) && matchString(event.type, filter.type) && matchString(event.subject, filter.subject) && matchString(event.severity, filter.severity) && matchRecord(event.data, filter.data) && matchRecord(event.metadata, filter.metadata);
7015
7054
  }
@@ -7616,9 +7655,66 @@ function taskEventData(task, extra = {}) {
7616
7655
  started_at: task.started_at,
7617
7656
  completed_at: task.completed_at,
7618
7657
  due_at: task.due_at,
7658
+ requires_approval: task.requires_approval,
7659
+ approved_by: task.approved_by,
7660
+ approved_at: task.approved_at,
7619
7661
  ...extra
7620
7662
  };
7621
7663
  }
7664
+ function booleanField(value) {
7665
+ if (typeof value === "boolean")
7666
+ return value;
7667
+ if (typeof value === "number") {
7668
+ if (value === 1)
7669
+ return true;
7670
+ if (value === 0)
7671
+ return false;
7672
+ }
7673
+ if (typeof value === "string") {
7674
+ const normalized = value.trim().toLowerCase();
7675
+ if (["true", "1", "yes", "on"].includes(normalized))
7676
+ return true;
7677
+ if (["false", "0", "no", "off"].includes(normalized))
7678
+ return false;
7679
+ }
7680
+ return;
7681
+ }
7682
+ function objectField(value) {
7683
+ return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
7684
+ }
7685
+ function firstBoolean(records, keys) {
7686
+ for (const record of records) {
7687
+ for (const key of keys) {
7688
+ const value = booleanField(record[key]);
7689
+ if (value !== undefined)
7690
+ return value;
7691
+ }
7692
+ }
7693
+ return;
7694
+ }
7695
+ function routingAutomationMetadata(task) {
7696
+ const automation = objectField(task.metadata.automation);
7697
+ const records = [task.metadata];
7698
+ if (automation)
7699
+ records.push(automation);
7700
+ const result = {};
7701
+ const aliases = [
7702
+ ["allowed", ["allowed", "automation_allowed", "automationAllowed"]],
7703
+ ["no_auto", ["no_auto", "noAuto"]],
7704
+ ["manual", ["manual"]],
7705
+ ["manual_required", ["manual_required", "manualRequired"]],
7706
+ ["requires_approval", ["requires_approval", "requiresApproval"]],
7707
+ ["approval_required", ["approval_required", "approvalRequired"]]
7708
+ ];
7709
+ for (const [canonical, keys] of aliases) {
7710
+ const value = firstBoolean(records, keys);
7711
+ if (value !== undefined)
7712
+ result[canonical] = value;
7713
+ }
7714
+ if (task.requires_approval)
7715
+ result.requires_approval = true;
7716
+ return Object.keys(result).length > 0 ? result : undefined;
7717
+ }
7622
7718
  function taskEventMetadata(task) {
7623
7719
  const metadata = {
7624
7720
  package: "@hasna/todos",
@@ -7629,6 +7725,14 @@ function taskEventMetadata(task) {
7629
7725
  task_list_id: task.task_list_id,
7630
7726
  working_dir: task.working_dir
7631
7727
  };
7728
+ const routeEnabled = booleanField(task.metadata.route_enabled);
7729
+ if (routeEnabled !== undefined) {
7730
+ metadata.route_enabled = routeEnabled;
7731
+ }
7732
+ const automation = routingAutomationMetadata(task);
7733
+ if (automation) {
7734
+ metadata.automation = automation;
7735
+ }
7632
7736
  try {
7633
7737
  const project = task.project_id ? getProject(task.project_id) : null;
7634
7738
  const projectPath = project ? readMachineLocalPath(project) ?? project.path : task.working_dir;
@@ -7646,9 +7750,6 @@ function taskEventMetadata(task) {
7646
7750
  if (projectPath) {
7647
7751
  metadata.project_kind = classifyProjectKind(projectPath);
7648
7752
  metadata.project_is_worktree = isWorktreePath(projectPath);
7649
- if (typeof task.metadata.route_enabled === "boolean") {
7650
- metadata.route_enabled = task.metadata.route_enabled;
7651
- }
7652
7753
  metadata.working_dir = task.working_dir ?? projectPath;
7653
7754
  }
7654
7755
  const taskList = task.task_list_id ? getTaskList(task.task_list_id) ?? (project ? getTaskListBySlug(task.task_list_id, project.id) : null) : project?.task_list_id ? getTaskListBySlug(project.task_list_id, project.id) : null;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "packageName": "@hasna/todos",
3
- "packageVersion": "0.11.59",
3
+ "packageVersion": "0.11.61",
4
4
  "repository": "https://github.com/hasna/todos.git",
5
- "gitCommit": "b507930ee09c9b1f5c7a46881679965fd35b03fb",
6
- "generatedAt": "2026-06-27T09:34:02.825Z"
5
+ "gitCommit": "7dced09f93029fe58db334ff712e10094beefae2",
6
+ "generatedAt": "2026-06-27T14:41:53.504Z"
7
7
  }
@@ -4099,7 +4099,7 @@ var init_event_hooks = __esm(() => {
4099
4099
  VALID_TARGETS = new Set(["stdout", "file", "socket", "script"]);
4100
4100
  });
4101
4101
 
4102
- // node_modules/.bun/@hasna+events@0.1.9/node_modules/@hasna/events/dist/index.js
4102
+ // node_modules/.bun/@hasna+events@0.1.11/node_modules/@hasna/events/dist/index.js
4103
4103
  import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
4104
4104
  import { existsSync as existsSync6 } from "fs";
4105
4105
  import { homedir } from "os";
@@ -4116,6 +4116,19 @@ function getPathValue(input, path) {
4116
4116
  return;
4117
4117
  }, input);
4118
4118
  }
4119
+ function getFieldValues(input, path) {
4120
+ const values = [];
4121
+ const push = (value) => {
4122
+ if (!values.some((item) => Object.is(item, value)))
4123
+ values.push(value);
4124
+ };
4125
+ if (path.includes(".") && path in input)
4126
+ push(input[path]);
4127
+ const nestedValue = getPathValue(input, path);
4128
+ if (nestedValue !== undefined || !path.includes("."))
4129
+ push(nestedValue);
4130
+ return values;
4131
+ }
4119
4132
  function wildcardToRegExp(pattern, options = {}) {
4120
4133
  let body = "";
4121
4134
  for (let index = 0;index < pattern.length; index += 1) {
@@ -4145,15 +4158,41 @@ function matchRecord(input, matcher) {
4145
4158
  if (!matcher)
4146
4159
  return true;
4147
4160
  return Object.entries(matcher).every(([path, expected]) => {
4148
- const actual = getPathValue(input, path);
4149
- if (typeof expected === "string" || Array.isArray(expected)) {
4150
- return matchString(actual === undefined ? undefined : String(actual), expected, {
4151
- segmentSafe: path.endsWith("_path") || path.endsWith(".path")
4152
- });
4153
- }
4154
- return actual === expected;
4161
+ const actualValues = getFieldValues(input, path);
4162
+ return matchField(actualValues, expected, path);
4155
4163
  });
4156
4164
  }
4165
+ function matchField(actualValues, expected, path) {
4166
+ if (isNegativeMatcher(expected)) {
4167
+ return !actualValues.some((actual) => matchPositiveField(actual, expected.not, path));
4168
+ }
4169
+ return actualValues.some((actual) => matchPositiveField(actual, expected, path));
4170
+ }
4171
+ function matchPositiveField(actual, expected, path) {
4172
+ if (typeof expected === "string" || Array.isArray(expected)) {
4173
+ return stringCandidates(actual).some((candidate) => matchString(candidate, expected, {
4174
+ segmentSafe: path.endsWith("_path") || path.endsWith(".path")
4175
+ }));
4176
+ }
4177
+ if (Array.isArray(actual)) {
4178
+ return actual.some((item) => item === expected);
4179
+ }
4180
+ return actual === expected;
4181
+ }
4182
+ function stringCandidates(actual) {
4183
+ if (actual === undefined)
4184
+ return [];
4185
+ if (Array.isArray(actual)) {
4186
+ return actual.flatMap((item) => isPrimitiveFieldValue(item) ? [String(item)] : []);
4187
+ }
4188
+ return [String(actual)];
4189
+ }
4190
+ function isPrimitiveFieldValue(value) {
4191
+ return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
4192
+ }
4193
+ function isNegativeMatcher(value) {
4194
+ return Boolean(value && typeof value === "object" && !Array.isArray(value) && "not" in value);
4195
+ }
4157
4196
  function eventMatchesFilter(event, filter) {
4158
4197
  return matchString(event.source, filter.source) && matchString(event.type, filter.type) && matchString(event.subject, filter.subject) && matchString(event.severity, filter.severity) && matchRecord(event.data, filter.data) && matchRecord(event.metadata, filter.metadata);
4159
4198
  }
@@ -4761,9 +4800,66 @@ function taskEventData(task, extra = {}) {
4761
4800
  started_at: task.started_at,
4762
4801
  completed_at: task.completed_at,
4763
4802
  due_at: task.due_at,
4803
+ requires_approval: task.requires_approval,
4804
+ approved_by: task.approved_by,
4805
+ approved_at: task.approved_at,
4764
4806
  ...extra
4765
4807
  };
4766
4808
  }
4809
+ function booleanField(value) {
4810
+ if (typeof value === "boolean")
4811
+ return value;
4812
+ if (typeof value === "number") {
4813
+ if (value === 1)
4814
+ return true;
4815
+ if (value === 0)
4816
+ return false;
4817
+ }
4818
+ if (typeof value === "string") {
4819
+ const normalized = value.trim().toLowerCase();
4820
+ if (["true", "1", "yes", "on"].includes(normalized))
4821
+ return true;
4822
+ if (["false", "0", "no", "off"].includes(normalized))
4823
+ return false;
4824
+ }
4825
+ return;
4826
+ }
4827
+ function objectField(value) {
4828
+ return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
4829
+ }
4830
+ function firstBoolean(records, keys) {
4831
+ for (const record of records) {
4832
+ for (const key of keys) {
4833
+ const value = booleanField(record[key]);
4834
+ if (value !== undefined)
4835
+ return value;
4836
+ }
4837
+ }
4838
+ return;
4839
+ }
4840
+ function routingAutomationMetadata(task) {
4841
+ const automation = objectField(task.metadata.automation);
4842
+ const records = [task.metadata];
4843
+ if (automation)
4844
+ records.push(automation);
4845
+ const result = {};
4846
+ const aliases = [
4847
+ ["allowed", ["allowed", "automation_allowed", "automationAllowed"]],
4848
+ ["no_auto", ["no_auto", "noAuto"]],
4849
+ ["manual", ["manual"]],
4850
+ ["manual_required", ["manual_required", "manualRequired"]],
4851
+ ["requires_approval", ["requires_approval", "requiresApproval"]],
4852
+ ["approval_required", ["approval_required", "approvalRequired"]]
4853
+ ];
4854
+ for (const [canonical, keys] of aliases) {
4855
+ const value = firstBoolean(records, keys);
4856
+ if (value !== undefined)
4857
+ result[canonical] = value;
4858
+ }
4859
+ if (task.requires_approval)
4860
+ result.requires_approval = true;
4861
+ return Object.keys(result).length > 0 ? result : undefined;
4862
+ }
4767
4863
  function taskEventMetadata(task) {
4768
4864
  const metadata = {
4769
4865
  package: "@hasna/todos",
@@ -4774,6 +4870,14 @@ function taskEventMetadata(task) {
4774
4870
  task_list_id: task.task_list_id,
4775
4871
  working_dir: task.working_dir
4776
4872
  };
4873
+ const routeEnabled = booleanField(task.metadata.route_enabled);
4874
+ if (routeEnabled !== undefined) {
4875
+ metadata.route_enabled = routeEnabled;
4876
+ }
4877
+ const automation = routingAutomationMetadata(task);
4878
+ if (automation) {
4879
+ metadata.automation = automation;
4880
+ }
4777
4881
  try {
4778
4882
  const project = task.project_id ? getProject(task.project_id) : null;
4779
4883
  const projectPath = project ? readMachineLocalPath(project) ?? project.path : task.working_dir;
@@ -4791,9 +4895,6 @@ function taskEventMetadata(task) {
4791
4895
  if (projectPath) {
4792
4896
  metadata.project_kind = classifyProjectKind(projectPath);
4793
4897
  metadata.project_is_worktree = isWorktreePath(projectPath);
4794
- if (typeof task.metadata.route_enabled === "boolean") {
4795
- metadata.route_enabled = task.metadata.route_enabled;
4796
- }
4797
4898
  metadata.working_dir = task.working_dir ?? projectPath;
4798
4899
  }
4799
4900
  const taskList = task.task_list_id ? getTaskList(task.task_list_id) ?? (project ? getTaskListBySlug(task.task_list_id, project.id) : null) : project?.task_list_id ? getTaskListBySlug(project.task_list_id, project.id) : null;
package/dist/storage.js CHANGED
@@ -4503,7 +4503,7 @@ async function testLocalEventHook(name, input) {
4503
4503
  return emitLocalEventHooks({ ...input, hooks: [hook] });
4504
4504
  }
4505
4505
 
4506
- // node_modules/.bun/@hasna+events@0.1.9/node_modules/@hasna/events/dist/index.js
4506
+ // node_modules/.bun/@hasna+events@0.1.11/node_modules/@hasna/events/dist/index.js
4507
4507
  import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
4508
4508
  import { existsSync as existsSync5 } from "fs";
4509
4509
  import { homedir } from "os";
@@ -4520,6 +4520,19 @@ function getPathValue(input, path) {
4520
4520
  return;
4521
4521
  }, input);
4522
4522
  }
4523
+ function getFieldValues(input, path) {
4524
+ const values = [];
4525
+ const push = (value) => {
4526
+ if (!values.some((item) => Object.is(item, value)))
4527
+ values.push(value);
4528
+ };
4529
+ if (path.includes(".") && path in input)
4530
+ push(input[path]);
4531
+ const nestedValue = getPathValue(input, path);
4532
+ if (nestedValue !== undefined || !path.includes("."))
4533
+ push(nestedValue);
4534
+ return values;
4535
+ }
4523
4536
  function wildcardToRegExp(pattern, options = {}) {
4524
4537
  let body = "";
4525
4538
  for (let index = 0;index < pattern.length; index += 1) {
@@ -4549,15 +4562,41 @@ function matchRecord(input, matcher) {
4549
4562
  if (!matcher)
4550
4563
  return true;
4551
4564
  return Object.entries(matcher).every(([path, expected]) => {
4552
- const actual = getPathValue(input, path);
4553
- if (typeof expected === "string" || Array.isArray(expected)) {
4554
- return matchString(actual === undefined ? undefined : String(actual), expected, {
4555
- segmentSafe: path.endsWith("_path") || path.endsWith(".path")
4556
- });
4557
- }
4558
- return actual === expected;
4565
+ const actualValues = getFieldValues(input, path);
4566
+ return matchField(actualValues, expected, path);
4559
4567
  });
4560
4568
  }
4569
+ function matchField(actualValues, expected, path) {
4570
+ if (isNegativeMatcher(expected)) {
4571
+ return !actualValues.some((actual) => matchPositiveField(actual, expected.not, path));
4572
+ }
4573
+ return actualValues.some((actual) => matchPositiveField(actual, expected, path));
4574
+ }
4575
+ function matchPositiveField(actual, expected, path) {
4576
+ if (typeof expected === "string" || Array.isArray(expected)) {
4577
+ return stringCandidates(actual).some((candidate) => matchString(candidate, expected, {
4578
+ segmentSafe: path.endsWith("_path") || path.endsWith(".path")
4579
+ }));
4580
+ }
4581
+ if (Array.isArray(actual)) {
4582
+ return actual.some((item) => item === expected);
4583
+ }
4584
+ return actual === expected;
4585
+ }
4586
+ function stringCandidates(actual) {
4587
+ if (actual === undefined)
4588
+ return [];
4589
+ if (Array.isArray(actual)) {
4590
+ return actual.flatMap((item) => isPrimitiveFieldValue(item) ? [String(item)] : []);
4591
+ }
4592
+ return [String(actual)];
4593
+ }
4594
+ function isPrimitiveFieldValue(value) {
4595
+ return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
4596
+ }
4597
+ function isNegativeMatcher(value) {
4598
+ return Boolean(value && typeof value === "object" && !Array.isArray(value) && "not" in value);
4599
+ }
4561
4600
  function eventMatchesFilter(event, filter) {
4562
4601
  return matchString(event.source, filter.source) && matchString(event.type, filter.type) && matchString(event.subject, filter.subject) && matchString(event.severity, filter.severity) && matchRecord(event.data, filter.data) && matchRecord(event.metadata, filter.metadata);
4563
4602
  }
@@ -5164,9 +5203,66 @@ function taskEventData(task, extra = {}) {
5164
5203
  started_at: task.started_at,
5165
5204
  completed_at: task.completed_at,
5166
5205
  due_at: task.due_at,
5206
+ requires_approval: task.requires_approval,
5207
+ approved_by: task.approved_by,
5208
+ approved_at: task.approved_at,
5167
5209
  ...extra
5168
5210
  };
5169
5211
  }
5212
+ function booleanField(value) {
5213
+ if (typeof value === "boolean")
5214
+ return value;
5215
+ if (typeof value === "number") {
5216
+ if (value === 1)
5217
+ return true;
5218
+ if (value === 0)
5219
+ return false;
5220
+ }
5221
+ if (typeof value === "string") {
5222
+ const normalized = value.trim().toLowerCase();
5223
+ if (["true", "1", "yes", "on"].includes(normalized))
5224
+ return true;
5225
+ if (["false", "0", "no", "off"].includes(normalized))
5226
+ return false;
5227
+ }
5228
+ return;
5229
+ }
5230
+ function objectField(value) {
5231
+ return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
5232
+ }
5233
+ function firstBoolean(records, keys) {
5234
+ for (const record of records) {
5235
+ for (const key of keys) {
5236
+ const value = booleanField(record[key]);
5237
+ if (value !== undefined)
5238
+ return value;
5239
+ }
5240
+ }
5241
+ return;
5242
+ }
5243
+ function routingAutomationMetadata(task) {
5244
+ const automation = objectField(task.metadata.automation);
5245
+ const records = [task.metadata];
5246
+ if (automation)
5247
+ records.push(automation);
5248
+ const result = {};
5249
+ const aliases = [
5250
+ ["allowed", ["allowed", "automation_allowed", "automationAllowed"]],
5251
+ ["no_auto", ["no_auto", "noAuto"]],
5252
+ ["manual", ["manual"]],
5253
+ ["manual_required", ["manual_required", "manualRequired"]],
5254
+ ["requires_approval", ["requires_approval", "requiresApproval"]],
5255
+ ["approval_required", ["approval_required", "approvalRequired"]]
5256
+ ];
5257
+ for (const [canonical, keys] of aliases) {
5258
+ const value = firstBoolean(records, keys);
5259
+ if (value !== undefined)
5260
+ result[canonical] = value;
5261
+ }
5262
+ if (task.requires_approval)
5263
+ result.requires_approval = true;
5264
+ return Object.keys(result).length > 0 ? result : undefined;
5265
+ }
5170
5266
  function taskEventMetadata(task) {
5171
5267
  const metadata = {
5172
5268
  package: "@hasna/todos",
@@ -5177,6 +5273,14 @@ function taskEventMetadata(task) {
5177
5273
  task_list_id: task.task_list_id,
5178
5274
  working_dir: task.working_dir
5179
5275
  };
5276
+ const routeEnabled = booleanField(task.metadata.route_enabled);
5277
+ if (routeEnabled !== undefined) {
5278
+ metadata.route_enabled = routeEnabled;
5279
+ }
5280
+ const automation = routingAutomationMetadata(task);
5281
+ if (automation) {
5282
+ metadata.automation = automation;
5283
+ }
5180
5284
  try {
5181
5285
  const project = task.project_id ? getProject(task.project_id) : null;
5182
5286
  const projectPath = project ? readMachineLocalPath(project) ?? project.path : task.working_dir;
@@ -5194,9 +5298,6 @@ function taskEventMetadata(task) {
5194
5298
  if (projectPath) {
5195
5299
  metadata.project_kind = classifyProjectKind(projectPath);
5196
5300
  metadata.project_is_worktree = isWorktreePath(projectPath);
5197
- if (typeof task.metadata.route_enabled === "boolean") {
5198
- metadata.route_enabled = task.metadata.route_enabled;
5199
- }
5200
5301
  metadata.working_dir = task.working_dir ?? projectPath;
5201
5302
  }
5202
5303
  const taskList = task.task_list_id ? getTaskList(task.task_list_id) ?? (project ? getTaskListBySlug(task.task_list_id, project.id) : null) : project?.task_list_id ? getTaskListBySlug(project.task_list_id, project.id) : null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/todos",
3
- "version": "0.11.59",
3
+ "version": "0.11.61",
4
4
  "description": "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -89,7 +89,7 @@
89
89
  "author": "Andrei Hasna <andrei@hasna.com>",
90
90
  "license": "Apache-2.0",
91
91
  "dependencies": {
92
- "@hasna/events": "^0.1.9",
92
+ "@hasna/events": "^0.1.11",
93
93
  "@modelcontextprotocol/sdk": "^1.12.1",
94
94
  "chalk": "^5.4.1",
95
95
  "commander": "^13.1.0",