@aiwg/cli 2026.10.10 → 2026.10.12

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.
Files changed (156) hide show
  1. package/agentic/code/providers/model-capabilities.v1.json +78 -14
  2. package/agentic/code/providers/model-catalog.v1.json +62 -12
  3. package/dist/src/artifacts/binding-target-identity.js +51 -0
  4. package/dist/src/artifacts/checksum-manifest.js +12 -0
  5. package/dist/src/artifacts/index-builder.js +11 -13
  6. package/dist/src/artifacts/query-engine.js +5 -11
  7. package/dist/src/cli/handlers/doctor-help.js +32 -0
  8. package/dist/src/cli/handlers/gates.js +19 -18
  9. package/dist/src/cli/handlers/models-help.js +7 -1
  10. package/dist/src/cli/handlers/models.js +3 -3
  11. package/dist/src/cli/handlers/script-runner.js +8 -5
  12. package/dist/src/cli/handlers/sessions-intake.js +104 -0
  13. package/dist/src/cli/handlers/sessions-tail.js +172 -0
  14. package/dist/src/cli/handlers/sessions.js +287 -31
  15. package/dist/src/cli/handlers/skill-lint.js +10 -3
  16. package/dist/src/cli/handlers/steward-format.js +100 -0
  17. package/dist/src/cli/handlers/steward-models.js +167 -0
  18. package/dist/src/cli/handlers/steward.js +14 -248
  19. package/dist/src/cli/handlers/use-bundle-counts.js +61 -0
  20. package/dist/src/cli/handlers/use.js +18 -28
  21. package/dist/src/cli/handlers/utilities.js +1 -24
  22. package/dist/src/config/aiwg-config.js +7 -0
  23. package/dist/src/config/authorization-cli.js +89 -0
  24. package/dist/src/config/cli-help.js +1 -0
  25. package/dist/src/config/cli.js +10 -1
  26. package/dist/src/config/worker-context.js +337 -0
  27. package/dist/src/decision/context-pruning.js +3 -2
  28. package/dist/src/decision/ensemble/contract.js +15 -4
  29. package/dist/src/decision/ensemble-study/artifacts.js +5 -0
  30. package/dist/src/decision/ensemble-study/score.js +4 -3
  31. package/dist/src/decision/feature-export.js +3 -9
  32. package/dist/src/decision/heldout/collector.js +2 -2
  33. package/dist/src/decision/issue-triage/runtime.js +3 -9
  34. package/dist/src/decision/qualification/gates.js +2 -1
  35. package/dist/src/decision/qualification/release.js +2 -3
  36. package/dist/src/decision/routing/report.js +2 -0
  37. package/dist/src/decision/sdlc-screening.js +3 -5
  38. package/dist/src/decision/sensitivity/comparative.js +5 -3
  39. package/dist/src/gates/driver.js +6 -77
  40. package/dist/src/gates/evaluate.js +4 -11
  41. package/dist/src/gates/floors.js +4 -4
  42. package/dist/src/gates/index.js +3 -3
  43. package/dist/src/gates/lattice.js +26 -0
  44. package/dist/src/gates/registry.js +2 -1
  45. package/dist/src/gates/report.js +19 -7
  46. package/dist/src/issues/audit-cli.js +120 -0
  47. package/dist/src/issues/audit-contract.js +64 -0
  48. package/dist/src/issues/audit-local.js +82 -0
  49. package/dist/src/issues/audit.js +220 -0
  50. package/dist/src/lint/rule-activation.js +15 -1
  51. package/dist/src/mcp/tools/worker-invoke.mjs +108 -10
  52. package/dist/src/mission-protocol/canonical-codecs.js +270 -0
  53. package/dist/src/mission-protocol/codecs.js +22 -550
  54. package/dist/src/mission-protocol/health-trace-codecs.js +281 -0
  55. package/dist/src/mission-protocol/health-trace-types.js +43 -0
  56. package/dist/src/mission-protocol/types.js +2 -42
  57. package/dist/src/mission-protocol/worker-contract-json.js +10 -0
  58. package/dist/src/mission-protocol/worker-review-gate.js +100 -0
  59. package/dist/src/mission-protocol/worker-review-types.js +2 -0
  60. package/dist/src/models/catalog-policy.js +280 -0
  61. package/dist/src/models/index.js +1 -1
  62. package/dist/src/models/model-capabilities.v1.json +78 -14
  63. package/dist/src/models/model-catalog.v1.json +62 -12
  64. package/dist/src/models/policy-resources.js +28 -0
  65. package/dist/src/models/provider-policy.js +10 -539
  66. package/dist/src/models/resolver.js +26 -8
  67. package/dist/src/models/router.js +15 -2
  68. package/dist/src/models/tiers.js +13 -0
  69. package/dist/src/models/types.js +0 -6
  70. package/dist/src/models/worker-ceilings.js +59 -0
  71. package/dist/src/models/worker-invocation-policy.js +243 -0
  72. package/dist/src/models/worker-plan.js +16 -20
  73. package/dist/src/models/worker-review-lineage.js +55 -0
  74. package/dist/src/models/worker-selection-policy.js +1 -1
  75. package/dist/src/models/wrapper-route.js +6 -3
  76. package/dist/src/policy/authorization.js +30 -1
  77. package/dist/src/policy/hitl-config.js +63 -0
  78. package/dist/src/policy/hitl-receipt.js +57 -0
  79. package/dist/src/policy/hitl-types.js +4 -0
  80. package/dist/src/policy/hitl-validation.js +39 -0
  81. package/dist/src/policy/hitl.js +117 -0
  82. package/dist/src/process/aiwg-probe-command.d.mts +15 -0
  83. package/dist/src/process/aiwg-probe-command.mjs +37 -0
  84. package/dist/src/rules/activation.d.mts +1 -1
  85. package/dist/src/rules/activation.mjs +0 -2
  86. package/dist/src/rules/gate-coverage.js +51 -0
  87. package/dist/src/rules/gate-references.js +16 -0
  88. package/dist/src/rules/rule-frontmatter.schema.json +7 -1
  89. package/dist/src/serve/worker-command-request.js +21 -4
  90. package/dist/src/serve/worker-invocation-preparation.js +3 -1
  91. package/dist/src/serve/worker-invocation.js +21 -10
  92. package/dist/src/sessions/adapters/deepseek-harness.js +1 -0
  93. package/dist/src/sessions/adapters/grok-build-native.js +269 -0
  94. package/dist/src/sessions/adapters/grok-build.js +57 -15
  95. package/dist/src/sessions/adapters/grokbot.js +7 -0
  96. package/dist/src/sessions/adapters/hermes-state-db.js +252 -0
  97. package/dist/src/sessions/adapters/hermes.js +42 -3
  98. package/dist/src/sessions/adapters/warp.js +1 -1
  99. package/dist/src/sessions/batch-import.js +44 -6
  100. package/dist/src/sessions/contracts.js +20 -1
  101. package/dist/src/sessions/coverage-preflight.js +7 -1
  102. package/dist/src/sessions/discovery-capability.js +38 -0
  103. package/dist/src/sessions/fortemi-export-mapping.js +33 -3
  104. package/dist/src/sessions/host-catalog.js +157 -0
  105. package/dist/src/sessions/image-blobs.js +141 -0
  106. package/dist/src/sessions/importer.js +9 -232
  107. package/dist/src/sessions/index.js +6 -0
  108. package/dist/src/sessions/intake-dir.js +71 -0
  109. package/dist/src/sessions/normalization.js +251 -0
  110. package/dist/src/sessions/refresh.js +25 -2
  111. package/dist/src/sessions/repository.js +164 -22
  112. package/dist/src/sessions/tail.js +91 -0
  113. package/dist/src/sessions/workspace-discovery.js +88 -9
  114. package/dist/src/skills/agent-skill-sidecar.js +28 -0
  115. package/dist/src/skills/agent-skills.js +4 -23
  116. package/dist/src/skills/claude-surface-projection.js +25 -0
  117. package/dist/src/skills/deployer.js +8 -288
  118. package/dist/src/skills/deployment-policy.js +114 -0
  119. package/dist/src/skills/deployment-projection.js +152 -0
  120. package/dist/src/skills/deployment-types.js +20 -0
  121. package/dist/src/skills/exporter.js +8 -31
  122. package/dist/src/skills/provider-extensions.js +41 -0
  123. package/dist/src/skills/run.js +6 -0
  124. package/dist/src/skills/types.js +0 -8
  125. package/dist/src/skills/validator-resources.js +68 -0
  126. package/dist/src/skills/validator.js +6 -55
  127. package/dist/src/storage/cli-migrate-options.js +125 -0
  128. package/dist/src/storage/cli-migrate.js +111 -0
  129. package/dist/src/storage/cli.js +4 -279
  130. package/dist/src/storage/migration-adapter.js +79 -0
  131. package/dist/src/storage/migration-copy.js +108 -0
  132. package/dist/src/storage/migration-protocol.js +14 -197
  133. package/dist/src/storage/migration-records.js +186 -0
  134. package/dist/src/storage/migration-types.js +12 -0
  135. package/dist/src/tracker/forge-http.js +2 -1
  136. package/dist/src/tracker/issue-pages.js +110 -0
  137. package/package.json +7 -1
  138. package/schemas/mission-v1.schema.json +335 -0
  139. package/schemas/models/model-catalog.v1.schema.json +19 -1
  140. package/schemas/models/model-config-compatibility.v1.schema.json +3 -2
  141. package/schemas/models/model-policy.v1.schema.json +1 -1
  142. package/tools/agents/providers/provider-paths-staging.mjs +35 -0
  143. package/tools/agents/providers/rule-binding-targets.mjs +146 -0
  144. package/tools/agents/providers/rule-delivery.mjs +7 -126
  145. package/tools/ralph-external/lib/muse-adapter.mjs +10 -0
  146. package/tools/ralph-external/lib/worker-context.mjs +237 -0
  147. package/tools/ralph-external/lib/worker-invocation-ancestry.mjs +132 -0
  148. package/tools/ralph-external/lib/worker-invocation-processes.mjs +123 -0
  149. package/tools/ralph-external/lib/worker-isolation-status.mjs +48 -0
  150. package/tools/ralph-external/lib/worker-isolation.mjs +17 -29
  151. package/tools/ralph-external/lib/worker-launch-cli.mjs +3 -1
  152. package/tools/ralph-external/lib/worker-overflow.mjs +48 -13
  153. package/tools/ralph-external/lib/worker-retry-evidence.mjs +103 -0
  154. package/tools/ralph-external/lib/worker-termination.mjs +82 -0
  155. package/tools/ralph-external/session-launcher.mjs +51 -96
  156. package/vscode-extension/schemas/aiwg.config.v1.json +177 -0
