@versatly/workgraph 1.0.0 → 1.1.0

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
@@ -15,6 +15,7 @@ Agent-first workgraph workspace for multi-agent collaboration.
15
15
  - Native skill primitive lifecycle (`workgraph skill write/load/propose/promote`)
16
16
  - Primitive-registry manifest + auto-generated `.base` files
17
17
  - Orientation loop commands (`workgraph status/brief/checkpoint/intake`)
18
+ - Deterministic context lenses (`workgraph lens list/show`) for real-time situational awareness
18
19
  - Multi-filter primitive query (`workgraph query ...`)
19
20
  - Core + QMD-compatible keyword search (`workgraph search ...`)
20
21
  - Obsidian Kanban board generation/sync (`workgraph board generate|sync`)
@@ -61,6 +62,8 @@ workgraph thread create "Ship command center" \
61
62
  workgraph thread next --claim --actor agent-worker --json
62
63
  workgraph status --json
63
64
  workgraph brief --actor agent-worker --json
65
+ workgraph lens list --json
66
+ workgraph lens show my-work --actor agent-worker --json
64
67
  workgraph query --type thread --status open --limit 10 --json
65
68
  workgraph search "auth" --mode auto --json
66
69
  workgraph checkpoint "Completed API layer" --next "implement tests" --actor agent-worker --json
