@hasna/todos 0.11.59 → 0.11.60

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.10/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";
@@ -6286,14 +6286,40 @@ function matchRecord(input, matcher) {
6286
6286
  return true;
6287
6287
  return Object.entries(matcher).every(([path, expected]) => {
6288
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;
6289
+ return matchField(actual, expected, path);
6295
6290
  });
6296
6291
  }
6292
+ function matchField(actual, expected, path) {
6293
+ if (isNegativeMatcher(expected)) {
6294
+ return !matchPositiveField(actual, expected.not, path);
6295
+ }
6296
+ return matchPositiveField(actual, expected, path);
6297
+ }
6298
+ function matchPositiveField(actual, expected, path) {
6299
+ if (typeof expected === "string" || Array.isArray(expected)) {
6300
+ return stringCandidates(actual).some((candidate) => matchString(candidate, expected, {
6301
+ segmentSafe: path.endsWith("_path") || path.endsWith(".path")
6302
+ }));
6303
+ }
6304
+ if (Array.isArray(actual)) {
6305
+ return actual.some((item) => item === expected);
6306
+ }
6307
+ return actual === expected;
6308
+ }
6309
+ function stringCandidates(actual) {
6310
+ if (actual === undefined)
6311
+ return [];
6312
+ if (Array.isArray(actual)) {
6313
+ return actual.flatMap((item) => isPrimitiveFieldValue(item) ? [String(item)] : []);
6314
+ }
6315
+ return [String(actual)];
6316
+ }
6317
+ function isPrimitiveFieldValue(value) {
6318
+ return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
6319
+ }
6320
+ function isNegativeMatcher(value) {
6321
+ return Boolean(value && typeof value === "object" && !Array.isArray(value) && "not" in value);
6322
+ }
6297
6323
  function eventMatchesFilter(event, filter) {
6298
6324
  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
6325
  }
@@ -6901,9 +6927,66 @@ function taskEventData(task, extra = {}) {
6901
6927
  started_at: task.started_at,
6902
6928
  completed_at: task.completed_at,
6903
6929
  due_at: task.due_at,
6930
+ requires_approval: task.requires_approval,
6931
+ approved_by: task.approved_by,
6932
+ approved_at: task.approved_at,
6904
6933
  ...extra
6905
6934
  };
6906
6935
  }