@@ -1,4 +1,4 @@
1
- import { existsSync, realpathSync } from 'node:fs';
1
+ import { existsSync, mkdirSync, realpathSync } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { requireFeaturePackage } from '../features/runtime.js';
4
4
  import { readAiwgConfig } from '../config/aiwg-config.js';
@@ -6,6 +6,7 @@ import { createSessionAdapterRegistry } from './adapter-registry.js';
6
6
  import { resolveImportExclusions } from './import-exclusions.js';
7
7
  import { resolveImportLimits } from './import-limits.js';
8
8
  import { acquireImportLease } from './import-lease.js';
9
+ import { defaultHostCatalogPath, manifestNeedsHostCatalog } from './host-catalog.js';
9
10
  import { SessionRepository } from './repository.js';
10
11
  import { importDiscoveryManifest } from './batch-import.js';
11
12
  import { defaultDiscoveryManifestPath, discoverWorkspaceHistories, readDiscoveryManifest, writeDiscoveryManifest, } from './workspace-discovery.js';
@@ -69,8 +70,26 @@ export async function refreshSessionCatalog(workspace, full, waitMs, signal, wor
69
70
  return 'no authorized provider roots';
70
71
  }
71
72
  const lease = await acquireImportLease(database, `index-refresh-${process.pid}`, { waitMs });