@@ -6027,12 +6027,14 @@ export {
6027
6027
  CursorCloudAdapter,
6028
6028
  createRun,
6029
6029
  stop,
6030
+ listRuns,
6030
6031
  dispatch_exports,
6031
6032
  parseCronExpression,
6032
6033
  matchesCronSchedule,
6033
6034
  nextCronMatch,
6034
6035
  trigger_engine_exports,
6035
6036
  createThread,
6037
+ listReadyThreads,
6036
6038
  release,
6037
6039
  thread_exports,
6038
6040
  thread_audit_exports,
@@ -9,6 +9,8 @@ import {
9
9
  historyOf,
10
10
  keywordSearch,
11
11
  list,
12
+ listReadyThreads,
13
+ listRuns,
12
14
  listTypes,
13
15
  loadPolicyRegistry,
14
16
  loadRegistry,
@@ -21,7 +23,7 @@ import {
21
23
  saveRegistry,
22
24
  stop,
23
25
  update
24
- } from "./chunk-R2MLGBHB.js";
26
+ } from "./chunk-OTSECVE5.js";
25
27
 
26
28
  // src/workspace.ts
27
29
  var workspace_exports = {};
@@ -532,14 +534,480 @@ function writeSkillManifest(workspacePath, slug, skill, actor) {
532
534
  fs4.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n", "utf-8");
533
535
  }
534
536
 
537
+ // src/lens.ts
538
+ var lens_exports = {};
539
+ __export(lens_exports, {
540
+ generateContextLens: () => generateContextLens,
541
+ listContextLenses: () => listContextLenses,
542
+ materializeContextLens: () => materializeContextLens
543
+ });
544
+ import fs5 from "fs";
545
+ import path5 from "path";
546
+ var DEFAULT_LOOKBACK_HOURS = 24;
547
+ var DEFAULT_STALE_HOURS = 24;
548
+ var DEFAULT_LIMIT = 10;
549
+ var PRIORITY_ORDER = {
550
+ urgent: 0,
551
+ high: 1,
552
+ medium: 2,
553
+ low: 3
554
+ };
555
+ var HIGH_RISK_PRIORITIES = /* @__PURE__ */ new Set(["urgent", "high"]);
556
+ var HIGH_RISK_SEVERITIES = /* @__PURE__ */ new Set(["sev0", "sev1", "sev2"]);
557
+ var BUILT_IN_LENSES = [
558
+ {
559
+ id: "my-work",
560
+ description: "Actor workload, blockers, stale claims, and ready-next queue"
561
+ },
562
+ {
563
+ id: "team-risk",
564
+ description: "High-risk blockers, stale active claims, failed runs, and incidents"
565
+ },
566
+ {
567
+ id: "customer-health",
568
+ description: "Customer-tagged delivery health, blockers, and related incidents"
569
+ },
570
+ {
571
+ id: "exec-brief",
572
+ description: "Top priorities, momentum, risks, and recent decisions"
573
+ }
574
+ ];
575
+ function listContextLenses() {
576
+ return BUILT_IN_LENSES.map((lens) => ({ ...lens }));
577
+ }
578
+ function generateContextLens(workspacePath, lensId, options = {}) {
579
+ const normalizedLensId = normalizeLensId(lensId);
580
+ const normalizedOptions = normalizeLensOptions(options);
581
+ switch (normalizedLensId) {
582
+ case "my-work":
583
+ return buildMyWorkLens(workspacePath, normalizedOptions);
584
+ case "team-risk":
585
+ return buildTeamRiskLens(workspacePath, normalizedOptions);
586
+ case "customer-health":
587
+ return buildCustomerHealthLens(workspacePath, normalizedOptions);
588
+ case "exec-brief":
589
+ return buildExecBriefLens(workspacePath, normalizedOptions);
590
+ default:
591
+ return assertNever(normalizedLensId);
592
+ }
593
+ }
594
+ function materializeContextLens(workspacePath, lensId, options) {
595
+ const result = generateContextLens(workspacePath, lensId, options);
596
+ const absOutputPath = resolvePathWithinWorkspace2(workspacePath, options.outputPath);
597
+ const relOutputPath = path5.relative(workspacePath, absOutputPath).replace(/\\/g, "/");
598
+ const parentDir = path5.dirname(absOutputPath);
599
+ if (!fs5.existsSync(parentDir)) fs5.mkdirSync(parentDir, { recursive: true });
600
+ const existed = fs5.existsSync(absOutputPath);
601
+ fs5.writeFileSync(absOutputPath, result.markdown, "utf-8");
602
+ append(
603
+ workspacePath,
604
+ options.actor ?? result.actor ?? "system",
605
+ existed ? "update" : "create",
606
+ relOutputPath,
607
+ "lens",
608
+ {
609
+ lens: result.lens,
610
+ sections: result.sections.length
611
+ }
612
+ );
613
+ return {
614
+ ...result,
615
+ outputPath: relOutputPath,
616
+ created: !existed
617
+ };
618
+ }
619
+ function buildMyWorkLens(workspacePath, options) {
620
+ const actor = options.actor;
621
+ const nowMs = Date.now();
622
+ const staleCutoffMs = nowMs - options.staleHours * 60 * 60 * 1e3;
623
+ const claims = [...allClaims(workspacePath).entries()];
624
+ const myClaimedThreads = claims.filter(([, owner]) => owner === actor).map(([target]) => read(workspacePath, target)).filter((instance) => !!instance && instance.type === "thread").sort(compareThreadsByPriorityThenUpdated);
625
+ const myBlockedThreads = myClaimedThreads.filter((instance) => String(instance.fields.status ?? "") === "blocked").slice(0, options.limit);
626
+ const staleClaims = myClaimedThreads.filter((instance) => isStale(instance, staleCutoffMs)).slice(0, options.limit);
627
+ const nextReady = listReadyThreads(workspacePath).filter((instance) => !instance.fields.owner).sort(compareThreadsByPriorityThenUpdated).slice(0, options.limit);
628
+ const sections = [
629
+ {
630
+ id: "my_claims",
631
+ title: `Claimed Threads (${actor})`,
632
+ items: myClaimedThreads.slice(0, options.limit).map((instance) => toThreadItem(instance, nowMs))
633
+ },
634
+ {
635
+ id: "my_blockers",
636
+ title: `Blocked Threads (${actor})`,
637
+ items: myBlockedThreads.map((instance) => toThreadItem(instance, nowMs))
638
+ },
639
+ {
640
+ id: "stale_claims",
641
+ title: `Stale Claims (${options.staleHours}h+)`,
642
+ items: staleClaims.map((instance) => toThreadItem(instance, nowMs))
643
+ },
644
+ {
645
+ id: "next_ready",
646
+ title: "Next Ready Threads",
647
+ items: nextReady.map((instance) => toThreadItem(instance, nowMs))
648
+ }
649
+ ];
650
+ return finalizeLensResult("my-work", {
651
+ actor,
652
+ options,
653
+ metrics: {
654
+ myClaims: myClaimedThreads.length,
655
+ blocked: myBlockedThreads.length,
656
+ staleClaims: staleClaims.length,
657
+ nextReady: nextReady.length
658
+ },
659
+ sections
660
+ });
661
+ }
662
+ function buildTeamRiskLens(workspacePath, options) {
663
+ const nowMs = Date.now();
664
+ const staleCutoffMs = nowMs - options.staleHours * 60 * 60 * 1e3;
665
+ const lookbackCutoffMs = nowMs - options.lookbackHours * 60 * 60 * 1e3;
666
+ const threads = list(workspacePath, "thread");
667
+ const blockedHighPriority = threads.filter((instance) => String(instance.fields.status ?? "") === "blocked").filter((instance) => HIGH_RISK_PRIORITIES.has(normalizePriority(instance.fields.priority))).sort(compareThreadsByPriorityThenUpdated).slice(0, options.limit);
668
+ const staleActiveClaims = [...allClaims(workspacePath).entries()].map(([target, owner]) => ({ owner, instance: read(workspacePath, target) })).filter((entry) => !!entry.instance && entry.instance.type === "thread").filter((entry) => String(entry.instance.fields.status ?? "") === "active").filter((entry) => isStale(entry.instance, staleCutoffMs)).slice(0, options.limit);
669
+ const failedRuns = listRuns(workspacePath, { status: "failed" }).filter((run) => parseTimestamp(run.updatedAt) >= lookbackCutoffMs).slice(0, options.limit);
670
+ const highSeverityIncidents = list(workspacePath, "incident").filter((incident) => String(incident.fields.status ?? "") === "active").filter((incident) => HIGH_RISK_SEVERITIES.has(normalizeSeverity(incident.fields.severity))).slice(0, options.limit);
671
+ const sections = [
672
+ {
673
+ id: "blocked_high_priority_threads",
674
+ title: "High Priority Blocked Threads",
675
+ items: blockedHighPriority.map((instance) => toThreadItem(instance, nowMs))
676
+ },
677
+ {
678
+ id: "stale_active_claims",
679
+ title: `Stale Active Claims (${options.staleHours}h+)`,
680
+ items: staleActiveClaims.map((entry) => ({
681
+ ...toThreadItem(entry.instance, nowMs),
682
+ owner: entry.owner
683
+ }))
684
+ },
685
+ {
686
+ id: "failed_runs",
687
+ title: `Failed Runs (${options.lookbackHours}h window)`,
688
+ items: failedRuns.map(toRunItem)
689
+ },
690
+ {
691
+ id: "active_high_severity_incidents",
692
+ title: "Active High-Severity Incidents",
693
+ items: highSeverityIncidents.map((incident) => toIncidentItem(incident, nowMs))
694
+ }
695
+ ];
696
+ return finalizeLensResult("team-risk", {
697
+ actor: options.actor,
698
+ options,
699
+ metrics: {
700
+ blockedHighPriority: blockedHighPriority.length,
701
+ staleActiveClaims: staleActiveClaims.length,
702
+ failedRuns: failedRuns.length,
703
+ activeHighSeverityIncidents: highSeverityIncidents.length
704
+ },
705
+ sections
706
+ });
707
+ }
708
+ function buildCustomerHealthLens(workspacePath, options) {
709
+ const nowMs = Date.now();
710
+ const customerThreads = list(workspacePath, "thread").filter(isCustomerLinked).sort(compareThreadsByPriorityThenUpdated);
711
+ const activeCustomerThreads = customerThreads.filter((instance) => ["open", "active"].includes(String(instance.fields.status ?? ""))).slice(0, options.limit);
712
+ const blockedCustomerThreads = customerThreads.filter((instance) => String(instance.fields.status ?? "") === "blocked").slice(0, options.limit);
713
+ const customerIncidents = list(workspacePath, "incident").filter((incident) => String(incident.fields.status ?? "") === "active").filter(isCustomerLinked).slice(0, options.limit);
714
+ const clients = list(workspacePath, "client").slice(0, options.limit);
715
+ const sections = [
716
+ {
717
+ id: "active_customer_threads",
718
+ title: "Active Customer Threads",
719
+ items: activeCustomerThreads.map((instance) => toThreadItem(instance, nowMs))
720
+ },
721
+ {
722
+ id: "blocked_customer_threads",
723
+ title: "Blocked Customer Threads",
724
+ items: blockedCustomerThreads.map((instance) => toThreadItem(instance, nowMs))
725
+ },
726
+ {
727
+ id: "customer_incidents",
728
+ title: "Customer Incidents",
729
+ items: customerIncidents.map((incident) => toIncidentItem(incident, nowMs))
730
+ },
731
+ {
732
+ id: "client_records",
733
+ title: "Client Records",
734
+ items: clients.map((instance) => ({
735
+ title: String(instance.fields.title ?? instance.path),
736
+ path: instance.path,
737
+ status: stringOrUndefined(instance.fields.status),
738
+ detail: stringOrUndefined(instance.fields.health ?? instance.fields.risk),
739
+ ageHours: ageHours(instance, nowMs)
740
+ }))
741
+ }
742
+ ];
743
+ return finalizeLensResult("customer-health", {
744
+ actor: options.actor,
745
+ options,
746
+ metrics: {
747
+ activeCustomerThreads: activeCustomerThreads.length,
748
+ blockedCustomerThreads: blockedCustomerThreads.length,
749
+ customerIncidents: customerIncidents.length,
750
+ clients: clients.length
751
+ },
752
+ sections
753
+ });
754
+ }
755
+ function buildExecBriefLens(workspacePath, options) {
756
+ const nowMs = Date.now();
757
+ const lookbackCutoffMs = nowMs - options.lookbackHours * 60 * 60 * 1e3;
758
+ const threads = list(workspacePath, "thread");
759
+ const topPriorities = threads.filter((instance) => ["open", "active"].includes(String(instance.fields.status ?? ""))).sort(compareThreadsByPriorityThenUpdated).slice(0, options.limit);
760
+ const momentum = threads.filter((instance) => String(instance.fields.status ?? "") === "done").filter((instance) => parseTimestamp(instance.fields.updated) >= lookbackCutoffMs).sort(compareThreadsByPriorityThenUpdated).slice(0, options.limit);
761
+ const blockedHighPriority = threads.filter((instance) => String(instance.fields.status ?? "") === "blocked").filter((instance) => HIGH_RISK_PRIORITIES.has(normalizePriority(instance.fields.priority))).sort(compareThreadsByPriorityThenUpdated).slice(0, options.limit);
762
+ const failedRuns = listRuns(workspacePath, { status: "failed" }).filter((run) => parseTimestamp(run.updatedAt) >= lookbackCutoffMs).slice(0, options.limit);
763
+ const decisions = list(workspacePath, "decision").filter((instance) => ["proposed", "approved", "active"].includes(String(instance.fields.status ?? ""))).filter((instance) => parseTimestamp(instance.fields.updated ?? instance.fields.date) >= lookbackCutoffMs).slice(0, options.limit);
764
+ const sections = [
765
+ {
766
+ id: "top_priorities",
767
+ title: "Top Priorities",
768
+ items: topPriorities.map((instance) => toThreadItem(instance, nowMs))
769
+ },
770
+ {
771
+ id: "momentum",
772
+ title: `Momentum (${options.lookbackHours}h completed)`,
773
+ items: momentum.map((instance) => toThreadItem(instance, nowMs))
774
+ },
775
+ {
776
+ id: "key_risks",
777
+ title: "Key Risks",
778
+ items: [
779
+ ...blockedHighPriority.map((instance) => toThreadItem(instance, nowMs)),
780
+ ...failedRuns.map(toRunItem)
781
+ ].slice(0, options.limit)
782
+ },
783
+ {
784
+ id: "recent_decisions",
785
+ title: `Decisions (${options.lookbackHours}h window)`,
786
+ items: decisions.map((instance) => ({
787
+ title: String(instance.fields.title ?? instance.path),
788
+ path: instance.path,
789
+ status: stringOrUndefined(instance.fields.status),
790
+ detail: stringOrUndefined(instance.fields.date),
791
+ ageHours: ageHours(instance, nowMs)
792
+ }))
793
+ }
794
+ ];
795
+ return finalizeLensResult("exec-brief", {
796
+ actor: options.actor,
797
+ options,
798
+ metrics: {
799
+ topPriorities: topPriorities.length,
800
+ momentumDone: momentum.length,
801
+ risks: blockedHighPriority.length + failedRuns.length,
802
+ decisions: decisions.length
803
+ },
804
+ sections
805
+ });
806
+ }
807
+ function finalizeLensResult(lens, input) {
808
+ const base = {
809
+ lens,
810
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
811
+ actor: input.actor,
812
+ options: {
813
+ lookbackHours: input.options.lookbackHours,
814
+ staleHours: input.options.staleHours,
815
+ limit: input.options.limit
816
+ },
817
+ metrics: input.metrics,
818
+ sections: input.sections
819
+ };
820
+ return {
821
+ ...base,
822
+ markdown: renderLensMarkdown(base)
823
+ };
824
+ }
825
+ function toThreadItem(instance, nowMs) {
826
+ return {
827
+ title: String(instance.fields.title ?? instance.path),
828
+ path: instance.path,
829
+ status: stringOrUndefined(instance.fields.status),
830
+ priority: stringOrUndefined(instance.fields.priority),
831
+ owner: stringOrUndefined(instance.fields.owner),
832
+ detail: renderThreadDependencies(instance),
833
+ ageHours: ageHours(instance, nowMs)
834
+ };
835
+ }
836
+ function toIncidentItem(instance, nowMs) {
837
+ return {
838
+ title: String(instance.fields.title ?? instance.path),
839
+ path: instance.path,
840
+ status: stringOrUndefined(instance.fields.status),
841
+ priority: stringOrUndefined(instance.fields.severity),
842
+ owner: stringOrUndefined(instance.fields.owner),
843
+ ageHours: ageHours(instance, nowMs)
844
+ };
845
+ }
846
+ function toRunItem(run) {
847
+ return {
848
+ title: run.objective,
849
+ path: `runs/${run.id}.md`,
850
+ status: run.status,
851
+ owner: run.actor,
852
+ detail: run.error ?? run.output,
853
+ ageHours: ageHoursFromIso(run.updatedAt)
854
+ };
855
+ }
856
+ function renderLensMarkdown(input) {
857
+ const lines = [
858
+ `# Workgraph Context Lens: ${input.lens}`,
859
+ "",
860
+ `Generated: ${input.generatedAt}`,
861
+ ...input.actor ? [`Actor: ${input.actor}`] : [],
862
+ `Lookback: ${input.options.lookbackHours}h`,
863
+ `Stale threshold: ${input.options.staleHours}h`,
864
+ `Section limit: ${input.options.limit}`,
865
+ "",
866
+ "## Metrics",
867
+ "",
868
+ ...Object.entries(input.metrics).map(([metric, value]) => `- ${metric}: ${value}`),
869
+ ""
870
+ ];
871
+ for (const section of input.sections) {
872
+ lines.push(`## ${section.title}`);
873
+ lines.push("");
874
+ if (section.items.length === 0) {
875
+ lines.push("- None");
876
+ lines.push("");
877
+ continue;
878
+ }
879
+ for (const item of section.items) {
880
+ lines.push(`- ${renderLensItem(item)}`);
881
+ }
882
+ lines.push("");
883
+ }
884
+ return lines.join("\n");
885
+ }
886
+ function renderLensItem(item) {
887
+ const components = [item.title];
888
+ if (item.path) components.push(`(\`${item.path}\`)`);
889
+ const metadata = [];
890
+ if (item.status) metadata.push(`status=${item.status}`);
891
+ if (item.priority) metadata.push(`priority=${item.priority}`);
892
+ if (item.owner) metadata.push(`owner=${item.owner}`);
893
+ if (typeof item.ageHours === "number") metadata.push(`age=${item.ageHours.toFixed(1)}h`);
894
+ if (metadata.length > 0) components.push(`[${metadata.join(", ")}]`);
895
+ if (item.detail) components.push(`- ${item.detail}`);
896
+ return components.join(" ");
897
+ }
898
+ function resolvePathWithinWorkspace2(workspacePath, outputPath) {
899
+ const base = path5.resolve(workspacePath);
900
+ const resolved = path5.resolve(base, outputPath);
901
+ if (!resolved.startsWith(base + path5.sep) && resolved !== base) {
902
+ throw new Error(`Invalid lens output path: ${outputPath}`);
903
+ }
904
+ return resolved;
905
+ }
906
+ function normalizeLensId(value) {
907
+ const normalized = String(value).trim().toLowerCase().replace(/^lens:\/\//, "");
908
+ if (normalized === "my-work") return "my-work";
909
+ if (normalized === "team-risk") return "team-risk";
910
+ if (normalized === "customer-health") return "customer-health";
911
+ if (normalized === "exec-brief") return "exec-brief";
912
+ const valid = BUILT_IN_LENSES.map((item) => item.id).join(", ");
913
+ throw new Error(`Unknown context lens "${value}". Valid lenses: ${valid}`);
914
+ }
915
+ function normalizeLensOptions(options) {
916
+ return {
917
+ actor: String(options.actor ?? "anonymous").trim() || "anonymous",
918
+ lookbackHours: parsePositiveNumber(options.lookbackHours, "lookbackHours", DEFAULT_LOOKBACK_HOURS),
919
+ staleHours: parsePositiveNumber(options.staleHours, "staleHours", DEFAULT_STALE_HOURS),
920
+ limit: parsePositiveInteger(options.limit, "limit", DEFAULT_LIMIT)
921
+ };
922
+ }
923
+ function parsePositiveNumber(value, fieldName, defaultValue) {
924
+ if (value === void 0 || value === null) return defaultValue;
925
+ const parsed = Number(value);
926
+ if (!Number.isFinite(parsed) || parsed <= 0) {
927
+ throw new Error(`Invalid ${fieldName}: expected a positive number.`);
928
+ }
929
+ return parsed;
930
+ }
931
+ function parsePositiveInteger(value, fieldName, defaultValue) {
932
+ if (value === void 0 || value === null) return defaultValue;
933
+ const parsed = Number.parseInt(String(value), 10);
934
+ if (!Number.isInteger(parsed) || parsed <= 0) {
935
+ throw new Error(`Invalid ${fieldName}: expected a positive integer.`);
936
+ }
937
+ return parsed;
938
+ }
939
+ function compareThreadsByPriorityThenUpdated(a, b) {
940
+ const priorityDelta = rankPriority(a) - rankPriority(b);
941
+ if (priorityDelta !== 0) return priorityDelta;
942
+ return parseTimestamp(b.fields.updated) - parseTimestamp(a.fields.updated);
943
+ }
944
+ function rankPriority(instance) {
945
+ const priority = normalizePriority(instance.fields.priority);
946
+ return PRIORITY_ORDER[priority] ?? PRIORITY_ORDER.medium;
947
+ }
948
+ function normalizePriority(value) {
949
+ return String(value ?? "medium").trim().toLowerCase();
950
+ }
951
+ function normalizeSeverity(value) {
952
+ return String(value ?? "sev4").trim().toLowerCase();
953
+ }
954
+ function isStale(instance, staleCutoffMs) {
955
+ const updatedAt = parseTimestamp(instance.fields.updated ?? instance.fields.created);
956
+ if (!Number.isFinite(updatedAt)) return false;
957
+ return updatedAt <= staleCutoffMs;
958
+ }
959
+ function parseTimestamp(value) {
960
+ const parsed = Date.parse(String(value ?? ""));
961
+ return Number.isFinite(parsed) ? parsed : Number.NEGATIVE_INFINITY;
962
+ }
963
+ function ageHours(instance, nowMs) {
964
+ const updatedAt = parseTimestamp(instance.fields.updated ?? instance.fields.created);
965
+ if (!Number.isFinite(updatedAt)) return void 0;
966
+ return Math.max(0, (nowMs - updatedAt) / (60 * 60 * 1e3));
967
+ }
968
+ function ageHoursFromIso(value) {
969
+ const nowMs = Date.now();
970
+ const ts = parseTimestamp(value);
971
+ if (!Number.isFinite(ts)) return void 0;
972
+ return Math.max(0, (nowMs - ts) / (60 * 60 * 1e3));
973
+ }
974
+ function renderThreadDependencies(instance) {
975
+ const deps = instance.fields.deps;
976
+ if (!Array.isArray(deps) || deps.length === 0) return void 0;
977
+ const visible = deps.slice(0, 3).map((value) => String(value));
978
+ const suffix = deps.length > visible.length ? ` +${deps.length - visible.length} more` : "";
979
+ return `deps: ${visible.join(", ")}${suffix}`;
980
+ }
981
+ function isCustomerLinked(instance) {
982
+ const tags = normalizeTags(instance.fields.tags);
983
+ if (tags.includes("customer") || tags.includes("client")) return true;
984
+ const candidateFields = ["client", "client_ref", "customer", "customer_ref", "account", "account_ref"];
985
+ return candidateFields.some((key) => {
986
+ const value = instance.fields[key];
987
+ return typeof value === "string" && value.trim().length > 0;
988
+ });
989
+ }
990
+ function normalizeTags(value) {
991
+ if (!Array.isArray(value)) return [];
992
+ return value.map((item) => String(item).trim().toLowerCase()).filter(Boolean);
993
+ }
994
+ function stringOrUndefined(value) {
995
+ if (value === void 0 || value === null) return void 0;
996
+ const normalized = String(value).trim();
997
+ return normalized.length > 0 ? normalized : void 0;
998
+ }
999
+ function assertNever(value) {
1000
+ throw new Error(`Unhandled lens variant: ${String(value)}`);
1001
+ }
1002
+
535
1003
  // src/board.ts
536
1004
  var board_exports = {};
537
1005
  __export(board_exports, {
538
1006
  generateKanbanBoard: () => generateKanbanBoard,
539
1007
  syncKanbanBoard: () => syncKanbanBoard
540
1008
  });
541
- import fs5 from "fs";
542
- import path5 from "path";
1009
+ import fs6 from "fs";
1010
+ import path6 from "path";
543
1011
  function generateKanbanBoard(workspacePath, options = {}) {
544
1012
  const threads = list(workspacePath, "thread");
545
1013
  const grouped = groupThreads(threads);
@@ -555,12 +1023,12 @@ function generateKanbanBoard(workspacePath, options = {}) {
555
1023
  }
556
1024
  const content = renderKanbanMarkdown(lanes);
557
1025
  const relOutputPath = options.outputPath ?? "ops/Workgraph Board.md";
558
- const absOutputPath = resolvePathWithinWorkspace2(workspacePath, relOutputPath);
559
- const parentDir = path5.dirname(absOutputPath);
560
- if (!fs5.existsSync(parentDir)) fs5.mkdirSync(parentDir, { recursive: true });
561
- fs5.writeFileSync(absOutputPath, content, "utf-8");
1026
+ const absOutputPath = resolvePathWithinWorkspace3(workspacePath, relOutputPath);
1027
+ const parentDir = path6.dirname(absOutputPath);
1028
+ if (!fs6.existsSync(parentDir)) fs6.mkdirSync(parentDir, { recursive: true });
1029
+ fs6.writeFileSync(absOutputPath, content, "utf-8");
562
1030
  return {
563
- outputPath: path5.relative(workspacePath, absOutputPath).replace(/\\/g, "/"),
1031
+ outputPath: path6.relative(workspacePath, absOutputPath).replace(/\\/g, "/"),
564
1032
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
565
1033
  counts: {
566
1034
  backlog: grouped.open.length,
@@ -658,10 +1126,10 @@ function renderKanbanMarkdown(lanes) {
658
1126
  lines.push("");
659
1127
  return lines.join("\n");
660
1128
  }
661
- function resolvePathWithinWorkspace2(workspacePath, outputPath) {
662
- const base = path5.resolve(workspacePath);
663
- const resolved = path5.resolve(base, outputPath);
664
- if (!resolved.startsWith(base + path5.sep) && resolved !== base) {
1129
+ function resolvePathWithinWorkspace3(workspacePath, outputPath) {
1130
+ const base = path6.resolve(workspacePath);
1131
+ const resolved = path6.resolve(base, outputPath);
1132
+ if (!resolved.startsWith(base + path6.sep) && resolved !== base) {
665
1133
  throw new Error(`Invalid board output path: ${outputPath}`);
666
1134
  }
667
1135
  return resolved;
@@ -1157,8 +1625,8 @@ __export(diagnostics_exports, {
1157
1625
  });
1158
1626
 
1159
1627
  // src/diagnostics/doctor.ts
1160
- import fs6 from "fs";
1161
- import path7 from "path";
1628
+ import fs7 from "fs";
1629
+ import path8 from "path";
1162
1630
  import YAML2 from "yaml";
1163
1631
 
1164
1632
  // src/diagnostics/format.ts
@@ -1222,7 +1690,7 @@ function inferPrimitiveTypeFromPath(targetPath) {
1222
1690
  }
1223
1691
 
1224
1692
  // src/diagnostics/primitives.ts
1225
- import path6 from "path";
1693
+ import path7 from "path";
1226
1694
  function loadPrimitiveInventory(workspacePath) {
1227
1695
  const registry = loadRegistry(workspacePath);
1228
1696
  const allPrimitives = queryPrimitives(workspacePath);
@@ -1240,7 +1708,7 @@ function loadPrimitiveInventory(workspacePath) {
1240
1708
  const requiredFields = Object.entries(typeDef?.fields ?? {}).filter(([, fieldDef]) => fieldDef.required === true).map(([fieldName]) => fieldName);
1241
1709
  const presentCount = requiredFields.filter((fieldName) => hasRequiredValue(instance.fields[fieldName])).length;
1242
1710
  const frontmatterCompleteness = requiredFields.length === 0 ? 1 : presentCount / requiredFields.length;
1243
- const slug = path6.basename(instance.path, ".md");
1711
+ const slug = path7.basename(instance.path, ".md");
1244
1712
  return {
1245
1713
  ...instance,
1246
1714
  slug,
@@ -1582,8 +2050,8 @@ function collectStaleRuns(workspacePath, staleAfterMs, now) {
1582
2050
  }
1583
2051
  function collectPrimitiveRegistryReferenceIssues(workspacePath, inventory) {
1584
2052
  const issues = [];
1585
- const manifestPath = path7.join(workspacePath, ".workgraph", "primitive-registry.yaml");
1586
- if (!fs6.existsSync(manifestPath)) {
2053
+ const manifestPath = path8.join(workspacePath, ".workgraph", "primitive-registry.yaml");
2054
+ if (!fs7.existsSync(manifestPath)) {
1587
2055
  issues.push({
1588
2056
  code: "broken-primitive-registry-reference",
1589
2057
  severity: "error",
@@ -1594,7 +2062,7 @@ function collectPrimitiveRegistryReferenceIssues(workspacePath, inventory) {
1594
2062
  }
1595
2063
  let parsed;
1596
2064
  try {
1597
- parsed = YAML2.parse(fs6.readFileSync(manifestPath, "utf-8"));
2065
+ parsed = YAML2.parse(fs7.readFileSync(manifestPath, "utf-8"));
1598
2066
  } catch (error) {
1599
2067
  issues.push({
1600
2068
  code: "broken-primitive-registry-reference",
@@ -1646,7 +2114,7 @@ function collectPrimitiveRegistryReferenceIssues(workspacePath, inventory) {
1646
2114
  path: ".workgraph/primitive-registry.yaml"
1647
2115
  });
1648
2116
  }
1649
- if (!fs6.existsSync(path7.join(workspacePath, directory))) {
2117
+ if (!fs7.existsSync(path8.join(workspacePath, directory))) {
1650
2118
  issues.push({
1651
2119
  code: "broken-primitive-registry-reference",
1652
2120
  severity: "error",
@@ -1681,8 +2149,8 @@ function collectPrimitiveRegistryReferenceIssues(workspacePath, inventory) {
1681
2149
  function collectEmptyPrimitiveDirectoryIssues(workspacePath, inventory) {
1682
2150
  const issues = [];
1683
2151
  for (const typeDef of inventory.typeDefs.values()) {
1684
- const directoryPath = path7.join(workspacePath, typeDef.directory);
1685
- if (!fs6.existsSync(directoryPath)) continue;
2152
+ const directoryPath = path8.join(workspacePath, typeDef.directory);
2153
+ if (!fs7.existsSync(directoryPath)) continue;
1686
2154
  const markdownCount = listMarkdownFilesRecursive(directoryPath).length;
1687
2155
  if (markdownCount > 0) continue;
1688
2156
  issues.push({
@@ -1711,10 +2179,10 @@ function removeOrphanLinks(workspacePath, orphanLinks) {
1711
2179
  }
1712
2180
  let removedLinks = 0;
1713
2181
  for (const [sourcePath, tokenSet] of tokensBySource.entries()) {
1714
- const absPath = path7.join(workspacePath, sourcePath);
1715
- if (!fs6.existsSync(absPath)) continue;
2182
+ const absPath = path8.join(workspacePath, sourcePath);
2183
+ if (!fs7.existsSync(absPath)) continue;
1716
2184
  try {
1717
- const raw = fs6.readFileSync(absPath, "utf-8");
2185
+ const raw = fs7.readFileSync(absPath, "utf-8");
1718
2186
  let fileRemoved = 0;
1719
2187
  const updated = raw.replace(/\[\[([^[\]]+)\]\]/g, (token) => {
1720
2188
  if (!tokenSet.has(token)) return token;
@@ -1722,7 +2190,7 @@ function removeOrphanLinks(workspacePath, orphanLinks) {
1722
2190
  return "";
1723
2191
  });
1724
2192
  if (fileRemoved === 0) continue;
1725
- fs6.writeFileSync(absPath, updated, "utf-8");
2193
+ fs7.writeFileSync(absPath, updated, "utf-8");
1726
2194
  removedLinks += fileRemoved;
1727
2195
  filesUpdated.push(sourcePath);
1728
2196
  } catch (error) {
@@ -1787,10 +2255,10 @@ function cancelStaleRuns(workspacePath, staleRuns, actor) {
1787
2255
  return { cancelled, errors };
1788
2256
  }
1789
2257
  function readDispatchRunsSnapshot(workspacePath) {
1790
- const runsPath = path7.join(workspacePath, ".workgraph", "dispatch-runs.json");
1791
- if (!fs6.existsSync(runsPath)) return [];
2258
+ const runsPath = path8.join(workspacePath, ".workgraph", "dispatch-runs.json");
2259
+ if (!fs7.existsSync(runsPath)) return [];
1792
2260
  try {
1793
- const parsed = JSON.parse(fs6.readFileSync(runsPath, "utf-8"));
2261
+ const parsed = JSON.parse(fs7.readFileSync(runsPath, "utf-8"));
1794
2262
  return Array.isArray(parsed.runs) ? parsed.runs : [];
1795
2263
  } catch {
1796
2264
  return [];
@@ -1801,9 +2269,9 @@ function listMarkdownFilesRecursive(rootDirectory) {
1801
2269
  const stack = [rootDirectory];
1802
2270
  while (stack.length > 0) {
1803
2271
  const current = stack.pop();
1804
- const entries = fs6.readdirSync(current, { withFileTypes: true });
2272
+ const entries = fs7.readdirSync(current, { withFileTypes: true });
1805
2273
  for (const entry of entries) {
1806
- const absPath = path7.join(current, entry.name);
2274
+ const absPath = path8.join(current, entry.name);
1807
2275
  if (entry.isDirectory()) {
1808
2276
  stack.push(absPath);
1809
2277
  continue;
@@ -1947,7 +2415,7 @@ function toNullableString(value) {
1947
2415
  }
1948
2416
 
1949
2417
  // src/diagnostics/viz.ts
1950
- import path8 from "path";
2418
+ import path9 from "path";
1951
2419
  var TYPE_COLORS = ["cyan", "magenta", "yellow", "green", "blue", "red"];
1952
2420
  function visualizeVaultGraph(workspacePath, options = {}) {
1953
2421
  const inventory = loadPrimitiveInventory(workspacePath);
@@ -2087,7 +2555,7 @@ function resolveFocusPath(focusInput, inventory) {
2087
2555
  const directCandidate = normalized.endsWith(".md") ? normalized : `${normalized}.md`;
2088
2556
  if (inventory.byPath.has(normalized)) return normalized;
2089
2557
  if (inventory.byPath.has(directCandidate)) return directCandidate;
2090
- const slug = path8.basename(normalized, ".md");
2558
+ const slug = path9.basename(normalized, ".md");
2091
2559
  const candidates = inventory.slugToPaths.get(slug) ?? [];
2092
2560
  if (candidates.length === 1) return candidates[0];
2093
2561
  if (candidates.length > 1) {
@@ -2395,8 +2863,8 @@ __export(autonomy_daemon_exports, {
2395
2863
  startAutonomyDaemon: () => startAutonomyDaemon,
2396
2864
  stopAutonomyDaemon: () => stopAutonomyDaemon
2397
2865
  });
2398
- import fs7 from "fs";
2399
- import path9 from "path";
2866
+ import fs8 from "fs";
2867
+ import path10 from "path";
2400
2868
  import { spawn } from "child_process";
2401
2869
  var DAEMON_DIR = ".workgraph/daemon";
2402
2870
  var AUTONOMY_PID_FILE = "autonomy.pid";
@@ -2405,29 +2873,29 @@ var AUTONOMY_LOG_FILE = "autonomy.log";
2405
2873
  var AUTONOMY_META_FILE = "autonomy-process.json";
2406
2874
  function startAutonomyDaemon(workspacePath, input) {
2407
2875
  const daemonDir = ensureDaemonDir(workspacePath);
2408
- const pidPath = path9.join(daemonDir, AUTONOMY_PID_FILE);
2409
- const heartbeatPath = input.heartbeatPath ? resolvePathWithinWorkspace3(workspacePath, input.heartbeatPath) : path9.join(daemonDir, AUTONOMY_HEARTBEAT_FILE);
2410
- const logPath = input.logPath ? resolvePathWithinWorkspace3(workspacePath, input.logPath) : path9.join(daemonDir, AUTONOMY_LOG_FILE);
2411
- const metaPath = path9.join(daemonDir, AUTONOMY_META_FILE);
2876
+ const pidPath = path10.join(daemonDir, AUTONOMY_PID_FILE);
2877
+ const heartbeatPath = input.heartbeatPath ? resolvePathWithinWorkspace4(workspacePath, input.heartbeatPath) : path10.join(daemonDir, AUTONOMY_HEARTBEAT_FILE);
2878
+ const logPath = input.logPath ? resolvePathWithinWorkspace4(workspacePath, input.logPath) : path10.join(daemonDir, AUTONOMY_LOG_FILE);
2879
+ const metaPath = path10.join(daemonDir, AUTONOMY_META_FILE);
2412
2880
  const existing = readAutonomyDaemonStatus(workspacePath, { cleanupStalePidFile: true });
2413
2881
  if (existing.running) {
2414
2882
  throw new Error(`Autonomy daemon already running (pid=${existing.pid}). Stop it before starting a new one.`);
2415
2883
  }
2416
- const logFd = fs7.openSync(logPath, "a");
2884
+ const logFd = fs8.openSync(logPath, "a");
2417
2885
  const args = buildAutonomyDaemonArgs(workspacePath, input, heartbeatPath);
2418
2886
  const child = spawn(process.execPath, args, {
2419
2887
  detached: true,
2420
2888
  stdio: ["ignore", logFd, logFd],
2421
2889
  env: process.env
2422
2890
  });
2423
- fs7.closeSync(logFd);
2891
+ fs8.closeSync(logFd);
2424
2892
  child.unref();
2425
2893
  if (!child.pid) {
2426
2894
  throw new Error("Failed to start autonomy daemon: missing child process pid.");
2427
2895
  }
2428
- fs7.writeFileSync(pidPath, `${child.pid}
2896
+ fs8.writeFileSync(pidPath, `${child.pid}
2429
2897
  `, "utf-8");
2430
- fs7.writeFileSync(metaPath, JSON.stringify({
2898
+ fs8.writeFileSync(metaPath, JSON.stringify({
2431
2899
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
2432
2900
  pid: child.pid,
2433
2901
  args,
@@ -2461,9 +2929,9 @@ async function stopAutonomyDaemon(workspacePath, input = {}) {
2461
2929
  await waitForProcessExit(pid, 1500);
2462
2930
  stopped = !isProcessAlive(pid);
2463
2931
  }
2464
- const pidPath = path9.join(ensureDaemonDir(workspacePath), AUTONOMY_PID_FILE);
2465
- if (stopped && fs7.existsSync(pidPath)) {
2466
- fs7.rmSync(pidPath, { force: true });
2932
+ const pidPath = path10.join(ensureDaemonDir(workspacePath), AUTONOMY_PID_FILE);
2933
+ if (stopped && fs8.existsSync(pidPath)) {
2934
+ fs8.rmSync(pidPath, { force: true });
2467
2935
  }
2468
2936
  return {
2469
2937
  stopped,
@@ -2474,14 +2942,14 @@ async function stopAutonomyDaemon(workspacePath, input = {}) {
2474
2942
  }
2475
2943
  function readAutonomyDaemonStatus(workspacePath, options = {}) {
2476
2944
  const daemonDir = ensureDaemonDir(workspacePath);
2477
- const pidPath = path9.join(daemonDir, AUTONOMY_PID_FILE);
2478
- const meta = readDaemonMeta(path9.join(daemonDir, AUTONOMY_META_FILE));
2479
- const logPath = meta?.logPath ? String(meta.logPath) : path9.join(daemonDir, AUTONOMY_LOG_FILE);
2480
- const heartbeatPath = meta?.heartbeatPath ? String(meta.heartbeatPath) : path9.join(daemonDir, AUTONOMY_HEARTBEAT_FILE);
2945
+ const pidPath = path10.join(daemonDir, AUTONOMY_PID_FILE);
2946
+ const meta = readDaemonMeta(path10.join(daemonDir, AUTONOMY_META_FILE));
2947
+ const logPath = meta?.logPath ? String(meta.logPath) : path10.join(daemonDir, AUTONOMY_LOG_FILE);
2948
+ const heartbeatPath = meta?.heartbeatPath ? String(meta.heartbeatPath) : path10.join(daemonDir, AUTONOMY_HEARTBEAT_FILE);
2481
2949
  const pid = readPid(pidPath);
2482
2950
  const running = pid ? isProcessAlive(pid) : false;
2483
- if (!running && pid && options.cleanupStalePidFile !== false && fs7.existsSync(pidPath)) {
2484
- fs7.rmSync(pidPath, { force: true });
2951
+ if (!running && pid && options.cleanupStalePidFile !== false && fs8.existsSync(pidPath)) {
2952
+ fs8.rmSync(pidPath, { force: true });
2485
2953
  }
2486
2954
  return {
2487
2955
  running,
@@ -2494,7 +2962,7 @@ function readAutonomyDaemonStatus(workspacePath, options = {}) {
2494
2962
  }
2495
2963
  function buildAutonomyDaemonArgs(workspacePath, input, heartbeatPath) {
2496
2964
  const args = [
2497
- path9.resolve(input.cliEntrypointPath),
2965
+ path10.resolve(input.cliEntrypointPath),
2498
2966
  "autonomy",
2499
2967
  "run",
2500
2968
  "-w",
@@ -2552,22 +3020,22 @@ function waitForProcessExit(pid, timeoutMs) {
2552
3020
  });
2553
3021
  }
2554
3022
  function ensureDaemonDir(workspacePath) {
2555
- const daemonDir = path9.join(workspacePath, DAEMON_DIR);
2556
- if (!fs7.existsSync(daemonDir)) fs7.mkdirSync(daemonDir, { recursive: true });
3023
+ const daemonDir = path10.join(workspacePath, DAEMON_DIR);
3024
+ if (!fs8.existsSync(daemonDir)) fs8.mkdirSync(daemonDir, { recursive: true });
2557
3025
  return daemonDir;
2558
3026
  }
2559
3027
  function readPid(pidPath) {
2560
- if (!fs7.existsSync(pidPath)) return void 0;
2561
- const raw = fs7.readFileSync(pidPath, "utf-8").trim();
3028
+ if (!fs8.existsSync(pidPath)) return void 0;
3029
+ const raw = fs8.readFileSync(pidPath, "utf-8").trim();
2562
3030
  if (!raw) return void 0;
2563
3031
  const parsed = Number(raw);
2564
3032
  if (!Number.isInteger(parsed) || parsed <= 0) return void 0;
2565
3033
  return parsed;
2566
3034
  }
2567
3035
  function readHeartbeat(heartbeatPath) {
2568
- if (!fs7.existsSync(heartbeatPath)) return void 0;
3036
+ if (!fs8.existsSync(heartbeatPath)) return void 0;
2569
3037
  try {
2570
- const parsed = JSON.parse(fs7.readFileSync(heartbeatPath, "utf-8"));
3038
+ const parsed = JSON.parse(fs8.readFileSync(heartbeatPath, "utf-8"));
2571
3039
  if (!parsed || typeof parsed !== "object") return void 0;
2572
3040
  return parsed;
2573
3041
  } catch {
@@ -2575,9 +3043,9 @@ function readHeartbeat(heartbeatPath) {
2575
3043
  }
2576
3044
  }
2577
3045
  function readDaemonMeta(metaPath) {
2578
- if (!fs7.existsSync(metaPath)) return void 0;
3046
+ if (!fs8.existsSync(metaPath)) return void 0;
2579
3047
  try {
2580
- const parsed = JSON.parse(fs7.readFileSync(metaPath, "utf-8"));
3048
+ const parsed = JSON.parse(fs8.readFileSync(metaPath, "utf-8"));
2581
3049
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return void 0;
2582
3050
  return parsed;
2583
3051
  } catch {
@@ -2595,9 +3063,9 @@ function isProcessAlive(pid) {
2595
3063
  }
2596
3064
  function isZombieProcess(pid) {
2597
3065
  const statPath = `/proc/${pid}/stat`;
2598
- if (!fs7.existsSync(statPath)) return false;
3066
+ if (!fs8.existsSync(statPath)) return false;
2599
3067
  try {
2600
- const stat = fs7.readFileSync(statPath, "utf-8");
3068
+ const stat = fs8.readFileSync(statPath, "utf-8");
2601
3069
  const closingIdx = stat.indexOf(")");
2602
3070
  if (closingIdx === -1 || closingIdx + 2 >= stat.length) return false;
2603
3071
  const state = stat.slice(closingIdx + 2, closingIdx + 3);
@@ -2606,10 +3074,10 @@ function isZombieProcess(pid) {
2606
3074
  return false;
2607
3075
  }
2608
3076
  }
2609
- function resolvePathWithinWorkspace3(workspacePath, filePath) {
2610
- const base = path9.resolve(workspacePath);
2611
- const resolved = path9.resolve(base, filePath);
2612
- if (!resolved.startsWith(base + path9.sep) && resolved !== base) {
3077
+ function resolvePathWithinWorkspace4(workspacePath, filePath) {
3078
+ const base = path10.resolve(workspacePath);
3079
+ const resolved = path10.resolve(base, filePath);
3080
+ if (!resolved.startsWith(base + path10.sep) && resolved !== base) {
2613
3081
  throw new Error(`Invalid path outside workspace: ${filePath}`);
2614
3082
  }
2615
3083
  return resolved;
@@ -2624,6 +3092,7 @@ export {
2624
3092
  workspace_exports,
2625
3093
  command_center_exports,
2626
3094
  skill_exports,
3095
+ lens_exports,
2627
3096
  board_exports,
2628
3097
  agent_exports,
2629
3098
  onboard_exports,
package/dist/cli.js CHANGED
@@ -7,12 +7,13 @@ import {
7
7
  command_center_exports,
8
8
  diagnostics_exports,
9
9
  integration_exports,
10
+ lens_exports,
10
11
  onboard_exports,
11
12
  search_qmd_adapter_exports,
12
13
  skill_exports,
13
14
  trigger_exports,
14
15
  workspace_exports
15
- } from "./chunk-OJ6KOGB2.js";
16
+ } from "./chunk-TOFSHG5S.js";
16
17
  import {
17
18
  autonomy_exports,
18
19
  dispatch_exports,
@@ -28,7 +29,7 @@ import {
28
29
  thread_audit_exports,
29
30
  thread_exports,
30
31
  trigger_engine_exports
31
- } from "./chunk-R2MLGBHB.js";
32
+ } from "./chunk-OTSECVE5.js";
32
33
 
33
34
  // src/cli.ts
34
35
  import fs from "fs";
@@ -1053,6 +1054,57 @@ addWorkspaceOption(
1053
1054
  (result) => [`Captured intake: ${result.intake.path}`]
1054
1055
  )
1055
1056
  );
1057
+ var lensCmd = program.command("lens").description("Generate deterministic context lenses for situational awareness");
1058
+ addWorkspaceOption(
1059
+ lensCmd.command("list").description("List built-in context lenses").option("--json", "Emit structured JSON output")
1060
+ ).action(
1061
+ (opts) => runCommand(
1062
+ opts,
1063
+ () => ({
1064
+ lenses: lens_exports.listContextLenses()
1065
+ }),
1066
+ (result) => result.lenses.map((lens) => `lens://${lens.id} - ${lens.description}`)
1067
+ )
1068
+ );
1069
+ addWorkspaceOption(
1070
+ lensCmd.command("show <lensId>").description("Generate one context lens snapshot").option("-a, --actor <name>", "Actor identity for actor-scoped lenses", DEFAULT_ACTOR).option("--lookback-hours <hours>", "Lookback window in hours", "24").option("--stale-hours <hours>", "Stale threshold in hours", "24").option("--limit <n>", "Maximum items per section", "10").option("-o, --output <path>", "Write lens markdown to workspace-relative output path").option("--json", "Emit structured JSON output")
1071
+ ).action(
1072
+ (lensId, opts) => runCommand(
1073
+ opts,
1074
+ () => {
1075
+ const workspacePath = resolveWorkspacePath(opts);
1076
+ const lensOptions = {
1077
+ actor: opts.actor,
1078
+ lookbackHours: parsePositiveNumberOption(opts.lookbackHours, "lookback-hours"),
1079
+ staleHours: parsePositiveNumberOption(opts.staleHours, "stale-hours"),
1080
+ limit: parsePositiveIntegerOption(opts.limit, "limit")
1081
+ };
1082
+ if (opts.output) {
1083
+ return lens_exports.materializeContextLens(workspacePath, lensId, {
1084
+ ...lensOptions,
1085
+ outputPath: opts.output
1086
+ });
1087
+ }
1088
+ return lens_exports.generateContextLens(workspacePath, lensId, lensOptions);
1089
+ },
1090
+ (result) => {
1091
+ const metricSummary = Object.entries(result.metrics).map(([metric, value]) => `${metric}=${value}`).join(" ");
1092
+ const sectionSummary = result.sections.map((section) => `${section.id}:${section.items.length}`).join(" ");
1093
+ const lines = [
1094
+ `Lens: ${result.lens}`,
1095
+ `Generated: ${result.generatedAt}`,
1096
+ ...result.actor ? [`Actor: ${result.actor}`] : [],
1097
+ `Metrics: ${metricSummary || "none"}`,
1098
+ `Sections: ${sectionSummary || "none"}`
1099
+ ];
1100
+ if (isMaterializedLensResult(result)) {
1101
+ lines.push(`Saved markdown: ${result.outputPath}`);
1102
+ return lines;
1103
+ }
1104
+ return [...lines, "", ...result.markdown.split("\n")];
1105
+ }
1106
+ )
1107
+ );
1056
1108
  addWorkspaceOption(
1057
1109
  program.command("query").description("Query primitive instances with multi-field filters").option("--type <type>", "Primitive type").option("--status <status>", "Status value").option("--owner <owner>", "Owner/actor value").option("--tag <tag>", "Tag filter").option("--text <text>", "Full-text contains filter").option("--path-includes <text>", "Path substring filter").option("--updated-after <iso>", "Updated at or after").option("--updated-before <iso>", "Updated at or before").option("--created-after <iso>", "Created at or after").option("--created-before <iso>", "Created at or before").option("--limit <n>", "Result limit").option("--offset <n>", "Result offset").option("--json", "Emit structured JSON output")
1058
1110
  ).action(
@@ -1952,6 +2004,13 @@ function parsePositiveIntOption(value, name) {
1952
2004
  }
1953
2005
  return parsed;
1954
2006
  }
2007
+ function parsePositiveNumberOption(value, optionName) {
2008
+ const parsed = Number(value);
2009
+ if (!Number.isFinite(parsed) || parsed <= 0) {
2010
+ throw new Error(`Invalid --${optionName}. Expected a positive number.`);
2011
+ }
2012
+ return parsed;
2013
+ }
1955
2014
  function parseNonNegativeIntOption(value, name) {
1956
2015
  const parsed = Number.parseInt(String(value ?? ""), 10);
1957
2016
  if (!Number.isFinite(parsed) || parsed < 0) {
@@ -1959,6 +2018,16 @@ function parseNonNegativeIntOption(value, name) {
1959
2018
  }
1960
2019
  return parsed;
1961
2020
  }
2021
+ function parsePositiveIntegerOption(value, optionName) {
2022
+ const parsed = Number.parseInt(String(value), 10);
2023
+ if (!Number.isInteger(parsed) || parsed <= 0) {
2024
+ throw new Error(`Invalid --${optionName}. Expected a positive integer.`);
2025
+ }
2026
+ return parsed;
2027
+ }
2028
+ function isMaterializedLensResult(value) {
2029
+ return typeof value.outputPath === "string";
2030
+ }
1962
2031
  function normalizeRunStatus(status) {
1963
2032
  const normalized = String(status).toLowerCase();
1964
2033
  if (normalized === "running" || normalized === "succeeded" || normalized === "failed" || normalized === "cancelled") {
package/dist/index.d.ts CHANGED
@@ -132,6 +132,51 @@ interface WorkgraphBrief {
132
132
  nextReadyThreads: PrimitiveInstance[];
133
133
  recentActivity: LedgerEntry[];
134
134
  }
135
+ type WorkgraphLensId = 'my-work' | 'team-risk' | 'customer-health' | 'exec-brief';
136
+ interface WorkgraphLensDescriptor {
137
+ id: WorkgraphLensId;
138
+ description: string;
139
+ }
140
+ interface WorkgraphLensOptions {
141
+ actor?: string;
142
+ lookbackHours?: number;
143
+ staleHours?: number;
144
+ limit?: number;
145
+ }
146
+ interface WorkgraphLensItem {
147
+ title: string;
148
+ path?: string;
149
+ status?: string;
150
+ priority?: string;
151
+ owner?: string;
152
+ detail?: string;
153
+ ageHours?: number;
154
+ }
155
+ interface WorkgraphLensSection {
156
+ id: string;
157
+ title: string;
158
+ items: WorkgraphLensItem[];
159
+ }
160
+ interface WorkgraphLensResult {
161
+ lens: WorkgraphLensId;
162
+ generatedAt: string;
163
+ actor?: string;
164
+ options: {
165
+ lookbackHours: number;
166
+ staleHours: number;
167
+ limit: number;
168
+ };
169
+ metrics: Record<string, number>;
170
+ sections: WorkgraphLensSection[];
171
+ markdown: string;
172
+ }
173
+ interface WorkgraphMaterializeLensOptions extends WorkgraphLensOptions {
174
+ outputPath: string;
175
+ }
176
+ interface WorkgraphMaterializedLensResult extends WorkgraphLensResult {
177
+ outputPath: string;
178
+ created: boolean;
179
+ }
135
180
  interface PolicyParty {
136
181
  id: string;
137
182
  roles: string[];
@@ -655,6 +700,21 @@ declare namespace orientation {
655
700
  export { orientation_brief as brief, orientation_checkpoint as checkpoint, orientation_intake as intake, orientation_statusSnapshot as statusSnapshot };
656
701
  }
657
702
 
703
+ /**
704
+ * Deterministic context lenses for fast, runtime-agnostic orientation.
705
+ */
706
+
707
+ declare function listContextLenses(): WorkgraphLensDescriptor[];
708
+ declare function generateContextLens(workspacePath: string, lensId: WorkgraphLensId | string, options?: WorkgraphLensOptions): WorkgraphLensResult;
709
+ declare function materializeContextLens(workspacePath: string, lensId: WorkgraphLensId | string, options: WorkgraphMaterializeLensOptions): WorkgraphMaterializedLensResult;
710
+
711
+ declare const lens_generateContextLens: typeof generateContextLens;
712
+ declare const lens_listContextLenses: typeof listContextLenses;
713
+ declare const lens_materializeContextLens: typeof materializeContextLens;
714
+ declare namespace lens {
715
+ export { lens_generateContextLens as generateContextLens, lens_listContextLenses as listContextLenses, lens_materializeContextLens as materializeContextLens };
716
+ }
717
+
658
718
  /**
659
719
  * Wiki-link graph indexing and graph-intelligence queries.
660
720
  */
@@ -1720,4 +1780,4 @@ declare class CursorCloudAdapter implements DispatchAdapter {
1720
1780
  execute(input: DispatchAdapterExecutionInput): Promise<DispatchAdapterExecutionResult>;
1721
1781
  }
1722
1782
 
1723
- export { type CronField, type CronSchedule, CursorCloudAdapter, type DispatchAdapter, type DispatchAdapterCreateInput, type DispatchAdapterExecutionInput, type DispatchAdapterExecutionResult, type DispatchAdapterLogEntry, type DispatchAdapterRunStatus, type DispatchRun, type FieldDefinition, type InstallSkillIntegrationOptions, type InstallSkillIntegrationResult, type LedgerChainState, type LedgerEntry, type LedgerIndex, type LedgerOp, type PolicyParty, type PolicyRegistry, type PrimitiveInstance, type PrimitiveQueryFilters, type PrimitiveTypeDefinition, type Registry, type RunStatus, type SkillIntegrationProvider, THREAD_STATUS_TRANSITIONS, type ThreadStatus, type WorkgraphBrief, type WorkgraphStatusSnapshot, type WorkgraphWorkspaceConfig, agent, autonomy, autonomyDaemon, bases, board, claimLease, clawdapus, commandCenter, index as diagnostics, dispatch, fetchSkillMarkdownFromUrl, gate, graph, installSkillIntegration, integration, ledger, matchesCronSchedule, nextCronMatch, onboard, orientation, parseCronExpression, policy, query, registry, searchQmdAdapter, skill, store, thread, threadAudit, trigger, triggerEngine, workspace };
1783
+ export { type CronField, type CronSchedule, CursorCloudAdapter, type DispatchAdapter, type DispatchAdapterCreateInput, type DispatchAdapterExecutionInput, type DispatchAdapterExecutionResult, type DispatchAdapterLogEntry, type DispatchAdapterRunStatus, type DispatchRun, type FieldDefinition, type InstallSkillIntegrationOptions, type InstallSkillIntegrationResult, type LedgerChainState, type LedgerEntry, type LedgerIndex, type LedgerOp, type PolicyParty, type PolicyRegistry, type PrimitiveInstance, type PrimitiveQueryFilters, type PrimitiveTypeDefinition, type Registry, type RunStatus, type SkillIntegrationProvider, THREAD_STATUS_TRANSITIONS, type ThreadStatus, type WorkgraphBrief, type WorkgraphLensDescriptor, type WorkgraphLensId, type WorkgraphLensItem, type WorkgraphLensOptions, type WorkgraphLensResult, type WorkgraphLensSection, type WorkgraphMaterializeLensOptions, type WorkgraphMaterializedLensResult, type WorkgraphStatusSnapshot, type WorkgraphWorkspaceConfig, agent, autonomy, autonomyDaemon, bases, board, claimLease, clawdapus, commandCenter, index as diagnostics, dispatch, fetchSkillMarkdownFromUrl, gate, graph, installSkillIntegration, integration, ledger, lens, matchesCronSchedule, nextCronMatch, onboard, orientation, parseCronExpression, policy, query, registry, searchQmdAdapter, skill, store, thread, threadAudit, trigger, triggerEngine, workspace };
package/dist/index.js CHANGED
@@ -9,12 +9,13 @@ import {
9
9
  fetchSkillMarkdownFromUrl,
10
10
  installSkillIntegration,
11
11
  integration_exports,
12
+ lens_exports,
12
13
  onboard_exports,
13
14
  search_qmd_adapter_exports,
14
15
  skill_exports,
15
16
  trigger_exports,
16
17
  workspace_exports
17
- } from "./chunk-OJ6KOGB2.js";
18
+ } from "./chunk-TOFSHG5S.js";
18
19
  import {
19
20
  CursorCloudAdapter,
20
21
  THREAD_STATUS_TRANSITIONS,
@@ -36,7 +37,7 @@ import {
36
37
  thread_audit_exports,
37
38
  thread_exports,
38
39
  trigger_engine_exports
39
- } from "./chunk-R2MLGBHB.js";
40
+ } from "./chunk-OTSECVE5.js";
40
41
  export {
41
42
  CursorCloudAdapter,
42
43
  THREAD_STATUS_TRANSITIONS,
@@ -56,6 +57,7 @@ export {
56
57
  installSkillIntegration,
57
58
  integration_exports as integration,
58
59
  ledger_exports as ledger,
60
+ lens_exports as lens,
59
61
  matchesCronSchedule,
60
62
  mcp_server_exports as mcpServer,
61
63
  nextCronMatch,
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createWorkgraphMcpServer,
3
3
  startWorkgraphMcpServer
4
- } from "./chunk-R2MLGBHB.js";
4
+ } from "./chunk-OTSECVE5.js";
5
5
  export {
6
6
  createWorkgraphMcpServer,
7
7
  startWorkgraphMcpServer
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@versatly/workgraph",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Agent-first workgraph workspace for multi-agent coordination with dynamic primitives, append-only ledger, and markdown-native storage.",
5
5
  "workspaces": [
6
6
  "packages/*"