6936
+ function booleanField(value) {
6937
+ if (typeof value === "boolean")
6938
+ return value;
6939
+ if (typeof value === "number") {
6940
+ if (value === 1)
6941
+ return true;
6942
+ if (value === 0)
6943
+ return false;
6944
+ }
6945
+ if (typeof value === "string") {
6946
+ const normalized = value.trim().toLowerCase();
6947
+ if (["true", "1", "yes", "on"].includes(normalized))
6948
+ return true;
6949
+ if (["false", "0", "no", "off"].includes(normalized))
6950
+ return false;
6951
+ }
6952
+ return;
6953
+ }
6954
+ function objectField(value) {
6955
+ return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
6956
+ }
6957
+ function firstBoolean(records, keys) {
6958
+ for (const record of records) {
6959
+ for (const key of keys) {
6960
+ const value = booleanField(record[key]);
6961
+ if (value !== undefined)
6962
+ return value;
6963
+ }
6964
+ }
6965
+ return;
6966
+ }
6967
+ function routingAutomationMetadata(task) {
6968
+ const automation = objectField(task.metadata.automation);
6969
+ const records = [task.metadata];
6970
+ if (automation)
6971
+ records.push(automation);
6972
+ const result = {};
6973
+ const aliases = [
6974
+ ["allowed", ["allowed", "automation_allowed", "automationAllowed"]],
6975
+ ["no_auto", ["no_auto", "noAuto"]],
6976
+ ["manual", ["manual"]],
6977
+ ["manual_required", ["manual_required", "manualRequired"]],
6978
+ ["requires_approval", ["requires_approval", "requiresApproval"]],
6979
+ ["approval_required", ["approval_required", "approvalRequired"]]
6980
+ ];
6981
+ for (const [canonical, keys] of aliases) {
6982
+ const value = firstBoolean(records, keys);
6983
+ if (value !== undefined)
6984
+ result[canonical] = value;
6985
+ }
6986
+ if (task.requires_approval)
6987
+ result.requires_approval = true;
6988
+ return Object.keys(result).length > 0 ? result : undefined;
6989
+ }
6907
6990
  function taskEventMetadata(task) {
6908
6991
  const metadata = {
6909
6992
  package: "@hasna/todos",
@@ -6914,6 +6997,14 @@ function taskEventMetadata(task) {
6914
6997
  task_list_id: task.task_list_id,
6915
6998
  working_dir: task.working_dir
6916
6999
  };
7000
+ const routeEnabled = booleanField(task.metadata.route_enabled);
7001
+ if (routeEnabled !== undefined) {
7002
+ metadata.route_enabled = routeEnabled;
7003
+ }
7004
+ const automation = routingAutomationMetadata(task);
7005
+ if (automation) {
7006
+ metadata.automation = automation;
7007
+ }
6917
7008
  try {
6918
7009
  const project = task.project_id ? getProject(task.project_id) : null;
6919
7010
  const projectPath = project ? readMachineLocalPath(project) ?? project.path : task.working_dir;
@@ -6931,9 +7022,6 @@ function taskEventMetadata(task) {
6931
7022
  if (projectPath) {
6932
7023
  metadata.project_kind = classifyProjectKind(projectPath);
6933
7024
  metadata.project_is_worktree = isWorktreePath(projectPath);
6934
- if (typeof task.metadata.route_enabled === "boolean") {
6935
- metadata.route_enabled = task.metadata.route_enabled;
6936
- }
6937
7025
  metadata.working_dir = task.working_dir ?? projectPath;
6938
7026
  }
6939
7027
  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.10/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";
@@ -7002,14 +7002,40 @@ function matchRecord(input, matcher) {
7002
7002
  return true;
7003
7003
  return Object.entries(matcher).every(([path, expected]) => {
7004
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;
7005
+ return matchField(actual, expected, path);
7011
7006
  });
7012
7007
  }
7008
+ function matchField(actual, expected, path) {
7009
+ if (isNegativeMatcher(expected)) {
7010
+ return !matchPositiveField(actual, expected.not, path);
7011
+ }
7012
+ return matchPositiveField(actual, expected, path);
7013
+ }
7014
+ function matchPositiveField(actual, expected, path) {
7015
+ if (typeof expected === "string" || Array.isArray(expected)) {
7016
+ return stringCandidates(actual).some((candidate) => matchString(candidate, expected, {
7017
+ segmentSafe: path.endsWith("_path") || path.endsWith(".path")
7018
+ }));
7019
+ }
7020
+ if (Array.isArray(actual)) {
7021
+ return actual.some((item) => item === expected);
7022
+ }
7023
+ return actual === expected;
7024
+ }
7025
+ function stringCandidates(actual) {
7026
+ if (actual === undefined)
7027
+ return [];
7028
+ if (Array.isArray(actual)) {
7029
+ return actual.flatMap((item) => isPrimitiveFieldValue(item) ? [String(item)] : []);
7030
+ }
7031
+ return [String(actual)];
7032
+ }
7033
+ function isPrimitiveFieldValue(value) {
7034
+ return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
7035
+ }
7036
+ function isNegativeMatcher(value) {
7037
+ return Boolean(value && typeof value === "object" && !Array.isArray(value) && "not" in value);
7038
+ }
7013
7039
  function eventMatchesFilter(event, filter) {
7014
7040
  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
7041
  }
@@ -7616,9 +7642,66 @@ function taskEventData(task, extra = {}) {
7616
7642
  started_at: task.started_at,
7617
7643
  completed_at: task.completed_at,
7618
7644
  due_at: task.due_at,
7645
+ requires_approval: task.requires_approval,
7646
+ approved_by: task.approved_by,
7647
+ approved_at: task.approved_at,
7619
7648
  ...extra
7620
7649
  };
7621
7650
  }
7651
+ function booleanField(value) {
7652
+ if (typeof value === "boolean")
7653
+ return value;
7654
+ if (typeof value === "number") {
7655
+ if (value === 1)
7656
+ return true;
7657
+ if (value === 0)
7658
+ return false;
7659
+ }
7660
+ if (typeof value === "string") {
7661
+ const normalized = value.trim().toLowerCase();
7662
+ if (["true", "1", "yes", "on"].includes(normalized))
7663
+ return true;
7664
+ if (["false", "0", "no", "off"].includes(normalized))
7665
+ return false;
7666
+ }
7667
+ return;
7668
+ }
7669
+ function objectField(value) {
7670
+ return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
7671
+ }
7672
+ function firstBoolean(records, keys) {
7673
+ for (const record of records) {
7674
+ for (const key of keys) {
7675
+ const value = booleanField(record[key]);
7676
+ if (value !== undefined)
7677
+ return value;
7678
+ }
7679
+ }
7680
+ return;
7681
+ }
7682
+ function routingAutomationMetadata(task) {
7683
+ const automation = objectField(task.metadata.automation);
7684
+ const records = [task.metadata];
7685
+ if (automation)
7686
+ records.push(automation);
7687
+ const result = {};
7688
+ const aliases = [
7689
+ ["allowed", ["allowed", "automation_allowed", "automationAllowed"]],
7690
+ ["no_auto", ["no_auto", "noAuto"]],
7691
+ ["manual", ["manual"]],
7692
+ ["manual_required", ["manual_required", "manualRequired"]],
7693
+ ["requires_approval", ["requires_approval", "requiresApproval"]],
7694
+ ["approval_required", ["approval_required", "approvalRequired"]]
7695
+ ];
7696
+ for (const [canonical, keys] of aliases) {
7697
+ const value = firstBoolean(records, keys);
7698
+ if (value !== undefined)
7699
+ result[canonical] = value;
7700
+ }
7701
+ if (task.requires_approval)
7702
+ result.requires_approval = true;
7703
+ return Object.keys(result).length > 0 ? result : undefined;
7704
+ }
7622
7705
  function taskEventMetadata(task) {
7623
7706
  const metadata = {
7624
7707
  package: "@hasna/todos",
@@ -7629,6 +7712,14 @@ function taskEventMetadata(task) {
7629
7712
  task_list_id: task.task_list_id,
7630
7713
  working_dir: task.working_dir
7631
7714
  };
7715
+ const routeEnabled = booleanField(task.metadata.route_enabled);
7716
+ if (routeEnabled !== undefined) {
7717
+ metadata.route_enabled = routeEnabled;
7718
+ }
7719
+ const automation = routingAutomationMetadata(task);
7720
+ if (automation) {
7721
+ metadata.automation = automation;
7722
+ }
7632
7723
  try {
7633
7724
  const project = task.project_id ? getProject(task.project_id) : null;
7634
7725
  const projectPath = project ? readMachineLocalPath(project) ?? project.path : task.working_dir;
@@ -7646,9 +7737,6 @@ function taskEventMetadata(task) {
7646
7737
  if (projectPath) {
7647
7738
  metadata.project_kind = classifyProjectKind(projectPath);
7648
7739
  metadata.project_is_worktree = isWorktreePath(projectPath);
7649
- if (typeof task.metadata.route_enabled === "boolean") {
7650
- metadata.route_enabled = task.metadata.route_enabled;
7651
- }
7652
7740
  metadata.working_dir = task.working_dir ?? projectPath;
7653
7741
  }
7654
7742
  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.10/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";
@@ -7107,14 +7107,40 @@ function matchRecord(input, matcher) {
7107
7107
  return true;
7108
7108
  return Object.entries(matcher).every(([path, expected]) => {
7109
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;
7110
+ return matchField(actual, expected, path);
7116
7111
  });
7117
7112
  }
7113
+ function matchField(actual, expected, path) {
7114
+ if (isNegativeMatcher(expected)) {
7115
+ return !matchPositiveField(actual, expected.not, path);
7116
+ }
7117
+ return matchPositiveField(actual, expected, path);
7118
+ }
7119
+ function matchPositiveField(actual, expected, path) {
7120
+ if (typeof expected === "string" || Array.isArray(expected)) {
7121
+ return stringCandidates(actual).some((candidate) => matchString(candidate, expected, {
7122
+ segmentSafe: path.endsWith("_path") || path.endsWith(".path")
7123
+ }));
7124
+ }
7125
+ if (Array.isArray(actual)) {
7126
+ return actual.some((item) => item === expected);
7127
+ }
7128
+ return actual === expected;
7129
+ }
7130
+ function stringCandidates(actual) {
7131
+ if (actual === undefined)
7132
+ return [];
7133
+ if (Array.isArray(actual)) {
7134
+ return actual.flatMap((item) => isPrimitiveFieldValue(item) ? [String(item)] : []);
7135
+ }
7136
+ return [String(actual)];
7137
+ }
7138
+ function isPrimitiveFieldValue(value) {
7139
+ return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
7140
+ }
7141
+ function isNegativeMatcher(value) {
7142
+ return Boolean(value && typeof value === "object" && !Array.isArray(value) && "not" in value);
7143
+ }
7118
7144
  function eventMatchesFilter(event, filter) {
7119
7145
  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
7146
  }
@@ -7721,9 +7747,66 @@ function taskEventData(task, extra = {}) {
7721
7747
  started_at: task.started_at,
7722
7748
  completed_at: task.completed_at,
7723
7749
  due_at: task.due_at,
7750
+ requires_approval: task.requires_approval,
7751
+ approved_by: task.approved_by,
7752
+ approved_at: task.approved_at,
7724
7753
  ...extra
7725
7754
  };
7726
7755
  }
7756
+ function booleanField(value) {
7757
+ if (typeof value === "boolean")
7758
+ return value;
7759
+ if (typeof value === "number") {
7760
+ if (value === 1)
7761
+ return true;
7762
+ if (value === 0)
7763
+ return false;
7764
+ }
7765
+ if (typeof value === "string") {
7766
+ const normalized = value.trim().toLowerCase();
7767
+ if (["true", "1", "yes", "on"].includes(normalized))
7768
+ return true;
7769
+ if (["false", "0", "no", "off"].includes(normalized))
7770
+ return false;
7771
+ }
7772
+ return;
7773
+ }
7774
+ function objectField(value) {
7775
+ return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
7776
+ }
7777
+ function firstBoolean(records, keys) {
7778
+ for (const record of records) {
7779
+ for (const key of keys) {
7780
+ const value = booleanField(record[key]);
7781
+ if (value !== undefined)
7782
+ return value;
7783
+ }
7784
+ }
7785
+ return;
7786
+ }
7787
+ function routingAutomationMetadata(task) {
7788
+ const automation = objectField(task.metadata.automation);
7789
+ const records = [task.metadata];
7790
+ if (automation)
7791
+ records.push(automation);
7792
+ const result = {};
7793
+ const aliases = [
7794
+ ["allowed", ["allowed", "automation_allowed", "automationAllowed"]],
7795
+ ["no_auto", ["no_auto", "noAuto"]],
7796
+ ["manual", ["manual"]],
7797
+ ["manual_required", ["manual_required", "manualRequired"]],
7798
+ ["requires_approval", ["requires_approval", "requiresApproval"]],
7799
+ ["approval_required", ["approval_required", "approvalRequired"]]
7800
+ ];
7801
+ for (const [canonical, keys] of aliases) {
7802
+ const value = firstBoolean(records, keys);
7803
+ if (value !== undefined)
7804
+ result[canonical] = value;
7805
+ }
7806
+ if (task.requires_approval)
7807
+ result.requires_approval = true;
7808
+ return Object.keys(result).length > 0 ? result : undefined;
7809
+ }
7727
7810
  function taskEventMetadata(task) {
7728
7811
  const metadata = {
7729
7812
  package: "@hasna/todos",
@@ -7734,6 +7817,14 @@ function taskEventMetadata(task) {
7734
7817
  task_list_id: task.task_list_id,
7735
7818
  working_dir: task.working_dir
7736
7819
  };
7820
+ const routeEnabled = booleanField(task.metadata.route_enabled);
7821
+ if (routeEnabled !== undefined) {
7822
+ metadata.route_enabled = routeEnabled;
7823
+ }
7824
+ const automation = routingAutomationMetadata(task);
7825
+ if (automation) {
7826
+ metadata.automation = automation;
7827
+ }
7737
7828
  try {
7738
7829
  const project = task.project_id ? getProject(task.project_id) : null;
7739
7830
  const projectPath = project ? readMachineLocalPath(project) ?? project.path : task.working_dir;
@@ -7751,9 +7842,6 @@ function taskEventMetadata(task) {
7751
7842
  if (projectPath) {
7752
7843
  metadata.project_kind = classifyProjectKind(projectPath);
7753
7844
  metadata.project_is_worktree = isWorktreePath(projectPath);
7754
- if (typeof task.metadata.route_enabled === "boolean") {
7755
- metadata.route_enabled = task.metadata.route_enabled;
7756
- }
7757
7845
  metadata.working_dir = task.working_dir ?? projectPath;
7758
7846
  }
7759
7847
  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.10/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";
@@ -8785,14 +8785,40 @@ function matchRecord(input, matcher) {
8785
8785
  return true;
8786
8786
  return Object.entries(matcher).every(([path, expected]) => {
8787
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;
8788
+ return matchField(actual, expected, path);
8794
8789
  });
8795
8790
  }
8791
+ function matchField(actual, expected, path) {
8792
+ if (isNegativeMatcher(expected)) {
8793
+ return !matchPositiveField(actual, expected.not, path);
8794
+ }
8795
+ return matchPositiveField(actual, expected, path);
8796
+ }
8797
+ function matchPositiveField(actual, expected, path) {
8798
+ if (typeof expected === "string" || Array.isArray(expected)) {
8799
+ return stringCandidates(actual).some((candidate) => matchString(candidate, expected, {
8800
+ segmentSafe: path.endsWith("_path") || path.endsWith(".path")
8801
+ }));
8802
+ }
8803
+ if (Array.isArray(actual)) {
8804
+ return actual.some((item) => item === expected);
8805
+ }
8806
+ return actual === expected;
8807
+ }
8808
+ function stringCandidates(actual) {
8809
+ if (actual === undefined)
8810
+ return [];
8811
+ if (Array.isArray(actual)) {
8812
+ return actual.flatMap((item) => isPrimitiveFieldValue(item) ? [String(item)] : []);
8813
+ }
8814
+ return [String(actual)];
8815
+ }
8816
+ function isPrimitiveFieldValue(value) {
8817
+ return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
8818
+ }
8819
+ function isNegativeMatcher(value) {
8820
+ return Boolean(value && typeof value === "object" && !Array.isArray(value) && "not" in value);
8821
+ }
8796
8822
  function eventMatchesFilter(event, filter) {
8797
8823
  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
8824
  }
@@ -9400,9 +9426,66 @@ function taskEventData(task, extra = {}) {
9400
9426
  started_at: task.started_at,
9401
9427
  completed_at: task.completed_at,
9402
9428
  due_at: task.due_at,
9429
+ requires_approval: task.requires_approval,
9430
+ approved_by: task.approved_by,
9431
+ approved_at: task.approved_at,
9403
9432
  ...extra
9404
9433
  };
9405
9434
  }
9435
+ function booleanField(value) {
9436
+ if (typeof value === "boolean")
9437
+ return value;
9438
+ if (typeof value === "number") {
9439
+ if (value === 1)
9440
+ return true;
9441
+ if (value === 0)
9442
+ return false;
9443
+ }
9444
+ if (typeof value === "string") {
9445
+ const normalized = value.trim().toLowerCase();
9446
+ if (["true", "1", "yes", "on"].includes(normalized))
9447
+ return true;
9448
+ if (["false", "0", "no", "off"].includes(normalized))
9449
+ return false;
9450
+ }
9451
+ return;
9452
+ }
9453
+ function objectField(value) {
9454
+ return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
9455
+ }
9456
+ function firstBoolean(records, keys) {
9457
+ for (const record of records) {
9458
+ for (const key of keys) {
9459
+ const value = booleanField(record[key]);
9460
+ if (value !== undefined)
9461
+ return value;
9462
+ }
9463
+ }
9464
+ return;
9465
+ }
9466
+ function routingAutomationMetadata(task) {
9467
+ const automation = objectField(task.metadata.automation);
9468
+ const records = [task.metadata];
9469
+ if (automation)
9470
+ records.push(automation);
9471
+ const result = {};
9472
+ const aliases = [
9473
+ ["allowed", ["allowed", "automation_allowed", "automationAllowed"]],
9474
+ ["no_auto", ["no_auto", "noAuto"]],
9475
+ ["manual", ["manual"]],
9476
+ ["manual_required", ["manual_required", "manualRequired"]],
9477
+ ["requires_approval", ["requires_approval", "requiresApproval"]],
9478
+ ["approval_required", ["approval_required", "approvalRequired"]]
9479
+ ];
9480
+ for (const [canonical, keys] of aliases) {
9481
+ const value = firstBoolean(records, keys);
9482
+ if (value !== undefined)
9483
+ result[canonical] = value;
9484
+ }
9485
+ if (task.requires_approval)
9486
+ result.requires_approval = true;
9487
+ return Object.keys(result).length > 0 ? result : undefined;
9488
+ }
9406
9489
  function taskEventMetadata(task) {
9407
9490
  const metadata = {
9408
9491
  package: "@hasna/todos",
@@ -9413,6 +9496,14 @@ function taskEventMetadata(task) {
9413
9496
  task_list_id: task.task_list_id,
9414
9497
  working_dir: task.working_dir
9415
9498
  };
9499
+ const routeEnabled = booleanField(task.metadata.route_enabled);
9500
+ if (routeEnabled !== undefined) {
9501
+ metadata.route_enabled = routeEnabled;
9502
+ }
9503
+ const automation = routingAutomationMetadata(task);
9504
+ if (automation) {
9505
+ metadata.automation = automation;
9506
+ }
9416
9507
  try {
9417
9508
  const project = task.project_id ? getProject(task.project_id) : null;
9418
9509
  const projectPath = project ? readMachineLocalPath(project) ?? project.path : task.working_dir;
@@ -9430,9 +9521,6 @@ function taskEventMetadata(task) {
9430
9521
  if (projectPath) {
9431
9522
  metadata.project_kind = classifyProjectKind(projectPath);
9432
9523
  metadata.project_is_worktree = isWorktreePath(projectPath);
9433
- if (typeof task.metadata.route_enabled === "boolean") {
9434
- metadata.route_enabled = task.metadata.route_enabled;
9435
- }
9436
9524
  metadata.working_dir = task.working_dir ?? projectPath;
9437
9525
  }
9438
9526
  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.10/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";
@@ -7002,14 +7002,40 @@ function matchRecord(input, matcher) {
7002
7002
  return true;
7003
7003
  return Object.entries(matcher).every(([path, expected]) => {
7004
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;
7005
+ return matchField(actual, expected, path);
7011
7006
  });
7012
7007
  }
7008
+ function matchField(actual, expected, path) {
7009
+ if (isNegativeMatcher(expected)) {
7010
+ return !matchPositiveField(actual, expected.not, path);
7011
+ }
7012
+ return matchPositiveField(actual, expected, path);
7013
+ }
7014
+ function matchPositiveField(actual, expected, path) {
7015
+ if (typeof expected === "string" || Array.isArray(expected)) {
7016
+ return stringCandidates(actual).some((candidate) => matchString(candidate, expected, {
7017
+ segmentSafe: path.endsWith("_path") || path.endsWith(".path")
7018
+ }));
7019
+ }
7020
+ if (Array.isArray(actual)) {
7021
+ return actual.some((item) => item === expected);
7022
+ }
7023
+ return actual === expected;
7024
+ }
7025
+ function stringCandidates(actual) {
7026
+ if (actual === undefined)
7027
+ return [];
7028
+ if (Array.isArray(actual)) {
7029
+ return actual.flatMap((item) => isPrimitiveFieldValue(item) ? [String(item)] : []);
7030
+ }
7031
+ return [String(actual)];
7032
+ }
7033
+ function isPrimitiveFieldValue(value) {
7034
+ return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
7035
+ }
7036
+ function isNegativeMatcher(value) {
7037
+ return Boolean(value && typeof value === "object" && !Array.isArray(value) && "not" in value);
7038
+ }
7013
7039
  function eventMatchesFilter(event, filter) {
7014
7040
  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
7041
  }
@@ -7616,9 +7642,66 @@ function taskEventData(task, extra = {}) {
7616
7642
  started_at: task.started_at,
7617
7643
  completed_at: task.completed_at,
7618
7644
  due_at: task.due_at,
7645
+ requires_approval: task.requires_approval,
7646
+ approved_by: task.approved_by,
7647
+ approved_at: task.approved_at,
7619
7648
  ...extra
7620
7649
  };
7621
7650
  }
7651
+ function booleanField(value) {
7652
+ if (typeof value === "boolean")
7653
+ return value;
7654
+ if (typeof value === "number") {
7655
+ if (value === 1)
7656
+ return true;
7657
+ if (value === 0)
7658
+ return false;
7659
+ }
7660
+ if (typeof value === "string") {
7661
+ const normalized = value.trim().toLowerCase();
7662
+ if (["true", "1", "yes", "on"].includes(normalized))
7663
+ return true;
7664
+ if (["false", "0", "no", "off"].includes(normalized))
7665
+ return false;
7666
+ }
7667
+ return;
7668
+ }
7669
+ function objectField(value) {
7670
+ return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
7671
+ }
7672
+ function firstBoolean(records, keys) {
7673
+ for (const record of records) {
7674
+ for (const key of keys) {
7675
+ const value = booleanField(record[key]);
7676
+ if (value !== undefined)
7677
+ return value;
7678
+ }
7679
+ }
7680
+ return;
7681
+ }
7682
+ function routingAutomationMetadata(task) {
7683
+ const automation = objectField(task.metadata.automation);
7684
+ const records = [task.metadata];
7685
+ if (automation)
7686
+ records.push(automation);
7687
+ const result = {};
7688
+ const aliases = [
7689
+ ["allowed", ["allowed", "automation_allowed", "automationAllowed"]],
7690
+ ["no_auto", ["no_auto", "noAuto"]],
7691
+ ["manual", ["manual"]],
7692
+ ["manual_required", ["manual_required", "manualRequired"]],
7693
+ ["requires_approval", ["requires_approval", "requiresApproval"]],
7694
+ ["approval_required", ["approval_required", "approvalRequired"]]
7695
+ ];
7696
+ for (const [canonical, keys] of aliases) {
7697
+ const value = firstBoolean(records, keys);
7698
+ if (value !== undefined)
7699
+ result[canonical] = value;
7700
+ }
7701
+ if (task.requires_approval)
7702
+ result.requires_approval = true;
7703
+ return Object.keys(result).length > 0 ? result : undefined;
7704
+ }
7622
7705
  function taskEventMetadata(task) {
7623
7706
  const metadata = {
7624
7707
  package: "@hasna/todos",
@@ -7629,6 +7712,14 @@ function taskEventMetadata(task) {
7629
7712
  task_list_id: task.task_list_id,
7630
7713
  working_dir: task.working_dir
7631
7714
  };
7715
+ const routeEnabled = booleanField(task.metadata.route_enabled);
7716
+ if (routeEnabled !== undefined) {
7717
+ metadata.route_enabled = routeEnabled;
7718
+ }
7719
+ const automation = routingAutomationMetadata(task);
7720
+ if (automation) {
7721
+ metadata.automation = automation;
7722
+ }
7632
7723
  try {
7633
7724
  const project = task.project_id ? getProject(task.project_id) : null;
7634
7725
  const projectPath = project ? readMachineLocalPath(project) ?? project.path : task.working_dir;
@@ -7646,9 +7737,6 @@ function taskEventMetadata(task) {
7646
7737
  if (projectPath) {
7647
7738
  metadata.project_kind = classifyProjectKind(projectPath);
7648
7739
  metadata.project_is_worktree = isWorktreePath(projectPath);
7649
- if (typeof task.metadata.route_enabled === "boolean") {
7650
- metadata.route_enabled = task.metadata.route_enabled;
7651
- }
7652
7740
  metadata.working_dir = task.working_dir ?? projectPath;
7653
7741
  }
7654
7742
  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.60",
4
4
  "repository": "https://github.com/hasna/todos.git",
5
- "gitCommit": "b507930ee09c9b1f5c7a46881679965fd35b03fb",
6
- "generatedAt": "2026-06-27T09:34:02.825Z"
5
+ "gitCommit": "ab13bc3c275c0fd348e6ff5a2a1a6109002baf04",
6
+ "generatedAt": "2026-06-27T12:02:52.731Z"
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.10/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";
@@ -4146,14 +4146,40 @@ function matchRecord(input, matcher) {
4146
4146
  return true;
4147
4147
  return Object.entries(matcher).every(([path, expected]) => {
4148
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;
4149
+ return matchField(actual, expected, path);
4155
4150
  });
4156
4151
  }
4152
+ function matchField(actual, expected, path) {
4153
+ if (isNegativeMatcher(expected)) {
4154
+ return !matchPositiveField(actual, expected.not, path);
4155
+ }
4156
+ return matchPositiveField(actual, expected, path);
4157
+ }
4158
+ function matchPositiveField(actual, expected, path) {
4159
+ if (typeof expected === "string" || Array.isArray(expected)) {
4160
+ return stringCandidates(actual).some((candidate) => matchString(candidate, expected, {
4161
+ segmentSafe: path.endsWith("_path") || path.endsWith(".path")
4162
+ }));
4163
+ }
4164
+ if (Array.isArray(actual)) {
4165
+ return actual.some((item) => item === expected);
4166
+ }
4167
+ return actual === expected;
4168
+ }
4169
+ function stringCandidates(actual) {
4170
+ if (actual === undefined)
4171
+ return [];
4172
+ if (Array.isArray(actual)) {
4173
+ return actual.flatMap((item) => isPrimitiveFieldValue(item) ? [String(item)] : []);
4174
+ }
4175
+ return [String(actual)];
4176
+ }
4177
+ function isPrimitiveFieldValue(value) {
4178
+ return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
4179
+ }
4180
+ function isNegativeMatcher(value) {
4181
+ return Boolean(value && typeof value === "object" && !Array.isArray(value) && "not" in value);
4182
+ }
4157
4183
  function eventMatchesFilter(event, filter) {
4158
4184
  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
4185
  }
@@ -4761,9 +4787,66 @@ function taskEventData(task, extra = {}) {
4761
4787
  started_at: task.started_at,
4762
4788
  completed_at: task.completed_at,
4763
4789
  due_at: task.due_at,
4790
+ requires_approval: task.requires_approval,
4791
+ approved_by: task.approved_by,
4792
+ approved_at: task.approved_at,
4764
4793
  ...extra
4765
4794
  };
4766
4795
  }
4796
+ function booleanField(value) {
4797
+ if (typeof value === "boolean")
4798
+ return value;
4799
+ if (typeof value === "number") {
4800
+ if (value === 1)
4801
+ return true;
4802
+ if (value === 0)
4803
+ return false;
4804
+ }
4805
+ if (typeof value === "string") {
4806
+ const normalized = value.trim().toLowerCase();
4807
+ if (["true", "1", "yes", "on"].includes(normalized))
4808
+ return true;
4809
+ if (["false", "0", "no", "off"].includes(normalized))
4810
+ return false;
4811
+ }
4812
+ return;
4813
+ }
4814
+ function objectField(value) {
4815
+ return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
4816
+ }
4817
+ function firstBoolean(records, keys) {
4818
+ for (const record of records) {
4819
+ for (const key of keys) {
4820
+ const value = booleanField(record[key]);
4821
+ if (value !== undefined)
4822
+ return value;
4823
+ }
4824
+ }
4825
+ return;
4826
+ }
4827
+ function routingAutomationMetadata(task) {
4828
+ const automation = objectField(task.metadata.automation);
4829
+ const records = [task.metadata];
4830
+ if (automation)
4831
+ records.push(automation);
4832
+ const result = {};
4833
+ const aliases = [
4834
+ ["allowed", ["allowed", "automation_allowed", "automationAllowed"]],
4835
+ ["no_auto", ["no_auto", "noAuto"]],
4836
+ ["manual", ["manual"]],
4837
+ ["manual_required", ["manual_required", "manualRequired"]],
4838
+ ["requires_approval", ["requires_approval", "requiresApproval"]],
4839
+ ["approval_required", ["approval_required", "approvalRequired"]]
4840
+ ];
4841
+ for (const [canonical, keys] of aliases) {
4842
+ const value = firstBoolean(records, keys);
4843
+ if (value !== undefined)
4844
+ result[canonical] = value;
4845
+ }
4846
+ if (task.requires_approval)
4847
+ result.requires_approval = true;
4848
+ return Object.keys(result).length > 0 ? result : undefined;
4849
+ }
4767
4850
  function taskEventMetadata(task) {
4768
4851
  const metadata = {
4769
4852
  package: "@hasna/todos",
@@ -4774,6 +4857,14 @@ function taskEventMetadata(task) {
4774
4857
  task_list_id: task.task_list_id,
4775
4858
  working_dir: task.working_dir
4776
4859
  };
4860
+ const routeEnabled = booleanField(task.metadata.route_enabled);
4861
+ if (routeEnabled !== undefined) {
4862
+ metadata.route_enabled = routeEnabled;
4863
+ }
4864
+ const automation = routingAutomationMetadata(task);
4865
+ if (automation) {
4866
+ metadata.automation = automation;
4867
+ }
4777
4868
  try {
4778
4869
  const project = task.project_id ? getProject(task.project_id) : null;
4779
4870
  const projectPath = project ? readMachineLocalPath(project) ?? project.path : task.working_dir;
@@ -4791,9 +4882,6 @@ function taskEventMetadata(task) {
4791
4882
  if (projectPath) {
4792
4883
  metadata.project_kind = classifyProjectKind(projectPath);
4793
4884
  metadata.project_is_worktree = isWorktreePath(projectPath);
4794
- if (typeof task.metadata.route_enabled === "boolean") {
4795
- metadata.route_enabled = task.metadata.route_enabled;
4796
- }
4797
4885
  metadata.working_dir = task.working_dir ?? projectPath;
4798
4886
  }
4799
4887
  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.10/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";
@@ -4550,14 +4550,40 @@ function matchRecord(input, matcher) {
4550
4550
  return true;
4551
4551
  return Object.entries(matcher).every(([path, expected]) => {
4552
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;
4553
+ return matchField(actual, expected, path);
4559
4554
  });
4560
4555
  }
4556
+ function matchField(actual, expected, path) {
4557
+ if (isNegativeMatcher(expected)) {
4558
+ return !matchPositiveField(actual, expected.not, path);
4559
+ }
4560
+ return matchPositiveField(actual, expected, path);
4561
+ }
4562
+ function matchPositiveField(actual, expected, path) {
4563
+ if (typeof expected === "string" || Array.isArray(expected)) {
4564
+ return stringCandidates(actual).some((candidate) => matchString(candidate, expected, {
4565
+ segmentSafe: path.endsWith("_path") || path.endsWith(".path")
4566
+ }));
4567
+ }
4568
+ if (Array.isArray(actual)) {
4569
+ return actual.some((item) => item === expected);
4570
+ }
4571
+ return actual === expected;
4572
+ }
4573
+ function stringCandidates(actual) {
4574
+ if (actual === undefined)
4575
+ return [];
4576
+ if (Array.isArray(actual)) {
4577
+ return actual.flatMap((item) => isPrimitiveFieldValue(item) ? [String(item)] : []);
4578
+ }
4579
+ return [String(actual)];
4580
+ }
4581
+ function isPrimitiveFieldValue(value) {
4582
+ return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
4583
+ }
4584
+ function isNegativeMatcher(value) {
4585
+ return Boolean(value && typeof value === "object" && !Array.isArray(value) && "not" in value);
4586
+ }
4561
4587
  function eventMatchesFilter(event, filter) {
4562
4588
  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
4589
  }
@@ -5164,9 +5190,66 @@ function taskEventData(task, extra = {}) {
5164
5190
  started_at: task.started_at,
5165
5191
  completed_at: task.completed_at,
5166
5192
  due_at: task.due_at,
5193
+ requires_approval: task.requires_approval,
5194
+ approved_by: task.approved_by,
5195
+ approved_at: task.approved_at,
5167
5196
  ...extra
5168
5197
  };
5169
5198
  }
5199
+ function booleanField(value) {
5200
+ if (typeof value === "boolean")
5201
+ return value;
5202
+ if (typeof value === "number") {
5203
+ if (value === 1)
5204
+ return true;
5205
+ if (value === 0)
5206
+ return false;
5207
+ }
5208
+ if (typeof value === "string") {
5209
+ const normalized = value.trim().toLowerCase();
5210
+ if (["true", "1", "yes", "on"].includes(normalized))
5211
+ return true;
5212
+ if (["false", "0", "no", "off"].includes(normalized))
5213
+ return false;
5214
+ }
5215
+ return;
5216
+ }
5217
+ function objectField(value) {
5218
+ return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
5219
+ }
5220
+ function firstBoolean(records, keys) {
5221
+ for (const record of records) {
5222
+ for (const key of keys) {
5223
+ const value = booleanField(record[key]);
5224
+ if (value !== undefined)
5225
+ return value;
5226
+ }
5227
+ }
5228
+ return;
5229
+ }
5230
+ function routingAutomationMetadata(task) {
5231
+ const automation = objectField(task.metadata.automation);
5232
+ const records = [task.metadata];
5233
+ if (automation)
5234
+ records.push(automation);
5235
+ const result = {};
5236
+ const aliases = [
5237
+ ["allowed", ["allowed", "automation_allowed", "automationAllowed"]],
5238
+ ["no_auto", ["no_auto", "noAuto"]],
5239
+ ["manual", ["manual"]],
5240
+ ["manual_required", ["manual_required", "manualRequired"]],
5241
+ ["requires_approval", ["requires_approval", "requiresApproval"]],
5242
+ ["approval_required", ["approval_required", "approvalRequired"]]
5243
+ ];
5244
+ for (const [canonical, keys] of aliases) {
5245
+ const value = firstBoolean(records, keys);
5246
+ if (value !== undefined)
5247
+ result[canonical] = value;
5248
+ }
5249
+ if (task.requires_approval)
5250
+ result.requires_approval = true;
5251
+ return Object.keys(result).length > 0 ? result : undefined;
5252
+ }
5170
5253
  function taskEventMetadata(task) {
5171
5254
  const metadata = {
5172
5255
  package: "@hasna/todos",
@@ -5177,6 +5260,14 @@ function taskEventMetadata(task) {
5177
5260
  task_list_id: task.task_list_id,
5178
5261
  working_dir: task.working_dir
5179
5262
  };
5263
+ const routeEnabled = booleanField(task.metadata.route_enabled);
5264
+ if (routeEnabled !== undefined) {
5265
+ metadata.route_enabled = routeEnabled;
5266
+ }
5267
+ const automation = routingAutomationMetadata(task);
5268
+ if (automation) {
5269
+ metadata.automation = automation;
5270
+ }
5180
5271
  try {
5181
5272
  const project = task.project_id ? getProject(task.project_id) : null;
5182
5273
  const projectPath = project ? readMachineLocalPath(project) ?? project.path : task.working_dir;
@@ -5194,9 +5285,6 @@ function taskEventMetadata(task) {
5194
5285
  if (projectPath) {
5195
5286
  metadata.project_kind = classifyProjectKind(projectPath);
5196
5287
  metadata.project_is_worktree = isWorktreePath(projectPath);
5197
- if (typeof task.metadata.route_enabled === "boolean") {
5198
- metadata.route_enabled = task.metadata.route_enabled;
5199
- }
5200
5288
  metadata.working_dir = task.working_dir ?? projectPath;
5201
5289
  }
5202
5290
  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.60",
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.10",
93
93
  "@modelcontextprotocol/sdk": "^1.12.1",
94
94
  "chalk": "^5.4.1",
95
95
  "commander": "^13.1.0",