73
+ let hostLease;
72
74
  try {
73
75
  const repository = new SessionRepository(database);
76
+ // Account-scoped providers import once into the host catalog; the
77
+ // hourly host refresh (#310) routes them there instead of copying
78
+ // them into every workspace catalog (#3232). The host catalog is only
79
+ // opened when a manifest actually holds an account-scoped source.
80
+ const hostCatalogPath = defaultHostCatalogPath();
81
+ const needsHost = manifests.some((entry) => manifestNeedsHostCatalog(entry.manifest.sources));
82
+ let hostRepository;
83
+ if (needsHost) {
84
+ mkdirSync(path.dirname(hostCatalogPath), { recursive: true, mode: 0o700 });
85
+ // The workspace lease above does not cover the shared host catalog,
86
+ // so overlapping refreshes from two workspaces would stage the same
87
+ // replicas concurrently (#3232). Serialize host writers with the
88
+ // same lock pattern; the shared checkpoint then turns the second
89
+ // writer into duplicate references instead of a double import.
90
+ hostLease = await acquireImportLease(hostCatalogPath, `host-refresh-${process.pid}`, { waitMs });
91
+ hostRepository = new SessionRepository(hostCatalogPath);
92
+ }
74
93
  try {
75
94
  if (full)
76
95
  repository.reindex();
@@ -82,7 +101,9 @@ export async function refreshSessionCatalog(workspace, full, waitMs, signal, wor
82
101
  continue;
83
102
  // Persist the exact discovered batch before its confirmed import.
84
103
  await writeDiscoveryManifest(entry.path, entry.manifest);
85
- const receipt = await importDiscoveryManifest({ manifest: entry.manifest, repository, signal, limits, exclude });
104
+ const receipt = await importDiscoveryManifest({
105
+ manifest: entry.manifest, repository, hostRepository, hostCatalogPath, signal, limits, exclude,
106
+ });
86
107
  if (receipt.totals.pending) {
87
108
  throw new Error(`session import incomplete: ${receipt.totals.rejected} rejected, ${receipt.totals.pending} pending`);
88
109
  }
@@ -94,10 +115,12 @@ export async function refreshSessionCatalog(workspace, full, waitMs, signal, wor
94
115
  }
95
116
  }
96
117
  finally {
118
+ hostRepository?.close();
97
119
  repository.close();
98
120
  }
99
121
  }
100
122
  finally {
123
+ await hostLease?.release();
101
124
  await lease.release();
102
125
  }
103
126
  return undefined;
@@ -2,8 +2,10 @@ import { requireFeaturePackage } from '../features/runtime.js';
2
2
  import { CandidateReviewReceiptSchema, DeletionReceiptSchema, IntelligenceCandidateSchema, PromotionDependencyDecisionSchema, PromotionReceiptSchema, SessionContractError, sha256, } from './contracts.js';
3
3
  import { coverageFromBatchRun, } from './batch-contracts.js';
4
4
  import { redactSessionText, redactSessionValue, sanitizeSessionEvent, sanitizeNativeExtensions } from './policy.js';
5
+ import { extractBatchEventImages, isImageDigest, } from './image-blobs.js';
5
6
  import { effectiveSessionLifecycle } from './lifecycle.js';
6
7
  import { deriveSessionAnalytics, } from './analytics.js';
8
+ import { HOST_REFERENCE_TABLE_DDL, } from './host-catalog.js';
7
9
  const POLICY_PROVIDER_MIGRATION = 'policy-provider-identity:v2';
8
10
  const EVENT_ORIGIN_INTENT_MIGRATION = 'event-origin-intent:v1';
9
11
  const VISIBLE_SESSION_PREDICATE = `s.lifecycle != 'tombstoned' AND EXISTS (
@@ -114,6 +116,17 @@ export class SessionRepository {
114
116
  CREATE TABLE IF NOT EXISTS session_catalog_meta (
115
117
  key TEXT PRIMARY KEY, value TEXT NOT NULL
116
118
  );
119
+ -- Content-addressed tool-output image bytes (#3265). One row per
120
+ -- content hash per catalog: repeat imports of identical images share
121
+ -- the row (INSERT OR IGNORE). Events reference blobs by digest and
122
+ -- keep only a bracketed image placeholder (sha256, type, size) in
123
+ -- searchableText, so image bytes never reach FTS. Pre-#3265 rows with
124
+ -- inline bytes are left untouched (no destructive migration).
125
+ CREATE TABLE IF NOT EXISTS session_image_blobs (
126
+ digest TEXT PRIMARY KEY, media_type TEXT NOT NULL,
127
+ byte_length INTEGER NOT NULL, source_locator TEXT,
128
+ source_offset INTEGER, bytes BLOB NOT NULL
129
+ );
117
130
  CREATE TABLE IF NOT EXISTS session_analytics_facts (
118
131
  fact_id TEXT PRIMARY KEY, workspace_id TEXT NOT NULL,
119
132
  session_id TEXT NOT NULL, event_id TEXT NOT NULL,
@@ -144,6 +157,9 @@ export class SessionRepository {
144
157
  -- alignment without reading searchable text or event JSON.
145
158
  CREATE INDEX IF NOT EXISTS idx_fts_event_id ON session_event_fts_content(c0);
146
159
  `);
160
+ // Workspace pointers at account-scoped sessions stored in the host
161
+ // catalog (#3232). References, never copies; legacy copied rows stay.
162
+ this.db.exec(HOST_REFERENCE_TABLE_DDL);
147
163
  const deletionReceiptColumns = this.db.prepare('PRAGMA table_info(deletion_receipts)').all().map((row) => String(row.name));
148
164
  if (!deletionReceiptColumns.includes('workspace_id')) {
149
165
  this.db.exec('ALTER TABLE deletion_receipts ADD COLUMN workspace_id TEXT');
@@ -479,6 +495,18 @@ export class SessionRepository {
479
495
  };
480
496
  }
481
497
  applyImport(batch, checkpoint, publish = true) {
498
+ // Lift inline image payloads out of event text before redaction (#3265):
499
+ // base64 data URLs in text survive redaction and would otherwise be
500
+ // stored inline. Provider image blocks in extensions keep their existing
501
+ // redaction to markers; their bytes are not retained.
502
+ // Idempotent -- placeholder text from an earlier pass matches no image
503
+ // pattern, so importer-extracted batches and direct writers converge.
504
+ // Bytes are parked beside the batch: the redaction pass below walks
505
+ // every value and would flatten Uint8Array into plain objects.
506
+ const extracted = extractBatchEventImages(batch);
507
+ const imageBlobs = extracted.imageBlobs;
508
+ batch = { ...batch, events: extracted.events };
509
+ delete batch.imageBlobs;
482
510
  // Guard callers that submit normalized batches directly as well as adapters.
483
511
  batch = redactSessionValue({
484
512
  ...batch,
@@ -525,6 +553,17 @@ export class SessionRepository {
525
553
  const merged = mergeSessionAggregate(prior, session);
526
554
  this.db.prepare(`UPDATE sessions SET source_digest=?, lifecycle=?, data=? WHERE session_id=?`).run(merged.sourceDigest, merged.lifecycle, JSON.stringify(merged), merged.sessionId);
527
555
  }
556
+ // Persist image bytes once per content hash per catalog (#3265).
557
+ // INSERT OR IGNORE keeps the first-seen row when identical images
558
+ // arrive in later batches or replayed records.
559
+ const insertImageBlob = this.db.prepare(`INSERT OR IGNORE INTO session_image_blobs
560
+ (digest, media_type, byte_length, source_locator, source_offset, bytes)
561
+ VALUES (?, ?, ?, ?, ?, ?)`);
562
+ for (const image of imageBlobs) {
563
+ if (!isImageDigest(image.digest))
564
+ continue;
565
+ insertImageBlob.run(image.digest, image.mediaType, image.byteLength, image.locatorClass ?? null, image.offset ?? null, Buffer.from(image.bytes));
566
+ }
528
567
  const insertEvent = this.db.prepare(`INSERT OR IGNORE INTO session_events
529
568
  (event_id, session_id, source_id, import_run_id, sequence_no, digest, data)
530
569
  VALUES (?, ?, ?, ?, ?, ?, ?)`);
@@ -746,6 +785,49 @@ export class SessionRepository {
746
785
  listWorkspaceIds() {
747
786
  return this.db.prepare('SELECT DISTINCT workspace_id FROM sessions ORDER BY workspace_id').all().map((row) => String(row.workspace_id));
748
787
  }
788
+ /**
789
+ * Record a workspace pointer at an account-scoped session stored in the
790
+ * host catalog (#3232). Non-destructive: existing copied rows are left
791
+ * untouched and remain readable.
792
+ */
793
+ recordHostSessionReference(reference) {
794
+ this.db.prepare(`INSERT INTO session_host_refs
795
+ (provider, native_session_id, session_id, source_id, host_catalog)
796
+ VALUES (?, ?, ?, ?, ?)
797
+ ON CONFLICT(provider, native_session_id) DO UPDATE SET
798
+ session_id=excluded.session_id, source_id=excluded.source_id,
799
+ host_catalog=excluded.host_catalog`).run(reference.provider, reference.nativeSessionId, reference.sessionId, reference.sourceId, reference.hostCatalog);
800
+ }
801
+ listHostSessionReferences() {
802
+ return this.db.prepare(`SELECT provider, native_session_id, session_id, source_id, host_catalog
803
+ FROM session_host_refs ORDER BY provider, native_session_id`).all().map((row) => ({
804
+ provider: String(row.provider),
805
+ nativeSessionId: String(row.native_session_id),
806
+ sessionId: String(row.session_id),
807
+ sourceId: String(row.source_id),
808
+ hostCatalog: String(row.host_catalog),
809
+ }));
810
+ }
811
+ /**
812
+ * Visible sessions reduced to aggregate identities (plus the internal ids
813
+ * reference recording needs). Aggregate search/analytics over several
814
+ * catalogs dedupe on provider + native session so legacy copied rows
815
+ * count once (#3232).
816
+ */
817
+ listSessionIdentities(workspaceId) {
818
+ const where = workspaceId
819
+ ? `s.workspace_id=? AND ${VISIBLE_SESSION_PREDICATE}`
820
+ : VISIBLE_SESSION_PREDICATE;
821
+ const params = workspaceId ? [workspaceId] : [];
822
+ return this.db.prepare(`SELECT json_extract(s.data, '$.provider') AS provider, s.native_session_id,
823
+ s.session_id, s.source_id
824
+ FROM sessions s WHERE ${where} ORDER BY s.session_id`).all(...params).map((row) => ({
825
+ provider: String(row.provider),
826
+ nativeSessionId: String(row.native_session_id),
827
+ sessionId: String(row.session_id),
828
+ sourceId: String(row.source_id),
829
+ }));
830
+ }
749
831
  saveBatchImportRun(run) {
750
832
  run = redactSessionValue(run).value;
751
833
  const save = this.db.transaction(() => {
@@ -889,6 +971,62 @@ export class SessionRepository {
889
971
  ORDER BY e.sequence_no, e.event_id`).all(...(workspaceId ? [sessionId, workspaceId] : [sessionId]))
890
972
  .map((row) => JSON.parse(String(row.data)));
891
973
  }
974
+ /**
975
+ * Distinct image blobs in this catalog. Identical images share one row
976
+ * per content hash, so this counts unique images, not references (#3265).
977
+ */
978
+ countImageBlobs() {
979
+ return Number(this.db.prepare('SELECT COUNT(*) AS count FROM session_image_blobs').get()?.count ?? 0);
980
+ }
981
+ /**
982
+ * Fetch one image blob by content digest. Returns null when the blob is
983
+ * unknown or the digest is malformed -- callers report that as an
984
+ * "unavailable" reference rather than failing the whole read (#3265).
985
+ * Pre-#3265 rows carry inline bytes instead of references and never reach
986
+ * this lookup; they load verbatim through `listEvents`.
987
+ */
988
+ getImageBlob(digest) {
989
+ if (!isImageDigest(digest))
990
+ return null;
991
+ const row = this.db.prepare(`SELECT digest, media_type, byte_length, source_locator, source_offset, bytes
992
+ FROM session_image_blobs WHERE digest=?`).get(digest);
993
+ if (!row)
994
+ return null;
995
+ return {
996
+ digest: String(row.digest),
997
+ mediaType: String(row.media_type),
998
+ byteLength: Number(row.byte_length),
999
+ sourceLocator: row.source_locator === null ? null : String(row.source_locator),
1000
+ sourceOffset: row.source_offset === null ? null : Number(row.source_offset),
1001
+ bytes: Uint8Array.from(row.bytes),
1002
+ };
1003
+ }
1004
+ /**
1005
+ * Resolve an event's image references against the blob store. Entries
1006
+ * whose bytes are present resolve `available`; entries whose blob is
1007
+ * missing (pruned catalog, legacy reference) resolve `unavailable` with
1008
+ * the reference metadata intact so callers can report it (#3265). Events
1009
+ * without references -- including pre-#3265 inline rows -- resolve to [].
1010
+ */
1011
+ resolveEventImages(event) {
1012
+ return (event.imageRefs ?? []).map((ref) => {
1013
+ const stored = this.getImageBlob(ref.digest);
1014
+ if (!stored)
1015
+ return { ...ref, status: 'unavailable' };
1016
+ return {
1017
+ digest: ref.digest,
1018
+ mediaType: stored.mediaType,
1019
+ byteLength: stored.byteLength,
1020
+ ...(ref.locatorClass !== undefined
1021
+ ? { locatorClass: ref.locatorClass }
1022
+ : stored.sourceLocator !== null ? { locatorClass: stored.sourceLocator } : {}),
1023
+ ...(ref.offset !== undefined
1024
+ ? { offset: ref.offset }
1025
+ : stored.sourceOffset !== null ? { offset: stored.sourceOffset } : {}),
1026
+ status: 'available',
1027
+ };
1028
+ });
1029
+ }
892
1030
  listTimelineInputs(workspaceId) {
893
1031
  return this.db.prepare(`SELECT s.data AS session_data, e.data AS event_data
894
1032
  FROM session_events e
@@ -1168,28 +1306,7 @@ export class SessionRepository {
1168
1306
  }
1169
1307
  analyticsSummary(options) {
1170
1308
  const facts = this.listAnalyticsFacts({ ...options, limit: options.limit ?? 5_000 });
1171
- const byCategory = countBy(facts, (fact) => fact.category);
1172
- const byStatus = countBy(facts, (fact) => fact.status);
1173
- const byProvider = countBy(facts, (fact) => fact.provider);
1174
- const byTool = countBy(facts.filter((fact) => fact.toolName), (fact) => fact.toolName);
1175
- return {
1176
- analyticsVersion: '1.0.0',
1177
- workspaceId: options.workspaceId,
1178
- totals: {
1179
- facts: facts.length,
1180
- sessions: new Set(facts.map((fact) => fact.sessionId)).size,
1181
- toolCalls: facts.filter((fact) => fact.category === 'tool-call').length,
1182
- toolFailures: facts.filter((fact) => fact.category === 'tool-result' && fact.status === 'failed').length,
1183
- escalations: facts.filter((fact) => fact.category === 'escalation').length,
1184
- hitlDecisions: facts.filter((fact) => fact.category === 'hitl').length,
1185
- indicators: facts.filter((fact) => fact.category === 'indicator').length,
1186
- retryGroups: new Set(facts.map((fact) => fact.retryGroupId).filter(Boolean)).size,
1187
- },
1188
- byCategory,
1189
- byStatus,
1190
- byProvider,
1191
- byTool,
1192
- };
1309
+ return summarizeSessionAnalyticsFacts(facts, options.workspaceId);
1193
1310
  }
1194
1311
  getAnalyticsEvidence(id, workspaceId) {
1195
1312
  const row = this.db.prepare(`SELECT f.data AS fact_data, e.data AS event_data
@@ -1852,6 +1969,31 @@ function canonicalDevinLocator(value) {
1852
1969
  ? `devin-desktop-${value.slice('windsurf-'.length)}`
1853
1970
  : value;
1854
1971
  }
1972
+ /**
1973
+ * Summarize analytics facts without touching a catalog, so cross-catalog
1974
+ * merges (workspace facts plus referenced host facts, #3232) report the
1975
+ * same shape as a single-catalog summary.
1976
+ */
1977
+ export function summarizeSessionAnalyticsFacts(facts, workspaceId) {
1978
+ return {
1979
+ analyticsVersion: '1.0.0',
1980
+ workspaceId,
1981
+ totals: {
1982
+ facts: facts.length,
1983
+ sessions: new Set(facts.map((fact) => fact.sessionId)).size,
1984
+ toolCalls: facts.filter((fact) => fact.category === 'tool-call').length,
1985
+ toolFailures: facts.filter((fact) => fact.category === 'tool-result' && fact.status === 'failed').length,
1986
+ escalations: facts.filter((fact) => fact.category === 'escalation').length,
1987
+ hitlDecisions: facts.filter((fact) => fact.category === 'hitl').length,
1988
+ indicators: facts.filter((fact) => fact.category === 'indicator').length,
1989
+ retryGroups: new Set(facts.map((fact) => fact.retryGroupId).filter(Boolean)).size,
1990
+ },
1991
+ byCategory: countBy(facts, (fact) => fact.category),
1992
+ byStatus: countBy(facts, (fact) => fact.status),
1993
+ byProvider: countBy(facts, (fact) => fact.provider),
1994
+ byTool: countBy(facts.filter((fact) => fact.toolName), (fact) => fact.toolName),
1995
+ };
1996
+ }
1855
1997
  function countBy(items, key) {
1856
1998
  const counts = {};
1857
1999
  for (const item of items) {
@@ -0,0 +1,91 @@
1
+ // Follow a native source through the importer's normalization and checkpoint rules.
2
+ import { setTimeout } from 'node:timers/promises';
3
+ import { SessionContractError, assertSessionResourceLimit, assertSupportedSchemaMajor, sha256 } from './contracts.js';
4
+ import { IncrementalSessionImporter } from './importer.js';
5
+ import { normalizeSessionBatch } from './normalization.js';
6
+ import { assertImportByteLimit, DEFAULT_IMPORT_BYTE_LIMITS } from './import-limits.js';
7
+ export function assertTailSupported(adapter) {
8
+ if (typeof adapter.stream !== 'function' || !adapter.supportedOperations.includes('stream')) {
9
+ throw new SessionContractError('UNSUPPORTED_PROVIDER', `sessions tail is unsupported for provider ${adapter.provider}: adapter has no stream operation`);
10
+ }
11
+ }
12
+ export async function tailSession(options) {
13
+ assertTailSupported(options.adapter);
14
+ assertSupportedSchemaMajor(options.source.sourceSchemaVersion, options.adapter.sourceSchemaMajor ?? 1);
15
+ if (options.workspaceId !== options.selectedSource.authorizedScope.workspaceId) {
16
+ throw new SessionContractError('SOURCE_NOT_AUTHORIZED', 'tail workspace is outside the authorized source scope');
17
+ }
18
+ const pollMs = options.pollMs ?? 500;
19
+ if (!Number.isInteger(pollMs) || pollMs < 1 || pollMs > 500) {
20
+ throw new SessionContractError('INVALID_ARGUMENT', 'tail poll interval must be between 1 and 500 ms');
21
+ }
22
+ let checkpoint = options.repository.getCheckpoint(options.source.sourceId, options.adapter.adapterVersion);
23
+ const port = {
24
+ getCheckpoint: () => checkpoint,
25
+ getBatchCheckpoint: () => null,
26
+ commitStagedImports: () => 0,
27
+ applyImport: (batch, next) => {
28
+ // Publish through the existing import transaction only with explicit opt-in.
29
+ const receipt = options.repository.applyImport(batch, next, true);
30
+ checkpoint = { ...next };
31
+ for (const event of batch.events) {
32
+ if (event.sessionId === options.sessionId)
33
+ options.onEvent(event);
34
+ }
35
+ return receipt;
36
+ },
37
+ };
38
+ const importer = new IncrementalSessionImporter(port);
39
+ while (!options.signal?.aborted) {
40
+ try {
41
+ if (options.import) {
42
+ await importer.import({
43
+ ...options, publish: true, batchRunId: undefined,
44
+ limits: { ...options.limits, batchSize: 1 },
45
+ });
46
+ }
47
+ else {
48
+ // Do not fingerprint or persist snapshots: export adapters may rewrite
49
+ // a JSON document as it grows. Each adapter owns its resume semantics.
50
+ for await (const record of options.adapter.stream(options.selectedSource, checkpoint ? { value: checkpoint.cursor } : undefined)) {
51
+ if (options.signal?.aborted)
52
+ return;
53
+ const serialized = JSON.stringify(record);
54
+ const limits = { ...DEFAULT_IMPORT_BYTE_LIMITS, ...options.limits };
55
+ assertImportByteLimit('maxRecordBytes', limits, Buffer.byteLength(serialized));
56
+ const count = (checkpoint?.recordsRead ?? 0) + 1;
57
+ assertSessionResourceLimit('maxRecords', options.limits?.maxRecords ?? 1_000_000, count);
58
+ const digest = sha256(serialized);
59
+ const runId = sha256([options.source.sourceId, options.adapter.adapterVersion,
60
+ count - 1, count, digest].join('\0'));
61
+ const { events } = normalizeSessionBatch(options, [record], [digest], runId);
62
+ checkpoint = { ...checkpoint, cursor: record.sourceCursor ?? String(count),
63
+ recordsRead: count, bytesRead: (checkpoint?.bytesRead ?? 0) + Buffer.byteLength(serialized) };
64
+ for (const event of events) {
65
+ if (event.sessionId === options.sessionId)
66
+ options.onEvent(event);
67
+ }
68
+ }
69
+ }
70
+ }
71
+ catch (error) {
72
+ if (options.signal?.aborted)
73
+ return;
74
+ // Some native JSONL adapters report an in-progress final line explicitly.
75
+ if (!(options.follow && error instanceof SessionContractError
76
+ && error.code === 'TRUNCATED_SOURCE'))
77
+ throw error;
78
+ }
79
+ await options.onPoll?.();
80
+ if (!options.follow || options.signal?.aborted)
81
+ return;
82
+ try {
83
+ await setTimeout(pollMs, undefined, { signal: options.signal });
84
+ }
85
+ catch (error) {
86
+ if (!options.signal?.aborted)
87
+ throw error;
88
+ }
89
+ }
90
+ }
91
+ //# sourceMappingURL=tail.js.map
@@ -4,6 +4,8 @@ import { basename, dirname, join, resolve } from 'node:path';
4
4
  import { inspectMuseNativeWorkspace } from './adapters/muse.js';
5
5
  import { readOmpSessionHeader } from './adapters/omp.js';
6
6
  import { grokbotConversationIdFromLocator } from './adapters/grokbot-locator.js';
7
+ import { GROK_BUILD_ACTIVE_FRESHNESS_MS, grokBuildLiveState, grokBuildSourceWorkspaceMatch, readGrokBuildActiveRegistry, } from './adapters/grok-build-native.js';
8
+ import { hermesSnapshotStagingDir } from './adapters/hermes-state-db.js';
7
9
  import { resolveOmpPaths } from '../providers/omp-paths.mjs';
8
10
  import { readAiwgConfig } from '../config/aiwg-config.js';
9
11
  import { SessionCoveragePreflightSchema, SessionContractError, sha256, } from './contracts.js';
@@ -15,7 +17,7 @@ import { matchSessionWorkspace } from './workspace-matching.js';
15
17
  import { matchImportExclusion, nativeSessionIdFromLocator, resolveImportExclusions } from './import-exclusions.js';
16
18
  export const DISCOVERY_MANIFEST_VERSION = '1.0.0';
17
19
  const MANUAL_EXPORT_PROVIDERS = new Set([
18
- 'copilot', 'hermes', 'opencode', 'openclaw', 'openhuman', 'grok-build',
20
+ 'copilot', 'opencode', 'openclaw', 'openhuman',
19
21
  'warp', 'devin-desktop', 'generic',
20
22
  ]);
21
23
  export async function discoverWorkspaceHistories(options) {
@@ -31,6 +33,8 @@ export async function discoverWorkspaceHistories(options) {
31
33
  ...(options.museRoot ? ['muse'] : []),
32
34
  ...(options.dshRoot ? ['deepseek-harness'] : []),
33
35
  ...(options.grokbotRoot ? ['grokbot'] : []),
36
+ ...(options.grokRoot ? ['grok-build'] : []),
37
+ ...(options.hermesRoot ? ['hermes'] : []),
34
38
  ];
35
39
  const savedRoots = options.authorizedRoots ?? options.refreshFrom?.authorizedRoots;
36
40
  const authorizedProviders = options.refreshFrom
@@ -68,6 +72,15 @@ export async function discoverWorkspaceHistories(options) {
68
72
  // no-filtering behavior already used for `pi`/`deepseek-harness`).
69
73
  { provider: 'grokbot', roots: options.grokbotRoot
70
74
  ? [resolve(options.grokbotRoot)] : [] },
75
+ // Grok Build authorizes the sessions directory itself, never Grok home:
76
+ // `<root>/<url-encoded cwd>/<id>/chat_history.jsonl`. No fallback through
77
+ // $HOME, GROK_HOME, or --provider-home grants access.
78
+ { provider: 'grok-build', roots: options.grokRoot
79
+ ? [resolve(options.grokRoot)] : [] },
80
+ // Hermes authorizes `<root>/state.db` for a read-only backup snapshot.
81
+ // Staging joins the scope below; nothing else under the root is read.
82
+ { provider: 'hermes', roots: options.hermesRoot
83
+ ? [resolve(options.hermesRoot)] : [] },
71
84
  {
72
85
  provider: 'claude',
73
86
  // Same scope rule as codex: configured providers keep their root grant
@@ -155,14 +168,17 @@ export async function discoverWorkspaceHistories(options) {
155
168
  }
156
169
  }
157
170
  if (availableRoots.length === 0) {
158
- const codexNeedsAuthorization = (entry.provider === 'codex' || entry.provider === 'omp'
171
+ const needsExplicitRoot = (entry.provider === 'codex' || entry.provider === 'omp'
159
172
  || entry.provider === 'deepseek-harness' || entry.provider === 'muse'
160
- || entry.provider === 'grokbot')
173
+ || entry.provider === 'grokbot' || entry.provider === 'grok-build'
174
+ || entry.provider === 'hermes')
161
175
  && entry.roots.length === 0;
162
- reports.set(entry.provider, providerReport(entry.provider, codexNeedsAuthorization ? 'export-required' : 'unavailable', codexNeedsAuthorization ? 'manual-export' : 'discoverable', [], codexNeedsAuthorization
176
+ const expectedFlag = entry.provider === 'deepseek-harness' ? 'dsh' : entry.provider;
177
+ const rootKind = entry.provider === 'hermes' ? 'Hermes home (state.db is snapshotted read-only)' : 'sessions root';
178
+ reports.set(entry.provider, providerReport(entry.provider, needsExplicitRoot ? 'export-required' : 'unavailable', needsExplicitRoot ? 'manual-export' : 'discoverable', [], needsExplicitRoot
163
179
  ? 'SHARED_ROOT_AUTHORIZATION_REQUIRED'
164
- : 'PROVIDER_ROOT_UNAVAILABLE', codexNeedsAuthorization
165
- ? `Pass --${entry.provider === 'deepseek-harness' ? 'dsh' : entry.provider}-root with an explicitly authorized sessions root.`
180
+ : 'PROVIDER_ROOT_UNAVAILABLE', needsExplicitRoot
181
+ ? `Pass --${expectedFlag}-root with an explicitly authorized ${rootKind}.`
166
182
  : `No authorized ${entry.provider} workspace history root was found.`));
167
183
  if (entry.provider === 'codex')
168
184
  reports.get(entry.provider).rootCounts = [...rootCounts.values()];
@@ -173,13 +189,48 @@ export async function discoverWorkspaceHistories(options) {
173
189
  workspaceId,
174
190
  allowedRoots: availableRoots,
175
191
  };
192
+ if (entry.provider === 'hermes') {
193
+ // The consistent snapshot is staged under owned workspace staging (the
194
+ // provider home stays read-only); staged locators need scope cover, and
195
+ // refresh must retain both the origin grant and the staging grant.
196
+ const staging = hermesSnapshotStagingDir(workspacePath);
197
+ if (!scope.allowedRoots.includes(staging))
198
+ scope.allowedRoots.push(staging);
199
+ authorizedRoots[entry.provider] = [...new Set([...availableRoots, staging])];
200
+ if (!rootCounts.has(staging)) {
201
+ rootCounts.set(staging, {
202
+ root: redactSourceLocator(staging), rootId: sha256(staging),
203
+ status: 'checked', sourceCount: 0, duplicateCount: 0,
204
+ });
205
+ }
206
+ }
176
207
  const providerSources = [];
177
208
  let matchedBySubfolder = 0;
178
209
  let matchedByWorktree = 0;
179
210
  const museSourceInspections = [];
211
+ // Grok Build live detection reads exactly one explicitly authorized file
212
+ // (never the sessions root's parent implicitly); absence means unknown.
213
+ let grokActiveRegistry = null;
214
+ let grokActiveRegistryLoaded = false;
215
+ const grokActiveEvidence = async () => {
216
+ if (grokActiveRegistryLoaded || entry.provider !== 'grok-build' || !options.grokActiveSessions)
217
+ return grokActiveRegistry;
218
+ grokActiveRegistryLoaded = true;
219
+ try {
220
+ grokActiveRegistry = await readGrokBuildActiveRegistry(resolve(options.grokActiveSessions), [resolve(options.grokActiveSessions)].map((file) => dirname(file)));
221
+ }
222
+ catch {
223
+ grokActiveRegistry = null;
224
+ }
225
+ return grokActiveRegistry;
226
+ };
180
227
  for await (const descriptor of adapter.discover(scope)) {
181
228
  const known = options.knownSources?.find(source => source.provider === descriptor.provider && source.locator === descriptor.locator);
182
- const nativeSessionId = known?.nativeSessionId ?? descriptor.nativeSessionId ?? nativeSessionIdFromLocator(descriptor);
229
+ // A Hermes projection holds many sessions, so its locator basename is
230
+ // never a session identity.
231
+ const nativeSessionId = entry.provider === 'hermes'
232
+ ? (known?.nativeSessionId ?? descriptor.nativeSessionId)
233
+ : (known?.nativeSessionId ?? descriptor.nativeSessionId ?? nativeSessionIdFromLocator(descriptor));
183
234
  const exclusion = matchImportExclusion({ ...descriptor, nativeSessionId }, exclusions);
184
235
  if (exclusion) {
185
236
  const locator = resolve(descriptor.locator);
@@ -224,6 +275,13 @@ export async function discoverWorkspaceHistories(options) {
224
275
  if (!workspaceMatch)
225
276
  continue;
226
277
  }
278
+ if (entry.provider === 'grok-build') {
279
+ // The url-encoded cwd directory is the only workspace evidence; an
280
+ // unresolvable or conflicting value never attaches to the caller.
281
+ workspaceMatch = await grokBuildSourceWorkspaceMatch(locator, workspacePath);
282
+ if (!workspaceMatch)
283
+ continue;
284
+ }
227
285
  const ompHeader = entry.provider === 'omp' ? await readOmpSessionHeader({ ...descriptor, sourceId: 'discovery', authorizedScope: scope }) : undefined;
228
286
  if (ompHeader) {
229
287
  workspaceMatch = await matchSessionWorkspace(ompHeader.cwd, workspacePath);
@@ -262,6 +320,24 @@ export async function discoverWorkspaceHistories(options) {
262
320
  rootCount.sourceCount += 1;
263
321
  const source = sourceFromDescriptor(descriptor, locator, authorizedRoot, details, fingerprint.digest);
264
322
  source.nativeSessionId = rolloutId ?? ompHeader?.id ?? nativeSessionId;
323
+ if (entry.provider === 'hermes') {
324
+ // A state.db projection holds many sessions; the locator basename is
325
+ // not a session identity.
326
+ delete source.nativeSessionId;
327
+ }
328
+ if (entry.provider === 'grok-build' && source.nativeSessionId) {
329
+ // Provider-reported activity only; histories stay usable without it.
330
+ const observedAt = options.createdAt ?? new Date().toISOString();
331
+ const registry = await grokActiveEvidence();
332
+ const evidence = grokBuildLiveState(registry, source.nativeSessionId);
333
+ source.liveEvidence = {
334
+ ...evidence,
335
+ observedAt,
336
+ expiresAt: evidence.state === 'active'
337
+ ? new Date(Date.parse(observedAt) + GROK_BUILD_ACTIVE_FRESHNESS_MS).toISOString()
338
+ : null,
339
+ };
340
+ }
265
341
  if (ompHeader)
266
342
  source.sourceId = sha256(['omp-native-source-v1', workspaceId, ompHeader.id].join('\0'));
267
343
  if (entry.provider === 'grokbot') {
@@ -269,10 +345,13 @@ export async function discoverWorkspaceHistories(options) {
269
345
  // old entries, so a content-digest sourceId (the default above)
270
346
  // would churn every re-discovery and break checkpoint continuity.
271
347
  // Key it on the conversation id instead, same rationale as the OMP
272
- // override above.
348
+ // override above. Unlike OMP, the key carries no workspaceId: Grok
349
+ // Bot conversations are account-scoped (#3232), so the same replica
350
+ // must resolve to one host-catalog source no matter which workspace
351
+ // authorizes it; workspace catalogs keep a reference instead of a copy.
273
352
  const conversationId = grokbotConversationIdFromLocator(locator);
274
353
  if (conversationId) {
275
- source.sourceId = sha256(['grokbot-native-source-v1', workspaceId, conversationId].join('\0'));
354
+ source.sourceId = sha256(['grokbot-native-source-v1', conversationId].join('\0'));
276
355
  }
277
356
  }
278
357
  providerSources.push(source);
@@ -0,0 +1,28 @@
1
+ // Preserve non-portable skill metadata across the portable boundary.
2
+ import { AGENT_SKILLS_SIDECAR_SCHEMA } from './agent-skills.js';
3
+ export function createAgentSkillSidecar(document, provenance, validationProfile, trust) {
4
+ return {
5
+ $schema: AGENT_SKILLS_SIDECAR_SCHEMA,
6
+ schemaVersion: 1,
7
+ aiwg: structuredClone(document.aiwg),
8
+ ...(document.providers ? { providers: structuredClone(document.providers) } : {}),
9
+ provenance: structuredClone(provenance),
10
+ validationProfile,
11
+ trust: { ...trust },
12
+ };
13
+ }
14
+ /**
15
+ * Restore AIWG metadata omitted at a strict portability boundary. Portable
16
+ * content remains authoritative for standard fields, body, and resources.
17
+ */
18
+ export function restoreAgentSkillFromSidecar(standard, body, resources, sidecar) {
19
+ return {
20
+ standard: structuredClone(standard),
21
+ body,
22
+ resources: resources.map((resource) => ({ ...resource })),
23
+ aiwg: structuredClone(sidecar.aiwg),
24
+ ...(sidecar.providers ? { providers: structuredClone(sidecar.providers) } : {}),
25
+ unknownFields: [],
26
+ };
27
+ }
28
+ //# sourceMappingURL=agent-skill-sidecar.js.map
@@ -6,6 +6,9 @@
6
6
  *
7
7
  * @implements #1875
8
8
  */
9
+ import { CLAUDE_SKILL_FIELDS } from './provider-extensions.js';
10
+ export { CLAUDE_SKILL_FIELDS } from './provider-extensions.js';
11
+ export { createAgentSkillSidecar, restoreAgentSkillFromSidecar } from './agent-skill-sidecar.js';
9
12
  export const AGENT_SKILLS_BASELINE = {
10
13
  repository: 'agentskills/agentskills',
11
14
  revision: '38a2ff82958afee88dadf4831509e6f7e9d8ef4e',
@@ -174,6 +177,7 @@ export function validateCompatibleAgentSkillMetadata(frontmatter, directoryName,
174
177
  const recognizedFields = new Set([
175
178
  ...STANDARD_SKILL_FIELDS,
176
179
  ...AIWG_SKILL_CONTROL_FIELDS,
180
+ ...CLAUDE_SKILL_FIELDS,
177
181
  ]);
178
182
  for (const key of Object.keys(frontmatter).sort()) {
179
183
  if (!recognizedFields.has(key)) {
@@ -223,27 +227,4 @@ export function equivalentPortableAllowedTools(aiwg) {
223
227
  }
224
228
  return aiwg.allowedTools.join(' ');
225
229
  }
226
- export function createAgentSkillSidecar(document, provenance, validationProfile, trust) {
227
- return {
228
- $schema: AGENT_SKILLS_SIDECAR_SCHEMA,
229
- schemaVersion: 1,
230
- aiwg: structuredClone(document.aiwg),
231
- provenance: structuredClone(provenance),
232
- validationProfile,
233
- trust: { ...trust },
234
- };
235
- }
236
- /**
237
- * Restore AIWG metadata omitted at a strict portability boundary. Portable
238
- * content remains authoritative for standard fields, body, and resources.
239
- */
240
- export function restoreAgentSkillFromSidecar(standard, body, resources, sidecar) {
241
- return {
242
- standard: structuredClone(standard),
243
- body,
244
- resources: resources.map((resource) => ({ ...resource })),
245
- aiwg: structuredClone(sidecar.aiwg),
246
- unknownFields: [],
247
- };
248
- }
249
230
  //# sourceMappingURL=agent-skills.js.map